Monthly Archive

Categories

PowerShell and CIM

Monitor resolution

A question on the forum about getting monitor resolution led to this code

Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorId | 
foreach { 
   
  $filter = ("InstanceName = '$($psitem.InstanceName)'").Replace("`\", "`\`\") 
    
  $maxres = Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorListedSupportedSourceModes -Filter $filter | 
  Select-Object -ExpandProperty MonitorSourceModes | 
  Sort-Object -Property {$_.HorizontalActivePixels * $_.VerticalActivePixels} -Descending | 
  Select-Object -First 1 
  
  if ($psitem.UserFriendlyNameLength -gt 0) { 
    $name = ($psitem.UserFriendlyName -notmatch '^0$' | foreach {[char]$_}) -join "" 
  } 
  else { 
    $name = 'Unknown' 
  }

  $props = [ordered]@{ 
     Manufacturer = ($psitem.ManufacturerName -notmatch '^0$' | foreach {[char]$_}) -join "" 
     Name = $name 
     Serial = ($_.SerialNumberID -notmatch '^0$' | foreach {[char]$_}) -join "" 
     MaximumResolution = "$($maxres.HorizontalActivePixels) x $($maxres.VerticalActivePixels)" 
  } 
  
  New-Object -TypeName PSObject -Property $props 

}

NOTE – we’re working in the root\wmi namespace not PowerShell’s default of root\cimv2

Use WmiMonitorId to get the attached monitors and for each of them create a filter using the instance name. You have to replace \ with \\ when dealing with WMI.

Use WmiMonitorListedSupportedSourceModes and expand MonitorSourceModes. use sort-object to find the maximum resolution (multiply horizontal by vertical)

Create an output object. I had to deal with the name of the monitor separately because one of my monitors didn’t have a user friendly name.

Results look like this

Manufacturer Name    Serial       MaximumResolution 
 ------------ ----    ------       ----------------- 
 GSM          22EA63  304NDJX51788 1920 x 1080      
 LEN          Unknown 0            1600 x 900

Use CIM cmdlets not WMI cmdlets

WMI and CIM seem to cause a LOT of confusion. Its really simple. CIM is an industry standard from DMTF.org. WMI was Microsoft’s implementation of CIM way back in Windows NT days. The complication is that Microsoft had a set of WMI cmdlets in PowerShell v2. In PowerShell v3 they introduced a set of CIM cmdlets that work differently. The bottom line is you should use CIM cmdlets not WMI cmdlets.

Here’s three reasons  why you should use CIM cmdlets not WMI cmdlets.

 

Reason 1: the WMI cmdlets are deprecated and won’t have much if any further work done on them. The WMI help files tell you to use the CIM cmdlets! PowerShell v6 doesn’t have the WMI cmdlets – just the CIM cmdlets.

 

Reason 2: the CIM cmdlets use WS-MAN for remote access. The WMI cmdlets use DCOM which is blocked by default on Windows firewall. Using the CIM cmdlets makes your remoting easier.

 

Reason 3: The CIM cmdlets do some extra work for you. For instance finding the time a machine was started:

PS> Get-WmiObject -Class Win32_OperatingSystem | select LastBootUpTime

LastBootUpTime
--------------
20171001113546.966894+060

 

Read it left to right = Year = 2017; Month = 10; day = 01; hour = 11; minute = 35; second = 46.966894 with a +60 minute offset for daylight saving time.

That date format is painful to deal with so you convert it. the nice PowerShell Team added a conversion method for you

PS> Get-WmiObject -Class Win32_OperatingSystem | select @{N='LastBootUpTime'; E={$_.ConvertToDateTime($_.LastBootUpTime) }}

LastBootUpTime
--------------
01/10/2017 11:35:46

 

It works great but means you have to do more work.

By contrast the CIM cmdlets do the work for you

PS> Get-CimInstance -ClassName Win32_OperatingSystem | select LastBootUpTime

LastBootUpTime
--------------
01/10/2017 11:35:46

 

On a slightly off topic point anyone thinking of attending the PowerShell Summit next April will be aware that we have an Iron Scripter event planned for the last day. IF access to WMI classes is required anyone using the WMI cmdlets instead of the CIM cmdlets will be marked down. You have been warned Smile

Examples of replacing WMI cmdlet with CIM cmdlet

Following my last post I was asked about these Examples of replacing WMI cmdlet with CIM cmdlet.

Example 1

gwmi win32_operatingsystem -computername $Computer -credential $creds,

$cs = New-CimSession -Credential $creds -ComputerName $computer
Get-CimInstance -ClassName Win32_operatingsystem -CimSession $cs

Example 2

get-wmiobject -query “SELECT * FROM Meta_Class WHERE __Class = ‘Win32_Process'” -namespace “root\cimv2” -computername $computer -credential $creds

$cs = New-CimSession -Credential $creds -ComputerName $computer
Get-CimInstance -Query “SELECT * FROM Meta_Class WHERE __Class = ‘Win32_Process'” -CimSession $cs

Example 3

Get-WmiObject -Namespace $namespace -Class SMS_fullcollectionmembership -ComputerName $SCCMServer -filter “Name = ‘$computer'” -credential $creds

$cs = New-CimSession -Credential $creds -ComputerName $SCCMServer
Get-CimInstance -Namespace $namespace -ClassName SMS_fullcollectionmembership ” -filter “Name = ‘$computer'” -CimSession $cs

CIM not WMI

I still see a lot of people using the WMI cmdlets – Get-WmiObject etc. You really should be using CIM nit WMI. In other words use Get-CimInstance rather than get-WmiObject etc etc.

Why do I say that?

Two main reasons.

Firstly, the WMI cmdlets are effectively deprecated. Any further development effort will be for the CIM cmdlets.

Secondly, and to my mind more important, is that the CIM cmdlets use WS-MAN for access to remote machines. If you have PowerShell remoting enabled you have access to the machine via a CIM session – either ephemeral using the cmdlet name or persistent using a CIM session.

The WMI cmdlets use DCOM for remoting which is blocked by default on the Windows firewall and most other firewalls and routers giving the RPC server is unavailable error.

The only time there is justification for using the WMI cmdlets is if you’re on a machine that has Powershell v2 installed and if that’s the case why haven’t you upgraded? If you can’t does that mean you’re running an application (usually Exchange or System Center) that doesn’t allow you to upgrade PowerShell.

Maybe  its time to perform that upgrade.

As another thought PowerShell v6 includes the CIM cmdlets but not the WMI cmdlets!

Change a computer’s description

The Win32_OperatingSystem class exposes the machines Description. This is how you can easily change a computer’s description.

PS> Get-CimInstance -ClassName Win32_OperatingSystem | select Description

Description 
-----------

PS> Get-CimInstance -ClassName Win32_OperatingSystem | Set-CimInstance -Property @{Description = 'Richards Laptop'} 
PS> Get-CimInstance -ClassName Win32_OperatingSystem | select Description

Description 
----------- 
Richards Laptop

You can see that the description is originally blank. Get the CimInstance of Win32_OperatingSystem and pipe it to Set-CimInstance. The property to change and its new value are held in the hash table that’s the value given to the –Property parameter. You can modify multiple properties at once – just add them as property-value pairs to the hash table

More diskinfo

Yesterday I showed how to get the disk, partition and logical disk information using CIM. Today I want to show more diskinfo techniques.

This time we’ll use the Storage module which was introduced with Windows 8. Underneath the covers it uses CIM – just different classes. The storage module doesn’t differentiate between volumes  and logical disks – it just uses volumes.

To start at the physical disk and get the partition and volumes:

$diskinfo = Get-Disk | foreach {

  $parts = Get-Partition -DiskNumber $psitem.DiskNumber | where DriveLetter

  $disk = $psitem

  foreach ($part in $parts) { 
    
    Get-Volume -Partition $part | 
    foreach { 
      $props = $null

      $props = [ordered]@{ 
        Disk = $disk.Number 
         Model = $disk.Model 
        Firmware = $disk.FirmwareVersion 
        SerialNUmber = $disk.SerialNumber 
        'DiskSize(GB)' = [math]::Round(($disk.AllocatedSize / 1GB ), 2) 
        Partitions = $disk.NumberOfPartitions 
        Partition = $part.PartitionNumber 
        BootPartition = $part.IsBoot 
        'PartitionSize(GB)' = [math]::Round(($part.Size / 1GB ), 2) 
        VolumeBlockSize = $psitem.AllocationUnitSize 
        LDiskName = $psitem.DriveLetter 
        FileSystem = $psitem.FileSystem 
         LDiskSize =  [math]::Round(($psitem.Size / 1GB ), 2) 
        LDiskFree =  [math]::Round(($psitem.SizeRemaining / 1GB ), 2) 
       }

      New-Object -TypeName PSObject -Property $props 
    } 
  } 
 } 
 $diskinfo

And to go the other way

$diskinfo =  Get-Volume | 
 where {$_.DriveLetter -AND $_.DriveType -eq 'Fixed'} | 
foreach {

      $part = Get-Partition -DriveLetter $psitem.DriveLetter 
       
      $disk = Get-Disk -Partition $part

      $props = $null

      $props = [ordered]@{ 
        Disk = $disk.Number 
         Model = $disk.Model 
        Firmware = $disk.FirmwareVersion 
        SerialNUmber = $disk.SerialNumber 
        'DiskSize(GB)' = [math]::Round(($disk.AllocatedSize / 1GB ), 2) 
        Partitions = $disk.NumberOfPartitions 
        Partition = $part.PartitionNumber 
        BootPartition = $part.IsBoot 
        'PartitionSize(GB)' = [math]::Round(($part.Size / 1GB ), 2) 
         VolumeBlockSize = $psitem.AllocationUnitSize 
        LDiskName = $psitem.DriveLetter 
         FileSystem = $psitem.FileSystem 
        LDiskSize =  [math]::Round(($psitem.Size / 1GB ), 2) 
        LDiskFree =  [math]::Round(($psitem.SizeRemaining / 1GB ), 2) 
      }

      New-Object -TypeName PSObject -Property $props 
 } 
 $diskinfo

The number of blocks doesn’t seem to be available – suppose you could calculate it – otherwise the information is the same as with the CIM classes you saw last time. Some of the property names are different.

Linking disks, partitions and logical drives

A question of the forums was asking about discovering disk information. They were trying to pipe the output of Get-WmiObject into another Get-WmiObject. that won’t work. There is another way. On Windows machines physical drives are divided into 1 or more partitions which are each divided into one or more logical disks. Linking disks, partitions and logical drives is a relatively simple process.

You can start at the physical disk and work down to the logical disks or start at the logical disk and work back to the physical disk. Lets start with the logical disk.

$diskinfo = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType = 3" | 
foreach { 
  $props = $null 
  
  $part = Get-CimAssociatedInstance -InputObject $psitem -ResultClass Win32_DiskPartition 
  $disk = Get-CimAssociatedInstance -InputObject $part -ResultClassName Win32_DiskDrive 
  
  $props = [ordered]@{ 
     Disk = $disk.Index 
     Model = $disk.Model 
     Firmware = $disk.FirmwareRevision 
     SerialNUmber = $disk.SerialNumber 
     'DiskSize(GB)' = [math]::Round(($disk.Size / 1GB ), 2) 
     Partitions = $disk.Partitions 
     Partition = $part.index 
     BootPartition = $part.BootPartition 
     'PartitionSize(GB)' = [math]::Round(($part.Size / 1GB ), 2) 
     Blocks = $part.NumberOfBlocks 
     BlockSize = $part.BlockSize 
     LDiskName = $psitem.Caption 
     FileSystem = $psitem.FileSystem 
      LDiskSize =  [math]::Round(($psitem.Size / 1GB ), 2) 
     LDiskFree =  [math]::Round(($psitem.FreeSpace / 1GB ), 2) 
  }

  New-Object -TypeName PSObject -Property $props

}

$diskinfo

Use Get-CimInstance to retrieve the instances of the Win32_LogicalDisk class. Use a filter for DriveType = 3 – which is local disks (as far as the server is concerned – they could be on a SAN or NAS).

Foreach of the disks get the associated partition and use that object to get the associated physical drive.

CIM (WMI) has the concept of associators and references.

A reference is a pointer showing you which instance is associated with another instance. For example:

PS> Get-CimInstance -ClassName Win32_LogicalDiskToPartition


Antecedent      : Win32_DiskPartition (DeviceID = "Disk #0, Partition #1") 
Dependent       : Win32_LogicalDisk (DeviceID = "C:") 
EndingAddress   : 511578663935 
StartingAddress : 368050176 
PSComputerName  :

Logical disk C: is associated with partition #1 on disk #0

If you want to actually get the associated class then you do this

PS> $ld = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DeviceID = "C:"' 
 PS> Get-CimAssociatedInstance -InputObject $ld -ResultClass Win32_DiskPartition

Name             NumberOfBlocks       BootPartition        PrimaryPartition     Size                Index 
 ----             --------------       -------------        ----------------     ----                ----- 
 Disk #0, Part... 998458230            False                True                 511210613760        1

or

PS> Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DeviceID = "C:"' |   Get-CimAssociatedInstance -ResultClass Win32_DiskPartition

Name             NumberOfBlocks       BootPartition        PrimaryPartition     Size                Index 
 ----             --------------       -------------        ----------------     ----                ----- 
 Disk #0, Part... 998458230            False                True                 511210613760        1

Once you’ve go the partition and physical disk instances. Populate your output object and loop. Notice that the pipeline is output directly to the variable $diskinfo. You don’t need to build arrays – get the pipeline to do it for you.

Each logical disk gets an output like this

Disk              : 0 
 Model             : Samsung SSD 840 PRO Series 
 Firmware          : DXM06B0Q 
SerialNUmber      : S1AXNSAF329511V 
DiskSize(GB)      : 476.93 
 Partitions        : 3 
 Partition         : 1 
BootPartition     : False 
PartitionSize(GB) : 476.1 
 Blocks            : 998458230 
BlockSize         : 512 
LDiskName         : C: 
FileSystem        : NTFS 
LDiskSize         : 476.1 
LDiskFree         : 212.33

That’s working up the stack. What about working down. That’s a similar process:

$diskinfo = Get-CimInstance -ClassName Win32_DiskDrive | 
foreach { 
  $disk = $psitem 
  
  $parts = Get-CimAssociatedInstance -InputObject $psitem -ResultClass Win32_DiskPartition

  foreach ($part in $parts) { 
    
    Get-CimAssociatedInstance -InputObject $part -ResultClassName Win32_LogicalDisk | 
    foreach { 
       $props = $null

      $props = [ordered]@{ 
        Disk = $disk.Index 
        Model = $disk.Model 
        Firmware = $disk.FirmwareRevision 
        SerialNUmber = $disk.SerialNumber 
        'DiskSize(GB)' = [math]::Round(($disk.Size / 1GB ), 2) 
        Partitions = $disk.Partitions 
        Partition = $part.index 
        BootPartition = $part.BootPartition 
         'PartitionSize(GB)' = [math]::Round(($part.Size / 1GB ), 2) 
        Blocks = $part.NumberOfBlocks 
        BlockSize = $part.BlockSize 
        LDiskName = $psitem.Caption 
        FileSystem = $psitem.FileSystem 
        LDiskSize =  [math]::Round(($psitem.Size / 1GB ), 2) 
        LDiskFree =  [math]::Round(($psitem.FreeSpace / 1GB ), 2) 
      }

      New-Object -TypeName PSObject -Property $props 
    } 
  } 
 } 
 $diskinfo

Start with getting the instances of Win32_Diskdrive. Foreach instance get the associated partitions - Win32_DiskPartition.

Iterate through the partitions and get the associated logical disk. Create your object and output.

NOTE: neither of these techniques will show the partitions that don’t contain logical drives so you won’t see the boot partition and other “hidden partitions” on modern Windows machines. if you need those look at Win32_DiskPartition directly.

Finding a CIM class

One of the problems you might find is finding a CIM class. You know its name but you don’t know which namespace its in.

The old WMI cmdlets allow you to search the namespaces recursively

PS> Get-WmiObject -Class Win32_Process -Namespace root -Recurse -List


   NameSpace: ROOT\CIMV2

Name                                Methods              Properties 
 ----                                -------              ---------- 
 Win32_Process                       {Create, Terminat... {Caption, CommandLine, CreationClassName, CreationDate...}

But the CIM cmdlets don’t have this functionality. I’ve been meaning to do something about this for ages but finally got motivated by something I read while proof reading PowerShell in Action – yes its getting closer, much closer.

What I ended up with is these 2 functions

function get-namespace { 
 [cmdletBinding()] 
param ([string]$namespace = 'root') 
  Get-CimInstance -Namespace $namespace -ClassName '__NAMESPACE' | 
  foreach { 
        "$namespace\" + $_.Name 
        get-namespace $("$namespace\" + $_.Name) 
  } 
 }

function find-cimclass { 
 [cmdletBinding()] 
param ( 
 [string]$namespace = 'root', 
 [string]$classname 
 )

$class = $null

## test namespace for class 
 $class = Get-CimClass -Namespace $namespace -ClassName $classname -ErrorAction SilentlyContinue

if (-not $class) { 
  $namespaces = get-namespace -namespace $namespace 
  foreach ($name in $namespaces){ 
    $class = $null 
    $class = Get-CimClass -Namespace $name -ClassName $classname -ErrorAction SilentlyContinue 
    if ($class){break} 
  } 
 }

$class 
 }

Find-Cimclass takes a namespace and class name as parameters. It tries to find the class in the given namespace. If it can’t find it then get-namespace is called to generate a list of namespaces to search. The function iterates over the collection of  namespaces testing each one for the class. When it finds the class it returns the class information.

Get-namespace  searches for all instances of the __Namespace class in the given namespace. it then recursively call itself to test each of those namespaces. That way you get the whole tree.

If you’re searching for a given class I recommend that you start at the root class to ensure that you test everywhere.

Find the logged on user

One method of finding the logged on users is to use CIM

$ComputerName = $env:COMPUTERNAME

Get-CimInstance -ClassName Win32_Process -ComputerName $ComputerName -Filter "Name = 'explorer.exe'" | 
foreach { 
 
 $lguser = Invoke-CimMethod -InputObject $psitem -MethodName GetOwner 
 
 $Properties = @{ 
 ComputerName = $ComputerName 
 User = $lguser.User 
 Domain = $lguser.Domain 
 Time = $User.CreationDate 
 } 
 
 New-Object -TypeName PSObject -Property $Properties 
 }

Get the Win32_Process instances for explorer.exe and foreach of them use the GetOwner method to get the owners names and domain. Create an object and ouput

 

Are your domain controllers real?

A question on the forum asked about discovering if domain controllers are physical or virtual machines.

This will do the job

foreach ($domain in (Get-ADForest).domains) {
 Get-ADDomainController -filter * -server $domain |
 sort hostname  |
 foreach {
 Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName $psitem.Hostname |
 select PSComputerName, Manufacturer, Model
 }
 }

 

Get the domains in your forest and then for each domain get the domain controllers. Get-ADDomainController outputs an object with a property of hostname – but you need a computername for Get-CimInstance. So, use a foreach-object and use the Hostname property as shown (you could create a property ComputerName on the pipeline object but its more work) and get the results. A virtual machine will show under the Model. You can sort or whatever once you have the results.