PS: Remotely updating devices

I recently had to force a collection of PCs to update – they were configured using Windows Update for Business, all the policies and settings were telling them when to update and how, yet they just hadn’t – whether there was something on the UI that the primary user was just ignoring, I’m not sure. Anyway they were stuck on Windows 10 2004, and on the July update.

As they’re all configured for WUfB there wasn’t anything I could realistically do through Config Manager, besides maybe run these steps as a PowerShell Script and push out that way. Instead I decided to look at PSWindowsUpdate. In this post I’ll go through what I did, and share the scripts I used. My aim here was to get the rogue devices patched and updated to 21H1. I did still use Config Manager to help with this task – to wake devices using either the Client Notification > Wake, or the Recast Right Click Tools Wake on LAN feature. I’m not going to go into all the features of PSWindowsUpdate in any detail, there are plenty of good posts on the Internet about this already which can be found with a quick search.

The first step here was installing the PSWindowsUpdate module on the device I wanted to manage things from. While you can pass a Credentials parameter to these commands I found it much easier to just run the PowerShell window as an admin user (which has admin privileges on all target devices). I’ve assumed that in the scripts and not included a Credential parameter. We also need an array of computers that we wish to update.

Install-Module PSWindowsUpdate -Force
Import-Module PSWindowsUpdate

$Computers = @("desktop-1","desktop-2")

PSWindowsUpdate has a variety of commands for managing Windows Updates. I will be using Invoke-WUJob (formerly known as Invoke-WUInstall) and Install-WindowsUpdate. The other command to note is Get-WindowsUpdate, which will show you the available updates for the target machine.

Initially here I was planning on just running Install-WindowsUpdate and giving it an array of computer names, but you can’t install Windows Updates remotely – not even if you open a PSSession and try there, you just get Access Denied. That’s where Invoke-WUJob comes into play.

Invoke-WUJob will create a scheduled task on the target device(s), set to run the command specified, and it runs as SYSTEM and immediately. It’s also supposed to install PSWindowsUpdate if it’s not already on the target device, although this didn’t work for me – hence the first part of the script. This will check if the module is already installed on each device, if not then install NuGet and PSWindowsUpdate with no prompts.

Invoke-Command ($Computers) {
    If ($null -eq (Get-Module -Name PSWindowsUpdate -ListAvailable) ) {
        Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
        Install-Module PSWindowsUpdate -Force
        Import-Module PSWindowsUpdate
    }
}

Now that we’ve got the module installed on the devices, we could have a look and see which version of Windows they are currently running. This will help us identify targets for the feature update. Unfortunately this won’t show you the patch level, but you can work out which version you are running.

$Results = @()
foreach ($Computer in $Computers) {
    $Version = Invoke-Command ($Computer) { [Environment]::OSVersion } -ErrorAction SilentlyContinue
    if ($null -eq $Version) {
        $Results += [PSCustomObject]@{
            "ComputerName" = $Computer
            "Version" = "(Device offline)"
        }
    } else {
        $Results += [PSCustomObject]@{
            "ComputerName" = $Computer
            "Version" = $Version.Version.ToString()
        }
    }
}
$Results

This script will grab the OSVersion data from each computer and creates a PSCustomObject containing the computer name and the version string. If the device is offline, or the Invoke-Command fails for some reason, it’ll put “(Device offline)” in the custom object. Finally we output the array of custom objects to give this kind of output:

Screenshot of output of computer version command, showing a table of ComputerName vs Version.
Check the version of the devices before or after running your update script.

You can see from the output that desktop-1 is already running 21H1, and desktop-2 is stuck on 2004. The version numbers can be found on the Microsoft Docs.

Next step is to install the updates. Here I’ve gone with installing all the available updates, and allowing reboots. You can alter this by passing different parameters to Install-WindowsUpdate:

  • AcceptAll: Accept all available updates
  • AutoReboot: Reboot the device automatically if an update requires it
  • IgnoreReboot: Don’t auto reboot the device
  • UpdateID: Specify the KB you want to install
Invoke-WuJob -ComputerName $Computers -Script { ipmo PSWindowsUpdate; Install-WindowsUpdate -AcceptAll -AutoReboot | Out-File "C:\Windows\PSWindowsUpdate.log"} -RunNow -Confirm:$false -Verbose -ErrorAction Ignore

This will run through all the computers, create a scheduled task on each and run powershell.exe -Command "ipmo PSWindowsUpdate; Install-WindowsUpdate -AcceptAll -AutoReboot | Out-File C:\Windows\PSWindowsUpdate.log" immediately as SYSTEM.

We can monitor the output if we wish, by viewing C:\Windows\PSWindowsUpdate.log on any of the devices, ideally in CMTrace or a similar tool which allows for “live viewing” a file as it updates. The output will show the computer name, status, KB number, size and title of each update – they’ll list three times once it’s completed, as shown in the screenshot:

Screenshot of the output of Install-WindowsUpdate showing the updates with various status (Accepted/Downloaded/Installed)
PSWindowsUpdate.log on a target device. The formatting lines up if you use a monospaced font.

You can monitor whether they have completed or not with the Get-WUJob cmdlet:

Get-WUJob -ComputerName $Computers

This will give you a table showing the computer name, the task name (PSWindowsUpdate), and the task action. When this list is empty, then it’s all completed.

Once you’re done, you can run the version check script again, and hopefully find all devices are now on 21H1. In my case I had to run the Invoke-WUJob bit a second time, to put 21H1 on.

Update: Off the back of this I realised I could use Scripts within Config Manager to query the windows version – there are other ways to do this but I find the device data in CM tends to lag behind. Simply create a script under Software Library > Scripts:

([Environment]::OSVersion).Version.ToString()

and run that on your target devices. You will then get script output showing the counts for each version found:

Config Manager Script Output showing windows versions and client counts
Quickly check how many of each Windows version you are running in real time with a Config Manager Script.

This can further be tweaked to show how many are on 21H1, and the computer names of those which are not:

$Version = ([Environment]::OSVersion).Version.ToString()
if ($Version -ne "10.0.19043.0") {
    Write-Host "$Env:Computername $Version"
} else {
    Write-Host "$Version"
}

 

2 Replies to “PS: Remotely updating devices”

  1. This is great. We started doing something similar, not nearly as well documented and written, last month, and got mixed results. One of the things we could not figure out, was why was 2021-09 showing as 103gb for the update. Did you run into any issues getting this working? If the computers are not directly accessible, if they are WFH and not on VPN, we weren’t able to call them from a $computers object. We wrapped our logic in a script, and then ran it. I like your idea of scheduled task – but didn’t get that far. Thanks!

Leave a Reply

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

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