Monthly Archive

PowerShell and WMI

Copy a file with WMI

A question came up on the forum about copying files with CIM (WMI). I normally use Copy-Item rather than CIM as its easier. The questioner was using CIM_LogicalFile when I’ve normally used CIM_DataFile so I decided to take a look at the class. In reality the two classes are very similar and CIM-datafile could be substituted for CIM_LogicalFile in the code that follows.

 

The obvious starting point is to use the Copy method on the CIM_LogicalFile class

 

$files = Get-WmiObject -Class CIM_LogicalFile -Filter "Path = '\\Test\\' AND Extension = 'txt'"

foreach ($file in $files) {
$newfile = "C:\Test2\$($file.FileName).$($file.Extension)"
 
$file.Copy($newfile)

}

 

Couple of points to note. In the Path part of the filter you have to escape the \ delimiter.  Extension doesn’t include the ‘.’

You have to give the full path – including file name - to the loaction to which you want to copy the file. In this case you don’t have to escape the \ delimiter. Consistency is a wonderful thing and usually absent from WMI.

 

You can also use Invoke-WmiMethod

 

$files = Get-WmiObject -Class CIM_LogicalFile -Filter "Path = '\\Test\\' AND Extension = 'txt'"

foreach ($file in $files) {
$newfile = "C:\Test2\$($file.FileName).$($file.Extension)"
 
Invoke-WmiMethod -InputObject $file  -Name Copy -ArgumentList $newfile

}

 

OR

use the new CIM cmdlets

 

$files = Get-CimInstance -ClassName CIM_LogicalFile -Filter "Path = '\\Test\\' AND Extension = 'txt'"

foreach ($file in $files) {
$newfile = "C:\Test2\$($file.FileName).$($file.Extension)"
 
Invoke-CimMethod -InputObject $file  -MethodName Copy -Arguments @{Filename = $newfile}

}

 

In this case you have to give the argument name for the method as well as its value. You can discover the method parameters using Get-CimClass

 

$class = Get-CimClass CIM_LogicalFile

£> $class.CimClassMethods["Copy"].Parameters

Scripting Guy CDXML series finished

My CDXML series on the Scripting Guy blog finished today.  The 4 articles are:

 

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/02/registry-cmdlets-manage-the-registry.aspx

 

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/03/registry-cmdlets-first-steps-with-cdxml.aspx

 

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/04/registry-cmdlets-advanced-cdxml.aspx

 

http://blogs.technet.com/b/heyscriptingguy/archive/2015/02/05/registry-cmdlets-using-advanced-cdxml.aspx

Testing for a hotfix

KB3000850 – the November roll up for Windows 2012 R2 contains some very useful updates.

I’ve installed it on some machines in my lab but not all. The update is huge so I’m installing it manually rather than through WSUS.

 

I need to test a remote machine to determine if the update  is installed.

If it is installed you get a this back

£> Get-HotFix -Id KB3000850 -ComputerName w12r2dsc

Source        Description             HotFixID         InstalledBy      
------          -----------                --------          -----------      
W12R2DSC      Update           KB3000850      MANTICORE\Richard

 

But if its not installed you get this

£> Get-HotFix -Id KB3000850 -ComputerName w12r2od01
Get-HotFix : Cannot find the requested hotfix on the 'w12r2od01' computer. Verify the input and run the command again.
At line:1 char:1
+ Get-HotFix -Id KB3000850 -ComputerName w12r2od01
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (:) [Get-HotFix], ArgumentException
    + FullyQualifiedErrorId : GetHotFixNoEntriesFound,Microsoft.PowerShell.Commands.GetHotFixCommand

Get-Hotfix actually uses the Win32_QuickFixEngineering CIM class so you need to have DCOM open on the remote system otherwise you get a

Get-HotFix : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)

error.

 

You need to wrap the call to Get-Hotfix in a try catch. You only need to know if the update is installed so creating a specialised output object manages that for you

Get-VM |
where State -eq 'Running' |
foreach {
 
$props = @{
   Machine = $($psitem.Name)
   Present = $false
}
 
try {
   $hf = Get-HotFix -Id KB3000850 -ComputerName $psitem.Name -ErrorAction Stop
   $props.Present = $true
}
catch {
   $props.Present = $false
}
 
New-Object -TypeName PSObject -Property $props
 
}

 

