Retrieve server/machine state's instantly

One of the biggest hurdles I have come across using SCCM is waiting for machines/servers to check in with their latest state during deployment. SCCM is referred to as a “SMS” (Slow moving server), and this is very true. When you are patching servers and have to wait several minutes even up to an hour or more it can become frustrating.

I have written a script that invokes a simple wmiquery command remotely on all servers within a specified patch window and it returns the server state in about 30 seconds or less. The script is written for my environment, so results will vary, but I will go through the script and explain what’s going on below.

To take advantage of multithreading I use a module called “SplitPipeline”.

In our window we have a few different patch windows setup.
Wed - we patch Test servers
Tue & Thursday - we patch prod servers

Each patch window also has 3 time stamps where we patch.

CLS

#===================

#Wed_10AM
#Wed_6PM
#Tues_10AM
#Tues_6PM
#Tues_9PM
#"Thurs_10AM"
#"Thurs_6PM"
#"Thurs_9PM"

##Below is the window I want to focus on in my environment, but the above are all valid options
$PatchWindow = 'Tues_6PM'

#===================

Load our Module

$LoadModules = @'
SplitPipeline
'@.Split("`n").Trim()

ForEach($Module in $LoadModules) 
{
    #Check if Module is Installed
    if($NULL -eq (Get-Module -ListAvailable $Module))
    {
        #Install Module
        Install-Module -Name $Module -Scope CurrentUser -Confirm:$False -Force
        #Import Module
        Import-Module $Module
    }

    #Check if Module is Imported
    if($NULL -eq (Get-Module -Name $Module))
    {
        #Install Module
        Import-Module $Module
    }
}

In our environment we put servers in specific security groups based on their patch window. Every environment is different, but this is how we manage ours. So to grab all the servers from these windows I use the code below.

#=================== Get All AD servers in the patch window above

$Filter = "GG_SCCM-WSUS_" + "*" + $PatchWindow + "*"

$Allservers = @()

$SCCMGroups = (Get-ADObject -Filter {(objectClass -eq "group") -and (name -like $Filter)}).DistinguishedName

