Free PowerShell Script: Use WSUS to Update Installation Media and Hyper-V Templates

In several articles and other works, I make the claim that backing up a Hyper-V host is largely a waste of time. A separate practice is to maintain templates and other offline images of systems for easy deployment of new systems. What these two topics have in common is the need (or at least the desire) to keep the images current with Windows patches. It doesn’t help much to save ten or fifteen minutes deploying Windows from a template or ISO if you then need to spend two hours installing updates. In the past, we would have use “slipstreamed” ISOs. That’s no longer possible with modern iterations of Windows. However, what we lack now is not the capability to update these systems, but the proper tools. Of course, if you’re willing (and able) to spend a lot of money on System Center, that suite can help you a great deal. For the rest of us, we have to resort to other options, usually homegrown.

To address that problem, I have crafted a script that will automatically update both VHDX files and WIM files. If you’re thinking that this doesn’t apply to you because you only deploy from physical media, think again! The issue of publicly-available Windows Server ISOs never being updated by Microsoft is the primary driver behind the creation of this script.

Script Features

Scripts to update WIMs and VHDs are numerous, so I’d like to take a few moments to enumerate the features in this script, especially the parts that set it apart from others that I’ve found.

  • Updates WIMs and VHDXs interchangeably
  • Updates multiple images with a single execution
  • Ability to scope updates; no more trying to apply Office 2013 patches to your Hyper-V Server image (unless that’s what you want to do; I’m not judging)
  • Subsequent runs against the same image will not try to re-apply previously installed updates
  • Designed to be run on a schedule, but can also be run interactively
  • Can update every image in a multi-pack WIM (if that’s what you want)

Prerequisites

There are a few things that you’ll need to provide for this script to function.

  • A willingness to put away your DVDs
  • A USB flash device that is recognized on the physical systems that you use with a capacity of at least 8 GB (not applicable if you’re only here to update VHDXs)
  • An installation of Windows Server Update Services (WSUS)
  • Space to store WIMs and/or VHDXs (varies, but anywhere from 5 to 20GB per)
  • Spare space for the update operation (varies depending on the total size of all updates to be applied, but usually no more than a few gigabytes per operation)
  • On the system where you wish to run the script, the 2012 R2 or later Windows Server Update Services console must be installed. It includes the UpdateServices PowerShell module. For Windows 8.1+, download the Remote Server Administration Tools. I don’t believe that this module is available for Windows 7 or Server 2008 R2 or earlier, but it would be in the WSUS 3.0 SP2 download if it were. The following screenshot shows where the option appears on Windows 10. For Server, it is in the same location on the Features page of the Add Roles and Features wizard.
    WSUS in RSAT

    WSUS in RSAT

     

  • PowerShell 4.0 or later on the system that will run the script.

Something I want to make clear right from the beginning is that I don’t know how to update a Windows ISO image or use this to create an ISO image that can then be burned to DVD. I have moved to USB flash deployments for any physical systems where PXE is not an option. DVD has had its day but it will soon be following the floppy into the museum. If you haven’t tried loading an operating system from a USB stick, today is a great day to learn.

Deploying a Physical Machine from a WIM and a USB Stick

If you’re only going to be using this script to update VHDX files, skip this entire section.

You may not have realized it, especially if you’re like me and have been around since the pre-WIM days, but any time you use a Microsoft-pressed DVD or a burned ISO to install Windows, you are deploying from a WIM. Check your installation media’s file structure for a file named install.wim in the Sources folder. That’s where your Windows installation comes from. The trick is to get that WIM updated with the latest patches before using it again. There are a few inefficiencies with the method that I’ve discovered, but it works perfectly without requiring any paid tools.

  1. Acquire an ISO of the Windows, Windows Server, or Hyper-V Server operating system. If you only have physical media to work from and you don’t already have a tool, I use ImgBurn. It’s tough to get to through all the adwalls but it’s free and does the job.
  2. Acquire a copy of the Windows USB/DVD Download Tool.
  3. Insert your USB stick into the computer. If it’s not empty, whatever is on it will be destroyed.
  4. Run the tool that you downloaded. It’s still branded as “Windows 7” but it will work with whatever Windows ISO you give it.
    Windows 7 DVD Tool

    Windows 7 DVD Tool

     

  5. Browse to the ISO image from step 1 that you want to convert for USB.
    Windows USB Tool ISO Selection

    Windows USB Tool ISO Selection

     

  6. Choose USB Device.
    Windows USB Media Selection

    Windows USB Media Selection

     

  7. Ensure that the correct USB device from step 3 is selected. Press Begin Copying.
    Windows USB Select Output Drive

    Windows USB Select Output Drive

     

  8. You will get a small popup box warning you that your device will be erased. Click Erase USB Device. You’ll get yet another dialog telling you the essentially the same thing that the previous dialog said. I guess they really want to make certain that no one can say they didn’t know that their drive was going to be erased. Click Yes.
  9. Wait for the process to complete.
    Windows USB File Copy

    Windows USB File Copy

     

  10. When it’s finished, you can just close the window or click Start Over to craft another USB drive.
  11. Copy the sourcesinstall.wim from the USB device to a location where it can be updated — I prefer having it on my WSUS host. Because every single Windows media uses the name install.wim, I would either set up a folder structure that clearly indicates exactly where the file came from or I would rename the file.

You’re now ready to begin. The procedure going forward will be:

  1. Update the WIM.
  2. Copy the WIM back to the USB device.
  3. Use the USB device to install.

If you have more images than USB keys, that’s a workable problem. You can always rebuild the USB device from the ISO image and then copy over the latest copy of the WIM. However, now that you’ve come this far, I strongly recommend that you research deployment from a WDS server with WIM. It’s not that tough and it’s nice to never worry about installation media.

Script Usage

This script is slow. Very slow. The strongest control on speed is your hardware, but it’s still going to be slow. Part of the delay is scanning for applicable updates, so I’ve set that so that the scan of the WSUS server only occurs once per iteration, no matter how many VHDXs/WIMs you specify to update. To make it even better, it will record which updates were successfully applied to the WIM. As long as you don’t move or rename the log, additional runs will skip updates that have already been applied. This means that the first run against any given image will likely take hours, but subsequent runs might only require minutes.

You can run the script by hand, if you wish. If you only update any given Windows Server 2012 R2 one time, that will literally save you at least a years’ worth of updates each time that you deploy from it. My recommendation is to schedule the update to run once a month over the weekend so that you’re always up-to-date. To make that easier, I’ll show you how to build a supporting script to call this one with your images.

There are two parameter sets. One is to run against a single image file, the other is for multiple image files. Because of the way that WIMs work, you can’t just supply a list of file names. The parameter sets are otherwise identical.

Single Image File Parameter Set

Update-WindowsImage.ps1
	-Path <String>
	[-Index <Int32>]
	[-WsusServerName <String>]
	[-WsusServerPort <UInt16>]
	[-WsusUsesSSL]
	-WsusContentFolder <String>
	[-TargetProduct <String[]>]
	[-MinimumAgeInDays <UInt16>]
	[-OfflineMountFolder <String>]
	[-IgnoreDeclinedStatus]
	[<CommonParameters>]

Multiple Image File Parameter Set

