In line with the last post on File Server inventory, here comes the another script to perform patching for Domain Servers from a remote machine.
PowerShell already has methods to perform patching on any server, where we can list what all patches are available and create a downloader to download and install them all, but a limitation to the capability is, the same can not be done remotely. As a workaround for that, we would be creating schedule task on the remote machine via the script and performing the patching via triggering that schedule task.
It would involve two scripts InstallPatches.ps1 and PatchServer.ps1, whose code I would be listing below. It would make use of c:\temp directory and would be keeping logs and scripts in the same location. Also we would need any shared location, where we would keep a copy of InstallPatches.ps1, which then would be copied dynamically over the servers during patching. I have kept script source path as “\\ABCXYZ\sources\installpatches.ps1” while it can be changed as per your environment.
This script is design to take max two passes for patching so that if in first run Server still left with some pending patches, it would perfom the patching again, but if in second run, there are still some patches left, one would be needed to perform it manually. The reason for keeping it so was related to my production environment as we anyway maintain regular monthly patching cycle, but can fine tune the script as per their requirement. Also I am forcing the server to be rebooted irrespective if it needs reboot post patching or not as that’s the part of our standard approach in production environment I am supporting, but the script can be changed as per requirements in your production.
The script also performing checks on which services, which were running before the patching reboot but found stopped post reboot and attempts to start them. I have also created a provision of whitelist services, which are not necessary to be ran again even if found stopped. But again, as I said, the script can be changed in different tune as per requirements.
Note: The remote server should have PowerShell remoting enabled as we are using Invoke-command. Sample output would be as given below (Have removed transcript part though)
Your feedback would be most welcome.
Here goes the PowerShell Code….
InstallPatches.ps1
#Variables to customize $FileReport = $true $FileReportPath = "c:\temp\" $AutoRestart = $true $AutoRestartIfPending = $true $ServiceStatusPath = $FileReportPath + "$env:ComputerName" +(Get-Date -Format dd-MM-yyyy).ToString() + ".csv" # Create Directory to keep data try { mkdir c:\temp } catch {} $Path = $FileReportPath + "$env:ComputerName" + "_" + (Get-Date -Format dd-MM-yyyy_HH-mm).ToString() + ".html" $Path1 = $FileReportPath + "$env:ComputerName" + "_" + (Get-Date -Format dd-MM-yyyy_HH-mm).ToString() + ".CSV" if (!([System.IO.File]::Exists($ServiceStatusPath))){ Get-Service | Select-object DisplayName, servicename, starttype, status | export-csv -nti $ServiceStatusPath } Else{ $Pre = import-csv $ServiceStatusPath $Post = Get-Service | Select-object DisplayName, servicename, starttype, status #"Following Services seems to be changed." | Out-File -FilePath $Path Compare-object -ReferenceObject $Pre -DifferenceObject $Post -Property Status,displayname,name | ?{$_.sideIndicator -eq "=>"}| export-csv -nti $Path1 } #Testing if there are any pending reboots from earlier Windows Update sessions if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"){ "WindowsUpdate was run on $env:ComputerName, but no new updates were found. Please try again later." | Out-File -FilePath $Path Exit } #Checking for available updates $updateSession = new-object -com "Microsoft.Update.Session" write-progress -Activity "Updating" -Status "Checking available updates" $criteria="IsInstalled=0 and Type='Software'" $updates=$updateSession.CreateupdateSearcher().Search($criteria).Updates $downloader = $updateSession.CreateUpdateDownloader() $downloader.Updates = $Updates #If no updates available, do nothing if ($downloader.Updates.Count -eq "0") { #Report to file if enabled if ($FileReport -eq $true) { "WindowsUpdate was run on $env:ComputerName, but no new updates were found. Please try again later." | Out-File -FilePath $Path } } else { #If updates are available, download and install write-progress -Activity 'Updating' -Status "Downloading $($downloader.Updates.count) updates" $resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" } $Result= $downloader.Download() if (($Result.Hresult -eq 0) –and (($result.resultCode –eq 2) -or ($result.resultCode –eq 3)) ) { $updatesToInstall = New-object -com "Microsoft.Update.UpdateColl" $Updates | where {$_.isdownloaded} | foreach-Object {$updatesToInstall.Add($_) | out-null } $installer = $updateSession.CreateUpdateInstaller() $installer.Updates = $updatesToInstall write-progress -Activity 'Updating' -Status "Installing $($Installer.Updates.count) updates" $installationResult = $installer.Install() $Global:counter=-1 $Report = $installer.updates | Select-Object -property Title,EulaAccepted,@{Name='Result';expression={$ResultCode[$installationResult.GetUpdateResult($Global:Counter++).resultCode ] }},@{Name='Reboot required';expression={$installationResult.GetUpdateResult($Global:Counter++).RebootRequired }} | ConvertTo-Html #Report to file if enabled if ($FileReport -eq $true) { $Report | Out-File -FilePath $path } #Reboot if autorestart is enabled and one or more updates are requiring a reboot if ($autoRestart -and $installationResult.rebootRequired) { # shutdown.exe /t 0 /r } } }
PatchServer.ps1
# Function to create the schedule task for launching script to install patches Function PatchIt { [CmdletBinding()] Param ( $Server ) $User = [Security.Principal.WindowsIdentity]::GetCurrent() $Role = (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) if(!$Role) { Write-Warning "To perform some operations you must run an elevated Windows PowerShell console. Use a user with Local Administrator permissions on $($Server) ..." Exit } $script = "powershell -NoProfile -NoLogo -NonInteractive -executionpolicy bypass -file c:\temp\installpatches.ps1" $ExprStart = "schtasks /Create /S " + $Server + " /sc once /sd 01/01/2901 /st 00:00" + " /TN " + "RemoteWindowsUpdate /TR ""$script"" /RU SYSTEM /RL Highest /F" Invoke-Expression -Command $ExprStart # Launch the schedule task and wait till it gets completed Start-ScheduledTask -TaskName RemoteWindowsUpdate write-host "Patching in progress on $($Server) ...`n" -Foregroundcolor YELLOW DO { $State = Get-ScheduledTask -TaskName RemoteWindowsUpdate } Until ($State.State -eq 'Ready') write-host "Patching pass completed on $($Server) ...`n" -Foregroundcolor GREEN } # Function to perform Service checks post Server reboot Function ServiceCheck($ServerName) { # List of common services, which do not need to restarted if found stopped during service checks $Whitelist = ("Windows Installer", "Portable Device Enumerator Service", "Application Information", "Smart Card Device Enumeration Service", "Windows Modules Installer","Windows Error Reporting Service", "Device Setup Manager","CNG Key Isolation","Microsoft Policy Platform Local Authority") $ServiceStatusPath = "\\$ServerName\c$\temp\$((Get-ChildItem \\$ServerName\c$\temp | ?{$_.Name -like "$ServerName*.csv"} | sort LastWriteTime | Select -Last 1).Name)" $ServiceStatus = import-csv $ServiceStatusPath $ServiceStatus | FT Displayname, servicename, Status $BaseStatusPath = "\\$ServerName\c$\temp\$ServerName"+(Get-Date -Format dd-MM-yyyy).ToString() + ".csv" If($ServiceStatusPath -ne $BaseStatusPath){ If($ServiceStatus.count -ge 11){ Write-Host "More than 10 Services seem to be in altered state, check manually `n" -Foregroundcolor RED Exit } ForEach($Service in $ServiceStatus){ If(($Service.Status -eq "Stopped") -AND !($Whitelist -contains $Service.DisplayName)){ Write-host "Starting $($Service.DisplayName) ....`n" -Foregroundcolor GREEN Get-Service $Service.servicename -Computername $ServerName | Start-Service } } } Get-WmiObject -Class "win32_quickfixengineering" -ComputerName $ServerName | Select-Object -Property "Description", "HotfixID", @{Name="InstalledOn"; Expression={([DateTime]($_.InstalledOn)).ToLocalTime()}} | ?{$_.InstalledOn -ge (Get-Date).AddDays(-1)} } # Input the server name and credentials $RemoteComputer = Read-Host "Enter the Server name to patch and reboot " $cred = Get-Credential If(Test-Connection $RemoteComputer -count 1 -quiet){ Try{ # Base path for the script which would perform the patching. Can be changed as per requirement $ScriptPath = "\\ABCXYZ\c$\temp\installpatches.ps1" Copy-Item -Path $ScriptPath -Destination \\$RemoteComputer\c$\temp\installpatches.ps1 -force Invoke-Command -ComputerName $RemoteComputer -Credential $cred -ScriptBlock ${Function:PatchIt} -ArgumentList $RemoteComputer -ErrorAction Continue write-host "Restarting $($RemoteComputer) ..." -Foregroundcolor GREEN Restart-Computer -ComputerName $RemoteComputer -Force -Wait -For PowerShell -Timeout 7200 -Delay 2 #Restart 1 write-host "Restart completed for $($RemoteComputer), would wait for 100 seconds before running next patching pass ..." -Foregroundcolor GREEN Start-Sleep -s 100 #Wait for 100 seconds before running the next check for available patches Invoke-Command -ComputerName $RemoteComputer -Credential $cred -ScriptBlock ${Function:PatchIt} -ArgumentList $RemoteComputer -ErrorAction Continue $PendingUpdates = Get-Content -tail 3 "\\$($RemoteComputer)\c$\Windows\SoftwareDistribution\ReportingEvents.log" If(!($PendingUpdates -match "Windows Update Client successfully detected 0 updates.")) { Write-host "There are pending patches on $($RemoteComputer), running the task again ..." Invoke-Command -ComputerName $RemoteComputer -Credential $cred -ScriptBlock ${Function:PatchIt} -ArgumentList $RemoteComputer -ErrorAction Continue Restart-Computer -ComputerName $RemoteComputer -Force -Wait -For PowerShell -Timeout 7200 -Delay 2 #Restart 2 write-host "Second Restart completed for $($RemoteComputer), would wait for 100 seconds before running next patching pass ..." -Foregroundcolor GREEN Start-Sleep -s 100 #Wait for 100 seconds before running the next check for available patches Invoke-Command -ComputerName $RemoteComputer -Credential $cred -ScriptBlock ${Function:PatchIt} -ArgumentList $RemoteComputer -ErrorAction Continue $PendingUpdates = Get-Content -tail 3 "\\$($RemoteComputer)\c$\Windows\SoftwareDistribution\ReportingEvents.log" If(!($PendingUpdates -match "Windows Update Client successfully detected 0 updates.")) { Write-host "There are still pending patches on $($RemoteComputer) even after two restarts and 3 patching passes, need to check manually..." ServiceCheck -ServerName $RemoteComputer } Else { Write-host "No patches are pending on $($RemoteComputer) (2) ..." ServiceCheck -ServerName $RemoteComputer } } Else { Write-host "No patches are pending on $($RemoteComputer) (1) ..." ServiceCheck -ServerName $RemoteComputer } } Catch{ $_.Exception.Message } } Else { write-host "$($RemoteComputer) not reachable." -Foregroundcolor RED }