Monthly Archive

Categories

PowerShell and WMI

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

 

wmic deprecated

I saw a forum post today where the question involved the use of the wmi command line tool wmic.

 

Wmic was deprecated in Windows Server 2012 - https://technet.microsoft.com/en-us/library/hh831568(v=ws.11).aspx. It will eventually be removed.

 

You should use the CIM cmdlets instead of wmic. The syntax is much easier and the resultant code is easier to understand.

 

A little known fact – the PowerShell project was originally started as a replacement for wmic.

Applying updates through WSUS

I like to keep the virtual machines in my test lab up to date so have a WSUS server to download and manage updates. The difficulty is applying the updates. With Windows 2012 R2 I used a module that would contact the WSUS server and apply the updates – the was especially useful on server core installations.

I found with Windows 2016 that this COM based module wasn’t reliable so after a bit of investigation discovered that there are some CIM classes that you can use to discover and apply available updates and see what updates have been applied.

 

All I need is a simple set of code so wrote a bare bones module that offers three functions:

#Scan for available updates
function Get-AvailableUpdate {
[CmdletBinding()]
param()
$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession
$result = $ci | Invoke-CimMethod -MethodName ScanForUpdates -Arguments @{SearchCriteria="IsInstalled=0";OnlineScan=$true}
$result.Updates
}

#Install all available updates
function Install-AvailableUpdate {
[CmdletBinding()]
param()
$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession
Invoke-CimMethod -InputObject $ci -MethodName ApplyApplicableUpdates
}

#list installed updates
function Get-InstalledUpdate {
[CmdletBinding()]
param()
$ci = New-CimInstance -Namespace root/Microsoft/Windows/WindowsUpdate -ClassName MSFT_WUOperationsSession
$result = $ci | Invoke-CimMethod -MethodName ScanForUpdates -Arguments @{SearchCriteria="IsInstalled=1";OnlineScan=$true}
$result.Updates
}

 

Testing so far seems to be good. As this is just for me I’m bothering with adding error testing or other production ready stuff. This works and I’ll fix problems as they occur

ComputerName parameters for CIM and WMI cmdlets

Accessing a remote system and running

Get-WmiObject -ClassName Win32_LogicalDisk -ComputerName $computer

or

Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computer

is a standard approach.

 

If you’re creating a function with that code in you may put the local machine as a default parameter:

$computer = $env:COMPUTERNAME

 

Running Get-WmiObject locally will work quite happily because you’re using COM to access the local machine.

Get-CimInstance may well fail with this error

PS> Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computer
Get-CimInstance : The client cannot connect to the destination specified in the request. Verify that the service on the destination is running and is accepting requests. Consult the logs anddocumentation for the WS-Management service running on the destination, most commonly IIS or WinRM.
If the destination is the WinRM service, run the following command on the destination to analyze and configure the WinRM service: "winrm quickconfig".
At line:1 char:1
+ Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ConnectionError: (root\cimv2:Win32_LogicalDisk:String) [Get-CimInstanc
   e], CimException
    + FullyQualifiedErrorId : HRESULT 0x80338012,Microsoft.Management.Infrastructure.CimCmdlets.GetC
   imInstanceCommand
    + PSComputerName        : RSSURFACEPRO2

 

The CIM cmdlets use WSMAN to connect to remote machines. This is triggered by using the –ComputerName parameter. The error means you haven’t got the winrm service running on the local machine. On modern Windows remoting, and therefore winrm, are enable by default for servers but disable for client OS e.g. Windows 10.

 

Easiest way to get this to work is run Enable-PSremoting from and elevated prompt.

Working with multiple CIM objects

Many of the CIM objects we work with in our computers come in multiple instances – disks and network cards are a couple of examples. Many times when you see examples you’ll see something like this:

$disks = Get-WmiObject -Class Win32_LogicalDisk