Update-WindowsImage.ps1
	-Images <Array>
	[-WsusServerName <String>]
	[-WsusServerPort <UInt16>]
	[-WsusUsesSSL]
	-WsusContentFolder <String>
	[-TargetProduct <String[]>]
	[-MinimumAgeInDays <UInt16>]
	[-OfflineMountFolder <String>]
	[-IgnoreDeclinedStatus]
	[<CommonParameters>]

There are only two required parameters: the image(s) and the WSUS system’s content folder. The hardest part is the image(s), so we’ll start there.

Specifying Image File(s)

The basic issue with specifying image files is that a single WIM can contain multiple images. If you’ve ever started an installation and been asked to choose between Standard and Datacenter and Standard Core and Datacenter Core or something similar, every single line that you see is a separate image in one WIM. When you update a WIM, you must select which image to work with. VHDX files, on the other hand, only have a single item so you don’t need to worry about specifying an index.

Specifying a Single VHDX

This is the easiest usage. Just use the full path of the VHDX with the WSUS content folder:

Update-WindowsImage.ps1 -Path D:Templatesw2k12r2template.vhdx -WsusContentFolder 'D:WSUSWsusContent'

This assumes that you are running the script locally on the WSUS server.

Specifying a Single WIM

You must specify the index of an image within a WIM to update. If you don’t know, or just want to update all of them, specify -1. Updating every image will take a very long time!

Update-WindowsImage.ps1 -Path D:FromISO2k12r2install.wim -Index -1 -WsusContentFolder 'D:WSUSWsusContent'

If you’d like to narrow it down to a specific image but you don’t know what image to choose, you can interactively and locally run the script and you’ll be prompted:

WIM Index Menu

WIM Index Menu

This list is pulled directly from Get-WindowsImage. You can look at the available indexes yourself in advance with Get-WindowsImage D:FromISO2k12r2install.wim. If you do not specify -1 or a valid index when running Update-WindowsImage either from a scheduled task or in a remote PowerShell session, the script will fail.

Specifying Multiple Target Images

In order to update multiple images at once, you must supply an array of hash tables. If you’re new to PowerShell, take heart; it sounds much worse than it is.

First, make an empty array:

$Images = @()

Then, make a hash table. This must have at least one component, a Path. For a WIM, it must also contain an Index. VHDX files can also have an index but they’ll be ignored.

$Image1 = @{'Path'='D:FromISOw2k12r2install.wim'; 'Index'='4')

Insert the hash table into the array:

$Images += $Image1

Finally, submit your array to the script:

Update-WindowsImage.ps1 -Images $Images -WsusContentFolder 'D:WSUSWSUSContent'

Easy, right? Now, let’s do a bunch in one shot:

$Images = @(
	@{'Path'='D:FromISOw2k12r2install.wim'; 'Index' = 1},
	@('Path'='D:FromISOw2k12r2install.wim'; 'Index' = 2),
	@{'Path'='D:Templatesw2k12r2.vhdx'},
	@{'Path'='D:FromISOhs2k12r2install.wim'; 'Index' = 1}
)
Update-WindowsImage.ps1 -Images $Images -WsusContentFolder 'D:WSUSWsusContent'

Any image that can’t be found will simply be skipped. It will not impact the success or failure of the others.

Specifying the Target Product(s)

To reduce the amount of time spent attempting to apply patches, I added a filter for specific products. By default, the only scanned product is Windows Server 2012 R2 (which will include Hyper-V Server 2012 R2). You can specify what products to search for by using the TargetProduct parameter:

-TargetProduct 'Windows Server 2012 R2', 'Microsoft SQL Server 2014'

The items you enter here must match their names in WSUS verbatim or the updates will not be scanned (and there will be no error). To see that list, use Get-WsusProduct. Unfortunately, the PowerShell cmdlets for WSUS leave a great deal to be desired and there’s no simple way to narrow down which products that your host is receiving in synchronization.

Understanding how Available Updates will be Selected

I’ve never been the biggest fan of WSUS for a number of reasons, and you’re about to encounter one. I can easily determine if an update has been Approved in at least one place on the server and if it has been Declined in at least one place on the server. Finding out which computer groups that it has been Approved or Declined for is much harder. So, the default rule is: if an update has been approved on at least one group and has not been declined on any groups, it will be eligible. If you specify the IgnoreDeclinedStatus parameter, then the rule will change to: if an update has been approved on at least one group, it will be eligible. There is also a MinimumPatchAgeInDays parameter.

Other Parameters

Let’s step through the other, more self-explanatory parameters quickly:

  • WsusServerName: this is the name (short or FQDN) or the IP address of the WSUS server to connect to. If not specified, the cmdlet will assume WSUS is running locally.
  • WsusServerPort: the port that WSUS runs on. By default, this is 8530, because that’s the default WSUS port.
  • WsusUsesSSL: this is a switch parameter. Include it if your WSUS server is using SSL. Leave it off otherwise.
  • MinimumPatchAgeInDays: this is a numeric parameter that indicates the minimum number of days that a patch must have been on the WSUS server before it can be eligible for your images.
  • OfflineMountFolder: by default, the script will create an Offline folder on the system’s system drive (usually C:) for its working space. If this folder already exists, it must be empty. The folder is not removed at the end of the cycle. Use this parameter to override the name of the folder.

Scripting the Script

My vision is that you’ll set this script to run on a schedule. To work with multiple items, I’d make a script that calls the update script. So, save something like the following and call it every Friday at 7 PM:

$Images = @(
	@{'Path'='D:FromISOw2k12r2install.wim'; 'Index' = 1},
	@('Path'='D:FromISOw2k12r2install.wim'; 'Index' = 2),
	@{'Path'='D:Templatesw2k12r2.vhdx'},
	@{'Path'='D:FromISOhs2k12r2install.wim'; 'Index' = 1}
)
C:ScriptsUpdate-WindowsImage.ps1 -Images $Images -WsusContentFolder 'D:WSUSWsusContent'

Depending on your scripting skills, you could make this far more elaborate. Just remember that each image is going to take quite some time to update, especially on the first run.

The Script Source

As included here, you simply run the script on demand. If you’d like to dot-source it or use it in your profile, uncomment the function definition lines right after the help section and at the end. They are clearly marked.

<#
.SYNOPSIS
	Updates an offline WIM or VHDX from WSUS contents.
.DESCRIPTION
	Updates an offline WIM or VHDX from WSUS contents.
	Can update one or all indexes in a WIM.
	By default, stores a log file next to the file to be updated. On subsequent runs against that file, it will not apply any items previously applied.
.PARAMETER Path
	The path to the WIM or VHDX to be updated.
.PARAMETER Index
	For WIM files only, selects which contained image will be updated. Enter -1 to update all.
	If not specified in a non-interactive session, ALL will be updated.
	If not specified in an interactive session, you will be prompted.
	If supplied with a VHDX file, will be ignored.
.PARAMETER Images
	An array of hash tables that contain the images to be updated.
	Entries must be in the format: @{'Path' = 'c:imagepathimagefilename'; 'Index' = 4 }
	The index item does not need to be present for .vhdx files and will be ignored.
