Monthly Archive

PowerShell and WMI

CDXML filter parameters

I was recently asked about adding a filter parameter to a cmdlet created through CDXML. If you’ve not seen it before (see PowerShell and WMI Chapters 18 & 19 from www.manning.com) CDXML allows you to creat ecmdlets by wrapping a WMI class in some simple XML.

 

The resultaing CDXML (Cmdlet Definition XML) is thn published as a module.  Here’s a simple example using the Win32_NetworkAdapterConfiguration class

<?xml version='1.0' encoding='utf-8'?>
<PowerShellMetadata xmlns='http://schemas.microsoft.com/cmdlets-over-objects/2009/11'>
  <Class ClassName='ROOT\cimv2\Win32_NetworkAdapterConfiguration'>
    <Version>1.0</Version>
    <DefaultNoun>NetworkAdapterConfiguration</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters DefaultCmdletParameterSet='DefaultSet'>
      </GetCmdletParameters>
    </InstanceCmdlets> 
  </Class>
</PowerShellMetadata>

 

The first 2 lines are boilerplate. The NameSpace and WMI class are defined on line 3, follwoed by a version number (arbitary) and a default noun for you cmdlet to use.  Instance cmdlets defines how you’ll pull the data for existing instances of the class – in other words the Get-NetworkAdapterConfiguration cmdlet.

 

Save as a CDXML file and import as a module

Import-Module .\NetworkAdapterConfiguration.cdxml

 

Get-Module will sjow it as a Cim module with a single exported command.   Use it like any other cmdlet

PS> Get-NetworkAdapterConfiguration | ft -a

ServiceName  DHCPEnabled Index Description
-----------  ----------- ----- -----------
kdnic        True        0     Microsoft Kernel Debug Network Adapter
mwlu97w8     True        1     Marvell AVASTAR Wireless Composite Device
msu64w8      False       2     Surface Ethernet Adapter
mwlu97w8     True        3     Marvell AVASTAR 350N Wireless Network Controller
RFCOMM       False       4     Bluetooth Device (RFCOMM Protocol TDI)
BthPan       True        5     Bluetooth Device (Personal Area Network)
vwifimp      True        6     Microsoft Wi-Fi Direct Virtual Adapter
vwifimp      True        7     Microsoft Wi-Fi Direct Virtual Adapter
RasSstp      False       8     WAN Miniport (SSTP)
RasAgileVpn  False       9     WAN Miniport (IKEv2)
Rasl2tp      False       10    WAN Miniport (L2TP)
PptpMiniport False       11    WAN Miniport (PPTP)
RasPppoe     False       12    WAN Miniport (PPPOE)
NdisWan      False       13    WAN Miniport (IP)
NdisWan      False       14    WAN Miniport (IPv6)
NdisWan      False       15    WAN Miniport (Network Monitor)

 

Using the cmdlet is equivalent to

Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration

 

but is easier and requires less typing.

Very often you’ll want to pick a specific adapter – for instance

 

Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter 'Index=3'

 

You can implement the same kind of filters using CDXML. You add a queryable properties section as shown below:

<?xml version='1.0' encoding='utf-8'?>
<PowerShellMetadata xmlns='http://schemas.microsoft.com/cmdlets-over-objects/2009/11'>
  <Class ClassName='ROOT\cimv2\Win32_NetworkAdapterConfiguration'>
    <Version>1.0</Version>
    <DefaultNoun>NetworkAdapterConfiguration</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters DefaultCmdletParameterSet='DefaultSet'>

        <QueryableProperties>
          <Property PropertyName='Index'>
            <Type PSType ='UInt32'/>
            <RegularQuery AllowGlobbing='true'>
              <CmdletParameterMetadata PSName='Index'  ValueFromPipelineByPropertyName='true' CmdletParameterSets='DefaultSet' />
            </RegularQuery>
          </Property>
        </QueryableProperties>       
             
      </GetCmdletParameters>
    </InstanceCmdlets> 
  </Class>
</PowerShellMetadata>

 

Set the paraemter name – same as property to use here – and the type (unsigned integer). Decide whether pipeline input and wildcards (globbing) are allowed and save the file.

 

Re-import the module (use the Force) and your new parameter is available

Get-NetworkAdapterConfiguration -Index 3

 

Its important to understand CDXMLMI – even if you never create a CDXML module – because 2/3 of the cmdlets in Windows Server 2012 and later are created this way.

