Install pending Windows updates deployed through SCCM (Configuration Manager) on a client

I created this script which includes various functions because sometimes there is a pending update left on the system after the update deployment has finished.

Imagine a server that performs updates within a maintenance window and this server performs a reboot after installation. Sometimes one of the updates is left in a pending state and before the system identifies this it could be that the maintenance window is no longer valid. This will leave you with a server that requires manual intervention or it needs to wait until the next maintenance window.

The script below determines if there are pending updates left on the system and identifies on the client if there is an active maintenance window that has at least 1 hour left of available time. In case of an active maintenance window the pending updates are being requested to be installed using the CCM_SoftwareUpdatesManager WMI class. Inside the script there are random sleeps to prevent clients (imagine hundreds of servers being patched at the same time/day) from performing actions at the same time. All information is written to the EventLog of the local server/client so that after every reboot you can see what actions have been performed in the background.

This script has been scheduled on the environment using as a task that is launched 5-10 minutes after system startup. The task is created on all servers that receive updates through SCCM compliance auto remediation rules.

Example:
In the below example there is only a single Maintenance Window displayed but if there are more assigned these will be shown.

Invoke-SCCMPendingUpdateCheck basic example output

Code:

<#
  .SYNOPSIS
  Verifies if pending updates need to be installed.
  .DESCRIPTION
  Performs checks to determine if pending updates are available and if the client has an active maintenance window with at least 1 hour of available time left.
#>
$ErrorActionPreference = "Continue"

Function CheckAndOrCreateEventLogSource($EventLogSourceName) {
  If ([System.Diagnostics.EventLog]::SourceExists($EventLogSourceName) -eq $false) {
    Write-Host "Creating Eventlog Source: $($EventLogSourceName)"
    [System.Diagnostics.EventLog]::CreateEventSource($($EventLogSourceName), "Application")
  }
}

Function CheckWMI($ApplicationEventLogSource) {
  Try {
    Get-WmiObject -namespace root\cimv2 -Class Win32_BIOS
  }
  Catch {
    Write-Warning "WMI Check Failed!"
    Write-Host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
    Write-Host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
    Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message "Error occured during WMI verification:`n`nException Type: $($_.Exception.GetType().FullName)`nException Message: $($_.Exception.Message)" -category 1 -rawdata 10, 20
    $error.clear()
    Continue
  }
}

Function InstallSCCMPendingUpdates($ApplicationEventLogSource, $SUWindowStartTime, $SUWindowEndTime) {
  Try {
    $DeployedUpdates = Get-WmiObject -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0
    $ApprovedUpdateCount = ($DeployedUpdates | Measure-Object).count
    $PendingUpdates = ($DeployedUpdates | Where-Object { $DeployedUpdates.EvaluationState -ne 8 } | Measure-Object).count
    $PendingReboot = ($DeployedUpdates | Where-Object { $DeployedUpdates.EvaluationState -eq 8 } | Measure-Object).count
    ForEach ($FoundUpdate in $DeployedUpdates) {
      $PendingUpdatesDescriptions += $FoundUpdate.Name + "`n"
    }
    $SystemRebootPending = "False"
    If ($PendingReboot -gt 0) {
      $SystemRebootPending = "True"
    }
    If ($PendingUpdates -gt 0) {
      $PendingUpdatesReformatted = @($DeployedUpdates | ForEach-Object { If ($_.ComplianceState -eq 0) { [WMI]$_.__PATH } })
      $InstallReturnValue = Invoke-WmiMethod -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (, $PendingUpdatesReformatted) -Namespace root\ccm\clientsdk
      If ($InstallReturnValue.ReturnValue -eq 0) {
        $EventLogMsg = "Detected Software Update Maintenance Window: Starts at $($SUWindowStartTime) and closes at $($SUWindowEndTime)`nNumber of approved updates deployed: $($ApprovedUpdateCount)`nNumber of updates pending: $($PendingUpdates)`nServer has a reboot pending: $($SystemRebootPending)`nInitiated installation of the following updates:`n$PendingUpdatesDescriptions"
        Write-Host $EventLogMsg
        Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Information -message $EventLogMsg -category 1 -rawdata 10, 20
      }
      else {
        $EventLogMsg = "ERROR RETURNED DURING KICKOFF OF PENDING UPDATES!`n`n$($InstallReturnValue)`n`nDetected Software Update Maintenance Window: Starts at $($SUWindowStartTime) and closes at $($SUWindowEndTime)`nNumber of approved updates deployed: $($ApprovedUpdateCount)`nNumber of updates pending: $($PendingUpdates)`nServer has a reboot pending: $($SystemRebootPending)`nInitiated installation of the following updates:`n$PendingUpdatesDescriptions"
        Write-Host $EventLogMsg
        Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message $EventLogMsg -category 1 -rawdata 10, 20
      }
    }
    else {
      If ($PendingReboot -gt 0) {
        $EventLogMsg = "Detected Software Update Maintenance Window: Starts at $($SUWindowStartTime) and closes at $($SUWindowEndTime)`nNumber of approved updates deployed: $($ApprovedUpdateCount)`nNumber of updates pending: $($PendingUpdates)`nServer has a reboot pending: $($SystemRebootPending)`n`nServer is compliant but needs a reboot!"
        Write-Host $EventLogMsg
        Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Warning -message $EventLogMsg -category 1 -rawdata 10, 20
      }
      else {
        $EventLogMsg = "Detected Software Update Maintenance Window: Starts at $($SUWindowStartTime) and closes at $($SUWindowEndTime)`nNumber of approved updates deployed: $($ApprovedUpdateCount)`nNumber of updates pending: $($PendingUpdates)`nServer has a reboot pending: $($SystemRebootPending)`n`nServer is compliant!"
        Write-Host $EventLogMsg
        Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Information -message $EventLogMsg -category 1 -rawdata 10, 20
      }
    }

  }
  Catch {
    Write-Warning "Error occured during SCCM Pending Updates Installation!"
    Write-Host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
    Write-Host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
    Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message "Error occured during SCCM Pending Updates Installation:`n`nException Type: $($_.Exception.GetType().FullName)`nException Message: $($_.Exception.Message)" -category 1 -rawdata 10, 20
    $error.clear()
    Continue
  }
}