Substitute any other method of getting a list of computer names, for my call to Get-VM, to match your environment.

WMI errors

Most PowerShell users will have done something like this:

£> Get-WmiObject -ClassName Win32_ComputerSystem
Domain              : WORKGROUP
Manufacturer        : Microsoft Corporation
Model               : Surface Pro 2
Name                : RSSURFACEPRO2
PrimaryOwnerName    :
TotalPhysicalMemory : 8506093568

 

Or you can use a computername to access a remote system

£> $computer = $env:COMPUTERNAME
£> Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer

Domain              : WORKGROUP
Manufacturer        : Microsoft Corporation
Model               : Surface Pro 2
Name                : RSSURFACEPRO2
PrimaryOwnerName    :
TotalPhysicalMemory : 8506093568

 

But if you try to access a remote machine that doesn’t exist

£> $computer = 'WillNotBeFound'
£> Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer
Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At line:1 char:1
+ Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], COMException
    + FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

You’ve heard of try – catch  so you do something like this

$computer = 'WillNotBeFound'

try {
  Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
  Write-Warning "An error occurred"
}

 

OR your catch may do something to record the error.

$computer = 'WillNotBeFound'

try {
  Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
  Write-Warning "An error occurred for machine: $computer"
}

 

If you make the warning message specific

$computer = 'WillNotBeFound'

try {
  Get-WmiObject -ClassName Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop
}
catch {
  Write-Warning "The RPC server is unavailable for machine: $computer"
}

 

What happens if you have a different error. Lets say you copy the code and change the class. You test the code

$computer = $env:COMPUTERNAME

try {
  Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
  Write-Warning "The RPC server is unavailable for machine: $computer"
}

 

And get a message

WARNING: The RPC server is unavailable for machine: RSSURFACEPRO2

 

What you should see is

Get-WmiObject : Invalid class "Win32_LogicalDsik"
At line:1 char:1
+ Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidType: (:) [Get-WmiObject], ManagementException
    + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

Because the class is mistyped.

 

You could try and create a number of catch statements – one for each error type.  Good luck with that. Its a lot of work.  A better approach, in my mind is to extract the error information you need

$computer = $env:COMPUTERNAME

try {
  Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @"
  Computer = $computer
  Exception = $($error[0].Exception)
  ErrorId = $($error[0].FullyQualifiedErrorId)
"@
 
  Write-Warning $errordata
}

 

If you look in the PowerShell automatic variable $error – you’ll find its a collection of error messages.  $error[0] contains the last error.  So now you get

£> $computer = $env:COMPUTERNAME

try {
  Get-WmiObject -ClassName Win32_LogicalDsik -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @"
  Computer = $computer
  Exception = $($error[0].Exception)
  ErrorId = $($error[0].FullyQualifiedErrorId)
"@
 
  Write-Warning $errordata
}

WARNING:   Computer = RSSURFACEPRO2
  Exception = System.Management.ManagementException: Invalid class "Win32_LogicalDsik"
  ErrorId = GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

For a set of computers

$computers = $env:COMPUTERNAME, 'WillNotBefound'

foreach ($computer in $computers){
try {
  Get-WmiObject -ClassName Win32_computersystem -ComputerName $computer -ErrorAction Stop
}
catch {
$errordata = @"
  Computer = $computer
  Exception = $($error[0].Exception)
  ErrorId = $($error[0].FullyQualifiedErrorId)
"@
 
  Write-Warning $errordata
}
}

 

You get the data where you can contact the machine – otherwise a warning message that the server is unavailable.  Rather than outputting a warning message create an object from the error data and save to a CSV file

WMI — identifying writable properties

One common mistake I see is people trying to set the value of a read only property on a WMI class.  There isn’t a quick way to see if a property is writable. Get-CimClass can be used but you have to dig into the Qualifiers for each property.

 

You can use this function to determine the read\write settings on all of the properties of a WMI class