WMI discovery

I’ve been working with System Center Configuration Manager this week. SCCM uses a lot of WMI. However, WMI discovery is still a bit of an art.

 

I needed to find the SMS_SCI_Component class.

 

If you know the namespace you can easily find the classes in that namespace but namespaces are hierarchical so you need to work down the tree.

The root namespace is at the top of the namespace tree so

C:\Scripts> Get-CimInstance -ClassName __NAMESPACE -Namespace root

Name
----
subscription
DEFAULT
SCCMDP
CIMV2
msdtc
Cli
nap
MicrosoftIISv2
SECURITY
CCMVDI
NetworkModel
RSOP
SMS
ccm
StandardCimv2
WMI
AccessLogging
directory
Policy
InventoryLogging
Interop
Hardware
ServiceModel
Microsoft
aspnet

 

After a bit of digging I found I needed the sms namespace

PS C:\Scripts> Get-CimInstance -ClassName __NAMESPACE -Namespace 'root\sms'

Name 
----    
site_PS1

 

which leads to:

Get-CimInstance -ClassName SMS_SCI_Component -Namespace 'root\sms\site_PS1'

 

What if you only know the class?

There’s a little trick using Get-WmiObject

Get-WmiObject -Namespace root -Recurse -List -Class SMS_SCI_Component

   NameSpace: ROOT\SMS\site_PS1

Name  
----      
SMS_SCI_Component      

 

The trick is to start with the root namespace and use the –Recurse (search through all child namespaces) and –List (just give class details) parameters.  You’ll see a listing by namespace for each instance of the class discovered.

BinaryMiLog cmdlets

It’s not often I come across soemthing brand new in PowerShell but yesterday when I was investigating New-CimInstance I discovered 2 cmdlets in the CimCmdlets module I hadn’t noticed before. These are:

Export-BinaryMiLog

Import-BinaryMiLog

 

The cmdlets are used to export, or import, CIM instances as a binary encoded file.  Think of them as  Export-Clixml and Import-Clixml but for CIM instances.

Their usage is very simple:

Get-CimInstance -ClassName Win32_OperatingSystem |
Export-BinaryMiLog -Path testfile.bmil

 

This creates a   30 KB binary file – its definitely not human readable!

You don’t need to use a bmil extension (its the one in the help file) and you can use a CIM instance object instead of the pipeline

$os = Get-CimInstance -ClassName Win32_OperatingSystem
Export-BinaryMiLog -InputObject $os -Path testfile2.stuff

 

Getting the data back is performed by Import-BinaryMiLog

$os1 = Import-BinaryMiLog -Path .\testfile.bmil
$os2 = Import-BinaryMiLog -Path .\testfile2.stuff

 

The results appear to be a standard CIM object

Compare-Object -ReferenceObject $os -DifferenceObject $os1 -IncludeEqual
Compare-Object -ReferenceObject $os -DifferenceObject $os2 -IncludeEqual
Compare-Object -ReferenceObject $os1 -DifferenceObject $os2 –IncludeEqual

 

These cmdlets give you way to persist CIM objects to disk so that they can be referenced at a later date. If you need to test for changes to a system this could be a useful technique

New-CimInstance cmdlet and the–Key parameter

I was asked a question about the –Key parameter on New-CimInstance today. I wasn’t sure of the answer so I’ve done some experimentation.

 

I tend to avoid New-CimInstance if I can preferring to use the Create method on the CIM class – however not all CIM classes have a create method so need to fall back on New-CimInstance.

 

I started by looking at the documentation. The help file for New-CimInstance says:

-Key<String[]>

Specifies the properties that are used as keys. CimSession and ComputerName cannot be used when Key is specified.

 

That then leads to the question how do I discover the Key or Keys of a CIM class.  You can’t use the –Qualifier parameter in Get-CimClass because that works at the class level and Key is a property qualifier.  Means you need to use some code

function Get-CimClassKey { 

    param (
   
        [string]$CIMnamespace = 'ROOT/cimv2',
       
        [string]$CIMclass
   
    )

    $class = Get-CimClass -Namespace $CIMnamespace -ClassName $CIMclass

    foreach ($property in $class.CimClassProperties) {
 
        $property | Select-Object -ExpandProperty Qualifiers |
        foreach {
            if ($_.Name -eq 'key'){
                $property
            }
        }

    }
}

 

