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.

#block 1

mount-vhd -path G:\Temp\Win2012DCRTM.vhdx

 

#block 2

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

 

#block 3

$command = "dism /image:i:\ /Cleanup-Image /spsuperseded" Invoke-Expression $command

 

#block 4

dismount-vhd -path G:\Temp\Win2012DCRTM.vhdx -confirm:$false

Below is an example execution in action:

PS D:\software\Windows 2012 Updates> mount-vhd -path G:\Temp\Win2012DCRTM.vhdx

PS D:\software\Windows 2012 Updates> $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
}


Here is the output from the example:

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Cumulative Security Update for Internet Explorer 10 for
Windows Server 2012 (KB2761465)\AMD64-all-windows8-rt-kb2761465-x64_71efb0756cf746951571a72c83ddb55775362418.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Microsoft .NET Framework 4.5 on Wind
ows 8 and Windows Server 2012 for x64-based Systems (KB2737084)\AMD64_X86_ARM-all-windows8-rt-kb2737084-x64_1a1b73c30d7bd20b
61fc522890a8fd61370ae1bb.msu
The operation completed successfully.
 ....


Here is the next command, with output:

PS D:\software\Windows 2012 Updates> $command = "dism /image:i:\ /Cleanup-Image /spsuperseded"
Invoke-Expression $command

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
The operation completed successfully.


 

And the next command:

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.

function Install-Patch
{
    <#
    .SYNOPSIS
        Patches a WIM or VHD file
    .DESCRIPTION
        Applies downloaded patches to a VHD or WIM file
    .NOTES
        File Name: Install-Patch.psm1
        Author: John Savill
        Requires: Tests on PowerShell 3 on Windows Server 2012
        Copyright (c) 2013 John Savill
    .LINK

        http://www.savilltech.com/
    .PARAMETER updateTargetPassed
        File (WIM, VHD or VHDX) to be patched
    .PARAMETER patchpath
        Path containing the updates
    .EXAMPLE
        Install-Patch d:\files\test.vhd d:\updates\win2012\
        Install-Patch d:\files\install.wim:4 d:\updates\win2012\
    #>
   
    [cmdletbinding()]
    Param(
    [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$updateTargetPassed,
    [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$patchpath)

    #$updateTargetPassed = "G:\Temp\Win2012DatacenterRTM.vhdx"
    #or
    #$updateTargetPassed = "d:\sources\install.wim:4"
    #$patchpath = "D:\software\Windows 2012 Updates\"

    if(($updateTargetPassed.ToLower().Contains(".vhd")) -eq $true) # if its VHD or VHDX. Contains is case sensitive so have to convert to lower when comparing
    {
        $isVHD = $true
    }
    else
    {
        $isVHD = $false
    }

    if($isVHD)
    {
        $updateTarget=$updateTargetPassed
        if ((Test-Path $updateTarget) -eq $false) #if not found
        {
            write-output "Source not found ($updateTarget)"
            break
        }
        else
        {
            mount-vhd -path $updateTarget
            $disks = Get-CimInstance -ClassName Win32_DiskDrive | where Caption -eq "Microsoft Virtual Disk"          
            foreach ($disk in $disks)
            {          
                $vols = Get-CimAssociatedInstance -CimInstance $disk -ResultClassName Win32_DiskPartition            
                foreach ($vol in $vols)
                {          
                    $updatedrive = Get-CimAssociatedInstance -CimInstance $vol -ResultClassName Win32_LogicalDisk |          
                    where VolumeName -ne 'System Reserved'        
                }          
            }
            $updatepath = $updatedrive.DeviceID + "\"
        }
    }
    if(!$isVHD)  #its a WIM file
    {
        #Need to extract the WIM part and the index
        #extract file name and the index number
        $updateTargetPassedSplit = $updateTargetPassed.Split(":")
        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
        {
            $updateTarget = $updateTargetPassedSplit[0] + ":" + $updateTargetPassedSplit[1]   #There are two colons. The first is drive letter then the folder!
            $updateTargetIndex = $updateTargetPassedSplit[2]
            $updatepath = "c:\wimmount\"

            #check if exists and if not create it
            if ((Test-Path $updatepath) -eq $false) #if not found
            {
                Write-Host "Creating folder " + $updatepath
                New-Item -Path $updatepath -ItemType directory
                #could have also used [system.io.directory]::CreateDirectory($updatepath)
            }

            # Mount it as folder
            #dism /get-wiminfo /wimfile:install.wim
            dism /Mount-Wim /wimfile:$updateTarget /index:$updateTargetIndex /mountdir:$updatepath
        }
        else
        {
            write-output "Missing index number for WIM file. Example: c:\temp\install.wim:4"
            break
        }
    }

    # For WIM or VHD
    $updates = get-childitem -path $patchpath -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname
    foreach($update in $updates)
    {
        write-debug $update.fullname
        $command = "dism /image:" + $updatepath + " /add-package /packagepath:'" + $update.fullname + "'"
        write-debug $command
        Invoke-Expression $command
    }

    $command = "dism /image:" + $updatepath + " /Cleanup-Image /spsuperseded"
    Invoke-Expression $command

    if($isVHD)
    {
        dismount-vhd -path $updateTarget -confirm:$false
    }
    else
    {
        dism /Unmount-Wim /mountdir:$updatepath /commit
        #dism /Unmount-Wim /mountdir:$updatepath /discard
    }
}

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

 

PS C:\> Install-Patch G:\temp\Win2012DatacenterRTM.vhdx 'D:\software\Windows 2012 Updates'

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
The operation completed successfully.

PS C:\> Install-Patch d:\sources\install.wim:4 'D:\software\Windows 2012 Updates'

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Mounting image
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image Version: 6.2.9200.16384

Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
The operation completed successfully.

Deployment Image Servicing and Management tool
Version: 6.2.9200.16384

Image File : d:\sources\install.wim
Image Index : 4
Saving image
Unmounting image
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.