Q: How can I add updates to an offline VHD or WIM file downloaded from the Microsoft Catalog?

A: Various solutions help in the automated updating of a virtual hard disk (VHD) file from Windows Update Services or Configuration Manager. However, you can also manually inject updates into a VHD using the DISM utility, and it's actually pretty simple.

After you download all the updates to a folder, you will have one folder with lots of subfolders (one for each downloaded update from the catalog). The Windows PowerShell script below mounts your VHD and assumes it has been mounted as drive letter I, then searches for all updates in subfolders of the current folder and applies them all to the VHD file, performs a cleanup of the image (if a Service Pack was installed there are many files that have been replaced) and then dismounts.

I save this as a script but actually run it manually by selecting the various blocks of the code within the Integrated Scripting Editor, so I can check each stage of the progress.

I use this process to keep my master Windows Server 2012 VHD patched with the latest update each month, so when it's deployed as part of a new OS deployment, it's already fully patched.

  1. #block 1
  2.  
  3. mount-vhd -path G:\Temp\Win2012DCRTM.vhdx

 

  1. #block 2
  2.  
  3. $updates = get-childitem -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname foreach($update in $updates) { write-debug $update.fullname $command = "dism /image:i:\ /add-package /packagepath:'" + $update.fullname + "'" write-debug $command Invoke-Expression $command }

 

  1. #block 3
  2.  
  3. $command = "dism /image:i:\ /Cleanup-Image /spsuperseded" Invoke-Expression $command

 

  1. #block 4
  2.  
  3. dismount-vhd -path G:\Temp\Win2012DCRTM.vhdx -confirm:$false

Below is an example execution in action:

  1. PS D:\software\Windows 2012 Updates> mount-vhd -path G:\Temp\Win2012DCRTM.vhdx
  2.  
  3. PS D:\software\Windows 2012 Updates> $updates = get-childitem -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname
  4. foreach($update in $updates)
  5. {
  6. write-debug $update.fullname
  7. $command = "dism /image:i:\ /add-package /packagepath:'" + $update.fullname + "'"
  8. write-debug $command
  9. Invoke-Expression $command
  10. }


Here is the output from the example:

  1. Deployment Image Servicing and Management tool
  2. Version: 6.2.9200.16384
  3.  
  4. Image Version: 6.2.9200.16384
  5.  
  6. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Cumulative Security Update for Internet Explorer 10 for
  7. Windows Server 2012 (KB2761465)\AMD64-all-windows8-rt-kb2761465-x64_71efb0756cf746951571a72c83ddb55775362418.msu
  8. The operation completed successfully.
  9.  
  10. Deployment Image Servicing and Management tool
  11. Version: 6.2.9200.16384
  12.  
  13. Image Version: 6.2.9200.16384
  14.  
  15. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Microsoft .NET Framework 4.5 on Wind
  16. ows 8 and Windows Server 2012 for x64-based Systems (KB2737084)\AMD64_X86_ARM-all-windows8-rt-kb2737084-x64_1a1b73c30d7bd20b
  17. 61fc522890a8fd61370ae1bb.msu
  18. The operation completed successfully.
  19.  ....


Here is the next command, with output:

  1. PS D:\software\Windows 2012 Updates> $command = "dism /image:i:\ /Cleanup-Image /spsuperseded"
  2. Invoke-Expression $command
  3.  
  4. Deployment Image Servicing and Management tool
  5. Version: 6.2.9200.16384
  6.  
  7. Image Version: 6.2.9200.16384
  8.  
  9. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  10. The operation completed successfully.


 

And the next command:

  1. PS D:\software\Windows 2012 Updates> dismount-vhd -path G:\Temp\Win2012DCRTM.vhdx -confirm:$false

Notice there was no Service Pack applied, so the step to clean up the image didn't perform any action.

The same set of commands can be used to patch a master WIM file that is used for new deployments or for deployed OSs to get updated or have corrupt features removed.

I got carried away creating this and created a full PowerShell function that basically does everything for you for a WIM or VHD building on those manually executed commands I showed above. Just pass the file to update and the patch folder location.