The Key property of a class HAS to be given a value when a new instance of the class is created.

 

The New-CimInstance help file shows an example using Win32_Environment.  Adapring the example:

PS> New-CimInstance -ClassName Win32_Environment @{Name='RStest1'; VariableValue='test1'; UserName='RSSURFACEPRO2\Richard'}

Name             UserName                                            VariableValue
----             --------                                            -------------
RStest1          RSSURFACEPRO2\Richard                               test1

Using our function to discover the Keys of Win32_Environment

PS> Get-CimClassKey -CIMclass Win32_Environment

Name               : Name
Value              :
CimType            : String
Flags              : Property, Key, NullValue
Qualifiers         : {read, key, MappingStrings, Override...}
ReferenceClassName :

Name               : UserName
Value              :
CimType            : String
Flags              : Property, Key, ReadOnly, NullValue
Qualifiers         : {key, MappingStrings, MaxLen, read}
ReferenceClassName :

 

Adding the –Key parameter

PS> New-CimInstance -ClassName Win32_Environment @{Name='RStest2'; VariableValue='test2'; UserName='RSSURFACEPRO2\Richard'} -Key 'Name', 'UserName'

Name             UserName                                            VariableValue
----             --------                                            -------------
RStest2          RSSURFACEPRO2\Richard                               test2

 

Using Win32_Environment you can use the Key parameter, or not, as long as you define values for the Name and Username properties.

Another example in the New-CimInstance help file uses the Win32_Process class.  The key for that class is the Handle property

PS> Get-CimClassKey -CIMclass Win32_process

Name               : Handle
Value              :
CimType            : String
Flags              : Property, Key, ReadOnly, NullValue
Qualifiers         : {key, MaxLen, read}
ReferenceClassName :

 

the Handle is appears to be identical to the ProcessId in value as far as I can determine

 

This now gets  messy:

 

Just the Handle.  BTW exmple 3 in the documentation has an error as Handle is a string not an integer

PS> New-CimInstance -ClassName Win32_Process -Property @{Handle='0'}
New-CimInstance : Provider is not capable of the attempted operation
At line:1 char:1
+ New-CimInstance -ClassName Win32_Process -Property @{Handle='0'}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Win32_Process:CimInstance) [New-CimInstance], CimException
    + FullyQualifiedErrorId : HRESULT 0x80041024,Microsoft.Management.Infrastructure.CimCmdlets.NewCimInstanceCommand

 

Add the Key parameter

PS> New-CimInstance -ClassName Win32_Process -Property @{Handle='0'} -Key Handle
New-CimInstance : Provider is not capable of the attempted operation
At line:1 char:1
+ New-CimInstance -ClassName Win32_Process -Property @{Handle='0'} -Key ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Win32_Process (Handle = "0"):CimInstance) [New-CimInstance], CimException
    + FullyQualifiedErrorId : HRESULT 0x80041024,Microsoft.Management.Infrastructure.CimCmdlets.NewCimInstanceCommand

 

The only way it works is if you use –ClientOnly to make an in memory CIM instance that only exists in your PowerShell session

PS> New-CimInstance -ClassName Win32_Process -Property @{Handle='0'} -Key Handle -ClientOnly

Handle PSComputerName
------ --------------
0

 

You can remove the –Key parameter

PS> New-CimInstance -ClassName Win32_Process -Property @{Handle='0'} -ClientOnly

Handle PSComputerName
------ --------------
0

 

Win32_Process has a Create method that takes these parameters

PS> $class.CimClassMethods['Create'].Parameters

Name                       CimType Qualifiers                                 ReferenceClassName
----                                 ------- ----------                                 ------------------
CommandLine                 String {ID, In, MappingStrings}
CurrentDirectory            String {ID, In, MappingStrings}
ProcessStartupInformation Instance {EmbeddedInstance, ID, In, MappingStrings}
ProcessId                   UInt32 {ID, MappingStrings, Out}

 

Using Invoke-CimMethod

PS> Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='notepad.exe'}

ProcessId ReturnValue PSComputerName
--------- ----------- --------------
     2648           0

 

Now trying New-CimInstance

