Windows patching using Ansible

     

    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