foreach ($disk in $disks){
  if ($disk.Size -gt 0) {
    $disk | select DeviceId,
    @{N='Free'; E={[math]::Round($disk.FreeSpace/1GB, 2)}},
    @{N='Size'; E={[math]::Round($disk.Size/1GB, 2)}},
    @{N='PercUsed'; E={[math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 2)}}
  }
}

 

Get a collection of objects. Iterate through them with foreach and do something.

 

You can, and should, do this as a pipeline operation. The code above is really a hangover from VBScript coding.

Converting the above to a pipeline gives

Get-WmiObject -Class Win32_LogicalDisk |
where Size -gt 0 |
foreach {
    $_ | select DeviceId,
    @{N='Free'; E={[math]::Round($_.FreeSpace/1GB, 2)}},
    @{N='Size'; E={[math]::Round($_.Size/1GB, 2)}},
    @{N='PercUsed'; E={[math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 2)}}
}

 

NOTE – before anyone starts complaining yes I know you can use a  filter on Get-WmiObject I’m explaining the principle! Also, I know that you could go straight into the select on the pipeline but if you want to add extra processing e.g. send an email if the disk is more than 80% used you need the foreach

 

You can do similar things with NICs for example

Get-WmiObject -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface |
where CurrentBandwidth -gt 0 |
foreach {
  
   $props = [ordered]@{
     Name = $psitem.Name
     Computer = $psitem.PSComputerName
     PercUtilisation = (($psitem.BytesTotalPersec * 8) / $psitem.CurrentBandWidth) * 100
   }
  
   New-Object -TypeName PSObject -Property $props

} |
where PercUtilisation -gt 0.5 |
foreach {
   $text =  @"
   $($_.Name) on $($_.Computer)
        
   Bandwidth utilized:  $($_.PercUtilisation) %
"@
 
$text
 
}

 

Rather than displaying $text send an email if the utilisation is too big. 

 

where PercUtilisation -gt 0.5

is used so I actually get to see results. You probably want 60% or more on your production machines

Server Uptime

Its easy to get the last boot time of a Windows machine but how do you get the uptime

 

function Get-Uptime {
[CmdletBinding()]
param (
   [string]$ComputerName = $env:COMPUTERNAME
)

$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName

$uptime = (Get-Date) - $os.LastBootUpTime

$uptime

}

 

Use Get-CimInstance to get the Win32_OperatingSystem class. To calculate the uptime subtract the value of LastBootTime from the current time and date.

You’ll get a Timespan object returned.

PS> Get-Uptime

Days              : 1
Hours             : 10
Minutes           : 32
Seconds           : 26
Milliseconds      : 838
Ticks             : 1243468385381
TotalDays         : 1.4391995201169
TotalHours        : 34.5407884828056
TotalMinutes      : 2072.44730896833
TotalSeconds      : 124346.8385381
TotalMilliseconds : 124346838.5381

 

Pick out whichever properties you need for your report

Filter early and WQL

What’s wrong with this:

Get-CimInstance -ClassName Win32_Service |
where {$_.State -eq 'Running' -and $_.StartName -notlike 'LocalSystem' -and $_.StartName -notlike 'NT Authority*'} |
select PSComputerName, Name, DisplayName, State, StartName

 

Nothing except that its inefficient. if you ran this against a remote machine the filtering would happen on the local machine AFTER you’d dragged everything across the network. May not matter for a few machines but when you get to hundreds or thousands of machines it will have an impact

 

You need to use a filter. First try would be something like this:

Get-CimInstance -ClassName Win32_Service  -Filter "State = 'Running' AND StartName != 'LocalSystem' AND NOT StartName LIKE 'NT Authority%'"|
select PSComputerName, Name, DisplayName, State, StartName

 

Unfortunately any services with a NULL StartName will also be filtered out

 

This will work

Get-CimInstance -ClassName Win32_Service  -Filter "State = 'Running' AND Startname != 'LocalSystem' AND StartName != 'NT AUTHORITY\\LocalService' AND StartName != 'NT AUTHORITY\\NetworkService'"|
select PSComputerName, Name, DisplayName, State, StartName

 