.PARAMETER WsusServerName
	A resolvable name or IP address of the computer that runs WSUS.
	Uses the local system by default.
.PARAMETER WsusServerPort
	The port that WSUS responds on. Defaults to 8530.
	Ignored when WsusServerName is not specified.
.PARAMETER $WsusUsesSSL
	Flag if you should connect to WSUS using SSL. Default is to not use SSL.
	Ignored when WsusServerName is not specified.
.PARAMETER WsusContentFolder
	The path of the WSUS system's WsusContent folder. Must be resolvable and accessible from the location that the script is executed.
.PARAMETER TargetProduct
	The target product(s) to limit available product updates to. Use Get-WsusProduct on your WSUS server for a list of available products.
	The default is 'Windows Server 2012 R2' (will also apply to Hyper-V Server 2012 R2).
	Use an empty string or array to select all products. WARNING: This will take an EXTREMELY long time if you have many products on your WSUS server.
.PARAMETER MinimumPatchAgeInDays
	The minimum number of days since a patch appeared on the WSUS host before it can be applied. Default is 0.
.PARAMETER OfflineMountFolder
	The temporary mount location for the WIM/VHDX. If this folder does not exist, it will be created. If this folder exists and is not empty, execution will halt.
	The location will not be removed at the end of execution, but it will be empty.
	The default is Offline on the system volume.
.PARAMETER IgnoreDeclinedStatus
	If specified, updates that appear as both Approved and Declined will be applied (meaning the update is approved in at least one location even though it is declined in another).
	If not specified, an update that is declined anywhere on the WSUS host will be not be applied.
.NOTES
	Written by Eric Siron
	(c) 2016 Altaro Software
	Version 1.6. October 29th, 2016

	- 1.6 -
	-------
	* If the target mount folder has remnants from a previous failed run, it is cleaned up. Suggested by commenter Steve.
	* Inserted more Verbose write points to aid in troubleshooting.
	* Minor bug fixes, typo corrections, and expansions to status displays.

	- 1.5 -
	-------
	* Added verbosity to patch application. Use -Verbose.
	* Adjustment to log reset behavior between target files.

	- 1.4 -
	-------
	* .a: Typos
	* Image logging mechanism reworked to include more information.

	- 1.3 -
	-------
	* Patch log will no longer contain duplicates.
	* An empty patch list will bypass the mount/unmount process.
	* Clarified some error messages.
	* Discrepancy between documentation and configuration for "MinimumPatchAgeInDays". Documentation said default of 0, script said 30. Both are now 0.

	- 1.2 -
	-------
	* Corrected behavior when a single VHDX is submitted (for real this time)
	* Adjusted matching pattern for previous patches

	- 1.1 -
	-------
	* Corrected variable naming mismatch for MinimumPatchAgeInDays
	* Corrected behavior when a single VHDX is submitted
.EXAMPLE
	Update-WindowsImage.ps1 -Path D:Templatesw2k12r2template.vhdx -WsusContentFolder 'D:WSUSWsusContent'
	
	Updates the specified VHDX using the local WSUS server.

.EXAMPLE
	Update-WindowsImage.ps1 -Path D:FromISO2k12r2install.wim -Index -1 -WsusContentFolder 'D:WSUSWsusContent'

	Updates the first image within the specified WIM using the local WSUS server.

.EXAMPLE
	$Images = @(
		@{'Path'='D:FromISOw2k12r2install.wim'; 'Index' = 1},
		@('Path'='D:FromISOw2k12r2install.wim'; 'Index' = 2),
		@{'Path'='D:Templatesw2k12r2.vhdx'},
		@{'Path'='D:FromISOhs2k12r2install.wim'; 'Index' = 1}
	)
	Update-WindowsImages -Images $Images -WsusContentFolder 'D:WSUSWsusContent'

	Updates all of the specified images using the local WSUS server.

.EXAMPLE
	Update-WindowsImage.ps1 -Path '\storage.domain.localTemplatesw2k12r2template.vhdx' -WsusServerName 'wsus.domain.local' -WsusContentFolder '\wsus.domain.locald$WSUSWsusContent'

	Updates the specified remote image using the specified remote WSUS server, which is running on port 8530.
#>
#requires -RunAsAdministrator
#requires -Version 4
#requires -Modules Dism, UpdateServices

