PowerShell script to install software updates deployed by SCCM and reboot the computer remotely

System Center Configuration Manager (SCCM) is a very popular tool to deploy software updates. Usually I can setup a deadline to force the computer to install software updates then reboot automatically. But I have a unique situation that there is another team need to take a long process which includes many maintenance steps to make sure the whole ERP/SAP system are shutdown properly and ready for patching and reboot. I only have a planned time but still need to wait until they send me an email then I can let the software updates installation start.

 

To avoid the computer start install update before they are ready, the software deployment deadline is 2 hours later than the planned time and system restart is suppressed. But I can’t wait just wait until deadline comes because the other team still wait until I finish all patching and reboot, so they can start bring the SAP system back online. There are about 50 servers in the patching group. I don’t want to manually start the installation and manually reboot them one by one. In this situation, I need a PowerShell script can go through each computer in the device collection (patching group) and install updates available in software center and can reboot the computer once the installation finish.

Thanks to Eswar koneti who wrote the script that install updates available in software center. https://gallery.technet.microsoft.com/SCCM-Configmgr-Powershell-ebbb2c0e

The key part of his code is to find the updates available in software center by Get-WmiObject:

$TargetedUpdates= Get-WmiObject -ComputerName $system -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0

$MissingUpdatesReformatted = @($TargetedUpdates | ForEach-Object {if($_.ComplianceState -eq 0){[WMI]$_.__PATH}})

And then invoke the WMI method to install the updates.

Invoke-WmiMethod -ComputerName $system -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$MissingUpdatesReformatted) -Namespace root\ccm\clientsdk
“$system,Targeted Patches :$approvedUpdates,Pending patches:$pendingpatches,Reboot Pending patches :$rebootpending,initiated $pendingpatches patches for install” | Out-File $log -append

Next is to watch the installation process and wait until all updates have been successfully installed. In my experience, installation sometimes did fail, then I need a loop to check update install result and retry failed updates.

$TargetedUpdates= Get-WmiObject -ComputerName $system -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0

$failedUpdates = ($TargetedUpdates |Where-Object {$_.EvaluationState -eq 13} |Measure-Object).count

 

The EvaluationState meaning can be found at https://docs.microsoft.com/en-us/sccm/develop/reference/core/clients/sdk/ccm_softwareupdate-client-wmi-class

 

To go through each computer in the patching group (device collection), I used an array to track each computer’s status. Each computer object in the array has a Status property with “No” value. When the computer finished installing all updates and reboot, the Status will be set to “Yes”. When all member’s status is “Yes” in the array, the script will be end. If any installation failed, it will try to install it again. The script sleeps 30 seconds between each loop. You can change the time or even use dynamic time based on the maximum numbers of updates are still waiting for installation.

Here are the whole codes.

 



$log      = "SAP_PROD_Install_Updates_Then_Reboot.log" # Change this to your log file.
# Create an array of all servers in the patching group (device collection group)
$ServerArray = @()
# SAP PROD PATCHING GROUP
$GroupId="P0500122" # Change this to your SCCM device collection ID 
$ColQuery = "select * from sms_cm_res_coll_"+$GroupId
# Change the computername to your SCCM server and the namespace to match your site code 
$CollectionMembers = gwmi -computername CGYUT249 -namespace root\sms\site_p05 -Query $ColQuery |sort Name
$i=0

foreach($item in $collectionmembers){
    $ServerObj = New-Object -TypeName PSObject
    Add-Member -InputObject $ServerObj -MemberType NoteProperty -Name 'ServerName' -Value $item.ServerName
    Add-Member -InputObject $ServerObj -MemberType NoteProperty -Name 'Status' -Value $item.Status
    $ServerObj.ServerName = $item.Name
    $ServerObj.Status = "No"
    $ServerArray += $ServerObj
}




# Repeat check members in the array until $EndJob is $FALSE ( all members' status is "Yes")
# Check each member if Status is "No" then set global $EndJob = $FALSE (If no member's status is "No" then $EndJob is $TURE, exit looping)
# If there is any failed update, install the update again
# If any update is waiting for install, intall the update.
# If all Updates have been installed and need reboot then reboot it
# If there is no update waiting for install and no pending reboot, set Status to Yes

