Update Offline Virtual Machine with PowerShell and WSUS Offline Update: Part 2

Summary: Microsoft PowerShell MVP, Sean Kearney, talks about updating an offline virtual machine with Windows PowerShell and WSUS Offline Update.

Honorary Scripting Guy, Sean Kearney, here. Yesterday, I introduced you to a tool you can use to updating offline virtual machines: Update Offline Virtual Machine with PowerShell and WSUS Offline Update: Part 1. Today, we’ll learn how to conclude our process.

Within the folder structure is a folder called Client. This is the structure we need to expose to our offline virtual machines. The process we’re going to follow will be very simple:

  • Create a VHD file structure
  • Mount the VHD file
  • Partition and format the VHD file
  • Copy the folder structure to the VHD file
  • Dismount the VHD file

The following process is repeated for each virtual machine you supply:

  • Get a list of virtual machines that are off.
  • Attach the VHD file to the virtual machines, one at a time.
  • Inject a setting to launch a script into the virtual machine Registry.
  • Temporarily adjust the Registry to automatically log in as a user with local Admin rights.
  • Temporarily disable the User Account Control (UAC).
  • Temporarily change the Windows PowerShell execution policy.
  • Inject a small script to identify our VHD and launch the update script.
  • Power up the virtual machine and wait for it to update, then shut down automatically.
  • Remove the WSUS Offline Updates VHD.
  • Power up the virtual machine and leave it running for about two hours to allow it to process the updates.
  • Power down after the allocated time period.

We do this until we’ve run out of virtual machines that we want to patch…

To get started, we make that new VHD file and format it as one giant NTFS file system. For a detailed explanation of the process that follows, please refer to this most excellent post by Ed Wilson: Use PowerShell to Initialize Raw Disks and to Partition and Format Volumes.

# Create the VHD file


New-VHD $VHDPath -SizeBytes 20GB -Dynamic

# Attach the VHD to the Windows File system

Mount-VHD $VHDPath

# Partition the VHD

$VHD=Get-VHD $VHDPath | Get-Disk

$Drive=$VHD | Initialize-Disk -PartitionStyle MBR –PassThru |

New-Partition –AssignDriveLetter –UseMaximumSize |

Format-Volume –FileSystem NTFS –NewFileSystemLabel ‘WSUSOffline’ –Confirm:$false

You might notice a coulple differences in this code when compared to Ed’s post. First, instead of filtering for Raw file systems, I have told Get-Disk to work explicitly with the file I’m working with.

The second key piece is that I am capturing the results of the formatting in a variable called $Drive. In this manner, I can programmatically grab the drive letter that is assigned to the newly formatted VHD file. (Remember, we need to copy the client structure from Wsusoffline into this VHD file.)

Now that the disk has been prepared, naturally, we can just copy the folder structure with a little Windows PowerShell:

Copy-Item -path ‘C:wsusofflineclient’ -Recurse -destination (“$($Drive.DriveLetter):client”) -Force

Now that the files are in the VHD file, we need to dismount it so we can use it within the virtual machines:

Dismount-VHD $VHDPath

Before all of this, we need a few commands in Windows PowerShell so our script will:

  • Find the attached disk with the name WsusOffline.
  • Obtain its drive letter.
  • Launch the command under the folder named update.cmd.

All this is simply this little Windows PowerShell script:

$Drive=Get-Volume | where { $_.FileSystemLabel -eq ‘WSUSOffline’ }


invoke-expression $appname


Now we access Hyper-V and get a list of all virtual machines that are presently offline with this simple loop:

$VMlist=Get-VM | Where { $_.State –eq ‘Off’ }

Foreach ($VM in $VMlist)



The contents of the loop will look like the following. We will be running under the presumption that the first hard disk is the boot disk.

# Get virtual machine Name


# Get the location of the VHD

$VMDiskPath=(Get-VM $VMName | Get-VMHardDiskDrive).Path

# Mount the VHD and get it’s Drive letter

Mount-DiskImage $VMDiskPath

$DriveLetter=((Get-DiskImage $VMDiskpath | get-disk | get-partition | Where { $_.Type -eq ‘Basic’ }).DriveLetter)+”:”

# Then create a folder to hold our autolaunch Script


New-Item $ScriptFolder -ItemType Directory -force

# Copy over our little PowerShell script to trigger to update media

Copy-Item “C:WsusofflineUpdatePC.PS1” $ScriptFolder

Now for the fun part…