#function Update-WindowsImage						#Uncomment this line to use this script dot-sourced or in a profile. Also the next line and the very last line.
#{															#uncomment this line to use this script dot-sourced or in a profile. Also the previous line and the very last line.
	[CmdletBinding(DefaultParameterSetName='Single Item')]
	Param(
		[Alias('ImagePath')]
		[ValidateNotNullOrEmpty()]
		[Parameter(Mandatory=$true, ParameterSetName='Single Item', Position=1)]
		[String]$Path,

		[Parameter(ParameterSetName='Single Item', Position=2)]
		[Int]$Index,

		[Parameter(Mandatory=$true, ParameterSetName='Multiple Items', Position=1)]
		[Array]$Images,
	
		[Parameter()]
		[String]$WsusServerName,

		[Alias('Port')]
		[Parameter()]
		[UInt16]$WsusServerPort = 8530,

		[Alias('SSL', 'WithSSL')]
		[Parameter()]
		[Switch]$WsusUsesSSL,

		[Parameter(Mandatory=$true)]
		[String]$WsusContentFolder,

		[Parameter()]
		[String[]]$TargetProduct = @('Windows Server 2012 R2'),

		[Parameter()]
		[UInt16]$MinimumPatchAgeInDays = 0,

		[Parameter()]
		[String]$OfflineMountFolder = "$env:SystemDriveOffline",

		[Parameter()]
		[Switch]$IgnoreDeclinedStatus
	)

	Write-Progress -Activity 'Validating environment' -Status 'Checking image information' -PercentComplete 25
	$ImageList = @()
	if($PSCmdlet.ParameterSetName -eq 'Single Item')
	{
		try
		{
			Write-Verbose -Message 'Locating specified image file'
			$WindowsImage = Get-WindowsImage -ImagePath $Path -ErrorAction Stop
		}
		catch
		{
			throw('Specified image file "{0}" is not valid' -f $Path)
		}
		if(Test-Path -Path $Path)
		{
			$SelectedIndexes = @()
			if($Path -imatch 'wim$')
			{
				if(-not $Index -or -not ($WindowsImage.ImageIndex -contains $Index))
				{
					if([Environment]::UserInteractive)
					{
						$ValidOptions = @(-1)
						$CurrentSelection = -999
						while($ValidOptions -notcontains $CurrentSelection)
						{
							Write-Host -Object 'You must specify an index in the image to apply updates to. Choose one of the following' -ForegroundColor Cyan -BackgroundColor DarkMagenta
							Write-Host -Object '-1: Update All (this will take an EXTREMELY long time'
							$WindowsImage | foreach {
								$ValidOptions += $_.ImageIndex
								Write-Host -Object "$($_.ImageIndex): $($_.ImageName)"
							}
							Write-Host
							$CurrentSelection = Read-Host -Prompt 'Enter a numerical selection from above list or [CTRL+C] to cancel.'
							if($CurrentSelection -eq -1)
							{
								foreach($Option in $ValidOptions)
								{
									if($Option -gt 0)
									{
										$SelectedIndexes += $Option
									}
								}
							}
							else
							{
								$SelectedIndexes = @($CurrentSelection)
							}
						}
					}
					else
					{
						throw('No index was selected for "{0}" or the index is invalid' -f $Path)
					}
				}
				else
				{
					$SelectedIndexes += $Index
				}
			}
			else
			{
				$SelectedIndexes += 1
			}
			$SelectedIndexes | foreach {
				Write-Verbose ('Adding image file "{0}", index {1} to the patching list.' -f $Path, $_)
				$ImageList += @{'Path' = $Path; 'Index' = $_ }
			}
		}
	}
	else
	{
		foreach($SpecifiedImage in $Images)
		{
			try
			{
				$Index = 1
				if($SpecifiedImage.Path -imatch 'wim$')
				{
					$Index = $SpecifiedImage.Index
				}
				$WindowsImage = Get-WindowsImage -ImagePath $SpecifiedImage.Path -Index $Index -ErrorAction Stop
				Write-Verbose ('Adding image file "{0}", index {1} to the patching list.' -f $SpecifiedImage.Path, $Index)
				$ImageList += @{'Path' = $SpecifiedImage.Path; 'Index' = $Index}
			}
			catch
			{
				Write-Warning -Message ('Invalid file({0}) or index ({1}) specified. This entry will be ignored.' -f $SpecifiedImage.Path, $SpecifiedImage.Index)
			}
		}
	}

	Write-Progress -Activity 'Validating environment' -Status 'Verifying WSUS server' -PercentComplete 50
	$GetWsusServerParameters = @{}
	if(-not [String]::IsNullOrEmpty($WsusServerName))
	{
		$GetWsusServerParameters.Add('Name', $WsusServerName)
		$GetWsusServerParameters.Add('PortNumber', $WsusServerPort)
		$GetWsusServerParameters.Add('UseSsl', $WsusUsesSSL)
	}
	try
	{
		$WsusServer = Get-WsusServer @GetWsusServerParameters -ErrorAction Stop
	}
	catch
	{
		throw("Unable to contact the specified WSUS host`r`n$($_.Message)")
	}

	Write-Progress -Activity 'Validating environment' -Status 'Verifying WSUS content folder' -PercentComplete 75
	try
	{
		if(-not (Get-ChildItem -Path $WsusContentFolder -Directory -ErrorAction Stop | sort | foreach { if($_.Name -match '^[A-Z0-9]{2}$') { $true } } ))
		{
			throw('Folder exists but does not contain any of the expected content sub-folders.')
		}
	}
	catch
	{
		throw("Specified WSUS content folder cannot be reached or does not contain expected content files")
	}

	Write-Progress -Activity 'Validating environment' -Status 'Verifying offline mount folder' -PercentComplete 99
	if(Test-Path -Path $OfflineMountFolder)
	{
		if(Get-ChildItem -Path $OfflineMountFolder)
		{
			Dismount-WindowsImage -Path $OfflineMountFolder -Discard -ErrorAction SilentlyContinue
			if(Get-ChildItem -Path $OfflineMountFolder)
			{
				throw("$OfflineMountFolder is not empty.")
			}
		}
	}
	else
	{
		try
		{
			New-Item -Path $OfflineMountFolder -ItemType Directory -ErrorAction Stop	
		}
		catch
		{
			throw("Unable to locate or create $OfflineMountFolder")
		}
	}
	Write-Progress -Activity 'Validating environment' -Completed

	Write-Progress -Activity 'Loading updates' -Status 'Scanning for applicable updates' -PercentComplete -1
	$WSUSUpdates = Get-WsusUpdate -UpdateServer $WsusServer -Approval Approved |
		where { -not $_.Update.IsSuperseded `
			-and ($IgnoreDeclinedStatus -or -not $_.Update.IsDeclined) `
			-and (Compare-Object -DifferenceObject $_.Products -ReferenceObject $TargetProduct -ExcludeDifferent -IncludeEqual) `
			-and $_.Update.ArrivalDate.ToLocalTime().AddDays($MinimumPatchAgeInDays) -le [datetime]::Now }
	$UpdateFiles = @()
	$CurrentFile = 0
	foreach ($WSUSUpdate in $WSUSUpdates)
	{
		$CurrentFile += 1
		$CurrentFilePercent = 100 - ((($WSUSUpdates.Count - $CurrentFile) / $WSUSUpdates.Count) * 100)
		Write-Progress -Activity 'Loading updates' -Status 'Finding downloaded files for selected updates' -CurrentOperation "Checking $($WSUSUpdate.Update.Title)" -PercentComplete $CurrentFilePercent
		$WSUSUpdate.Update.GetInstallableItems().Files | foreach {
			if ($_.Type -eq [Microsoft.UpdateServices.Administration.FileType]::SelfContained -and ($_.FileUri -match '[cab|msu]$'))
			{
				$LocalFileName = ($_.FileUri -replace '.*/Content', $WsusContentFolder) -replace '/', ''
				if(Test-Path -Path $LocalFileName)
				{
					Write-Verbose ('Adding "{0}" to the list of available patches.' -f $WSUSUpdate.Update.Title)
					$UpdateFiles += @{'Path' = $LocalFileName; 'Title' = $WSUSUpdate.Update.Title }
				}
			}
			else
			{
				Write-Verbose -Message ('{0} not added to patches list. Files must end in .cab or .msu and must be of type "SelfContained"' -f $WSUSUpdate.Update.Title)
				Write-Verbose -Message ('|--> File name: {0}' -f $_.FileUri)
				Write-Verbose -Message ('---> Patch type: {0}' -f $_.Type.ToString())
			}
		}
	}
	Write-Verbose -Message ('Eligible patches: {0}' -f $UpdateFiles.Count)
	Write-Progress -Activity 'Loading updates' -Completed

	foreach ($ImageToUpdate in $ImageList)
	{
		$TargetRoot = Split-Path -Path $ImageToUpdate.Path
		$TargetFileName = Split-Path -Path $ImageToUpdate.Path -Leaf
		$LogFile = Join-Path -Path $TargetRoot -ChildPath "$TargetFileName.wulog.txt"
		$CurrentImageLog = @()
		$PermanentLog = @()
		if(Test-Path -Path $LogFile)
		{
			$PermanentLog = Get-Content -Path $LogFile
		}
		try
		{
			$Test = $PermanentLog.Count
		}
		catch
		{
			$PermanentLog = @()
		}

		$PermanentLog += ('------- Patch Cycle Initiated {0} -------' -f (Get-Date))
		try
		{
			# the Mount-WindowsImage cmdlet has its own progress display
			$OutNull = Mount-WindowsImage -ImagePath $ImageToUpdate.Path -Index $ImageToUpdate.Index -Path $OfflineMountFolder -ErrorAction Stop
		}
		catch
		{
			$CurrentMessage = "Could not mount $($ImageToUpdate.Path)`r`n$($_.Message)"
			Write-Error -Message $CurrentMessage
			$PermanentLog += $CurrentMessage
			break
		}
	 
		if($UpdateFiles.Count)
		{
			$CurrentFile = 0
			foreach($UpdateFile in $UpdateFiles)
			{
				$CurrentFile += 1
				$CurrentFilePercent = 100 - ((($UpdateFiles.Count - $CurrentFile) / $UpdateFiles.Count) * 100)
				$CurrentFileLogEntry = '{0}:{1}' -f $ImageToUpdate.Index, $UpdateFile.Title
				if(-not ($CurrentFileLogEntry -in $PermanentLog))
				{
					Write-Progress -Activity 'Updating image' -CurrentOperation "Applying $($UpdateFile.Title)" -Status "Applying images to $($ImageToUpdate.Path)" -PercentComplete $CurrentFilePercent
					try
					{
						Write-Verbose -Message ('Applying {0} to {1}' -f $UpdateFile.Path, $ImageToUpdate.Path)
						$AddWindowsPackageOut = Add-WindowsPackage -PackagePath $UpdateFile.Path -Path $OfflineMountFolder -ErrorAction Stop -WarningAction Stop
						Write-Verbose -Message ("Applied {0} to {1} index {2}:`r`n{3}" -f $UpdateFile.Title, $ImageToUpdate.Path, $ImageToUpdate.Index, $AddWindowsPackageOut)
						$CurrentImageLog += "$CurrentFileLogEntry`r`n"
					}
					catch
					{
						Write-Warning -Message ('Add-WindowsPackage failed for {0} on {1}: {2}' -f $UpdateFile.Title, $ImageToUpdate.Path, $_)
					}
				}
				else
				{
					Write-Verbose -Message ('{0} has already been applied to image file {1} index {2}' -f $UpdateFile.Title, $ImageToUpdate.Path, $ImageToUpdate.Index)
				}
			}
			Write-Progress -Activity 'Updating image' -Completed
			try
			{
				$OutNull = Dismount-WindowsImage -Path $OfflineMountFolder -Save -ErrorAction Stop
			}
			catch
			{
				$CurrentMessage = "Unable to save changes to $($ImageToUpdate.Path): $($_.Message)"
				Write-Error -Message $CurrentMessage
				$CurrentImageLog = @("$CurrentMessage`r`n") #note the re-assignment; the updates will not be logged because they never really happened
				$OutNull = Dismount-WindowsImage -Path $OfflineMountFolder -Discard
			}
		}
		else
		{
			$CurrentMessage = 'No updates were selected'
			Write-Warning -Message $CurrentMessage
			$PermanentLog += $CurrentMessage
		}
		$PermanentLog = $PermanentLog | select -Unique
		$PermanentLog += $CurrentImageLog
		$PermanentLog += ('------- Patch Cycle Completed {0} -------' -f (Get-Date))
		Set-Content -Path $LogFile -Value $PermanentLog 
	}
#}															#uncomment this line to use this script dot-sourced or in a profile. Also the function definition lines at the beginning.

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

56 thoughts on "Free PowerShell Script: Use WSUS to Update Installation Media and Hyper-V Templates"

  • Andrey says:

    Thanks for your work here, and let me add my 2 cents.
    Current version of the code (as of 09/08/2016) requres some corretions, i.e. you shoud:

    1. Assign correct image index for the VHD mount.

    When I specify a VHDX image to update, you code assigns ‘0’ as a value of index while we surely need ‘1’ when mounting VHD (see https://technet.microsoft.com/en-us/library/dn376493.aspx). Because of this, I’ve got an error ‘Could not mount image…’

    So I moved this code:

    $SelectedIndexes | foreach {
    $ImageList = @{‘Path’ = $Path; ‘Index’ = $_ }
    }

    inside the “if($Path -imatch ‘wim$’) { … }” clause and added else { … } section to handle the VHD:

    else {
    $ImageList = @{‘Path’ = $Path; ‘Index’ = 1 }
    }

    2. Use the same parameter name and default value across the script code and comments.

    – At this article you desribe the MinimumAgeInDays parameter
    – At the starting comment section of the script you state this parameter as .PARAMETER MinimumPageAgeInDays (note the different name) and define that the default value is 0
    – At the param section of the script you declare $MinimumAgeInDays = 30 (note the different default value)
    – And finally, at the code itself you reference this parameter as “.AddDays($MinimumPatchAgeInDays)” (note another different version of the parameter name)

    • Eric Siron says:

      Not sure how #1 got out of testing. I did spend more time on the WIM code because it was tougher, but I did use this against VHDXs as well. Anyway, I took a simpler route than you did.
      #2 I think is just exposing that I need to work on my internal version control. I fixed that in a version that appears to have not reached publication.
      I incorporated both fixes and brought the published script to 1.1 with the updates.
      Thanks for the bug reports.

      • Andrey says:

        Thanks for the prompt reply and code corrections. Just one more comment from my side.
        It appears that every time I run the script against the same VHDX, all the update titles are logged to the file again and again. I ran script twice and I see in the log two identical lists of updates.
        Does it mean that script somehow misses previously installed and logged updates, and tries to reapply all of them again? Just FYI, all the update titles in my case are mostly in Russian (contain non-Latin characters). Could this be the reason for this beavior?

        • Eric Siron says:

          Hmm… Maybe? I would assume that Get-Content and -notcontains would work just as well in any character set.
          You could test it with a little script:

          $PatchList = Get-Content $PathTologFile
          if($PatchList -contains ‘paste the text of a patch title here’)
          { ‘Found the patch title’ }

          • Andrey says:

            Thanks for the suggestion, I checked that and see that ‘-contains’ itself works just as it should, regardless of character set of the text in the file.
            But alas, the script behavior is still the same – after the two subsequent runs, I have two identical set of updates in my log, and assume that all of them have been applied twice.

            Also, i have reverted to my own variant of the code for the single VHD index assignment. For some weird reason your corrected code does still assign ‘0’ 🙁

            Nevertheless, for now I’m totally satisfied with your script and I will not further bother you with this code nitpicking.
            And thanks again for your work here.

          • Eric Siron says:

            I would not call this nitpicking. The script doesn’t work.
            v1.2 is posted with fixes.
            I redid the singular VHDX portion a bit. My earlier change did work to add an index of “1” but it also injected a “0” because I had never coded something that I had intended to. As it was, it would have patched a VHDX but not without throwing an error first.
            I did some debug testing and with a Latin character set, the matching of previous entries in the log file does work. However, I recall someone else on a previous script having an issue with a line that depended on -notcontains. I’ve had some suspicions about that operator. So, I replaced the -notcontains logic with -not/-in logic (line 327). If you get an opportunity, see if that works for you.

  • Andrey says:

    Thanks for your work here, and let me add my 2 cents.
    Current version of the code (as of 09/08/2016) requres some corretions, i.e. you shoud:

    1. Assign correct image index for the VHD mount.

    When I specify a VHDX image to update, you code assigns ‘0’ as a value of index while we surely need ‘1’ when mounting VHD (see https://technet.microsoft.com/en-us/library/dn376493.aspx). Because of this, I’ve got an error ‘Could not mount image…’

    So I moved this code:

    $SelectedIndexes | foreach {
    $ImageList = @{‘Path’ = $Path; ‘Index’ = $_ }
    }

    inside the “if($Path -imatch ‘wim$’) { … }” clause and added else { … } section to handle the VHD:

    else {
    $ImageList = @{‘Path’ = $Path; ‘Index’ = 1 }
    }

    2. Use the same parameter name and default value across the script code and comments.

    – At this article you desribe the MinimumAgeInDays parameter
    – At the starting comment section of the script you state this parameter as .PARAMETER MinimumPageAgeInDays (note the different name) and define that the default value is 0
    – At the param section of the script you declare $MinimumAgeInDays = 30 (note the different default value)
    – And finally, at the code itself you reference this parameter as “.AddDays($MinimumPatchAgeInDays)” (note another different version of the parameter name)

  • Andrey says:

    Yes, single VHDX index assignment works now. I didn’t have opportunity to test the multiple images update as like as WIM update, though. Sorry for that.

    Regarding the always-growing update list, i finally figured it out. It have nothing to do with strings comparison at all. Your previous matching algorithm always worked as it should, so updates are never re-applied actually, it was just the log file itself growing.

    If you carefully review what your code is doing now, you’ll see that:
    1. You read previous log
    2. You add newly dicovered and applied updates (if there are any of them)
    3. You ADD this list to the log, effectively DOUBLING its contents and size at every subsequent run (plus size of new updates discovered in between).

    So, the obvious solution here is to use Set-Content instead of Add-Content at line 345 of the script.
    In fact, I wasn’t sure how this might affect WIM file update logging process (remember, I haven’t got opportunity to test it at all?), so instead of using Set-Update, I have implemented more conservative approach with collecting distinct list of currently found and applied updates and later adding them to the log like this:


    foreach ($ImageToUpdate in $ImageList)
    {

    $CurrentFile = 0
    $CurrentlyAppliedUpdates = @() #Newly discovered updates list
    foreach($UpdateFile in $UpdateFiles)
    {

    $CurrentlyAppliedUpdates = “$($ImageToUpdate.Index):$($UpdateFile.Title)`r`n”

    }

    try
    {

    Add-Content -Path $LogFile -Value $CurrentlyAppliedUpdates
    }

    }

    For me (the case with single VHDX update), this code works like a charm. I’m sure, you could figure (and/or test) yourself if this works for the multiple images and WIM upades logging either.

    • JanDr says:

      I tested this issue (zero-length log file) while updating a WIM image file. I am testing the 1.2 script version. The condition on the line 327 seems to be working as expected. Although the script execution never reaches the line 333. I inserted a breakpoint there, but the execution never stops there. That is the reason why the object $PreviouslyAppliedUpdates on the line 345 is empty. The result of my tests is always a zero-length log file. (*.wulog.txt)

      • Eric Siron says:

        Is it possible that you didn’t have any patches arrive on your WSUS server older than 30 days? I noticed that the documentation said that the default minimum patch age was 0, but the script was set to 30. I have changed the default to match the text in the documentation and added a check that bypasses the entire mount procedure and writes to the image’s log if no patches were selected. That is the most probable reason for that bit of script to not execute.

    • Eric Siron says:

      I do have some that I can still test with, but the length of time makes it tricky.
      I have done a small re-work of the logging mechanism in 1.4 that more closely matches my original intent. It will also clear out any old duplicates.

      • Andrey says:

        Dude, it’s scaries me how much correction you make to your code with every other script build version. The limited time frame and eagerness to publish new – now “definitely corrected” – version, I assume, does not even allow you to test what you’ve written.

        In your fresh code (version 1.4) I found at least two places, where you haven’t replaced ol’ good $PreviouslyAppliedUpdates variable to the shiny new $PermanentLog variable

        At the line 344 this causes your code to fail to find previously applied updates in the log.
        At the line 380 this causes your code to save an empty log file, regardless of the found and applied updates.

        After appropriate corrections, the code seems to work fine for the single VHD update.
        As a tiny convenience correction, I have also removed extra line break (`r`n) from the line 350 of the code, as the update title itself ($UpdateFile.Title) appears to end with a line break.

  • Andrey says:

    Yes, single VHDX index assignment works now. I didn’t have opportunity to test the multiple images update as like as WIM update, though. Sorry for that.

    Regarding the always-growing update list, i finally figured it out. It have nothing to do with strings comparison at all. Your previous matching algorithm always worked as it should, so updates are never re-applied actually, it was just the log file itself growing.

    If you carefully review what your code is doing now, you’ll see that:
    1. You read previous log
    2. You add newly dicovered and applied updates (if there are any of them)
    3. You ADD this list to the log, effectively DOUBLING its contents and size at every subsequent run (plus size of new updates discovered in between).

    So, the obvious solution here is to use Set-Content instead of Add-Content at line 345 of the script.
    In fact, I wasn’t sure how this might affect WIM file update logging process (remember, I haven’t got opportunity to test it at all?), so instead of using Set-Update, I have implemented more conservative approach with collecting distinct list of currently found and applied updates and later adding them to the log like this:


    foreach ($ImageToUpdate in $ImageList)
    {

    $CurrentFile = 0
    $CurrentlyAppliedUpdates = @() #Newly discovered updates list
    foreach($UpdateFile in $UpdateFiles)
    {

    $CurrentlyAppliedUpdates = “$($ImageToUpdate.Index):$($UpdateFile.Title)`r`n”

    }

    try
    {

    Add-Content -Path $LogFile -Value $CurrentlyAppliedUpdates
    }

    }

    For me (the case with single VHDX update), this code works like a charm. I’m sure, you could figure (and/or test) yourself if this works for the multiple images and WIM upades logging either.

  • Bernd Oliver says:

    I tried this script also with W2016 install.wim and it does not work. Could anybody confirm this?

    • Eric Siron says:

      Can you be more specific? Did you remember to override -TargetProduct?

      • Bernd Oliver says:

        Yes, i used the -TargetProduct “Windows Server 2016” Switch.
        The updates are simply not applied to the install.wim image.

        In the install.wim.wulog.txt is only:
        ——- Patch Cycle Initiated 10/24/2016 5:33:56 PM
        ——- Patch Cycle Completed 10/24/2016 5:52:18 PM ——-

        • Eric Siron says:

          Run the following in an elevated PowerShell prompt on the WSUS server that contains the WS2016 updates, and post back the number that it displays:

          (Get-WsusUpdate -Approval Approved |
          where { -not $_.Update.IsSuperseded -and -not $_.Update.IsDeclined -and (Compare-Object -DifferenceObject $_.Products -ReferenceObject ‘Windows Server 2016’ -ExcludeDifferent -IncludeEqual) -and $_.Update.ArrivalDate.ToLocalTime().AddDays(0) -le [datetime]::Now }).Count

  • Bernd Oliver says:

    Hey Eric,
    yes, thanks.
    Output is: 12

    • Eric Siron says:

      I do not see any overt reason in the script that the patches would not be applied. I have updated the script to version 1.5 which includes the ability to see patch application status IF you use the -Verbose switch. It will tell you when it tries to apply a patch what the outcome is.

      • Bernd Oliver says:

        Hi Eric,
        verbose output shows me:
        VERBOSE: Dism PowerShell Cmdlets Version 6.3.0.0.

        • Eric Siron says:

          That’s not the expected output. It should show something, once for each attempted patch. I would like to see it for myself. But, WSUS being WSUS, it refuses to cooperate with me for even a basic installation. I’ll try to look at this at some point within the next few days.

          • Bernd Oliver says:

            Thanks Eric, very kind….!

          • Eric Siron says:

            I expanded the script with some more verbose points and allowed it to write messages for the Add-WindowsPackage line. Simply, the issue is that a Windows Server 2012 R2 version of the DISM executable and PowerShell module cannot update a Windows Server 2016 image. It might work if the latest MDT is installed: https://technet.microsoft.com/en-us/windows/dn475741.aspx. I will try this out when I can, if no one beats me to it.

          • Bernd Oliver says:

            I used your script now in a Windows 2016 Server Eviroment with installed WSUS, this did the trick for me and it works as expected.
            Again thanks a lot, Eric!

  • Bernd Oliver says:

    Hey Eric,
    yes, thanks.
    Output is: 12

  • Steve says:

    Nice script Eric,
    I have tried this but I found a couple of things that seem a little odd.
    1, if i pass multiple wims and indexes into the script i seem to get the same wsus log file copied next to the install.wim (with previous patch cycle dates odly), for example if i ran the patch run against 1607 on the 18th october, and then i ran the patch cycle against 1511 on the 20th october, my wsus log for 1511 includes the wsus log details from the 18th october. I am running it all again to test this, and i have also put an extra line of code to make the $PermenantLog = $Null after the run, see if that helps.
    the things im passing in for example are as follows:

    $Images = @(
    @{‘Path’=’E:InstallMediaWindows10-1511sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 3},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 4},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 3},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 4},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607NanoServerNanoServer.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607NanoServerNanoServer.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaVHDXWindowsClient.vhdx’}

    2. i have a vhdx which contains windows 10 and office 2016, although i have included office 2016 into the scoped updates these don’t seem to apply, only 1 flash player update applied to the vhdx according to the log file, just curious to know if VHDX patching works differently than wim, and if non windows updates should even apply to the vhdx at all in the first place? potentially there could be images out there with far more than just office which could benefit from patching, havent tried a captured wim including office yet.

    just remembered i didnt sysprep the vhdx either so i dont know if that would affect things.

    thanks
    Steve

    • Steve says:

      i’m also not sure if you actually want to be reading a log file so you can skip previously applied KB’s, i thought the new patching method of Windows 10 releases the same KB each month as a cumulative update but it’s just a new version of that KB isn’t it? if that’s the case, you could apply KB1234 in July, and miss a whole bunch of fixes that were released in KB1234 in Aug, Sept, Oct and so on. i haven’t looked much into windows 10 patching to be honest so if im wrong – im wrong, fair enough.

      • Eric Siron says:

        This script was not written with the new pattern in mind, so I’ll need to revisit that to see how it will work. It’s not log-matching on the KB article #, but on the entire update title. One character difference is enough.

    • Eric Siron says:

      Why would a log file not include previous runs? I’m not certain that I’m understanding what you’re saying.
      If you open the log file in a text editor, each entry is prefixed with a number. The number corresponds to the index in the image. VHDX will always be 1. I will look at the logic again to be sure that WIMs are properly reviewing previous runs.
      I will check the logic around multiple products. I do not have any way of my own to verify that Office products work against an offline image.
      Sysprep shouldn’t matter.

      • Steve says:

        Sorry i meant about the log files:
        say i patched 1511.wim a few times, it creates a log and appends to it – fine i’d expect that
        later, 1607.wim gets released and i want to patch that, but it appears that the log file from 1511.wim is added to the log file for 1607.wim

        for example if the first time i ever patched 1607.wim was the 25th october, i shouldnt see patch cycles in that log file for runs which took place on 18th october (those entries are relating to patching other wims).
        i think the script is reading the first log in full, then continues to append the patches, writes it out to the log file when done, but the variable never gets cleared so in the next wim it still contains the previous log file data, so i added a line to set the var to null and hoping that fixes the issue, just about to test it now actually.

    • Eric Siron says:

      I have made some changes that might address what you’re reporting. Delete your log files and try again. I added a control that will cause the outcome of any attempted patch to display IF you use the -Verbose parameter. Edit to add: there is new script above, version 1.5.

  • Steve says:

    Nice script Eric,
    I have tried this but I found a couple of things that seem a little odd.
    1, if i pass multiple wims and indexes into the script i seem to get the same wsus log file copied next to the install.wim (with previous patch cycle dates odly), for example if i ran the patch run against 1607 on the 18th october, and then i ran the patch cycle against 1511 on the 20th october, my wsus log for 1511 includes the wsus log details from the 18th october. I am running it all again to test this, and i have also put an extra line of code to make the $PermenantLog = $Null after the run, see if that helps.
    the things im passing in for example are as follows:

    $Images = @(
    @{‘Path’=’E:InstallMediaWindows10-1511sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 3},
    @{‘Path’=’E:InstallMediaWindows10-1607sourcesinstall.wim’; ‘Index’ = 4},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 3},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607sourcesinstall.wim’; ‘Index’ = 4},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607NanoServerNanoServer.wim’; ‘Index’ = 1},
    @{‘Path’=’E:InstallMediaWindows Server 2016 1607NanoServerNanoServer.wim’; ‘Index’ = 2},
    @{‘Path’=’E:InstallMediaVHDXWindowsClient.vhdx’}

    2. i have a vhdx which contains windows 10 and office 2016, although i have included office 2016 into the scoped updates these don’t seem to apply, only 1 flash player update applied to the vhdx according to the log file, just curious to know if VHDX patching works differently than wim, and if non windows updates should even apply to the vhdx at all in the first place? potentially there could be images out there with far more than just office which could benefit from patching, havent tried a captured wim including office yet.

    just remembered i didnt sysprep the vhdx either so i dont know if that would affect things.

    thanks
    Steve

  • Steve says:

    Oh, and also I noticed that if the server happened to crash/BSOD whilst the wim was mounted, it’s not as simple as just deleting the offline folder, it seems that the system still knows the wim was in a mounted state so it fails to mount next time the script is run. adding a Dismount-WindowsImage as part of the environment validation seemed to work nicely. i havent handled the error when the previous wim was cleanly dismounted but it works anyway

    to give context, i placed the code here. the dism command also works but i commented mine out

    Write-Progress -Activity ‘Validating environment’ -Status ‘Checking image information’ -PercentComplete 25
    #sometimes the previous run might leave a stale mounted folder especially if the server crashed or rebooted.
    #dismount the image if it already exists, then delete the offline folder
    Dismount-WindowsImage -Path $OfflineMountFolder -Discard
    #dism /cleanup-wim
    Remove-Item $OfflineMountFolder -recurse -Force $true

    • Eric Siron says:

      Well done! I was unable to simulate this particular failure type to be sure that sort of thing would work. Now that you’ve confirmed it, I’ll roll it into an upcoming iteration of the distribution script.

      • Steve says:

        the only thing i didnt figure out just yet, is whether the dismount-windowsimage is doing the same thing as dism /cleanup-wim
        i used the dism command when i really messed it up, by resetting permissions and taking ownership of the offline folder, deleting stuff from there, and deleting the original wim and copying it back (this was all done before i realised the dism command could be used for fixing this), since then what i did was simply restart the server whilst the image was mounted and the next time the script runs it dismounts cleanly first, this is more likely to be the situation people will be in rather than them trying to change perms and delete stuff to fix it. i probably will give it a test at some point soon.

        i would assume dismount-windowsimage does exactly the same as dism /cleanup-wim right? i couldnt seem to find any resource to confirm this, i’d rather do everything in PS than start mixing commands.

        also in reference to the patching office products, last time i updated a patched a wim in a scripted form i am sure i did office updates as well (though i could be wrong, it might have just been the OS in the wims), this is the first time i’ve tried a VHDX. i’ll try and remember to dig out my old script that did this over the weekend, and if im lucky get some time to test it. this kind of thing sure does take a lot of time to test due to the slowness, can’t be helped.

        • Eric Siron says:

          I’ve added several new -Verbose write points that might help. Get the current version of the script and run it with -Verbose to see if it says anything about the Office patches. I also integrated your automatic dismount, although I placed it differently.

  • Steve says:

    Oh, and also I noticed that if the server happened to crash/BSOD whilst the wim was mounted, it’s not as simple as just deleting the offline folder, it seems that the system still knows the wim was in a mounted state so it fails to mount next time the script is run. adding a Dismount-WindowsImage as part of the environment validation seemed to work nicely. i havent handled the error when the previous wim was cleanly dismounted but it works anyway

    to give context, i placed the code here. the dism command also works but i commented mine out

    Write-Progress -Activity ‘Validating environment’ -Status ‘Checking image information’ -PercentComplete 25
    #sometimes the previous run might leave a stale mounted folder especially if the server crashed or rebooted.
    #dismount the image if it already exists, then delete the offline folder
    Dismount-WindowsImage -Path $OfflineMountFolder -Discard
    #dism /cleanup-wim
    Remove-Item $OfflineMountFolder -recurse -Force $true

  • Steve says:

    Hi,
    I tested the Dismount-WindowsImage a bit more, it seems it properly cleans everything up. my test was to get the wim mounted and patches were being applied. i then rebooted the server, logged back in and reset the ownership and ACLS down the folders, deleted them, even renamed the offline folder but after this the new image won’t mount, ran the Dismount-WindowsImage and it works fine.

  • Steve says:

    Hi,
    I tested the Dismount-WindowsImage a bit more, it seems it properly cleans everything up. my test was to get the wim mounted and patches were being applied. i then rebooted the server, logged back in and reset the ownership and ACLS down the folders, deleted them, even renamed the offline folder but after this the new image won’t mount, ran the Dismount-WindowsImage and it works fine.

  • Steve says:

    Hi Eric,
    I found a useful little bit of info in the following Ignite Video about how Azure Stack will be patched and wondered if you could share your thoughts and ideas because I’m considering using (at least partially) this awesome script you have made in our environment if that’s allowed.

    The key bit is mentioned in just 2 minutes of this video “Learn about Azure Stack Infrastructure Operations and Management” at https://myignite.microsoft.com/videos/3114 (search the term “Azure Stack will only boot from VHD”).

    This sounds a really good way to patch a host hyper-v and I came up with an extremely similar idea myself (just blue sky thinking) a few months back when I thought about plans to upgrade our data centre environment to Server 2016. Our environment however does not use hyper-v clustering, but I feel I could automate a fair bit of this up to the point of needing a host reboot (that will be done during maintenance windows).

    What I want to achieve is to be able to reliably patch an installation and make sure the thing comes back up afterwards, with minimum downtime (preferably without the “configuring windows” stuff that takes an age to do). Our hosts are very difficult to patch and it can be months until we patch them due to causing downtime (you’re preaching to the converted if you tell me to use hyper-v clustering, I know – I have what I have I’m afraid and I have to make the most of it).

    How I think I want to achieve this is by doing something like this:
    • Boot to VHDX
    • This system will be joined to the domain, I’m hoping I’d be able to copy the VHDX which is booted (haven’t tested that but I can copy a VHD which is attached to a live running VM so I assume I can do this without error)
    • Patch the copied VHDX
    • Create a test VM, attach the patched VHDX, start it, use PS Direct or some other means to check the VM has successfully booted (haven’t fully thought of that yet), shut it down, delete the VM
    • Switch the boot loader to use the newly patched VHDX
    • Manual reboot during a maintenance window
    • Delete the original unpatched VHDX and start the process again next patch cycle

    What I’m not sure about is in the video the guy mentions “we don’t update the hypervisor host itself, we actually just reimage it….. we construct a new VHDX and just point it to the physical host and we flip the boot order”

    What’s your thoughts on this in terms of what he means here, and any further input on ideas for the process? Does he mean they create an empty new VHDX and get a new OS installed inside it and patch it, thus meaning it would have to re-join to the domain and get a new SID, or do you think he means the process is similar to what I mentioned above by copying the original actively used VHDX and just patch that? How do you think I could best do this?

    Would appreciate your thoughts
    Steve

    • Eric Siron says:

      Keep in mind that they do things in Azure that are not available to us. I’ve heard of a few things that make me very jealous, but NDA keeps me quiet.
      I’d also like to point out that they are using a cloud mentality. No virtual machine belongs to a host. They talk in the material about not using clustering because their “clusters” are so large, but the hosts are still generalized and they are still moving guests off for host maintenance. Using high-dollar physical storage underneath S2D with great big networking pipes would allow them to perform very rapid SNLM. If you can’t move your guests off, that might be the part that binds you.
      I don’t know exactly what they are doing, of course, but what they could do is lay down another VHDX on local storage right beside the running VHDX and use BCDEDIT to retarget it for the next boot. A few first-run scripts could easily put it back in production within a few minutes after startup.

  • JanDr says:

    Thanks for your work Eric. Script works fine with default product selection Windows Server 2012 R2. If another product is selected for update, for example Windows Vista, then none updates are applied with warning for every update package in dism logs.
    WARNING: Add-WindowsPackage failed for Program Internet Explorer 8 for system Windows Vista on install.wim: An error occurred. No operation was performed.
    Is this dism compatibility problem? Which products can be updated using this script?
    I am running script within Windows 10 environment and wsus part is available from the network.

    • Eric Siron says:

      The only product that I can guarantee it works with is 2012 R2, but anything else would probably be a DISM compatibility problem.
      However, if there are multiple products selected, then it will attempt to apply ALL patches that fit that product selection to the target, which will result in many warnings. For example, if you pick ‘Windows 10’, ‘Windows Vista’, then it will try to apply every Windows 10 and Windows Vista package to every WIM/VHDX that you point it to.
      Another poster confirmed that it can work for Server 2016 IF it is run on a host that has the 2016 DISM cmdlets. I would assume the same would be true for Windows 10.

  • JanDr says:

    Thanks for your work Eric. Script works fine with default product selection Windows Server 2012 R2. If another product is selected for update, for example Windows Vista, then none updates are applied with warning for every update package in dism logs.
    WARNING: Add-WindowsPackage failed for Program Internet Explorer 8 for system Windows Vista on install.wim: An error occurred. No operation was performed.
    Is this dism compatibility problem? Which products can be updated using this script?
    I am running script within Windows 10 environment and wsus part is available from the network.

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published.

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.