Installing Windows Updates on all your Hyper-V lab VMs using PowerShell and PSWindowsUpdate

I’m a Consultant who does a lot of Intune testing, and I use Hyper-V on my laptop with many VMs in customer environments. Some VMs are used more than others, but when you turn them on after a few weeks… They need updates and restarts, and I wanted to automate that process so that it can run in the background while working without me having to log in, click buttons, and track progress… In this blog post, I will show you how to automate that 🙂

How does the script work?

Because I use Hyper-V, I can connect to the Hyper-V VM using PowerShell Direct (I wrote a blog about that in the post here). The script connects to the VM (and will start it if it is not running) using that feature and installs the PSWindowsUpdate module if needed. (I wrote a blog about the PSWindowsUpdate module here). After installing or importing the module, it will install all pending updates and reboot the machine afterward. If there are no updates, it will shut down the VM unless specified not to do that using a parameter.

Requirements

The scripts will connect to the VM using PowerShell Direct and use a local account on the target VM. You must create a local Admin account on each VM first or inside Active Directory if you have Domain Joined VMs running with the same non-expiring password. (Because this a lab/test environment for me, that’s no problem)

Script parameters

The parameters that you can use are:

  • VMs – This parameter allows you to specify one or more (Separated by a comma; VMs with spaces need quotes around the name) VMs you want to update. For example, -VMs ‘Windows Server 2025’, Exam, ‘Windows 11 Test.Local’. If you don’t specify a VM or multiple VMs, you can also specify * to update all VMs.
  • AdminAccountName – This parameter allows you to specify the account name you have created on all the VMs. When running the script, it will prompt for a password. For example, -AdminAccountName ‘.\windowsupdate’ will use the local “windowsupdate” on the VM to connect to the VMs. (Because you specify “.\username,” this will also work on Domain Controllers and Active Directory Domain Joined VMs)
  • AdminAccountPassword – This parameter allows you to specify the password for the Admin account specified. Note: This will be exposed in your PowerShell history and should be used when executing it from within Management software or retrieving it from a KeyVault.
  • DelayafterStartInSeconds – This parameter allows you to specify how long the script should wait before starting to install Windows Updates. For example, -DelayafterStartInSeconds 30 will wait for 30 seconds. If not specified, it will use 15 seconds as the default.
  • DelayafterRestartInMinutes – This parameter allows you to specify how long the script should wait for updates to install after rebooting. For example, DelayafterRestartInMinutes 10 will wait for 10 minutes. If not specified, it will use 5 minutes as the default.
  • NoShutdown – This switch parameter will leave the VM running; by default, it will shut down after installing updates.

Using the script

In the example below, I used the script to update three VMs:

.\Start-WindowsUpdate-HyperV-VMs.ps1 -VMs 'Windows Server 2022 DC', Exam, 'Windows 11 Test.Local' -DelayafterStartInSeconds 15 -DelayafterRestartInMinutes 1 -AdminAccountName '.\windowsupdate'

The script will show the updates that it found and installed during the progress:

It will show the update progress like this for every update:

It will shutdown the VM afterward if it has been running for more than 1 minute and when the -NoShutdown switch was not used:

If any VM were still running at the end of the script because the shutdown command didn’t work or the machine was rebooting, it would also shut down any running VM when the -NoShutdown switch was not used.

The script

Below are the script’s contents; save it somewhere on the machine with the Hyper-V VMs. (For example, c:\scripts\Start-WindowsUpdate-HyperV-VMs.ps1)

param (
    [parameter(Mandatory = $true)][string[]]$VMs, 
    [parameter(Mandatory = $true)][string]$AdminAccountName,
    [parameter(Mandatory = $false)][securestring]$AdminAccountPassword,
    [parameter(Mandatory = $false)][int]$DelayafterStartInSeconds = 15,
    [parameter(Mandatory = $false)][int]$DelayafterRestartInMinutes = 5,
    [parameter(Mandatory = $false)][switch]$NoShutdown
)

#Validate if hyper-v module is available, install when needed
if (-not (Get-Module -ListAvailable -Name Hyper-V)) {
    Write-Warning "Hyper-V module is not installed, installing now..."
    Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -NoRestart:$true
}

#Set credentials, prompt for admin password for the account specified in $AdminAccountName if not specified in $AdminAccountPassowrd
if (-not $AdminAccountPassword) {
    $password = Read-Host "Please enter password for the specified admin account" -AsSecureString
}
else {
    $password = $AdminAccountPassword | ConvertTo-SecureString -AsPlainText -Force
}
$AdminCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminAccountName, $password

