ClusterNodePatching.yml
---
- name: Cluster node patching
hosts: all
gather_facts: false
tasks:
- name: Install patches
script: ClusterNodePatching.ps1
register: output
failed_when: '"Unhealthy" in output.stdout'
- name: Check pending reboot
script: Test-PendingReboot.ps1
register: pendingreboot
- name: Reboot node
win_reboot:
reboot_timeout: 1800
post_reboot_delay: 300
when: '"True" in pendingreboot.stdout'
- name: Post reboot
script: PostReboot.ps1
ClusterNodePatching.ps1
######
$ErrorActionPreference = "Stop";
#Function to get UTC time
function Get-Utctime()
{
$utcTime = (Get-Date).ToUniversalTime()
$timeFormat = "yyyy-MM-dd HH:mm:ss"
$utcTime = $utcTime.ToString($timeFormat)
return $utcTime
}
#Check if cluster
$Error.Clear()
try { Get-Service -Name ClusSvc -ErrorAction Stop | Out-Null } catch { $_.Exception.Message; Write-Host ($env:COMPUTERNAME + " Not a cluster node."); }
if ($Error) { $isCluster = $false } else { $isCluster = $true }
#Check if Hyper-V
$Error.Clear()
try { $isHyperV = (Get-WindowsFeature -Name Hyper-V -ErrorAction Stop).Installed } catch { $_.Exception.Message }
if(!$isHyperV)
{
Write-Host "Hyper-V role not installed."
}
#If Hyper-V Cluster
if ($isCluster -and $isHyperV)
{
Write-Host ($env:COMPUTERNAME + " is a Hyper-V Cluster node")
$vd = try { Get-VirtualDisk -ErrorAction Stop } catch { $_.Exception.Message }
if ($vd.Count -eq ($vd | Where-Object { $_.HealthStatus -eq "Healthy" }).Count)
{
Write-Host "VirtualDisk Healthy"
#Pause node
$Error.Clear()
try { Suspend-ClusterNode -Drain -Wait -ErrorAction Stop | Out-Null } catch { $_.Exception.Message }
if ($Error)
{
Write-Host "Suspend ClusterNode failed. Cannot continue patching..Unhealthy"
try { Resume-ClusterNode -ErrorAction Stop } catch { $_.Exception.Message }
exit
}
else
{
"Node suspended successfully."
}
}
else
{
"VirtualDisk Unhealthy. Cannot continue patching."
exit
}
}
$SCCMUpdatesStore = New-Object -ComObject Microsoft.CCM.UpdatesStore
$SCCMUpdatesStore.RefreshServerComplianceState()
#Start SCCM patching
$EndJob = $true
$loopcount = 1
Do {
# Set $EndJob to $TURE
$EndJob = $true
$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 -Namespace root\CCM\ClientSDK -Class CCM_SoftwareUpdate -Filter ComplianceState=0 #| Where-Object {$_.ArticleID -eq "5005112"}
if($loopcount -eq 1)
{
foreach($u in $TargetedUpdates)
{
Write-Host $u.Name.ToString()
}
} # print only once
$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
if($approvedUpdates -eq 0)
{
$date = Get-Utctime
Write-Host "$date There are no updates to install"
exit
}
}
catch
{
$date = Get-Utctime
Write-Host "$date Can't Get-WmiObject failed"
}
# EvaluationState - Check at https://docs.microsoft.com/en-us/sccm/develop/reference/core/clients/sdk/ccm_softwareupdate-client-wmi-class
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 -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (, $MissingUpdatesReformatted) -Namespace root\ccm\clientsdk
}
catch
{
$date = Get-Utctime
Write-Host "$date Failed udpates - $faieldUpdates but unable to install them, please check Further"
}
Finally
{
$failedUpdates = $MissingUpdatesReformatted | Select-Object Name
Write-Host "$date Failed Updates:$failedUpdates, initiated $failedUpdates patches for install."
$failedUpdates.Name
}
}
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 -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (, $MissingUpdatesReformatted) -Namespace root\ccm\clientsdk
$date = Get-Utctime
Write-Host "$date,Targeted Patches :$approvedUpdates,Pending patches:$pendingpatches,Reboot Pending patches :$rebootpending,initiated $pendingpatches patches for install"
}
catch
{
$date = Get-Utctime
Write-Host "$date Pending patches - $pendingpatches but unable to install them ,please check Further"
}
}
else
{
if (($pendingpatches -eq 0) -and ($rebootpending -gt 0) -and ($approvedUpdates -eq $rebootpending))
{
# If all Updates have been installed and need reboot
$date = Get-Utctime
Write-Host "$date ApprovedUpdates:$approvedUpdates PendingPathces:$pendingpatches RebootPending:$rebootpending"
$EndJob = $true
}
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
$EndJob = $true
}
else
{
# else - still need wait for updates installation finish - do nothing
Write-Host "$date, ApprovedUpdates:$approvedUpdates PendingPathces:$pendingpatches RebootPending:$rebootpending Waiting for status change "
$EndJob = $false
}
}
}
$loopcount++
if($loopcount -gt 120)
{
$EndJob = $true
}
if ( -not $EndJob) {
# Sleep some time between each loop.
Start-Sleep -Seconds 60
}
}
until ($EndJob -eq $true)
PostReboot.ps1
## Post reboot
#Check if cluster
$Error.Clear()
try { Get-Service -Name ClusSvc -ErrorAction Stop | Out-Null } catch { $_.Exception.Message; Write-Host ($env:COMPUTERNAME + "Not a cluster node."); }
if ($Error) { $isCluster = $false } else { $isCluster = $true }
#Check if Hyper-V
$Error.Clear()
try { $isHyperV = (Get-WindowsFeature -Name Hyper-V -ErrorAction Stop).Installed } catch { $_.Exception.Message }
#If Hyper-V Cluster
if ($isCluster -and $isHyperV)
{
try { Resume-ClusterNode -ErrorAction Stop } catch { $_.Exception.Message }
$pd = Get-PhysicalDisk
$vd = Get-VirtualDisk
if ($vd.Count -eq ($vd | Where-Object { $_.HealthStatus -eq "Healthy" }).Count)
{
Write-Host "VirtualDisk Healthy"
}
else
{
Write-Host "VirtualDisk Unhealthy"
}
if ($pd.Count -eq ($pd | Where-Object { $_.HealthStatus -eq "Healthy" }).Count)
{
Write-Host "PhysicalDisk Healthy"
}
else
{
Write-Host "PhysicalDisk Unhealthy"
}
}
Test-PendingReboot.ps1
function Test-PendingReboot
{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try
{
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if (($null -ne $status) -and $status.RebootPending) {
return $true
}
}
catch { }
return $false
}
Test-PendingReboot