Powershell – SCCM Software Center server update

In our organization, we have SCCM for manual update deployment. I have a lot of servers I need to patch, on some regular basis.

Most of my servers reside on VmWare infrastructure, and before every update I need to create a snapshot of each and every one.
With the help of Powershell scripts and VmWare PowerCli I managed to create snapshot for my list of servers, patch servers with Function Install-SCCMPatchesAvailable, restart if necessary, and then check services with Automatic startup but not Started status. I separated Creation and Deletion of snaphots, patching and service checking, because It was easier for now. Script will be upgraded, maybe transfered to MS System Center Orchestrator or some other automatization software, but for now her it is.

# PARAMETERS
$Enviroment = "TEST"
$SnapCD = "D" # "C" "D" (Create or delete snapshots)
$patching = "NO" # "NO" "YES"
$serviceChecking = "YES" #"YES" "NO"
$pViServer = "vcenter.company.com"
$email = "someone@company.com"

Set-Location -Path "$PSScriptRoot"
#GetCurrent Directory of a script - $PSScriptRoot
switch ($Enviroment) {
  "TEST" {
        $ppath = "$PSScriptRoot\VM_Servers_TEST.txt"
  }
  "PROD" {
		$ppath = "$PSScriptRoot\VM_Servers_Prod.txt"
  }
}
$cred = Get-Credential

$ServerList = Get-Content -path $ppath
$Date = Get-Date -Format "yyyyMMdd"
$Description = "Powershell created Snapshot"
$excludeServices = @("BITS", "CDPSvc", "DoSvc", "gupdate", "MapsBroker", "RemoteRegistry", "RtkAudioService", "sppsvc", "WbioSrvc", "UALSVC") #some services don't need to be checked
$from = "Patching@company.com"
$mailsrv = "mail.company.com"
$mailencoding = ([System.Text.Encoding]::UTF8)

