Nigel Boulton's Blog
20Jan/1114

Installing a Windows Hotfix on Multiple Machines using a PowerShell Script

A little while ago I was given a hotfix by Microsoft PSS for an issue we had been experiencing with the WMI repository intermittently becoming corrupted on Windows 2008 servers. As you will know from my previous posts we have quite a few servers, so after testing the hotfix carefully I was looking for a way to deploy this across the server estate with minimum effort. We do have a third-party deployment product, but this is geared around deploying regular Microsoft security patches as opposed to hotfixes intended to address specific issues. I wanted a way to do this semi-interactively so that I could monitor progress and deal with any issues arising during the deployment process.

My good friend and colleague Jonathan Medd kindly did the initial research on this for me (I figured if anybody could find a way to do it then he could!). We suspected that we might have some trouble getting PowerShell remoting to do this (see my previous post here), and after some searching and testing it soon became clear that this was in fact the case.

The approach we settled on was still based around PowerShell (of course) but we ended up having to make use of the trusty old utility PsExec, originally written by Mark Russinovich of Sysinternals, who now come under the Microsoft umbrella.

In this solution, PsExec.exe calls WUSA.exe, which is the Windows Update Stand-alone Installer. This is a means of installing update packages programmatically. Update packages have an .msu file extension. I can’t get out of the habit of calling them hotfixes though, sorry... WUSA is a simple and effective utility that surprisingly, I hadn’t encountered before. The final script code is shown below:

$Servers = $(1..176 | foreach {"SERVER$_"})

$HotfixPath = '\\Fileserver\Hotfixes\KB2464876\Windows6.0-KB2464876-x86.msu'

foreach ($Server in $Servers){
	if (Test-Path "\\$Server\c$\Temp"){
		Write-Host "Processing $Server..."
		# Copy update package to local folder on server
		Copy-Item $Hotfixpath "\\$Server\c$\Temp"
		# Run command as SYSTEM via PsExec (-s switch)
		& E:\SysinternalsSuite\PsExec -s \\$Server wusa C:\Temp\Windows6.0-KB2464876-x86.msu /quiet /norestart
		if ($LastExitCode -eq 3010) {
			$ConfirmReboot = $False
		} else {
			$ConfirmReboot = $True
		}
		# Delete local copy of update package
		Remove-Item "\\$Server\c$\Temp\Windows6.0-KB2464876-x86.msu"
		Write-Host "Restarting $Server..."
		Restart-Computer -ComputerName $Server -Force -Confirm:$ConfirmReboot
		Write-Host
	} else {
		Write-Host "Folder C:\Temp does not exist on the target server"
	}
}

To avoid authentication issues, we have PsExec run WUSA as SYSTEM (-s switch), which means that the update package needs to be available locally, so the script copies it to C:\Temp on the machine in question first. During testing, we were using the -i (interactive) switch, but doing this caused error 1008 "ERROR_NO_TOKEN" when I tried to run it for real – this appears to happen if you are not logged on to the server being processed.

The hotfix (sorry, "update package") in question required a restart after installation. I wanted the process to be as automated as possible, but still interactive, as I mentioned earlier. I wanted the restart to be performed automatically and for the script to proceed to the next server unprompted if the package installed as expected, but to prompt me if not so that I could troubleshoot.

To achieve this, WUSA installs the package with the /norestart switch. Because of this, the error code returned from WUSA (via PsExec) is 3010. In fact this isn’t an error. By testing PowerShell’s built in $LastExitCode variable it's possible to have the script proceed with the removal of the local file and the subsequent restart of the machine automatically if this is the result.

Of course, you can build the list of servers in a number of ways in the code above. I used a numbered range, but by simply substituting the line which sets the $Servers variable, you can easily read a list of machine names from a text file:

$Servers = Get-Content '\\Fileserver\Hotfixes\ServerList.txt'

I ran this against 176 servers in groups of about 50 in a few hours and it worked faultlessly. After all the servers had restarted I needed a quick way of verifying that the hotfix had been successfully installed on all of them. Shortly I will publish a further post which outlines how I did that. It’s neat and simple and potentially useful to be able to check for the presence of any hotfix on a number of machines.