# (Type 4 = Software Updates) Service Window Types -> https://msdn.microsoft.com/en-us/library/jj155419.aspx?f=255&amp;MSPPError=-2147217396
Function GetSoftwareUpdateServiceWindows($ApplicationEventLogSource) {
  Try {
    $AllSoftwareUpdateWindows = Get-WmiObject -Namespace 'ROOT\ccm\ClientSDK' -Class CCM_ServiceWindow | Where-Object { $_.Type -eq '4' } | select duration, id, type, @{Name = "StartTime"; Expression = { $_.ConvertToDateTime($_.StartTime).ToUniversalTime() } }, @{Name = "EndTime"; Expression = { $_.ConvertToDateTime($_.EndTime).ToUniversalTime() } }
    ForEach ($MaintenanceWindow in $AllSoftwareUpdateWindows) {
      $MaintenanceWindow
    }
  }
  Catch {
    Write-Warning "Error occured during SCCM Service Window Check!"
    Write-Host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
    Write-Host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
    Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message "Error occured during Service Window Check:`n`nException Type: $($_.Exception.GetType().FullName)`nException Message: $($_.Exception.Message)" -category 1 -rawdata 10, 20
    $error.clear()
    Continue
  }
}

Function PerformSCCMClientAction($ActionType) {
  switch ($ActionType) {
    "MachinePolicy" { $SCCMClientAction = "{00000000-0000-0000-0000-000000000021}" }
    "DiscoveryData" { $SCCMClientAction = "{00000000-0000-0000-0000-000000000003}" }
    "ComplianceEvaluation" { $SCCMClientAction = "{00000000-0000-0000-0000-000000000071}" }
    "SoftwareUpdateScan" { $SCCMClientAction = "SoftwareUpdates" }
    "UpdateDeployment" { $SCCMClientAction = "{00000000-0000-0000-0000-000000000108}" }
    default { $SCCMClientAction = "{00000000-0000-0000-0000-000000000021}" }
  }
  Try {
    If ($SCCMClientAction -eq "SoftwareUpdates") {
      $FoundSCCMClientActions = (New-Object -ComObject CPApplet.cpAppletMgr).GetClientActions() | Where-Object { $_.Name -like "*Updates*" } | Sort-Object Name
      ForEach ($ClientAction in $FoundSCCMClientActions) {
        Write-Host "Performing $($ClientAction.Name) SCCM Client Action"
        $ClientAction.PerformAction()
      }
    }
    Else {
      ([wmiclass]'ROOT\ccm:SMS_Client').TriggerSchedule($SCCMClientAction)
    }
  }
  Catch {
    Write-Warning "Error occured during SCCM Client action!"
    Write-Host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
    Write-Host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
    Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message "Error occured during SCCM client action:`n`nException Type: $($_.Exception.GetType().FullName)`nException Message: $($_.Exception.Message)" -category 1 -rawdata 10, 20
    $error.clear()
    Continue
  }
}

Function GetLastSoftwareUpdateScan() {
  Try {
    Get-WmiObject -query "SELECT * FROM CCM_UpdateStatus" -namespace "root\ccm\SoftwareUpdates\UpdatesStore" | % { if ($_.ScanTime -gt $ScanTime) { $ScanTime = $_.ScanTime } }; $LastScan = ([System.Management.ManagementDateTimeConverter]::ToDateTime($ScanTime))
    $LastScan
  }
  Catch {
    Write-Warning "Error occured during Last Software Update Scan Check!"
    Write-Host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
    Write-Host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red
    Write-Eventlog -logname Application -source $ApplicationEventLogSource -eventID 3030 -entrytype Error -message "Error occured during Last Software Update Scan Check:`n`nException Type: $($_.Exception.GetType().FullName)`nException Message: $($_.Exception.Message)" -category 1 -rawdata 10, 20
    $error.clear()
    Continue
  }
}