We’re going to connect to the remote software registry and adjust the settings. This involves capturing the original settings. We change them for the automatic log in and to disable the UAC. When the process is done, we revert them back:

# Connect to the Registry remotely and grab some settings


# Load the remote file registry


# Capture the original properties for Autologin

$Key=’HKLM:REMOTEPCMicrosoftWindows NTCurrentVersionWinlogon’

$Admin=(Get-ItemProperty $KEY -name AutoAdminLogon -ErrorAction SilentlyContinue).AutoAdminLogon

$Domain=(Get-ItemProperty $KEY -name DefaultDomainName -ErrorAction SilentlyContinue).DefaultDomainName

$Username=(Get-ItemProperty $KEY -name DefaultUserName -ErrorAction SilentlyContinue).DefaultUserName

$Password=(Get-ItemProperty $KEY -name DefaultPassword -ErrorAction SilentlyContinue).DefaultPassword

# Then pass in the new values which presume all

# Offline VMs have the same ID and Password for the local

# Admin account.

Set-ItemProperty $KEY -name AutoAdminLogon –value 1 -force

Set-ItemProperty $KEY -name DefaultDomainName -value ‘localhost’ -force

Set-ItemProperty $KEY -name DefaultUserName -Value ‘Administrator’ -force

Set-ItemProperty $KEY -name DefaultPassword -Value ‘P@ssw0rd’ -force

# We now do the same for UAC

# Capture the old settings first


$ConsentAdmin=(Get-ItemProperty $KEY -name ConsentPromptBehaviorAdmin -ErrorAction SilentlyContinue). ConsentPromptBehaviorAdmin

$ConsentUser=(Get-ItemProperty $KEY -name ConsentPromptBehaviorUser -ErrorAction SilentlyContinue). ConsentPromptBehaviorUser

$LUA=(Get-ItemProperty $KEY -name EnableLUA -ErrorAction SilentlyContinue).EnableLUA

$SecureDesk=(Get-ItemProperty $KEY -name PromptOnSecureDesktop -ErrorAction SilentlyContinue).PromptOnSecureDesktop

# And now we (Quick everybody HIDE!)

# Temporarily turn off UAC!

Set-ItemProperty $KEY -name ConsentPromptBehaviorAdmin –Value 0

Set-ItemProperty $KEY -name ConsentPromptBehaviorUser –Value 0

Set-ItemProperty $KEY -name EnableLUA –Value 1

Set-ItemProperty $KEY -name PromptOnSecureDesktop –Value 0

# Capture the Current Execution Policy for PowerShell


$PowershellPolicy=(Get-ItemProperty $KEY -name ExecutionPolicy -ErrorAction SilentlyContinue). ExecutionPolicy

# Set the Execution Policy to Bypass

Set-ItemProperty $KEY -name ExecutionPolicy –Value ‘Bypass’

# Finally we tell the remote computer at First run to execute the UpdatePC script.

NEW-ITEMPROPERTY “HKLM:REMOTEPCMicrosoftWindowsCurrentVersionRun” -Name “PoshStart” -Value “`”C:windowsSystem32WindowsPowerShellv1.0powershell.exe`” -file C:ProgramDataScriptsUpdatePC.PS1″

# Then disconnect the remote registry


# And now dismount the Disk

dismount-diskimage $VMDiskPath

We need to attach the VHD to the virtual machine. We’ll do this by adding in a SCSI controller to our virtual machine and then add the VHD to that SCSI controller. We are only going to add in a new SCSI controller if none already exists.

# Check for total SCSI Controllers

$TotalSCSI=(GET-VMScsiController -vmname $VMName).count

Get-VM –Vmname $VMName | Add-VMScsiController

# Attach the Updates VHDX file

Get-VM –Vmname $VMName | Add-VMHarddiskDrive –Controllertype SCSI –Path $VHDPath -ControllerNumber ($TotalScsi)

Now that we’ve done all the heavy lifting, here’s the easy part. Start the virtual machine, wait for its first shutdown so we can detach the VHD, and then start it one final time to allow the updates to apply:

# Start the virtual machine

Start-VM -vmname $VMName

# A slight for faster machines to ensure the virtual machine

# State is passed back properly first

Start-Sleep -seconds 60

# Wait until the machine has pulled in the updates

# and Stops when it’s done.

Do { $status=(Get-VM –vmname $VMname).state } until ($status –match ‘Off’)