Comments (14) Trackbacks (1)
  1. This works well with 2008. But on 2008R2 gives acces denied. what could be the problem ? Thanks.

    • You are quite right cw. I have just tested with Windows 2008 R2. The problem is the interaction between PSExec and the Windows 2008 R2 security model. I was able to get it to work against a non-domain joined 2008 R2 machine only by disabling UAC (User Account Control) on that machine. On a domain-joined machine, it worked provided I ran it from an elevated console to which I had authenticated using a domain admin account.

      Another option may be to make use of winrs.exe instead of PSExec, but you would need to experiment to see whether that approach would work for you.

  2. Hi

    is it possible that you update the script with the 2008 R2 “On a domain-joined machine, it worked provided I ran it from an elevated console to which I had authenticated using a domain admin account.”

    Thank you

  3. Should work against Server 2008R2 is you explicitly declare your credentials to psexec, and perhaps add the -h parameter to elevate your token. -S is probably a bad idea, system level access is far more than is necessary here.

  4. I tried using the invoke-command it worked like a charm. Specifically for example:

    Invoke-Command -ScriptBlock { & c:\windows\system32\wusa.exe c:\tmp\Windows6.0-KB969084-x86.msu /norestart }

    Looking at the technet syntax – http://technet.microsoft.com/en-us/library/dd347578.aspx and using the Invoke-async I can do this on parallel:
    https://gallery.technet.microsoft.com/scriptcenter/Invoke-Async-Allows-you-to-83b0c9f0

  5. Great !

    Any final solution about it ?

    Ideal world in Powershell

    Target; install/uninstall Remote Server Administration Tools for Windows 8.1 silent using powershell

    steps:
    Programmatically save the download package to a local computer or share

    Detect appropriate for your computer’s architecture: Windows8.1-KB2693643-x64.msu or Windows8.1-KB2693643-x86.msu

    Install programmatically silent Windows8.1-KB2693643-x64.msu or Windows8.1-KB2693643-x86.msu (Installation and Uninstallation of MSU in silent mode)

    Activate feature programmatically using Powershell Active Directory Module for Windows PowerShell

  6. Thanks a lot 🙂

  7. We have 2 hotfix to prevent the Xerox printers from printing junk characters. Please let me know if I can use this script to install the hotfix on win7 professional.

  8. Hi,

    I am not good at Scripting but, is it possible to get this script able to install multiples hotfixes on multiple machines ?
    Modifying this line ?
    $HotfixPath = ‘\\Fileserver\Hotfixes\KB2464876\Windows6.0-KB2464876-x86.msu’

    Dunno… please help. Thanks

    • How about something like this Aurelien? (It is untested by the way.)

      $RootHotfixPath = '\\Fileserver\Hotfixes\'
      
      $Hotfixes = @('Windows6.0-KB2464876-x86.msu','Windows6.0-KB2464877-x86.msu')
      $Servers = $(1..176 | foreach {"SERVER$_"})
      
      foreach ($Hotfix in $Hotfixes){
       	$HotfixPath = "$RootHotfixPath$Hotfix"
      	foreach ($Server in $Servers){
      		if (Test-Path "\\$Server\c$\Temp"){
      			Write-Host "Processing $Server..."
      			# Copy update package to local folder on server
      			Copy-Item $Hotfixpath "\\$Server\c$\Temp"
      			# Run command as SYSTEM via PsExec (-s switch)
      			& E:\SysinternalsSuite\PsExec -s \\$Server wusa C:\Temp\$Hotfix /quiet /norestart
      			write-host "& E:\SysinternalsSuite\PsExec -s \\$Server wusa C:\Temp\$Hotfix /quiet /norestart"
      			if ($LastExitCode -eq 3010) {
      				$ConfirmReboot = $False
      			} else {
      				$ConfirmReboot = $True
      			}
      			# Delete local copy of update package
      			Remove-Item "\\$Server\c$\Temp\$Hotfix"
      			Write-Host "Restarting $Server..."
      			Restart-Computer -ComputerName $Server -Force -Confirm:$ConfirmReboot
      			Write-Host
      		} else {
      			Write-Host "Folder C:\Temp does not exist on the target server"
      		}
      	}
      }
  9. Where can I find wusa.exe for 2008 R2? Seems as if Microsoft pulled it from their website.

    Thanks


Leave a comment