Note the script assumes it can use c:\wimmount as its mount and will create this folder if it does not exist. It also assumes if using a VHD that no other VHD file is attached on the system.

Click the link to my website here to download the psm1 file and save to the <your user folder>\Documents\WindowsPowerShell\Modules\Install-Patch folder as Install-Patch.psm1, which will allow the module to be available automatically and usable. Below is the actual code from the PSM1 file.

  1. function Install-Patch
  2. {
  3.     <#
  4.     .SYNOPSIS
  5.         Patches a WIM or VHD file
  6.     .DESCRIPTION
  7.         Applies downloaded patches to a VHD or WIM file
  8.     .NOTES
  9.         File Name: Install-Patch.psm1
  10.         Author: John Savill
  11.         Requires: Tests on PowerShell 3 on Windows Server 2012
  12.         Copyright (c) 2013 John Savill
  13.     .LINK
  14.  
  15.         http://www.savilltech.com/
  16.     .PARAMETER updateTargetPassed
  17.         File (WIM, VHD or VHDX) to be patched
  18.     .PARAMETER patchpath
  19.         Path containing the updates
  20.     .EXAMPLE
  21.         Install-Patch d:\files\test.vhd d:\updates\win2012\
  22.         Install-Patch d:\files\install.wim:4 d:\updates\win2012\
  23.     #>
  24.    
  25.     [cmdletbinding()]
  26.     Param(
  27.     [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$updateTargetPassed,
  28.     [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$patchpath)
  29.  
  30.     #$updateTargetPassed = "G:\Temp\Win2012DatacenterRTM.vhdx"
  31.     #or
  32.     #$updateTargetPassed = "d:\sources\install.wim:4"
  33.     #$patchpath = "D:\software\Windows 2012 Updates\"
  34.  
  35.     if(($updateTargetPassed.ToLower().Contains(".vhd")) -eq $true) # if its VHD or VHDX. Contains is case sensitive so have to convert to lower when comparing
  36.     {
  37.         $isVHD = $true
  38.     }
  39.     else
  40.     {
  41.         $isVHD = $false
  42.     }
  43.  
  44.     if($isVHD)
  45.     {
  46.         $updateTarget=$updateTargetPassed
  47.         if ((Test-Path $updateTarget) -eq $false) #if not found
  48.         {
  49.             write-output "Source not found ($updateTarget)"
  50.             break
  51.         }
  52.         else
  53.         {
  54.             mount-vhd -path $updateTarget
  55.             $disks = Get-CimInstance -ClassName Win32_DiskDrive | where Caption -eq "Microsoft Virtual Disk"          
  56.             foreach ($disk in $disks)
  57.             {          
  58.                 $vols = Get-CimAssociatedInstance -CimInstance $disk -ResultClassName Win32_DiskPartition            
  59.                 foreach ($vol in $vols)
  60.                 {          
  61.                     $updatedrive = Get-CimAssociatedInstance -CimInstance $vol -ResultClassName Win32_LogicalDisk |          
  62.                     where VolumeName -ne 'System Reserved'        
  63.                 }          
  64.             }
  65.             $updatepath = $updatedrive.DeviceID + "\"
  66.         }
  67.     }
  68.     if(!$isVHD)  #its a WIM file
  69.     {
  70.         #Need to extract the WIM part and the index
  71.         #extract file name and the index number
  72.         $updateTargetPassedSplit = $updateTargetPassed.Split(":")
  73.         if($updateTargetPassedSplit.Count -eq 3) #one for drive letter, one for folder and one for image number so would have been two colons in it c:\temp\install.wim:4
  74.         {
  75.             $updateTarget = $updateTargetPassedSplit[0] + ":" + $updateTargetPassedSplit[1]   #There are two colons. The first is drive letter then the folder!
  76.             $updateTargetIndex = $updateTargetPassedSplit[2]
  77.             $updatepath = "c:\wimmount\"
  78.  
  79.             #check if exists and if not create it
  80.             if ((Test-Path $updatepath) -eq $false) #if not found
  81.             {
  82.                 Write-Host "Creating folder " + $updatepath
  83.                 New-Item -Path $updatepath -ItemType directory
  84.                 #could have also used [system.io.directory]::CreateDirectory($updatepath)
  85.             }
  86.  
  87.             # Mount it as folder
  88.             #dism /get-wiminfo /wimfile:install.wim
  89.             dism /Mount-Wim /wimfile:$updateTarget /index:$updateTargetIndex /mountdir:$updatepath
  90.         }
  91.         else
  92.         {
  93.             write-output "Missing index number for WIM file. Example: c:\temp\install.wim:4"
  94.             break
  95.         }
  96.     }
  97.  
  98.     # For WIM or VHD
  99.     $updates = get-childitem -path $patchpath -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname
  100.     foreach($update in $updates)
  101.     {
  102.         write-debug $update.fullname
  103.         $command = "dism /image:" + $updatepath + " /add-package /packagepath:'" + $update.fullname + "'"
  104.         write-debug $command
  105.         Invoke-Expression $command
  106.     }
  107.  
  108.     $command = "dism /image:" + $updatepath + " /Cleanup-Image /spsuperseded"
  109.     Invoke-Expression $command
  110.  
  111.     if($isVHD)
  112.     {
  113.         dismount-vhd -path $updateTarget -confirm:$false
  114.     }
  115.     else
  116.     {
  117.         dism /Unmount-Wim /mountdir:$updatepath /commit
  118.         #dism /Unmount-Wim /mountdir:$updatepath /discard
  119.     }
  120. }

Below are some example usages of the module in action for both a VHDX and a WIM file.

 

  1. PS C:\> Install-Patch G:\temp\Win2012DatacenterRTM.vhdx 'D:\software\Windows 2012 Updates'
  2.  
  3. Deployment Image Servicing and Management tool
  4. Version: 6.2.9200.16384
  5.  
  6. Image Version: 6.2.9200.16384
  7.  
  8. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
  9. 4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
  10. The operation completed successfully.
  11.  
  12. Deployment Image Servicing and Management tool
  13. Version: 6.2.9200.16384
  14.  
  15. Image Version: 6.2.9200.16384
  16.  
  17. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
  18. 4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
  19. The operation completed successfully.
  20.  
  21. Deployment Image Servicing and Management tool
  22. Version: 6.2.9200.16384
  23.  
  24. Image Version: 6.2.9200.16384
  25.  
  26. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
  27. 4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
  28. The operation completed successfully.
  29.  
  30. Deployment Image Servicing and Management tool
  31. Version: 6.2.9200.16384
  32.  
  33. Image Version: 6.2.9200.16384
  34.  
  35. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  36. The operation completed successfully.
  37.  
  38. PS C:\> Install-Patch d:\sources\install.wim:4 'D:\software\Windows 2012 Updates'
  39.  
  40. Deployment Image Servicing and Management tool
  41. Version: 6.2.9200.16384
  42.  
  43. Mounting image
  44. The operation completed successfully.
  45.  
  46. Deployment Image Servicing and Management tool
  47. Version: 6.2.9200.16384
  48.  
  49. Image Version: 6.2.9200.16384
  50.  
  51. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
  52. 4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
  53. The operation completed successfully.
  54.  
  55. Deployment Image Servicing and Management tool
  56. Version: 6.2.9200.16384
  57.  
  58. Image Version: 6.2.9200.16384
  59.  
  60. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
  61. 4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
  62. The operation completed successfully.
  63.  
  64. Deployment Image Servicing and Management tool
  65. Version: 6.2.9200.16384
  66.  
  67. Image Version: 6.2.9200.16384
  68.  
  69. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
  70. 4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
  71. The operation completed successfully.
  72.  
  73. Deployment Image Servicing and Management tool
  74. Version: 6.2.9200.16384
  75.  
  76. Image Version: 6.2.9200.16384
  77.  
  78. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  79. The operation completed successfully.
  80.  
  81. Deployment Image Servicing and Management tool
  82. Version: 6.2.9200.16384
  83.  
  84. Image File : d:\sources\install.wim
  85. Image Index : 4
  86. Saving image
  87. Unmounting image
  88. The operation completed successfully.

I created a small video that walks through the script and its usage to help, which is at my website or you can find it on YouTube.