$HTML = @" 
<style> 
BODY{background-color :#FFFFF} 
TABLE{Border-width:thin;border-style: solid;border-color:Black;border-collapse: collapse;} 
TH{border-width: 1px;padding: 1px;border-style: solid;border-color: black;background-color: ThreeDShadow} 
TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color: Transparent} 
H2{color: #457dcf;font-family: Arial, Helvetica, sans-serif;font-size: medium; margin-left: 40px; 
</style> 
"@ 
#This is Create Snapshot part
IF ($SnapCD -eq "C") {
  #CONNECT TO Vmware
  try {
    Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
    Connect-ViServer -server $pViServer -credential $cred -Verbose:$false | Out-Null
    Write-Output "Succesfully connected to $pViServer"
  }
  catch {
    Write-Output "error connecting to Vcenter  - Force Disconnect"
    Disconnect-VIServer -server $pViServer -Force -Confirm:$false
  }

  $Failed = @()
   
  foreach ($server in $ServerList) {
    $SnapshotName = "$server-$Date-$shortcode"
    Try {
      Write-Output "Snapshot create for server $server"
      #CREATE VM Snapshot#                       
      New-Snapshot -VM $server -Name $SnapshotName -Description $Description -memory:$false -Quiesce:$false -ErrorAction Stop | Out-Null
      Write-Output "Snapshot created - $server"     
    }
    Catch {
      #Failed Snapshots
      $ErrorMessage = $_.Exception.Message
      Write-Output "Sumting wong with creating snapshot $server; $ErrorMessage `r`n"
      $failed += "$server ;$ErrorMessage"
      
    }         
  }
                    
  if ($Failed) {
    $MailBody = ($Failed | Out-String)
    send-mailmessage -to $email -from $from -Subject "Failed Snapshot CREATE" -body $MailBody  -smtpserver $mailsrv  -Encoding $mailencoding
  }    
        
  #DISCONNECT FROM VmWARE
  Disconnect-VIServer -server $pViServer -Force -Confirm:$false   
  Write-Output "Disconnected from $pViServer"
}
#Start patching segment
IF ($patching -eq "YES") {
  #CONNECT TO Vmware
  try {
    Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
    Connect-ViServer -server $pViServer -credential $cred -Verbose:$false | Out-Null
    Write-Output "Succesfully connected to $pViServer"
  }
  catch {
    Write-Output "GreŇ°ka kod spajanja na Vcenter  - Force Disconnect"
    Disconnect-VIServer -server $pViServer -Force -Confirm:$false
  }
  $Failed = @()
  foreach ($server in $ServerList) {
    $VM = Get-Vm $server | Get-View
    $toolsstatus = $VM.guest.ToolsStatus #if toolsOk then restart server - else just patch  !!!Automatic tool install automatically restarts VM!!! this is the step
    #Write-Output "$toolsstatus - $server"
    #Write-Output "Patching started $server"                
    #IF server available - connect and run patching
    if (Test-Connection -ComputerName $server ) {
      try {
        #Na serveru pusti patchiranje
        Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock {
          Function Install-SCCMPatchesAvailable {
			  
            [CmdletBinding()]
            param(
              [Parameter(
                Position = 0,
                Mandatory = $false,
                ValueFromPipelineByPropertyName = $true,
                HelpMessage = "Do not reboot server after patches install")]
              [ValidateNotNullOrEmpty()]
              [switch]
              $DoNotReboot
            )

            begin {
              Write-Verbose "Install-SCCMPatchesAvailable: Started"
            }

            process {
              try {
                ([wmiclass]'ROOT\ccm\ClientSDK:CCM_SoftwareUpdatesManager').InstallUpdates([System.Management.ManagementObject[]] `
                  (Get-WmiObject -Query 'SELECT * FROM CCM_SoftwareUpdate' -namespace 'ROOT\ccm\ClientSDK'))

                while (-not((Get-WmiObject -Namespace 'ROOT\ccm\ClientSDK' -Class 'CCM_ClientUtilities' -list).DetermineIfRebootPending().RebootPending)) {
                  $Time = (get-date).ToShortTimeString()
                  Write-Output "Still Patching @ $Time"
                  Start-Sleep -s 20
                }
                if (-not $PSBoundParameters.ContainsKey('DoNotReboot')) {
                  if ((Get-WmiObject -Namespace 'ROOT\ccm\ClientSDK' -Class 'CCM_ClientUtilities' -list).DetermineIfRebootPending().RebootPending) {
                    (Get-WmiObject -Namespace 'ROOT\ccm\ClientSDK' -Class 'CCM_ClientUtilities' -list).RestartComputer()
                  }
                }
              }
              catch {
                Write-Error -Message "Something went wrong with Install-SCCMPatchesAvailable.`n`nError.Exception.Message : $($_.Exception.Message)`nError.Exception.FullName: $($_.Exception.GetType().FullName)"
              }
            }

            end {
              Write-Verbose "Install-SCCMPatchesAvailable: Completed"
            }
          } #End 
                               
          #IF NO PATCHES
          IF (-NOT(Get-WmiObject -Query 'SELECT * FROM CCM_SoftwareUpdate' -namespace 'ROOT\ccm\ClientSDK')) {
            Write-Output "No patches for server $using:server `r`n"
          }
          else {
        
            IF ($using:toolsstatus.value -eq "toolsOk") {
              Write-Output "Install patches on $using:server and restart `r`n"
              Install-SCCMPatchesAvailable
            }
            else {
              Write-Output "Install patches on $using:server and DONT restart `r`n"
              Install-SCCMPatchesAvailable DoNotReboot
            }
            
          }#else

        } -ErrorAction Stop #scriptblock 
      }
      catch {
        $ErrorMessage = $_.Exception.Message
        Write-Output "Sumting wong with patching $server; $ErrorMessage `r`n"
        $failed += "$server ;$ErrorMessage"
      }

    }#if
  }#foreach
  if ($Failed) {
    $MailBody = ($Failed | Out-String)
    send-mailmessage -to $email -from $from -Subject "Failed Patch Installing" -body $MailBody  -smtpserver $mailsrv  -Encoding $mailencoding
  } 
}#IF
#Service checking segment
IF ($serviceChecking -eq "YES") {

  $Failed = @()
  foreach ($server in $ServerList) {
    #IF server connection OK connect and run command
    if (Test-Connection -ComputerName $server -Quiet) {
      try {
        Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock {

          If ($patching -eq "YES" -and $serviceChecking -eq "YES") {
            #if service checking is after patching, then wait 3 minutes to check services
            Start-Sleep -s 180 #wait 3 minutes
          }
          $services = Get-service | Where-Object { $_.Status -ne "Running" -and $_.StartType -eq "Automatic" -and $_.Name -notin $using:excludeServices } | Select-Object Name, Status, StartType, DisplayName
          IF ($services) {
            $MailBody = $services | ConvertTo-Html -Property "Name", "Status", "StartType", DisplayName -head $HTML -body  "<H2> Services Not running $env:computername</H2>" | Out-String
            #$enc = ([System.Text.Encoding]::UTF8)
            send-mailmessage -to $using:email -from $using:from -Subject "$using:server - Services not started" -body $MailBody  -smtpserver $using:mailsrv -BodyAsHtml  
          }#IF
          else {
            $MailBody = "<H2> Services OK - Patching Successfull $env:computername</H2>"
            send-mailmessage -to $using:email -from $using:from -Subject "$using:server - Services not started" -body $MailBody  -smtpserver $using:mailsrv -BodyAsHtml
          }#else
        } -ErrorAction Stop #Scriptblock
      }
      catch {
        $ErrorMessage = $_.Exception.Message
        Write-Output "Sumting wong with checking services $server; $ErrorMessage `r`n"
        $failed += "$server ;$ErrorMessage"
        
      }
    }#test
  }#foreach
  if ($Failed) {
    $MailBody = ($Failed | Out-String)
    send-mailmessage -to $email -from $from -Subject "Failed Services Check" -body $MailBody  -smtpserver $mailsrv  -Encoding $mailencoding
  } 
} #IF
#Snapshot delete segment
#Delete only snapshot that has specific description (from parameters)
if ($SnapCD -eq "D") {
  try {
    #CONNECT TO Vmware
    Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
    Connect-ViServer -server $pViServer -credential $cred | Out-Null
    Write-Output "Succesfully connected to $pViServer"
  }
  catch {
    Write-Output "error connecting to Vcenter  - Force Disconnect"
    Disconnect-VIServer -server $pViServer -Force -Confirm:$false
  }

  $Failed = @()
  $failed
  foreach ($server in $ServerList) {
    Try {
      #FINALY DELETE VmWareSnapshot
      Write-Output "Snapshot deletetion for server $server"
      Get-Snapshot -VM $server -ErrorAction Stop | where-object { $_.Description -match $Description } | Remove-Snapshot -Confirm:$false -ErrorAction Stop | Out-Null 
      Write-Output "Snapshot deleted - $server"
    }
    Catch {
      #Failed Snapshots
      $failed += $server
    }
  }
  if ($Failed) {
    #IF failed snapshots send mail
    $MailBody = ($Failed | Out-String)
    send-mailmessage -to $email -from $from -Subject "Failed Snapshot DELETE" -body $MailBody  -smtpserver $mailsrv  -Encoding $mailencoding
  }   
            
  #DISCONNECT FROM VmWARE
  Disconnect-VIServer -server $pViServer -Force -Confirm:$false
}

So this is semi-automatic patch deployment, but for now is quicker than all manual work. Hope this helps somebody.

Good Luck