Public/Get-ServerUpdateStatus.ps1
|
function Get-PendingRebootFromCim { <# .SYNOPSIS Internal helper detecting a pending restart over CIM. .DESCRIPTION Used when PowerShell remoting is unavailable, so that a pending restart is still reported. Returns $null when the registry could not be read. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ComputerName ) $hklm = [uint32]2147483650 $keys = @( @{ Path = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing'; Name = 'RebootPending' } @{ Path = 'SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update'; Name = 'RebootRequired' } ) $read = $false foreach ($key in $keys) { try { $subKeys = Invoke-CimMethod -ComputerName $ComputerName -Namespace 'root\cimv2' -ClassName 'StdRegProv' -MethodName 'EnumKey' -Arguments @{ hDefKey = $hklm sSubKeyName = $key.Path } -ErrorAction Stop $read = $true if (@($subKeys.sNames) -contains $key.Name) { return $true } } catch { Write-Verbose "Failed to read '$($key.Path)' on '$ComputerName': $($_.Exception.Message)" } } if ($read) { return $false } $null } function Get-WindowsUpdateStatus { <# .SYNOPSIS Internal helper returning the update status of a single computer. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ComputerName ) $result = [PSCustomObject]@{ LastInstalledUpdate = $null LastInstalledOn = $null AvailableUpdateCount = $null Installing = $null RebootPending = $null Status = 'unavailable' } try { $hotfix = Get-CimInstance -ComputerName $ComputerName -ClassName Win32_QuickFixEngineering -ErrorAction Stop | Sort-Object InstalledOn -Descending | Select-Object -First 1 } catch { Write-Verbose "Failed to retrieve Windows update status from '$ComputerName': $($_.Exception.Message)" return $result } if ($hotfix) { $result.LastInstalledUpdate = $hotfix.HotFixID $result.LastInstalledOn = $hotfix.InstalledOn $installed = '{0} installed {1:yyyy-MM-dd}' -f $hotfix.HotFixID, $hotfix.InstalledOn } else { $installed = 'no updates installed' } try { $pending = Invoke-Command -ComputerName $ComputerName -ErrorAction Stop -ScriptBlock { $session = New-Object -ComObject 'Microsoft.Update.Session' $rebootPending = $false try { $rebootPending = [bool](New-Object -ComObject 'Microsoft.Update.SystemInfo').RebootRequired } catch { $rebootPending = $false } if (-not $rebootPending) { $rebootPending = (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') -or (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') } # A pending restart can make the search fail, so it is reported even # when the available update count cannot be determined. $found = $null $searchError = $null try { $found = @($session.CreateUpdateSearcher().Search('IsInstalled=0 and IsHidden=0').Updates).Count } catch { $searchError = $_.Exception.Message } # IsBusy covers any installer on the computer; the task covers an # install started by Invoke-ServerUpdate that has not begun yet. $installing = $false try { $installing = [bool]$session.CreateUpdateInstaller().IsBusy } catch { $installing = $false } $task = Get-ScheduledTask -TaskName 'gcpstools-InstallWindowsUpdate' -ErrorAction SilentlyContinue if ($task -and "$($task.State)" -eq 'Running') { $installing = $true } [PSCustomObject]@{ AvailableUpdateCount = $found Installing = $installing RebootPending = $rebootPending SearchError = $searchError } } $info = @($pending)[0] $result.Installing = [bool]$info.Installing $result.RebootPending = [bool]$info.RebootPending if ($null -eq $info.AvailableUpdateCount) { Write-Verbose "Failed to check for available Windows updates on '$ComputerName': $($info.SearchError)" $available = 'available updates unknown' } else { $count = [int]$info.AvailableUpdateCount $result.AvailableUpdateCount = $count $available = switch ($count) { 0 { 'no updates available' } 1 { '1 update available' } default { "$count updates available" } } } if ($result.Installing) { $available = '{0}; installing updates' -f $available } if ($result.RebootPending) { $available = '{0}; restart pending' -f $available } } catch { Write-Verbose "Failed to check for available Windows updates on '$ComputerName': $($_.Exception.Message)" $available = 'available updates unknown' $result.RebootPending = Get-PendingRebootFromCim -ComputerName $ComputerName if ($result.RebootPending) { $available = '{0}; restart pending' -f $available } } $result.Status = '{0}; {1}' -f $installed, $available $result } function Get-WindowsUpdateStatusColor { <# .SYNOPSIS Internal helper mapping an update status string to a console color. #> param( [string]$Status ) if ($Status -match 'unavailable|unknown') { 'Red' } elseif ($Status -match 'installing updates') { 'Cyan' } elseif ($Status -match 'restart pending') { 'Yellow' } elseif ($Status -match 'not running') { 'DarkGray' } elseif ($Status -match 'no updates available') { 'Green' } else { 'Orange' } } function ConvertTo-UpdateStatusObject { <# .SYNOPSIS Internal helper flattening an update status into an output object. #> param( [string]$ComputerName, $HyperVServer, $State, $Status ) [PSCustomObject]@{ ComputerName = $ComputerName HyperVServer = $HyperVServer State = $State LastInstalledUpdate = $Status.LastInstalledUpdate LastInstalledOn = $Status.LastInstalledOn AvailableUpdateCount = $Status.AvailableUpdateCount Installing = $Status.Installing RebootPending = $Status.RebootPending UpdateStatus = $Status.Status } } function Write-ColorLine { <# .SYNOPSIS Internal helper writing a colored console line. .DESCRIPTION ConsoleColor has no orange, so 'Orange' is rendered with a 24-bit ANSI escape sequence and falls back to DarkYellow on hosts without virtual terminal support. #> param( [string]$Text, [string]$Color, [switch]$NoNewline ) if ($Color -eq 'Orange') { if ($Host.UI.SupportsVirtualTerminal) { $esc = [char]27 Write-Host "$esc[38;2;255;140;0m$Text$esc[0m" -NoNewline:$NoNewline return } $Color = 'DarkYellow' } Write-Host $Text -ForegroundColor $Color -NoNewline:$NoNewline } function Get-ServerUpdateStatus { <# .SYNOPSIS Reports the Windows update status of one or more servers, and optionally of the VMs they host. .DESCRIPTION For each computer, Get-ServerUpdateStatus reports three things: the most recently installed update (from Win32_QuickFixEngineering), how many updates are still available (from a Windows Update Agent search for updates that are neither installed nor hidden), and whether an installation is currently running on that computer. Any Windows computer can be queried; Hyper-V is only needed for -IncludeVM. An installation is reported as in progress when the Windows Update Agent installer is busy, or when the scheduled task that Invoke-ServerUpdate registers is still running. Those computers are shown in cyan and their status ends with 'installing updates'. A computer waiting to be restarted ends with 'restart pending' and is shown in yellow. A pending restart can also stop the update search from working, so it is reported even when the number of available updates is unknown, and it is read over CIM when PowerShell remoting is unavailable. RebootPending is null when neither method could determine it. By default the result is written to the console in color: HV01 Windows Update: KB5126043 installed 2026-09-17; no updates available APP01 - Running Windows Update: KB5126043 installed 2026-08-13; 3 updates available TEST01 - Off Windows Update: not running Computer names are white and VM state is colored by state (green running, gray off, yellow otherwise). The update status is green when a computer is current, orange when updates are available, cyan while updates are being installed, yellow while a restart is pending, and red when the status could not be retrieved. Because a single query can take several seconds, progress is reported per computer and each line is printed as soon as that computer has been checked, rather than after the whole server has been processed. Querying a computer requires CIM access (for the installed update) and PowerShell remoting (for the update search). Failures are not treated as errors: the computer is reported as 'unavailable', or as 'available updates unknown' when only the update search failed, and -Verbose explains the underlying reason. .PARAMETER ComputerName One or more computer names to query. Accepts pipeline input, including objects with a Name or ComputerName property. .PARAMETER IncludeVM Also reports each VM hosted by the queried computers. Requires the Hyper-V module locally. Computers that do not run Hyper-V are still reported and show '(VM list unavailable)'; hosts without VMs show '(no VMs)'. VMs that are not running are listed without being queried. .PARAMETER AsObject Returns an object per computer (and per VM with -IncludeVM) instead of writing colored output. Each object has these properties: ComputerName Name of the computer or VM. HyperVServer Host name for VM entries, otherwise null. State VM state for VM entries, otherwise null. LastInstalledUpdate Hotfix ID of the newest installed update. LastInstalledOn Date that update was installed. AvailableUpdateCount Number of updates available, or null if unknown. Installing Whether an installation is currently running. RebootPending Whether the computer is waiting for a restart. UpdateStatus The status text shown in the console output. .INPUTS System.String[]. Computer names can be piped to this command. .OUTPUTS None by default; the status is written to the host. With -AsObject, one PSCustomObject per computer and per VM. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'APP01', 'SQL01' Shows the update status of two servers. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'HV01', 'HV02' -IncludeVM Shows the update status of two Hyper-V hosts followed by their VMs. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'HV01' -IncludeVM -AsObject | Where-Object AvailableUpdateCount -gt 0 Lists only the machines that have updates waiting to be installed. .EXAMPLE Get-Content .\servers.txt | Get-ServerUpdateStatus -AsObject | Export-Csv .\update-status.csv -NoTypeInformation Builds a report from a list of servers. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'APP01' -Verbose Shows why a computer could not be queried. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'APP01', 'SQL01' -AsObject | Where-Object Installing Lists the servers that are installing updates right now. .EXAMPLE Get-ServerUpdateStatus -ComputerName 'APP01', 'SQL01' -AsObject | Where-Object RebootPending Lists the servers that are waiting to be restarted. .LINK Invoke-ServerUpdate #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Name')] [string[]]$ComputerName, [switch]$IncludeVM, [switch]$AsObject ) process { $serverNumber = 0 foreach ($server in $ComputerName) { $serverNumber++ Write-Progress -Id 1 -Activity 'Checking Windows update status' -Status "$server ($serverNumber of $($ComputerName.Count))" -PercentComplete (100 * ($serverNumber - 1) / $ComputerName.Count) $serverStatus = Get-WindowsUpdateStatus -ComputerName $server if ($AsObject) { ConvertTo-UpdateStatusObject -ComputerName $server -Status $serverStatus } else { Write-ColorLine -Text $server -Color White Write-ColorLine -Text (' Windows Update: {0}' -f $serverStatus.Status) -Color (Get-WindowsUpdateStatusColor -Status $serverStatus.Status) } if (-not $IncludeVM) { continue } $vms = @() $vmListFailed = $false if (-not (Get-Command -Name Get-VM -ErrorAction SilentlyContinue)) { Write-Verbose "The Hyper-V module is not available, so VMs on '$server' cannot be enumerated." $vmListFailed = $true } else { try { $vms = @(Get-VM -ComputerName $server -ErrorAction Stop) } catch { Write-Verbose "Failed to retrieve VMs from '$server': $($_.Exception.Message)" $vmListFailed = $true } } if ($vmListFailed) { if (-not $AsObject) { Write-ColorLine -Text ' (VM list unavailable)' -Color Red } continue } if ($vms.Count -eq 0) { if (-not $AsObject) { Write-ColorLine -Text ' (no VMs)' -Color DarkGray } continue } $vmNumber = 0 foreach ($vm in $vms) { $vmNumber++ $isRunning = "$($vm.State)" -eq 'Running' Write-Progress -Id 2 -ParentId 1 -Activity "Checking Windows update status on VMs of $server" -Status "$($vm.Name) ($vmNumber of $($vms.Count))" -PercentComplete (100 * ($vmNumber - 1) / $vms.Count) $vmStatus = if ($isRunning) { Get-WindowsUpdateStatus -ComputerName $vm.Name } else { [PSCustomObject]@{ LastInstalledUpdate = $null LastInstalledOn = $null AvailableUpdateCount = $null Installing = $null RebootPending = $null Status = 'not running' } } if ($AsObject) { ConvertTo-UpdateStatusObject -ComputerName $vm.Name -HyperVServer $server -State "$($vm.State)" -Status $vmStatus continue } $stateColor = if ($isRunning) { 'Green' } elseif ("$($vm.State)" -eq 'Off') { 'DarkGray' } else { 'Yellow' } Write-ColorLine -Text (' {0} - ' -f $vm.Name) -Color White -NoNewline Write-ColorLine -Text "$($vm.State)" -Color $stateColor Write-ColorLine -Text (' Windows Update: {0}' -f $vmStatus.Status) -Color (Get-WindowsUpdateStatusColor -Status $vmStatus.Status) } Write-Progress -Id 2 -ParentId 1 -Activity "Checking Windows update status on VMs of $server" -Completed } Write-Progress -Id 1 -Activity 'Checking Windows update status' -Completed } } |