$SCCMGroupMembers = foreach($SCCMGroup in $SCCMGroups)
{
    (Get-ADObject -Filter "MemberOf -EQ '$($SCCMGroup)'" -Properties "dNSHostName", "name") |% { if($NULL -ne $_) { [PSCustomObject]@{ 
        'ServerName' = "$_.Name"
        'dNSHostName' = "$($_.dNSHostName)"
        'Patch Window' = "$((((((($SCCMGroup -replace '^CN=|,\S.*$') -replace '"CN=','') -replace 'GG_SCCM-WSUS_','') -replace '_',' ') -replace '-',' ') -replace 'Reboot',' Reboot'))"} } }
}

Similarly to the above we now need to do the same for the DMZ environment (if you have one).

#=================== Get All DMZ servers in the patch window above

#Set DMZ Credentials
if($NULL -eq $DMZCred)
{
    #Store DMZ credentials
    $DMZCred = Get-Credential( (whoami) -replace "domain","dmz.local" )
}

#Create a DMZ session
if($NULL -eq $DMZSesh)
{
    $DMZSesh = New-PSSession -ComputerName "DMZ-ADDS-Server.dmz.local" -Credential $DMZCred
}

#Run our command(s) from the context of the DMZ
$DMZGroupMembers = Invoke-Command -Session $DMZSesh -ScriptBlock {

    $DMZFilter = "GG_SCCM-WSUS_" + "*" + $PatchWindow + "*"

    $DMZGroups = (Get-ADObject -Filter {(objectClass -eq "group") -and (name -like $DMZFilter)}).DistinguishedName

    foreach($DMZGroup in $DMZGroups)
    {
        (Get-ADObject -Filter "MemberOf -EQ '$($DMZGroup)'" -Properties "dNSHostName", "name") |% { if($NULL -ne $_) { [PSCustomObject]@{ 
		'ServerName' = "$_.Name"
        'dNSHostName' = "$($_.dNSHostName)" 
        'Patch Window' = "$((((((($DMZGroup -replace '^CN=|,\S.*$') -replace '"CN=','') -replace 'GG_SCCM-WSUS_','') -replace '_',' ') -replace '-',' ') -replace 'Reboot',' Reboot'))"} } }
    }
}

Now we take all the servers from both DMZ and AD and add them to our All Servers variable

$Allservers = $SCCMGroupMembers + $DMZGroupMembers

The next part of the code is the multithreading performed by our splitpipeline module.

#===================

$Output = $NULL

#Used for progress
$data = @{
    Count = $(($Allservers | measure).count)
	Done = 0
}

#===================

#Ping Servers to get the ones that are reachable
$Output = $Allservers | split-pipeline -count 64 -Variable data, Output -ErrorAction SilentlyContinue {

    Process
    {
        #Clear out Variables
        $RebootPending = $CMMissingUpdates = $Server = $NULL

        #Assign Variables
        $Server = $_
        
        #Check if Server(s) are pending Updates
        $CMMissingUpdates = @(Get-CimInstance -ComputerName $Server.ServerName -Query "SELECT * FROM CCM_SoftwareUpdate WHERE ComplianceState = '0'" -Namespace "ROOT\ccm\ClientSDK" -OperationTimeoutSec 5 )

        if($CMMissingUpdates)
        {
            Switch -Exact ($CMMissingUpdates.EvaluationState)
            {
                #You can use this link to view all Evaluation States:
                #https://learn.microsoft.com/en-us/mem/configmgr/develop/reference/core/clients/sdk/ccm_evaluationstate-client-wmi-class

                "0" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$TRUE"; "Update Progress" = "$NULL"; "Pending Reboot" = "$NULL" } -PassThru) }
                "6" { Write-Host "$($Server.ServerName) is waiting to install additional updates." -ForegroundColor Green } 
                "7" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$NULL"; "Update Progress" = "$($CMMissingUpdates.PercentComplete)"; "Pending Reboot" = "$NULL" } -PassThru) }
                "8" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$NULL"; "Update Progress" = "$NULL"; "Pending Reboot" = "$TRUE" } -PassThru) }
                "13" { Write-Host "$($Server.ServerName) is either in the middle of a reboot, or potentially has a failed Update." -ForegroundColor Red } 
                Default  { Write-Host $Server.ServerName -ForegroundColor Red }
            }
        }

        #=================== Progress bar

        $done = ++$data.Done

        # show progress
        Write-Progress -Activity "Done $done" -Status Processing -PercentComplete (100*$done/$data.Count)
    }
} | sort "dNSHostName" | ft -AutoSize

$Output 

The full code below:

CLS

#===================

#Wed_10AM
#Wed_6PM
#Tues_10AM
#Tues_6PM
#Tues_9PM
#"Thurs_10AM"
#"Thurs_6PM"
#"Thurs_9PM"

$PatchWindow = 'Tues_6PM'

#===================

$LoadModules = @'
SplitPipeline
'@.Split("`n").Trim()

ForEach($Module in $LoadModules) 
{
    #Check if Module is Installed
    if($NULL -eq (Get-Module -ListAvailable $Module))
    {
        #Install Module
        Install-Module -Name $Module -Scope CurrentUser -Confirm:$False -Force
        #Import Module
        Import-Module $Module
    }

    #Check if Module is Imported
    if($NULL -eq (Get-Module -Name $Module))
    {
        #Install Module
        Import-Module $Module
    }
}

#=================== Clear Variables

Clear-Variable Filter, Allservers, SCCMGroups, SCCMGroupMembers, DMZGroupMembers, Output, data -Force -Confirm:$False -ErrorAction SilentlyContinue

#=================== Get All AD servers in the patch window above

$Filter = "GG_SCCM-WSUS_" + "*" + $PatchWindow + "*"

$Allservers = @()

$SCCMGroups = (Get-ADObject -Filter {(objectClass -eq "group") -and (name -like $Filter)}).DistinguishedName

$SCCMGroupMembers = foreach($SCCMGroup in $SCCMGroups)
{
    (Get-ADObject -Filter "MemberOf -EQ '$($SCCMGroup)'" -Properties "dNSHostName", "name") |% { if($NULL -ne $_) { [PSCustomObject]@{ 
        'ServerName' = "$_.Name"
        'dNSHostName' = "$($_.dNSHostName)"
        'Patch Window' = "$((((((($SCCMGroup -replace '^CN=|,\S.*$') -replace '"CN=','') -replace 'GG_SCCM-WSUS_','') -replace '_',' ') -replace '-',' ') -replace 'Reboot',' Reboot'))"} } }
}

#=================== Get All DMZ servers in the patch window above

#Set DMZ Credentials
if($NULL -eq $DMZCred)
{
    #Store DMZ credentials
    $DMZCred = Get-Credential( (whoami) -replace "domain","dmz.local" )
}

#Create a DMZ session
if($NULL -eq $DMZSesh)
{
    $DMZSesh = New-PSSession -ComputerName "DMZ-ADDS-Server.dmz.local" -Credential $DMZCred
}

#Run our command(s) from the context of the DMZ
$DMZGroupMembers = Invoke-Command -Session $DMZSesh -ScriptBlock {

    $DMZFilter = "GG_SCCM-WSUS_" + "*" + $PatchWindow + "*"

    $DMZGroups = (Get-ADObject -Filter {(objectClass -eq "group") -and (name -like $DMZFilter)}).DistinguishedName

    foreach($DMZGroup in $DMZGroups)
    {
        (Get-ADObject -Filter "MemberOf -EQ '$($DMZGroup)'" -Properties "dNSHostName", "name") |% { if($NULL -ne $_) { [PSCustomObject]@{ 
		'ServerName' = "$_.Name"
        'dNSHostName' = "$($_.dNSHostName)"
        'Patch Window' = "$((((((($DMZGroup -replace '^CN=|,\S.*$') -replace '"CN=','') -replace 'GG_SCCM-WSUS_','') -replace '_',' ') -replace '-',' ') -replace 'Reboot',' Reboot'))"} } }
    }
}

$Allservers = $SCCMGroupMembers + $DMZGroupMembers

#===================

#Used for progress
$data = @{
    Count = $(($Allservers | measure).count)
	Done = 0
}

#===================

#Ping Servers to get the ones that are reachable
$Output = $Allservers | split-pipeline -count 64 -Variable data, Output -ErrorAction SilentlyContinue {

    Process
    {
        #Clear our Variables
        Clear-Variable RebootPending, CMMissingUpdates, Server -Force -Confirm:$False -ErrorAction SilentlyContinue

        #Assign Variables
        $Server = $_
        
        #Check if Server(s) are pending Updates
        $CMMissingUpdates = @(Get-CimInstance -ComputerName $Server.ServerName -Query "SELECT * FROM CCM_SoftwareUpdate WHERE ComplianceState = '0'" -Namespace "ROOT\ccm\ClientSDK" -OperationTimeoutSec 5 )

        if($CMMissingUpdates)
        {
            Switch -Exact ($CMMissingUpdates.EvaluationState)
            {
                #You can use this link to view all Evaluation States:
                #https://learn.microsoft.com/en-us/mem/configmgr/develop/reference/core/clients/sdk/ccm_evaluationstate-client-wmi-class

                "0" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$TRUE"; "Update Progress" = "$NULL"; "Pending Reboot" = "$NULL" } -PassThru) }
                "6" { Write-Host "$($Server.ServerName) is waiting to install additional updates." -ForegroundColor Green } 
                "7" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$NULL"; "Update Progress" = "$($CMMissingUpdates.PercentComplete)"; "Pending Reboot" = "$NULL" } -PassThru) }
                "8" { ($Server | Add-Member -NotePropertyMembers @{"Pending Updates" = "$NULL"; "Update Progress" = "$NULL"; "Pending Reboot" = "$TRUE" } -PassThru) }
                "13" { Write-Host "$($Server.ServerName) is either in the middle of a reboot, or potentially has a failed Update." -ForegroundColor Red } 
                Default  { Write-Host $Server.ServerName -ForegroundColor Red }
            }
        }

        #=================== Progress bar

        $done = ++$data.Done

        # show progress
        Write-Progress -Activity "Done $done" -Status Processing -PercentComplete (100*$done/$data.Count)
    }
} | sort "dNSHostName" | ft -AutoSize

$Output