PowerShell: Script for patching Domain Servers remotely


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)

Screenshot
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 }

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.