PS> New-CimInstance -ClassName Win32_Process -Property @{Handle='0'; CommandLine='notepad.exe'} -Key Handle
New-CimInstance : Provider is not capable of the attempted operation
At line:1 char:1
+ New-CimInstance -ClassName Win32_Process -Property @{Handle='0'; Comm ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Win32_Process (Handle = "0"):CimInstance) [New-CimInstance], CimException
    + FullyQualifiedErrorId : HRESULT 0x80041024,Microsoft.Management.Infrastructure.CimCmdlets.NewCimInstanceCommand

 

Other variants of not including the Handle property and changing the handle value all fail with same error

 

Botton line is that New-CimInstance is a bit of a mess to use – with or without the –Key parameter (which doesn’t seem to do much).

 

If the CIM class hasa create method Id recommend that you use that as a lot of CIm classes (or their providers) don’t work with New-cimInstance. In reality given that many of the CIM classes are effectively read only – you can’t create a new instance of Win32_ComputerSystem for example – it probably doesn’t matter.

Creating Registry Key

I had a question left on the blog asking how to create a registry key. My preferred method is to use the CIM class = StdRegProv. Its a static class so you don’t need to create an object

 

[uint32]$hklm = 2147483650
$newkey = 'SOFTWARE\NewKey'

Invoke-CimMethod -ClassName StdRegProv -MethodName CreateKey -Arguments @{hDefKey = $hklm; sSubKeyName = $newkey}

 

Define the variables for the HIVE in this case HKLM – local machine .  Notice that the value has to be an unsigned integer.

 

The key you want to create is just the path to the key. if you need to create multiple levels of subkeys they will all create from a single path.

 

Then use Invoke-CimMethod to call the CreateKey method on StdRegProv. The hash table in Arguments parameter has the method parameter names and appropriate  values.

 

If everything works you’ll get a return value of 0.

 

How did I know which parameters the method took?

 

I used Get-CimClass but that’s a story for another post.

Accessing WMI

There are 3 sets of cmdlets for working with WMI classes – the WMI cmdlets, the WSMAN cmdlets and the CIM cmdlets.  The protocols used by these 3 sets are different.

 

The WMI cmdlets introduced in PowerShell 1 & 2 use DCOM for local and remote access under all circumstances

 

The WSMAN cmdlets introduced in PowerShell 2 use WSMAN (WinRm)

The CIM cmdlets introduced in PowerShell 3 use:

- DCOM for local access if ComputerName parameter NOT used

- WSMAN for local access IF –ComputerName parameter is used

- WSMAN (WinRM) for remote access 

- WSMAN if a default CIM session is used for remote access

- DCOM if a CIM session is created using DCOM as the protocol option

 

The CIM cmdlets are easier to use than the WSMAN cmdlets and are the recommended way to access WMI classes.

WMI wildcards and filtering

A question on the forum asking about filtering WMI results raises a number of interesting points.

 

The user wanted to pass a computername and a filter term to pull product information from remote machines. I ended up with this

$computername = $env:COMPUTERNAME
$filter = 'Live'

$scriptblock = {
    param($filter)
    Get-WmiObject -Class Win32_product -Filter "Name LIKE '%$filter%'" |
    Select  IdentifyingNumber, Name, LocalPackage }

Invoke-Command -ComputerName $computername -ScriptBlock $scriptblock -ArgumentList $filter

 

You can pass an argument into the scriptblock you use with invoke-command by using the –Argumentlist parameter.

More interesting is the –Filter parameter on Get-Wmi-Object

-Filter "Name LIKE '%$filter%'"

 

Notice that % is the wildcard not * as you’d use for a string.  Its always better to filter the results from Get-WmiObject using –Filter rather than a where-object after the call.

 

Of course you can just use the wmi or cim cmdlets directly for this problem which is even better

Get-WmiObject -Class Win32_Product -ComputerName $computername -Filter "Name LIKE '%$filter%'" | Select  IdentifyingNumber, Name, LocalPackage

 

Get-CimInstance -ClassName Win32_Product -ComputerName $computername -Filter "Name LIKE '%$filter%'" | Select  IdentifyingNumber, Name, LocalPackage

WMI cmdlets and credentials

If you’re working with the WMI cmdlets and need to pass credentials you’ll end up with a statement something like this

Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computer -Credential $cred

 

If the computer name defaults to the local host or you use . or ‘localhost’ as the computer name you’ll get an error

PS> Get-WmiObject -Class Win32_ComputerSystem -ComputerName $env:COMPUTERNAME  -Credential $cred
Get-WmiObject : User credentials cannot be used for local connections
At line:1 char:1
+ Get-WmiObject -Class Win32_ComputerSystem -ComputerName $env:COMPUTER ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], ManagementException
    + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