#Validate if specified VM(s) is/are valid and running
foreach ($VM in Hyper-V\Get-VM $VMs | Sort-Object Name) {
    if (-not (Hyper-V\Get-VM -VMName $VM.Name -ErrorAction SilentlyContinue)) {
        Write-Warning ("Specified VM {0} can't be found, check spelling/name. Exiting..." -f $VM.Name)
        return
    }
        
    #Start VM is it was not started and wait X amount of seconds specified in $DelayAfterStart
    if (-not ((Hyper-V\Get-VM -Name $VM.Name).State -eq 'Running')) {
        Write-Warning ("Specified VM {0} was not started, starting now and waiting for {1} seconds..." -f $VM.Name, $DelayafterStartInSeconds)
        Hyper-V\Start-VM -Name $VM.Name
        Start-Sleep -Seconds $DelayafterStartInSeconds
    }

    #Connect to VM, install PSWindowsUpdate, install all updates found and reboot if needed and shutdown afterwards if $NoShutdown was not specified.
    Write-Host ("Checking/Installing updates on {0}" -f $VM.Name) -ForegroundColor Green
    try {
        Invoke-Command -VMName $VM.Name -Credential $AdminCredential -ScriptBlock {
            Set-ExecutionPolicy Bypass
            if (-not (Get-PackageProvider -Name Nuget | Where-Object Version -GT 2.8.5.201)) { 
                Write-Host ("Installing NuGet provider") -ForegroundColor Green
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Confirm:$false -Force:$true | Out-Null
            }
            if (-not (Get-Module -Name PSWindowsUpdate -ListAvailable)) {
                Write-Host ("Installing PSWindowsUpdate module") -ForegroundColor Green
                Install-Module PSWindowsUpdate -Scope CurrentUser -AllowClobber -Force                
            }
            Import-Module PSWindowsUpdate
            Write-Host ("Installing Update(s) if any... System will reboot afterwards if needed!") -ForegroundColor Green
            Install-WindowsUpdate -Install -ForceInstall -AcceptAll -AutoReboot
        } 
    }
    catch {
        Write-Warning ("Couldn't connect to {0}, check credentials! Skipping..." -f $VM.Name)
    }

    #Wait for VM to restart after updates
    Write-Host ("Waiting for 15 seconds before continuing....") -ForegroundColor Green
    Start-Sleep -Seconds 15

    #Wait for the VM to have an uptime of more than one minute and shutdown the VM if $NoShutdown was not specified
    if (-not $NoShutdown) {
        while ((Hyper-V\Get-VM $VM.Name).Uptime.TotalMinutes -le $DelayafterRestartInMinutes) {
            Write-Host ("Waiting for {0} to be online for more than {1} minute(s), sleeping for 15 seconds...(Current uptime is {2} minutes and {3} seconds)" -f $VM.Name, $DelayafterRestartInMinutes, $(Hyper-V\Get-VM $VM.Name).Uptime.Minutes, $(Hyper-V\Get-VM $VM.Name).Uptime.Seconds) -ForegroundColor Green
            Start-Sleep -Seconds 15
        }
        #Stop VM after waiting to $DelayafterRestartInMinutes
        Write-Host ("Shutting down {0} now..." -f $VM.Name) -ForegroundColor Green
        try {
        Hyper-V\Stop-VM -VMName $VM.Name -Force:$true -ErrorAction Stop
        }
        catch {
            Write-Warning ("Could not stop VM {0}, will try again at end of script!" -f $VM.Name)
        }
    }
    else {
        Write-Host ("The -NoShutdown parameter was used, not shutting down {0}..." -f $VM.Name)
    }        
}

#After waiting for the amount of minutes specified in #DelayafterRestartInMinutes,
#shutdown all running VMs if $NoShutdown was not specified
if (-not $NoShutdown) {
    Write-Host ("Waiting for {0} minutes before shutting down any remaining running VM" -f $DelayafterRestartInMinutes) -ForegroundColor Green
    Start-Sleep -Seconds $($DelayafterRestartInMinutes * 60)
    foreach ($VM in Get-VM | Where-Object State -EQ Running ) {
        Write-Host ("Shutting down {0} now..." -f $VM.Name) -ForegroundColor Green
        Hyper-V\Stop-VM -VMName $VM.Name -Force:$true
    }
}

Download the script(s) from GitHub here.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.