function get-cimreadwriteproperties {
[CmdletBinding()]
param (
[string]$classname
)

$props = @()

$class = Get-CimClass -ClassName $classname
$class.CimClassProperties |
foreach {
  $prop = [ordered]@{
    Name = $psitem.Name
    Read = $false
    Write = $false
  }
 
  $psitem |
  select -ExpandProperty Qualifiers |
  foreach {
    if ($_.Name.ToLower() -eq 'read') {
      $prop.Read = $true
    }
    if ($_.Name.ToLower() -eq 'write') {
      $prop.Write = $true
    }
  }

  $props += New-Object -TypeName PSObject -Property $prop
}

$props

}

 

Take the class name as a parameter and use Get-CimClass. Iterate through the properties and foreach create an output object. Test each qualifier to determine if read or write and set out to true. Add to array and output.

 

The output looks like this

 

£> get-cimreadwriteproperties -classname Win32_bios | ft -AutoSize

Name                  Read Write
----                  ---- -----
Caption               True False
Description           True False
InstallDate           True False
Name                  True False
Status                True False
BuildNumber           True False

etc

 

 

£> get-cimreadwriteproperties -classname Win32_LogicalDisk | ft -AutoSize

Name                          Read Write
----                          ---- -----
Caption                       True False
Description                   True False
InstallDate                   True False
<truncated>

ErrorMethodology              True False
NumberOfBlocks               False False
Purpose                       True False
<truncated>
VolumeDirty                   True False
VolumeName                    True  True
VolumeSerialNumber            True False

WMI integer properties – alternative decoding options

 

WMI has many properties where the the value is an integer:

£> Get-CimInstance -ClassName Win32_LogicalDisk | Format-Table DeviceId, DriveType, Size, FreeSpace -a

DeviceId DriveType         Size    FreeSpace
-------- ---------         ----    ---------
C:               3 135810510848 120492625920
D:               5

 

In the example drive type 3 is a standard hard disk and drive type 5 is defined as a compact disk

http://msdn.microsoft.com/en-us/library/aa394173%28v=vs.85%29.aspx

 

 

Remembering these can be a pain – there are a couple of ways to decode these values.

 

You could use a hash table – I showed many examples of this in PowerShell and WMI – www.manning.com/siddaway2

 