Next we will detach the VHD file from the virtual machine to prevent it from continually trying to load the updates:

# Detach Updates VHD from virtual machine

Get-VM –Vmname $VMName | Remove-VMHarddiskDrive –Controllertype SCSI –Path $VHDPath

If(!$TotalSCSI) { Remove-VMScsiController -vmname $VMname -ControllerNumber 0 }

Now we ask Windows PowerShell to twiddle its thumbs (or maybe thumb through the TV to find some more Doctor Who) to wait for this machine to update. In this case, we’ll take a nap for a couple of hours. If your virtual machines are a bit more up-to-date, you can crank this down as you need.

Start-VM –vmname $VMName

Start-Sleep –seconds 7200; # 60 seconds in a minute, 60 minutes in an hour times 2

Stop-VM –vmname $VMName

After about two hours of rebooting and operating, all of the updates should have been applied, right? You HAVE been keeping your virtual machines up-to-date, right?

Our next steps after finishing this round of updates are:

  • Mount the virtual machine VHD file
  • Restore all the settings in the virtual machine registry
  • Remove the Windows PowerShell script we placed on the virtual machine
  • Detach the VHD file

Here’s our script:

# Reconnect VHD to Host so we can clean the registry back up

Mount-DiskImage $VMDiskPath

$DriveLetter=((Get-DiskImage $VMDiskpath | get-disk | get-partition | Where { $_.Type -eq ‘Basic’ }).DriveLetter)+”:”

# Remove that PowerShell script


Remove-Item $ScriptFolder -ItemType Directory –recurse -force

# Connect to the Registry remotely and grab some settings


# Load the remote file registry


# Restore the original properties for Autologin

$Key=’HKLM:SoftwareMicrosoftWindows NTCurrentVersionWinlogon’

Set-ItemProperty $KEY -name AutoAdminLogon –value $Admin -force

Set-ItemProperty $KEY -name DefaultDomainName -value $Domain -force

Set-ItemProperty $KEY -name DefaultUserName -Value $Username -force

Set-ItemProperty $KEY -name DefaultPassword -Value $Password -force

# We now do the same for UAC

# Restore the old settings first

$Key=’HKLM:SoftwareMicrosoftWindows NTCurrentVersionPoliciesSystem’

Set-ItemProperty $KEY -name ConsentPromptBehaviorAdmin –Value $ConsentAdmin

Set-ItemProperty $KEY -name ConsentPromptBehaviorUser –Value $ConsentUser

Set-ItemProperty $KEY -name EnableLUA –Value $SilentlyContinue

Set-ItemProperty $KEY -name PromptOnSecureDesktop –Value $SecureDesk

# Restore the original Execution Policy


Set-ItemProperty $KEY -name ExecutionPolicy –Value $PowershellPolicy

# Then Remove the autostart for the Script

SET-ITEMPROPERTY “HKLM:REMOTEPCMicrosoftWindowsCurrentVersionRun” -Name “PoshStart” -Value $NULL

# Then disconnect the remote registry


# And now dismount the Disk

dismount-diskimage $VMDiskPath

There you have it. The interesting thing to consider is that you require no network access from any of these virtual machines for this to operate. If you play more with WSUS Offline Update, you’ll see that there are settings in the .ini files to automate more of its update process.

Of course, depending on how far out-of-date those virtual machines are, you may have to repeat the process a few times. But because this is automatable, you can sit back with a nice glass of iced tea and watch back-to-back episodes of Serenity while it happens.

Keep in mind that the media we created today only contains media for Windows 8.1 and Windows Server 2008 R2. You can expand it to contain all of the currently supported operating systems and Microsoft Office suites. All you need to do is check more boxes.

This was a long read today; but of course, what a volume of information and neat tricks in Windows PowerShell we got to play with today! Think about it. Turning on a virtual machine and programming it to do something without network access—and then undoing all of that in the end. 

We invite you to follow us on Twitter and Facebook. If you have any questions, send email to scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow.

Sean Kearney, Microsoft PowerShell MVP and Honorary Scripting Guy

Comments (2)

  1. Daku says:

    Great Job guys!! Exactly what I was looking for. Will the complete script be available for download anywhere?

  2. Disco Troy says:

    Is this really the official solution to the problem of offline updates? A bit of freeware from some Random Internet Guy and some hasty Powershell? How, in 2015, is there no official MS method of updating offline servers?

    Surely, for the love of all that is holy, there is a better way to do this?

Skip to main content