Monthly Archive

PowerShell and WMI

CIM filters

I was looking up Win32_SystemDriver on the MSDN site and noticed there was some PowerShell example code

 

Get-WmiObject -Class Win32_SystemDriver |
Where-Object -FilterScript {$_.State -eq "Running"} |
Where-Object -FilterScript {$_.StartMode -eq "Manual"} |
Format-Table -Property Name,DisplayName

 

A better way to write this would be:

Get-WmiObject -Class Win32_SystemDriver -Filter "State='Running' AND StartMode='Manual'" | Format-Table -Property Name, DisplayName –AutoSize

 

or

 

Get-CimInstance -ClassName Win32_SystemDriver -Filter "State='Running' AND StartMode='Manual'" | Format-Table -Property Name, DisplayName -AutoSize

 

Do the filtering in the CIM call – especially if you’re running this against a number of remote machines. That way you limit the network traffic you’re returning

OMI/CIM/WMI dictionary

Don Jones provides a very good summary of the similarities and differences between WMI, CIM and OMI http://powershell.org/wp/2015/04/24/management-information-the-omicimwmimidmtf-dictionary/

Recommended reading if you’re using these technologies

Query vs Filter

I’ve tended to advocate using the –Filter parameter rather than the –Query parameter with the CIM (and WMI) cmdlets but a recent post on the Windows Management Infrastructure blog has me questioning that decision.

 

Using Measure-Command I tried various pairs of commands – such as:

 

Measure-Command {Get-CimInstance -ClassName Win32_Directory -Filter "Name = 'C:\\Test2'"}

 

Measure-Command {Get-CimInstance -Query "SELECT * FROM Win32_Directory WHERE Name = 'C:\\Test2'"}

 

The results weren’t conclusive but it seems that at worst there is no significant difference between the approaches and at best using a query is significantly faster. 

 

At the moment my best advice would be use the –Filter parameter if you want to reduce typing but try –Query if speed becomes your main issue.

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