Same results are obtained with Get-WmiObject

Dealing with CIM properties that are integer arrays

Saw a post about WmiMonitorID that intrigued me

 

If you use the WmiMonitorID:

 

PS> Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID | select -f 1

Active                 : True
InstanceName           : DISPLAY\GSM598F\4&19086f00&0&UID200195_0
ManufacturerName       : {71, 83, 77, 0...}
ProductCodeID          : {53, 57, 56, 70...}
SerialNumberID         : {51, 48, 52, 78...}
UserFriendlyName       : {50, 50, 69, 65...}
UserFriendlyNameLength : 13
WeekOfManufacture      : 4
YearOfManufacture      : 2013
PSComputerName         :

 

You get a number of properties returned as an array of numbers. if you look at the property with Get-CimClass they unsigned 16 bit integers

Name               : ManufacturerName
Value              :
CimType            : UInt16Array
Flags              : Property, ReadOnly, NullValue
Qualifiers         : {MAX, read, WmiDataId}
ReferenceClassName :

 

Probably the easiest way to deal with them is a very simple function and calculated fields

 

function Convert-ArrayToName {
param ($array)

($array | foreach { [char][byte]$_} ) -join ''

}

Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID |
select Active,
@{N='Manufacturer'; E={Convert-ArrayToName -array $_.ManufacturerName }},
@{N='ProductCode'; E={Convert-ArrayToName -array $_.ProductCodeID}},
@{N='SerialNumber'; E={Convert-ArrayToName -array $_.SerialNumberID}},
@{N='UserFriendlyName'; E={Convert-ArrayToName -array $_.UserFriendlyName}},
WeekOfManufacture,YearOfManufacture

 

The function Convert-ArrayToName accepts an array.  Using foreach-object the integers are converted to bytes and then to chars. Join the resultant array of chars and you get the string versions of the property

 

Call the function in a calculated field to convert the numeric array to a string – repeat for all relevant properties. You could create an object rather than just using select if you wish

 

Run the code and

Active                 : True
InstanceName           : DISPLAY\GSM598F\4&19086f00&0&UID200195_0
ManufacturerName       : {71, 83, 77, 0...}
ProductCodeID          : {53, 57, 56, 70...}
SerialNumberID         : {51, 48, 52, 78...}
UserFriendlyName       : {50, 50, 69, 65...}
UserFriendlyNameLength : 13
WeekOfManufacture      : 4
YearOfManufacture      : 2013
PSComputerName         :

 

Active                 : True
InstanceName           : DISPLAY\SEC3242\4&19086f00&0&UID265988_0
ManufacturerName       : {83, 69, 67, 0...}
ProductCodeID          : {51, 50, 52, 50...}
SerialNumberID         : {48, 0, 0, 0...}
UserFriendlyName       :
UserFriendlyNameLength : 0
WeekOfManufacture      : 0
YearOfManufacture      : 2012
PSComputerName         :

 

 

becomes

 

Active            : True
Manufacturer      : GSM            
ProductCode       : 598F           
SerialNumber      : 304NDJX51788   
UserFriendlyName  : 22EA63      
WeekOfManufacture : 4
YearOfManufacture : 2013

 

Active            : True
Manufacturer      : SEC            
ProductCode       : 3242           
SerialNumber      : 0              
UserFriendlyName  : 
WeekOfManufacture : 0
YearOfManufacture : 2012

Optimising WMI calls–part 3

The next change just uses 1 call to get the disk information instead of 2

 