# Script Start
$IsInSUServiceWindow = $False
$CurrentSUServiceWindowStartTime = ""
$CurrentSUServiceWindowEndTime = ""
Write-Host "Script execution commencing...."
$NameOfEventLogSource = "WinOps Patch Management"
CheckAndOrCreateEventLogSource $NameOfEventLogSource
$WMICheck = CheckWMI $NameOfEventLogSource
If ($WMICheck) {
  Write-Host "WMI Check Successful."
  $RandomSeconds = Get-Random -Maximum 60
  Write-Host "Performing SCCM Client actions in $($RandomSeconds) seconds..."
  Start-Sleep -s $RandomSeconds
  $PerformSCCMClientAction = PerformSCCMClientAction "MachinePolicy"
  Write-Host "Requesting SCCM Client Maintenance Windows..."
  $AllSoftwareUpdateServiceWindows = GetSoftwareUpdateServiceWindows $NameOfEventLogSource
  ForEach ($SoftwareUpdateServiceWindow in $AllSoftwareUpdateServiceWindows) {
    $CurrentDateTime = Get-Date -Format 'dd/MM/yyyy HH:mm:ss'
    #$TempStartTime = Get-Date $SoftwareUpdateServiceWindow.StartTime -Format 'dd/MM/yyyy HH:mm:ss'
    #$TempEndTime = Get-Date $SoftwareUpdateServiceWindow.EndTime -Format 'dd/MM/yyyy HH:mm:ss'
    $CurrentSUServiceWindowStartTime = Get-Date $SoftwareUpdateServiceWindow.StartTime -Format 'dd/MM/yyyy HH:mm:ss'
    $CurrentSUServiceWindowEndTime = Get-Date $SoftwareUpdateServiceWindow.EndTime -Format 'dd/MM/yyyy HH:mm:ss'
    Write-Host "Software Update Window found which starts at $($CurrentSUServiceWindowStartTime) and closes at $($CurrentSUServiceWindowEndTime)"
    If (($CurrentDateTime -ge $CurrentSUServiceWindowStartTime) -and ($CurrentDateTime -le $CurrentSUServiceWindowEndTime)) {
      $IsInSUServiceWindow = $True
      Break;
    }
#    If ($IsInSUServiceWindow -eq $True) {
#      Break;
#    }
  }
  If ($IsInSUServiceWindow -eq $True) {
    Write-Host "Server is currently in a Software Update Maintenance Window."
    $LastSUScan = GetLastSoftwareUpdateScan
    $LastSUScanNiceDT = Get-Date $LastSUScan -Format 'dd/MM/yyyy HH:mm:ss'
    If (((get-date) - $LastSUScan).Hours -ge 1) {
      $RandomSeconds = Get-Random -Maximum 240
      Write-Host "Last Software Update Scan was more than 1 hour ago! ($LastSUScanNiceDT)"
      Start-Sleep -s $RandomSeconds
      PerformSCCMClientAction "SoftwareUpdateScan"
      $RandomSeconds = Get-Random -Maximum 600
      Write-Host "Waiting $RandomSeconds seconds to give Server -> Client enough time and prevent overload of SCCM server...."
      Start-Sleep -s $RandomSeconds
    }
    Else {
      Write-Host "Last Software Update Scan was performed on $LastSUScanNiceDT."
    }
    Write-Host "Checking for pending/needed updates and request installation if found."
    $PendingUpdateResult = InstallSCCMPendingUpdates $NameOfEventLogSource $CurrentSUServiceWindowStartTime $CurrentSUServiceWindowEndTime
  }
  else {
    Write-Host "Server is not currently in a Software Update Maintenance Window."
    $DeployedUpdates = Get-WmiObject -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0
    ForEach ($FoundUpdate in $DeployedUpdates) {
      $PendingUpdatesDescriptions += $FoundUpdate.Name + "`n"
    }
    $EventLogMsg = "Next Software Update Maintenance Window: Starts at $($CurrentSUServiceWindowStartTime) and closes at $($CurrentSUServiceWindowEndTime)`nThe following updates are available for installation during next maintenance window:`n$PendingUpdatesDescriptions"
    Write-Eventlog -logname Application -source $NameOfEventLogSource -eventID 3030 -entrytype Information -message $EventLogMsg -category 1 -rawdata 10, 20
  }
}
else {
  Write-Host "WMI Check failed, script aborted!" -ForegroundColor Red
}
Posted in Maintenance Window, PowerShell, System Center Configuration Manager, Windows Updates.

Leave a Reply

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