$EndJob = $true
Do 
{
    # Set $EndJob to $TURE, if all computer status is "Yes", the looping will be end)
    $EndJob = $true
    foreach ($ServerObj in $ServerArray)
    {
        $system = $ServerObj.ServerName
        $date     = Get-Date -Format "dd-MM-yyyy hh:mm:ss"

        if ($ServerObj.Status -eq "No")
        {
            $approvedUpdates= 0
            $pendingpatches= 0
            $rebootpending =0 
            try
            {
                # Get list of all instances of CCM_SoftwareUpdate from root\CCM\ClientSDK for missing updates https://msdn.microsoft.com/en-us/library/jj155450.aspx?f=255&MSPPError=-2147217396
                $TargetedUpdates= Get-WmiObject -ComputerName $ServerObj.ServerName -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0
                $approvedUpdates= ($TargetedUpdates |Measure-Object).count
                $nonestateupdates = ($TargetedUpdates |Where-Object {$_.EvaluationState -eq 0} |Measure-Object).count
                $pendingpatches=($TargetedUpdates |Where-Object {$_.EvaluationState -ne 8} |Measure-Object).count
                $rebootpending=($TargetedUpdates |Where-Object {$_.EvaluationState -eq 8} |Measure-Object).count
                # Need deal with the state 13 - ciJobStateError - usually the udpate installation failed. Then need retry
                $failedUpdates = ($TargetedUpdates |Where-Object {$_.EvaluationState -eq 13} |Measure-Object).count 

                #Debug $TargetedUpdates | Select name, EvaluationState

            }
            catch
            {
                Write-Verbose -Message "$date $ServerObj Can't Get-WmiObject failed" -Verbose
                "$date $ServerObj Can't Get-WmiObject failed"| Out-File $log -append 
            }

            # EvaluationState - meaning  https://docs.microsoft.com/en-us/sccm/develop/reference/core/clients/sdk/ccm_softwareupdate-client-wmi-class 
            #Debug Write-Verbose -Message "$date $system NoneStateUpdates:$nonestateupdates ApprovedUpdates:$approvedUpdates  PendingPathces:$pendingpatches   RebootPending:$rebootpending" -Verbose

            if ($failedUpdates -gt 0) # If there is any failed update, install the update again
            {
                # Install Updates
                $EndJob = $false               
                try
                {
	                $MissingUpdatesReformatted = @($TargetedUpdates | ForEach-Object {if($_.EvaluationState -eq 13){[WMI]$_.__PATH}}) 
	                # The following is the invoke of the CCM_SoftwareUpdatesManager.InstallUpdates with our found updates 
	                $InstallReturn = Invoke-WmiMethod -ComputerName $system -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$MissingUpdatesReformatted) -Namespace root\ccm\clientsdk 
	            }
	            catch
                {
                    Write-Verbose -Message "$system,failed udpates - $faieldUpdates but unable to install them ,please check Further" -Verbose
                    "$system,failed udpates - $faieldUpdates but unable to install them ,please check Further" | Out-File $log -append 
                }
                Finally
                {
                    $failedUpdates = $MissingUpdatesReformatted|select name
                    Write-Verbose -Message "$date $system, Failed Updates:$failedUpdates,  initiated $failedUpdates patches for install."  -Verbose
	                "$date $system, Failed Updates:$failedUpdates,  initiated $failedUpdates patches for install." | Out-File $log -append
                    $failedUpdates.name
                    $failedUpdates.name | Out-File $log -append
                }
            }

            if (($approvedUpdates -gt 0) -and ($nonestateupdates -gt 0)) #If any update is waiting for install, intall the update.
            {
                # Install Updates
                $EndJob = $false
                 try
                 {
	                $MissingUpdatesReformatted = @($TargetedUpdates | ForEach-Object {if($_.ComplianceState -eq 0){[WMI]$_.__PATH}}) 
	                # The following is the invoke of the CCM_SoftwareUpdatesManager.InstallUpdates with our found updates 
	                $InstallReturn = Invoke-WmiMethod -ComputerName $system -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$MissingUpdatesReformatted) -Namespace root\ccm\clientsdk 
                    Write-Verbose -Message "$date $system,Targeted Patches :$approvedUpdates,Pending patches:$pendingpatches,Reboot Pending patches :$rebootpending,initiated $pendingpatches patches for install" -Verbose
	                "$date $system,Targeted Patches :$approvedUpdates,Pending patches:$pendingpatches,Reboot Pending patches :$rebootpending,initiated $pendingpatches patches for install" | Out-File $log -append
	             }
	             catch
                 {
                    Write-Verbose -Message "$system,pending patches - $pendingpatches but unable to install them ,please check Further" -Verbose
                    "$system,pending patches - $pendingpatches but unable to install them ,please check Further" | Out-File $log -append 
                 }
            }
            else
            {
                if (($pendingpatches -eq 0) -and ($rebootpending -gt 0) -and ($approvedUpdates -eq $rebootpending))
                {
                    # If all Updates have been installed and need reboot then reboot it
                    $EndJob = $false
                    try 
                    {
                        Write-Verbose -Message "$date ApprovedUpdates:$approvedUpdates  PendingPathces:$pendingpatches   RebootPending:$rebootpending  Rebooting $system ......" -Verbose
                        "$date ApprovedUpdates:$approvedUpdates  PendingPathces:$pendingpatches   RebootPending:$rebootpending  Rebooting $system ......"  | Out-File $log -append 
                        Restart-Computer $ServerObj.ServerName -Force
			            $ServerObj.Status = "Yes"
                    }
                    catch 
                    {
                        Write-Verbose -Message "$date $system, all deployed updates have been installed but failed to reboot it,please check Further" -Verbose
                        "$date $system, all deployed updates have been installed but failed to reboot it,please check Further" | Out-File $log -append 
                    }
                }
                else
                {
                    # If there is no update waiting for install and no pending reboot, set Status to Yes
                    if (($pendingpatches -eq 0) -and ($rebootpending -eq 0))
                    {
                        # Server already patched and reboot
                        $ServerObj.Status = "Yes"
                    }
                    else
                    {   # else - still need wait for updates installation finish - do nothing
                        Write-Verbose -Message " $date $system, ApprovedUpdates:$approvedUpdates  PendingPathces:$pendingpatches   RebootPending:$rebootpending Waiting for status change " -Verbose
                        $EndJob=$false 
                    }
                }
            }
        }

    }
    if ( -not $EndJob) 
    {
        # Sleep some time between each loop. 
        Write-Verbose -Message "Sleep 30 seconds" -Verbose
        Start-Sleep -Seconds 30
    }
}
until ($EndJob -eq $true)



This script only reboot the computer right after all updates have been installed. In my real world, I have another group of servers need reboot in order, I changed the system restart part in the script to just set the status to “Yes” which will tag the device has been done. Then I use the script as the previous step in the SC Orchestrator runbook and then reboot the server in order controlled by the runbook.

Leave a Reply

Your email address will not be published. Required fields are marked *