Measure-Command -Expression {

$srvs = 'W16TP5TGT01', 'W16TP5TGT02'

for ($i=1; $i -le 150; $i++){

foreach ($srv in $srvs) {
$cs = New-CimSession -ComputerName $srv
$bootupMemory = Get-CimInstance -Query "SELECT * FROM Win32_OperatingSystem" -CimSession $cs
$cpuLoad = Get-CimInstance -Query "SELECT * FROM Win32_Processor" -CimSession $cs

$tSessions = Get-CimInstance -Query "SELECT * FROM Win32_TerminalService" -CimSession $cs

$sfilt = "Name='imaservice' OR Name='mfcom' OR Name='cpsvc' OR Name='msmq'"
$reqserv = Get-CimInstance -ClassName Win32_Service -Filter $sfilt -CimSession $cs
 
$ima = $reqserv | where Name -eq 'imaservice'
$mfcom = $reqserv | where Name -eq 'mfcom'
$ctxPrintMgr = $reqserv | where Name -eq 'cpsvc'
$msmqstatus = $reqserv | where Name -eq 'msmq'

$dfilt = "Deviceid='c:' OR Deviceid='D:'"
$drives = Get-CimInstance -ClassName Win32_Logicaldisk -Filter $dfilt -CimSession $cs
 
$cDrive = $drives | where deviceid -eq 'c:'
$dDrive = $drives | where deviceid -eq 'd:'
Remove-CimSession -CimSession $cs
}
}
}

 

Time now becomes

Days              : 0
Hours             : 0
Minutes           : 6
Seconds           : 36
Milliseconds      : 923
Ticks             : 3969235528
TotalDays         : 0.00459402260185185
TotalHours        : 0.110256542444444
TotalMinutes      : 6.61539254666667
TotalSeconds      : 396.9235528
TotalMilliseconds : 396923.5528

 

Not such a dramatic change but overall we’re now taking 26.4% less time to run the code. 

Optimising WMI calls–part 2

Last time we looked at using CIM sessions to make a set of WMI calls run quicker. This time we’ll reduce the number of calls.  I’m deliberately just reducing the number of calls to the Win32_Service class.  We’ll look at the disks another time

 

Our code becomes

Measure-Command -Expression {

$srvs = 'W16TP5TGT01', 'W16TP5TGT02'

for ($i=1; $i -le 150; $i++){

foreach ($srv in $srvs) {
$cs = New-CimSession -ComputerName $srv
$bootupMemory = Get-CimInstance -Query "SELECT * FROM Win32_OperatingSystem" -CimSession $cs
$cpuLoad = Get-CimInstance -Query "SELECT * FROM Win32_Processor" -CimSession $cs

$tSessions = Get-CimInstance -Query "SELECT * FROM Win32_TerminalService" -CimSession $cs

$sfilt = "Name='imaservice' OR Name='mfcom' OR Name='cpsvc' OR Name='msmq'"
$reqserv = Get-CimInstance -ClassName Win32_Service -Filter $sfilt -CimSession $cs
 
$ima = $reqserv | where Name -eq 'imaservice'
$mfcom = $reqserv | where Name -eq 'mfcom'
$ctxPrintMgr = $reqserv | where Name -eq 'cpsvc'
$msmqstatus = $reqserv | where Name -eq 'msmq'

$cDrive = Get-CimInstance -Query "SELECT * FROM Win32_Logicaldisk WHERE deviceid='c:'" -CimSession $cs
$dDrive = Get-CimInstance -Query "SELECT * FROM Win32_Logicaldisk WHERE deviceid='d:'" -CimSession $cs
Remove-CimSession -CimSession $cs
}
}
}

 

The change is to create a filter that pulls back JUST the services we want. Use that to create a collection of Win32_Service objects and then populate the variables with the required service data

 

Time drops dramatically

Days              : 0
Hours             : 0
Minutes           : 6
Seconds           : 50
Milliseconds      : 133
Ticks             : 4101339515
TotalDays         : 0.0047469207349537
TotalHours        : 0.113926097638889
TotalMinutes      : 6.83556585833333
TotalSeconds      : 410.1339515
TotalMilliseconds : 410133.9515

 

Total time goes from 539.42 seconds to 410.13 seconds.  That’s reduced the time by 23.96%

 

These are just simple coding changes remember- we’re not performing any clever parallel processing here