$dtype = DATA {ConvertFRom-StringData -StringData @'
3 = Hard Drive
5 = Compact Disk
'@}
Get-CimInstance -ClassName Win32_LogicalDisk |
Format-Table DeviceId, DriveTYpe, @{N='TYpe'; E={$dtype["$($_.DriveType)"]}}, Size, FreeSpace –a

 

 

DeviceId DriveTYpe TYpe                 Size    FreeSpace
-------- --------- ----                 ----    ---------
C:               3 Hard Drive   135810510848 120495980544
D:               5 Compact Disk
  

 

 

Define the hash table via ConvertFrom-StringData . You can then just use the hash table as a look up to convert the numeric value of drive type into a descriptive name.

With WMF 5.0 and PowerShell classes there is another option

 

enum dtype {
HardDrive = 3
CompactDisk = 5
}

Get-CimInstance -ClassName Win32_LogicalDisk |
Format-Table DeviceId, DriveTYpe, @{N='TYpe'; E={[dtype]$($_.DriveType)}}, Size, FreeSpace –a

 

DeviceId DriveTYpe        TYpe         Size    FreeSpace
-------- ---------        ----         ----    ---------
C:               3   HardDrive 135810510848 120496607232
D:               5 CompactDisk

 

Create a enumeration using the enum keyword. The descriptive text CANNOT have spaces (delimited strings don’t work either). You can then substitute the enum value into your calculated field.

WMI Associations

 

I saw a question regarding finding the Win32_NetworkAdapter instance using the matching Win32_NetworkAdapterConfiguration starting point.  This answers the “which adapter has an IP address of X” type question.

 

The Index property on a Win32_NetworkAdapterConfiguration instance has the same value as the DeviceId property on the corresponding Win32_NetworkAdapter.

 

An alternative is to use the ASSOCIATORS WQL keyword.

 

That approach get s a bit messy but looks like this:

 

$query = "ASSOCIATORS OF {Win32_NetworkAdapterConfiguration.Index='18'} WHERE RESULTCLASS = Win32_NetworkAdapter"
Get-WmiObject -Query $query

 

The CIM cmdlets get a bit better

 

$config = Get-CimInstance win32_networkadapterconfiguration -Filter "Index = 18"
Get-CimAssociatedInstance -InputObject $config -ResultClassName Win32_NetworkAdapter

 

Much simpler and you avoid the WQL

WMI and CIM dates

A question on the forum asked about extracting the year from the ReleaseDate property returned by Win32_BIOS

 

They were trying to do this

Get-CimInstance Win32_BIOS | Select-Object @{n="ReleaseDate";e={$_.ConvertToDateTime($_.ReleaseDate).year()}}

 

There are 2 problems with this approach – firstly the objects that Get-CimInstance produces don’t have the ConvertToDateTime method (its added by PowerShell to the objects produces by Get-WmiObject) and secondly on a DateTime object Year is a property not a method.

 

If you use the WMI cmdlet you see this

£> Get-WmiObject -Class Win32_Bios | select Releasedate

Releasedate
-----------
20140512000000.000000+000

 

The date is in WMI format and needs to be converted.

£> Get-WmiObject Win32_BIOS | Select-Object @{n="ReleaseDate";e={$_.ConvertToDateTime($_.ReleaseDate)}}

ReleaseDate
-----------
12/05/2014 01:00:00

 

If you want just the year

£> Get-WmiObject Win32_BIOS | Select-Object @{n="ReleaseDate";e={($_.ConvertToDateTime($_.ReleaseDate)).Year}}

ReleaseDate
-----------
       2014

 

This conversion is already done for you with the CIM cmdlets

£> Get-CimInstance -CimSession $c -ClassName Win32_Bios | select ReleaseDate

ReleaseDate
-----------
12/05/2014 01:00:00

 

Again if you just want the year

£> ((Get-CimInstance -CimSession $c -ClassName Win32_Bios).ReleaseDate).Year
2014

CimInstanceProperties

If you use Get-CimInstance and examine the object returnd – you’ll find some meta-data at the end of the object:

CimClass                                  : root/cimv2:Win32_OperatingSystem
CimInstanceProperties                     : {Caption, Description, InstallDate, Name...}
CimSystemProperties                       : Microsoft.Management.Infrastructure.CimSystemProperties

 

Expand CimInstanceProperties

 

£> Get-CimInstance Win32_OperatingSystem | select -ExpandProperty CimInstanceProperties | fl *

 

And you’ll see an entry like this for each property

Name            : Caption
Value           : Microsoft Windows 8.1 Pro
CimType         : String
Flags           : Property, ReadOnly, NotModified
IsValueModified : False

 

The CimType and Flags attributes are especially useful

 

For instance you can use them to discover which properties can be modified:

 

Get-CimInstance Win32_OperatingSystem |
select -ExpandProperty CimInstanceProperties |
foreach {
$flags =   ($psitem | select -ExpandProperty Flags)  -split ", "  

if ($flags -notcontains 'ReadOnly'){$psitem}
}

 

Remember that other properties may be modifiable via a method on the class.

 

You can find similar information via Get-CimClass

 

$class = Get-CimClass -ClassName Win32_OperatingSystem

$class.CimClassProperties |
foreach {
$flags =   ($psitem | select -ExpandProperty Flags)  -split ", "  

if ($flags -notcontains 'ReadOnly'){$psitem}
}

 

Get-CimClass gives you the qualifiers as well as the flags. Get-CimInstance gives you the value of the property if set.

Two options with slightly different results. All the better to investigate CIM classes

Finding a file version

Interesting question on the forum – how to find the file version of IE on remote machines?

 

Get-CimInstance -ClassName CIM_DataFile -Filter "Name = 'C:\\Program Files\\Internet Explorer\\iexplore.exe'"  | select -ExpandProperty Version

 

Use the CIM_dataFile class.  Its one of the few CIM_ classes that doesn’t have a Win32_ equivalent.

 

In this case you know the path to the file – note the \\ as you have to escape a single \ in WMI filters