You need to build in some logic to prevent credentials being used ig you’re working against the local machine.  One way to this is to create a bunch of Get-WmiObject statements and use if statements to decide which to use.

 

I think there’s a neater way if you use splatting

#$computer = $env:COMPUTERNAME
#$computer = 'localhost'
#$computer = '.'
$computer = 'server02'

$params = @{
    'Class' =  'Win32_ComputerSystem '
    'ComputerName' = $computer
}

switch ($computer){
    "$env:COMPUTERNAME" {break}
    'localhost' {break}
    '.'   {break}
    default {$params += @{'Credential' = $cred}}
}

Get-WmiObject @params

 

Splatting involves creating a hash table of the parameters and their values. You can then use a switch statement to decide if computer matches any of the local name variants. If it doesn’t then add the credential

 

You could extend this slightly to cope with not having a computer name  in the initial set of params and only add it if required

$params = @{
    'Class' =  'Win32_ComputerSystem'
}

if ($computer) {
    $params += @{'ComputerName' = $computer}
   
    switch ($computer){
        "$env:COMPUTERNAME" {break}
        'localhost' {break}
        '.'   {break}
        default {$params += @{'Credential' = $cred}}
    }
}

Get-WmiObject @params

 

Then you only test the computer name if you need to.

WMI linked classes

You will find that many WMI classes have links – some are associations built in to WMI (a subject for another time) while other classes can be linked based on property values. An example of the latter is the Win32_NetworkAdapter and Win32_NetworkAdapterConfiguration classes. The DeviceId on Win32_NetworkAdapter  matches Index on Win32_NetworkAdapterConfiguration .

 

The follwoing function (adapted from a question on the forum) shows one way this link can be used

function Get-NetworkInfo{
    [cmdletbinding()]
    param( [string]$ComputerName )

    $networkinfo = @()

    $networks = Get-WmiObject Win32_Networkadapter -Filter 'NetEnabled=True' -ComputerName $ComputerName
   
    $adapter = 0
    foreach($network in $networks){

        $id = $network.DeviceId
   
        $IPinfo = Get-WmiObject win32_networkadapterconfiguration -Filter "Index = $id" -ComputerName $ComputerName
        $winServers = 0
        $winServers = ($IPinfo.WinsPrimaryServer -join ','),($IPinfo.WinsSecondaryServer -join ',')

        $adapter += 1

        $props = @{
            'Adapter' = $adapter;
            'Manufacturer' = $network.Manufacturer;
            'Description' = $network.Description;
            'Connection' = $network.NetConnectionID;
            'SpeedGB' = [math]::Round($network.Speed / 1GB, 2)
            'IPAddress' = $IPinfo.IPAddress -join ','
            'Submask' = $IPinfo.IPSubnet -join ','
            'Gateway' = $IPinfo.DefaultIPGateway -join ','
            'DNSServers' = $IPinfo.DnsServerSearchOrder-join ','
            'WinServers' = $winServers -join ','
            'DomainSuffixes' = $IPinfo.DNSDomainSuffixSearchOrder -join ','
        }
   
        $networkinfo += New-Object -TypeName psobject -Property $props
    }
    $networkinfo
}
Get-NetworkInfo -ComputerName $env:COMPUTERNAME

NICs with IP addresses

A question on the forum asked about discovering those network adapters that had IP addresses configured. The user had tried

PS> Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPAddress IS NOT NULL"
Get-WmiObject : Invalid query "select * from Win32_NetworkAdapterConfiguration where IPAddress IS NOT NULL"
At line:1 char:1
+ Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPAdd ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-WmiObject], ManagementException
    + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

 

Actually they’d tried the full WQL query. I don’t do that because its more typing than using the class name and a filter.

 

The problem is that IPAddress is a string array and WQL won’t let you query against an array of properties.

 

You have 2 choices

Get-WmiObject -Class Win32_NetworkAdapterConfiguration | where IPAddress

 

which is fine fro local machines but means you are filtering out results on the local machine when querying a remote machine – wasteful.

 

A better solution is to use the IPEnabled property. This is a boolean that is set to TRUE when an IP address (static or DHCP) has been set on a NIC. Your query then becomes

Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPEnabled = $true"

OR

Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled = $true"