Recently i needed a quick way to automate patching and rebooting of servers in our data-center. We felt that just using standard WSUS with GPOs was to fuzzy and not specific enough regarding reporting after patching had completed.
I could have set up our event log collection servers to alert any errors post-reboot, or i could have used System Center 2012 R2 Configuration Manager, but just for kicks i opted to use powershell to patch the servers. I have a plan to get better at powershell, so this is a good way to just do the jobs i need done while learning something in the process. My plan is to modify this to use either SMA or DSC.
I found two good powershell modules for the job of patching servers. The first one is PSWindowsUpdate and the other is PowerShellLogging. Furthermore, the HTML reporting parts are originating from here, and i have modified them slightly to suit my needs.
What i ended up with was three scripts; One for deployment, one script for patching that patches and reboots the server and a second script that runs after boot to do the reporting. At some stages the scripts will write to an event log using a source created in the start of the first script and i also used the PowerShellLogging to give me some way to track what happens in the script when it runs.
To set up a server for patching, i just ran the following script on the server i wanted to set up. This can of course be automated better using SCCM if you have a lot of servers to set up.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
#Create directories New-Item -ErrorAction Ignore -ItemType directory -Path C:\Script New-Item -ErrorAction Ignore -ItemType directory -Path C:\Script\Patch #copy files to local server Copy-Item "\\YOURSERVER\Powershell\Modules\PowerShellLogging\" -Destination "C:\Program Files\WindowsPowerShell\Modules\" -Recurse -ErrorAction SilentlyContinue Copy-Item "\\YOURSERVER\Powershell\Modules\PSWindowsUpdate\" -Destination "C:\Program Files\WindowsPowerShell\Modules\" -Recurse -ErrorAction SilentlyContinue Copy-Item "\\YOURSERVER\Powershell\Script\Patch\ScriptFiles\*patching.ps1" -Destination "C:\Script\Patch\" -Force -Verbose #create task on windows 2008 R2 or newer #define variables $scriptName = "C:\Script\Patch\start-patching.ps1" $global:PathToPowershell = (Join-Path $env:windir "system32\WindowsPowerShell\v1.0\powershell.exe") $taskCommand = $global:PathToPowershell + " -File $scriptName" schtasks /delete /tn "PatchServer" /F schtasks /create /tn "PatchServer" /tr $taskCommand /sc MONTHLY /mo THIRD /d WED /st 02:00 /ru system /RL HIGHEST /F #Import certificate 2012 R2 only #Get-ChildItem –Path "\\YOURSERVER\Powershell\Cert\CodesignCert.cer" | Import-Certificate –CertStoreLocation Cert:\LocalMachine\TrustedPublisher\ #Import certificate 2008 R2 or 2012 R2 function Import-PfxCertificate { #.Synopsis # Import a pfx certificate to the specified local certificate store #.Example # Import-PfxCertificate CodeSigning.pfx Cert:\CurrentUser\My # # Imports a certificate from a file to the current user's personal cert store #.Example # Import-PfxCertificate CodeSigning.pfx Cert:\LocalMachine\TrustedPublisher # # Imports a certificate from a file to the machine's trusted publisher store. # Scripts signed by that cert will now be trusted by all users on the machine. [CmdletBinding()] param( # The cert file to import: defaults to all .pfx files in the current directory [Parameter(ValueFromPipelineByPropertyName=$true, Position=0)] [Alias("PSPath")] [String]$PfxCertificatePath = "*.pfx", # The certificate store path [Parameter(ValueFromPipelineByPropertyName=$true, Position=1)] [Alias("Target","Store")] [String]$CertificateStorePath = "Cert:\CurrentUser\My" ) process { $store = Get-Item $CertificateStorePath -EA 0 -EV StoreError | Where { $_ -is [System.Security.Cryptography.X509Certificates.X509Store] } if(!$Store) { $store = Get-Item Cert:\$CertificateStorePath -EA 0| Where { $_ -is [System.Security.Cryptography.X509Certificates.X509Store] } if(!$Store) { throw "Couldn't find X509 Certificate Store: $StoreError" } $CertificateStorePath = "Cert:\$CertificateStorePath" } try { $store.Open("MaxAllowed") } catch { throw "Couldn't open x509 Certificate Store: $_" } foreach($certFile in Get-Item $PfxCertificatePath) { Write-Warning "Attempting to load $($certfile.Name)" # Will automatically prompt for password, if the cert is properly protected $cert = Get-PfxCertificate -LiteralPath $certFile.FullName if(!$cert) { Write-Warning "Failed to load $($certfile.Name)" continue } $store.Add($cert) Get-Item "${CertificateStorePath}\$($cert.Thumbprint)" } $store.Close() } } Import-PfxCertificate "\\YOURSERVER\Powershell\Cert\CodesignCert.cer" Cert:\LocalMachine\TrustedPublisher #If running manual, tell me ExecutionPolicy #Remember to sign your scripts, if you have your servers set to AllSigned or RemoteSigned $execpol=Get-ExecutionPolicy write-host $execpol -foregroundcolor "Red" |
After the first patch script has finished patching the server, it creates a scheduled task that runs the second script at the next boot, and the first script then reboots the server. After the reboot, the second script will start of by cleaning up the scheduled task, from the first script, that started it and just collect which updates were installed, in addition to the system and application logs for the past hour. It will then put that info in some CSV files which will be used by the HTML report. The HTML reporting code will get some additional system info and present it, along with the installed updates and logs, taken from the generated CSVs, as an HTML report and then send a mail to the appropriate recipients.
So just a quick and easy way to patch and send a report after.
The scripts are here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
<# .SYNOPSIS Part 1/2. Patches and reboots a server, then collects event logs and emails them. .DESCRIPTION Patches and reboots a server, then collects event logs and emails them Prerequisites: PowerShellLogging, PSWindowsUpdate .NOTES File Name : start-patching.ps1 Author : Jan Ryen Prerequisite : PowerShell V3 and above. Copyright 2017 - Jan Ryen (www.janryen.com) #> ################################## #make temp mkdir c:\temp -ErrorAction SilentlyContinue ################################## #Enable logging Set-ExecutionPolicy Unrestricted -Force ipmo PowerShellLogging $LogFile = Enable-LogFile -Path C:\temp\start-patching.log $VerbosePreference = 'Continue' $DebugPreference = 'Continue' ################################## #define variables $scriptName = "C:\Script\Patch\continue-patching.ps1" $global:PathToPowershell = (Join-Path $env:windir "system32\WindowsPowerShell\v1.0\powershell.exe") $taskCommand = $global:PathToPowershell + " -Command $scriptName" $nameofhost = hostname ################################## #Script # ################################## #write event log New-EventLog –LogName Application –Source “Patch script” -ErrorAction SilentlyContinue ################################## #write event log Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message “Running patch script start-patching” ################################## #sendMail Write-Host "Sending Email" Send-MailMessage -To "NOTIFY@YOURDOMAIN.com" -From "autopatch@YOURDOMAIN.com" -Subject "Started patching $nameofhost" -SmtpServer smtp.YOURDOMAIN.com ################################## #Import-Module Set-ExecutionPolicy Unrestricted -Force ipmo PSWindowsUpdate ################################## #This patches the server, but does not reboot it Get-WUInstall -AcceptAll -IgnoreReboot -IgnoreUserInput | Out-File C:\temp\PSWindowsUpdate.log ################################## #write event log Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message “Patching is complete.” ################################## #Create scheduled job at next startup schtasks /create /tn "Continue-Patching-Job" /tr $taskCommand /sc ONSTART /ru system ################################## #write event log Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message "Computer will now reboot” ################################## #Write Reboot computer Write-Host "Rebooting computer" ################################## #Disable logging $LogFile | Disable-LogFile ################################## #Take a short nap write-host "Waiting 30 seconds" Start-Sleep -s 30 ################################## #Reboot computer shutdown -r -t 0 exit # SIG # Begin signature block # Your signatureblock goes here # SIG # End signature block |
The second script that runs after boot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
<# .SYNOPSIS Part 2/2. Patches and reboots a server, then collects event logs and emails them. .DESCRIPTION Patches and reboots a server, then collects event logs and emails them Prerequisites: PowerShellLogging, PSWindowsUpdate .NOTES File Name : Continue-patching.ps1 Author : Jan Ryen Prerequisite : PowerShell V3 and above. Copyright 2017 - Jan Ryen (www.janryen.com) HTML code originating from Sean Duffy (www.shogan.co.uk) #> ################################## #make temp mkdir c:\temp -ErrorAction SilentlyContinue ################################## #Enable logging Set-ExecutionPolicy Unrestricted -Force ipmo PowerShellLogging $LogFile = Enable-LogFile -Path C:\temp\Continue-patching.log $VerbosePreference = 'Continue' $DebugPreference = 'Continue' ################################## #region Variables and Arguments $getUpdatesInstalledCSV = "C:\temp\WUHistory.csv" $getEventLogSystemCSV = "C:\temp\sysevents.csv" $getEventLogApplicationCSV = "C:\temp\appevents.csv" $getPSWindowsUpdateLog = "C:\temp\PSWindowsUpdate.log" $nameofhost = hostname $computer = $nameofhost $users = "NOTIFY@YOURDOMAIN.com" # Users to email your report to (separated by comma) $fromemail = "autopatch@YOURDOMAIN.com" $server = "smtp.YOURDOMAIN.com" #enter your own SMTP server DNS name / IP address here #$list = $args[0] #This accepts the argument you add to your scheduled task for the list of servers. i.e. list.txt $thresholdspace = 10 # Set free disk space threshold in percent (default at 10%) #[int]$EventNum = 20 # How many events to add to report [int]$ProccessNumToFetch = 10 # How many proccesses to add to report #$ListOfAttachments = @() $Report = @() $CurrentTime = Get-Date #endregion ################################## Function Get-HostUptime { param ([string]$ComputerName) $Uptime = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName $LastBootUpTime = $Uptime.ConvertToDateTime($Uptime.LastBootUpTime) $Time = (Get-Date) - $LastBootUpTime Return '{0:00} Days, {1:00} Hours, {2:00} Minutes, {3:00} Seconds' -f $Time.Days, $Time.Hours, $Time.Minutes, $Time.Seconds } ################################## #Remove scheduled job schtasks /delete /tn "Continue-Patching-Job" /F ################################## #write event log Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message “Computer has rebooted. Cleared old task. Running script continue-patching” ################################## #Import-Module Set-ExecutionPolicy Unrestricted ipmo PSWindowsUpdate ################################## #GetStuff Get-WUHistory | Where-Object {$_.Date -gt [DateTime]::Now.AddHours(-24)} | Select-Object ComputerName,Date,KB,Title | export-csv $getUpdatesInstalledCSV Get-EventLog -LogName System -EntryType Error -After ([DateTime]::Now.AddHours(-1)) | export-csv $getEventLogSystemCSV Get-EventLog -LogName Application -EntryType Error -After ([DateTime]::Now.AddHours(-1)) | export-csv $getEventLogApplicationCSV ################################## # Assemble the HTML Header and CSS for the report $HTMLHeader = @" <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd"> <html><head><title>My Systems Report</title> <style type="text/css"> <!-- body { font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; } #report { width: 835px; } table{ border-collapse: collapse; border: none; font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif; color: black; margin-bottom: 10px; } table td{ font-size: 12px; padding-left: 0px; padding-right: 20px; text-align: left; } table th { font-size: 12px; font-weight: bold; padding-left: 0px; padding-right: 20px; text-align: left; } h2{ clear: both; font-size: 130%; } h3{ clear: both; font-size: 115%; margin-left: 20px; margin-top: 30px; } p{ margin-left: 20px; font-size: 12px; } table.list{ float: left; } table.list td:nth-child(1){ font-weight: bold; border-right: 1px grey solid; text-align: right; } table.list td:nth-child(2){ padding-left: 7px; } table tr:nth-child(even) td:nth-child(even){ background: #CCCCCC; } table tr:nth-child(odd) td:nth-child(odd){ background: #F2F2F2; } table tr:nth-child(even) td:nth-child(odd){ background: #DDDDDD; } table tr:nth-child(odd) td:nth-child(even){ background: #E5E5E5; } div.column { width: 320px; float: left; } div.first{ padding-right: 20px; border-right: 1px grey solid; } div.second{ margin-left: 30px; } table{ margin-left: 20px; } --> </style> </head> <body> "@ $DiskInfo= Get-WMIObject -ComputerName $computer Win32_LogicalDisk | Where-Object{$_.DriveType -eq 3} | Where-Object{ ($_.freespace/$_.Size)*100 -lt $thresholdspace} ` | Select-Object SystemName, DriveType, VolumeName, Name, @{n='Size (GB)';e={"{0:n2}" -f ($_.size/1gb)}}, @{n='FreeSpace (GB)';e={"{0:n2}" -f ($_.freespace/1gb)}}, @{n='PercentFree';e={"{0:n2}" -f ($_.freespace/$_.size*100)}} | ConvertTo-HTML -fragment ################################## #region System Info $OS = (Get-WmiObject Win32_OperatingSystem -computername $computer).caption $SystemInfo = Get-WmiObject -Class Win32_OperatingSystem -computername $computer | Select-Object Name, TotalVisibleMemorySize, FreePhysicalMemory $TotalRAM = $SystemInfo.TotalVisibleMemorySize/1MB $FreeRAM = $SystemInfo.FreePhysicalMemory/1MB $UsedRAM = $TotalRAM - $FreeRAM $RAMPercentFree = ($FreeRAM / $TotalRAM) * 100 $TotalRAM = [Math]::Round($TotalRAM, 2) $FreeRAM = [Math]::Round($FreeRAM, 2) $UsedRAM = [Math]::Round($UsedRAM, 2) $RAMPercentFree = [Math]::Round($RAMPercentFree, 2) #endregion $TopProcesses = Get-Process -ComputerName $computer | Sort WS -Descending | Select ProcessName, Id, WS -First $ProccessNumToFetch | ConvertTo-Html -Fragment ################################## #region Services Report $ServicesReport = @() $Services = Get-WmiObject -Class Win32_Service -ComputerName $computer | Where {($_.StartMode -eq "Auto") -and ($_.State -eq "Stopped")} foreach ($Service in $Services) { $row = New-Object -Type PSObject -Property @{ Name = $Service.Name Status = $Service.State StartMode = $Service.StartMode } $ServicesReport += $row } $ServicesReport = $ServicesReport | ConvertTo-Html -Fragment #endregion ################################## #region Event Logs Report $SystemEventsReport = @() $SystemEvents = Import-Csv -Path $getEventLogSystemCSV foreach ($event in $SystemEvents) { $row = New-Object -Type PSObject -Property @{ TimeGenerated = $event.TimeGenerated EntryType = $event.EntryType Source = $event.Source Message = $event.Message } $SystemEventsReport += $row } $SystemEventsReport = $SystemEventsReport | ConvertTo-Html -Fragment $ApplicationEventsReport = @() $ApplicationEvents = Import-Csv -Path $getEventLogApplicationCSV foreach ($event in $ApplicationEvents) { $row = New-Object -Type PSObject -Property @{ TimeGenerated = $event.TimeGenerated EntryType = $event.EntryType Source = $event.Source Message = $event.Message } $ApplicationEventsReport += $row } $ApplicationEventsReport = $ApplicationEventsReport | ConvertTo-Html -Fragment #endregion ################################## #region Uptime # Fetch the Uptime of the current system using our Get-HostUptime Function. $SystemUptime = Get-HostUptime -ComputerName $computer #endregion ################################## #region PSWindowsUpdateLog # $getPSWindowsUpdateLogReport = @() $getPSWindowsUpdateLogContent = Import-Csv -Path $getUpdatesInstalledCSV foreach ($Update in $getPSWindowsUpdateLogContent) { $row = New-Object -Type PSObject -Property @{ KB = $Update ResultCode = $Update.ResultCode Title = $Update.Title } $ServicesReport += $row } $getPSWindowsUpdateLogReport = $getPSWindowsUpdateLogContent | ConvertTo-Html -Fragment #endregion # Create HTML Report for the current System being looped through $CurrentSystemHTML = @" <hr noshade size=3 width="100%"> <div id="report"> <p><h2>$computer Report</h2></p> <h3>Events Report - The last $EventNum System/Application Log Events that were Warnings or Errors</h3> <p>The following is a list of the last $EventNum <b>System log</b> events that had an Event Type of either Warning or Error on $computer</p> <table class="normal">$SystemEventsReport</table> <p>The following is a list of the last $EventNum <b>Application log</b> events that had an Event Type of either Warning or Error on $computer</p> <table class="normal">$ApplicationEventsReport</table> <p>The following is a list of the <b>Updates installed</b> on $computer</p> <table class="normal">$getPSWindowsUpdateLogReport</table> <h3>System Info</h3> <table class="list"> <tr> <td>OS</td> <td>$OS</td> </tr> <tr> <td>Total RAM (GB)</td> <td>$TotalRAM</td> </tr> <tr> <td>Free RAM (GB)</td> <td>$FreeRAM</td> </tr> <tr> <td>Percent free RAM</td> <td>$RAMPercentFree</td> </tr> </table> <h3>Disk Info</h3> <p>Drive(s) listed below have less than $thresholdspace % free space. Drives above this threshold will not be listed.</p> <table class="normal">$DiskInfo</table> <br></br> <div class="first column"> <h3>System Processes - Top $ProccessNumToFetch Highest Memory Usage</h3> <p>The following $ProccessNumToFetch processes are those consuming the highest amount of Working Set (WS) Memory (bytes) on $computer</p> <table class="normal">$TopProcesses</table> </div> <div class="second column"> <h3>System Services - Automatic Startup but not Running</h3> <p>The following services are those which are set to Automatic startup type, yet are currently not running on $computer</p> <table class="normal"> $ServicesReport </table> </div> "@ # Add the current System HTML Report into the final HTML Report body $HTMLMiddle += $CurrentSystemHTML # Assemble the closing HTML for our report. $HTMLEnd = @" </div> </body> </html> "@ # Assemble the final report from all our HTML sections $HTMLmessage = $HTMLHeader + $HTMLMiddle + $HTMLEnd # Save the report out to a file in the current path #$HTMLmessage | Out-File ((Get-Location).Path + "\report.html") ################################## #sendMailAfter Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message “Sending Email after boot" send-mailmessage -from $fromemail -to $users -subject "$nameofhost has Finished patching" -Attachments $getPSWindowsUpdateLog -BodyAsHTML -body $HTMLmessage -priority Normal -smtpServer $server ################################## #write event log Write-EventLog –LogName Application –Source “Patch script” –EntryType Information –EventID 1 –Message “Finished patching.” ################################## #Disable logging $LogFile | Disable-LogFile Write-Host "Done" # SIG # Begin signature block # Your signatureblock goes here # SIG # End signature block |
By the way, mind the smart quotes if you copy the script from the text above.