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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s