Building PowerShell Tools for MSPs: Automating Windows Updates

Save to My DOJO

Building PowerShell Tools for MSPs: Automating Windows Updates

Let’s face it, no one likes Windows Updates – least of all Managed Service Providers. However, there is a way to make the process less tedious: through automation.

For MSPs managing Windows Updates for clients is always messy. No matter what patch management solution your using, it is inevitable that Windows Updates will still cause you headaches. Whether there is a bad patch that gets rolled out to a number of clients, or there is the never-ending burden of having to troubleshoot devices where Windows Patches just aren’t installing properly. Luckily, with the magic of PowerShell and the help of the PowerShell module PSWindowsUpdate we can manage windows updates in an automated fashion allowing us to develop scripts that ease some of our Windows Update pains.

Automating Windows Updates for MSPs

How to Install PSWindowsUpdate

PSWindowsUpdate was created by Michal Gajda and is available via the PowerShell Gallery which makes installation a breeze. To install PSWindowsUpdate, all we have to do, if we are running a Windows 10 OS, is open up a PowerShell cmd prompt and type in the following syntax:

Install-Module -Name PSWindowsUpdate

If we want to save this module off and put it on a network share so that other servers can import and run this module, then we will use the save-module cmdlet:

Save-Module -Name PSWindowsUpdate -Path

Using PSWindowsUpdate

Now that we have the module installed we can now run Windows Updates via PowerShell. There are a numerous amount of actions we can perform with this module, but for this post, we will go over the basics. We can use the Get-WindowsUpdate cmdlet to fetch a list of updates that are available for our machine. In my example, I have the module installed on a workstation and I run the following syntax to get the list of windows updates applicable to my machine:

Get-WindowsUpdate

Now that I can see what updates my machine is missing, I can use the -Install parameter to install the updates. If I wanted to install all available updates and automatically reboot and afterward, I would use the -autoreboot parameter and the syntax would look like this:

Get-WindowsUpdate -install -acceptall -autoreboot

if i want to just install a specific KB i can use the -KBArticleID parameter and specify the KB Article Number:

Get-WindowsUpdate -KBArticleID KB890830 -install

In the screenshot, you can see that we get a confirmation prompt and then each update step is listed as it occurs. If we wanted to remove an update from a machine we could use the Remove-WindowsUpdate cmdlet and specify the KB with the -KBArticleID parameter. I will use the -norestart parameter so the machine does not get rebooted after the patch is uninstalled:

Remove-WindowsUpdate -KBArticleID KB890830 -NoRestart

If we want to check available windows updates remotely from other computers, we can simply use the -ComputerName parameter:

Continuous Update Script

PSWindowsUpdate can be used in deployment scripts to make sure Windows is completely up to date before it is placed in production. Below I have created a script that will deploy all available windows updates to a Windows Server and restart when complete. Right after the restart is done, the update process can be started again and repeats itself until there are no more windows updates left. This is a very useful script to use for VM deployments. Unless you have the time to ensure your VM templates are always up to date every month, there are almost always going to be Microsoft Updates to install when deploying new Windows Virtual Machines. Also, the process of ensuring that all available Windows Updates on a system are installed can be a very time-consuming task as we all know Windows Updates isn’t the fastest updater in the world.

This script requires that you run it from an endpoint that has the PSWindowsUpdate module installed, it also should be run with an account that has local administrator permissions to the remote server that it is managing windows updates on. Essentially it will install PSWindowsUpdate on the remote server via PowerShell get and will use the cmdlet Invoke-WUJob which uses task scheduler to control windows updates remotely. We have to use Task Scheduler because there are certain limitations with some of the Windows Update methods that prevent them from being called from a remote computer.

Copy this script to a notepad and save it as a .ps1. I save mine as install-windowsupdates.ps1:

<#
.SYNOPSIS
This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed, after reboot, windows updates will continue to run until no more updates are available.
.PARAMETER URL
User the Computer parameter to specify the Computer to remotely install windows updates on.
#>

[CmdletBinding()]

param (

[parameter(Mandatory=$true,Position=1)]
[string[]]$computer


)

ForEach ($c in $computer){

    
    
    #install pswindows updates module on remote machine
    $nugetinstall = invoke-command -ComputerName $c -ScriptBlock {Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force}
    invoke-command -ComputerName $c -ScriptBlock {install-module pswindowsupdate -force}

    invoke-command -ComputerName $c -ScriptBlock {Import-Module PSWindowsUpdate -force}

    Do{
        #Reset Timeouts
        $connectiontimeout = 0
        $updatetimeout = 0
        
        #starts up a remote powershell session to the computer
        do{
            $session = New-PSSession -ComputerName $c
            "reconnecting remotely to $c"
            sleep -seconds 10
            $connectiontimeout++
        } until ($session.state -match "Opened" -or $connectiontimeout -ge 10)

        #retrieves a list of available updates

        "Checking for new updates available on $c"

        $updates = invoke-command -session $session -scriptblock {Get-wulist -verbose}

        #counts how many updates are available

        $updatenumber = ($updates.kb).count

        #if there are available updates proceed with installing the updates and then reboot the remote machine

        if ($updates -ne $null){

            #remote command to install windows updates, creates a scheduled task on remote computer

            invoke-command -ComputerName $c -ScriptBlock { Invoke-WUjob -ComputerName localhost -Script "ipmo PSWindowsUpdate; Install-WindowsUpdate -AcceptAll | Out-File C:\PSWindowsUpdate.log" -Confirm:$false -RunNow}

            #Show update status until the amount of installed updates equals the same as the amount of updates available

            sleep -Seconds 30

            do {$updatestatus = Get-Content \\$c\c$\PSWindowsUpdate.log

                "Currently processing the following update:"

                Get-Content \\$c\c$\PSWindowsUpdate.log | select-object -last 1

                sleep -Seconds 10

                $ErrorActionPreference = ‘SilentlyContinue’

                $installednumber = ([regex]::Matches($updatestatus, "Installed" )).count

                $Failednumber = ([regex]::Matches($updatestatus, "Failed" )).count

                $ErrorActionPreference = ‘Continue’

                $updatetimeout++


            }until ( ($installednumber + $Failednumber) -eq $updatenumber -or $updatetimeout -ge 720)

            #restarts the remote computer and waits till it starts up again

            "restarting remote computer"

             #removes schedule task from computer

            invoke-command -computername $c -ScriptBlock {Unregister-ScheduledTask -TaskName PSWindowsUpdate -Confirm:$false}

             # rename update log
             $date = Get-Date -Format "MM-dd-yyyy_hh-mm-ss"
             Rename-Item \\$c\c$\PSWindowsUpdate.log -NewName "WindowsUpdate-$date.log"

            Restart-Computer -Wait -ComputerName $c -Force

        }
   

    }until($updates -eq $null)

    

    "Windows is now up to date on $c"

}

Now that you have your .ps1 file created, remember the location where you saved it:

We will open up an administrative PowerShell console and type in the following command in order to deploy all available windows updates on our server “Server3”:

PowerShell "C:\users\Administrator\Documents\install-windowsupdates.ps1" -computer Server3

A remote connection is established with Server3 and we install the module remotely. Then we perform a get-windowsupdate to get a list of any available windows updates. Then we invoke the server to install those updates:

You can see in the screenshot, that after the updates are installed and the server is rebooted, the script picks back up again and starts the process of verifying if there are any new updates available, if there are, it will run through the steps of installing them again. Once there are no more available updates, the script will stop:

Wrap-Up

As you can see, this method is quite useful in several different situations. It’s simply another tool you can add to your toolbox to help your team assist your customers. More efficient operations is a good thing for everyone!

What about you? Do you have Windows Update stories to share? Any particularly favorite workarounds? Has this post and script been useful to you? Let us know in the comments section below!

Also, I’ve written a lot of automation for MSPs here. If you like this, check out the following blog posts:

Automation for MSPs: Getting started with Source Control

Automation for MSPs: HTML Tables

Automation for MSPs: Advanced PowerShell Functions

Thanks for reading!

Altaro O365 Backup for MSPs
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

131 thoughts on "Building PowerShell Tools for MSPs: Automating Windows Updates"

  • ovais humayun says:

    thanks for the great script , if i have to run this for all the computers in a text file how can i do that

    • To be honest I should have made the script with multiple devices in mind. In the near future, I will release an improved modified version of this which will be able to do multiple computers from the parameter. In the mean time, give this a try. Just replace the -path with a list of your computers, make sure there is no extra white space at the end of the list:

      $computers = Get-content -Path c:\listofcomputers.txt

      Foreach ($computer in $computers){PowerShell “C:\users\Administrator\Documents\install-windowsupdates.ps1” -computer $computer}

      Let me know if that works. Thanks for the comment!

      • Bee says:

        When I throw that into a powershell script and try to run it, i get this error.

        “\\domain.local\dfs\filepath\serverupdates.ps
        1 : The module ‘“’ could not be loaded. For more information, run ‘Import-Module “’.
        At line:1 char:1
        + “\\domain.local\dfs\filepath …
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : ObjectNotFound: (“\\domain.local\…rverupdates.ps1:String) [], CommandNotFoundException
        + FullyQualifiedErrorId : CouldNotAutoLoadModule

        • The workstations will need access to the internet in able to install the PSWindowsUpdate PowerShell module. Try running install-module PSWIndowsUpdate on one of the workstations and see what error you’re getting.

      • Bee says:

        Never mind on my last comment but here is what I wrote to have it loop through all of the servers in a list and open a new powershell console for them.
        I couldn’t figure out how to get a workflow to call another powershell script and pass the parameters into the PS script so I know this isn’t the cleanest/best practice but works if you have a beefier computer and/or not too many computers in your list.

        $computers = Get-content -Path \\somepath\serverlist.txt

        Foreach ($computer in $computers)
        {
        Start-Process powershell -argument “\\somepath\serverupdates.ps1 -computer $computer” -PassThru
        }

        • Yes this work well! Also, if your using Powershell 7 you can make use of the new -Parallel parameter for ForEach and run the script on multiple machines at a time.

          • Piyush says:

            This script is awesome work however when i try to use it on multiple devices i am getting a error message on few, mentioned as below.
            The term ‘Install-PackageProvider’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
            spelling of the name, or if a path was included, verify that the path is correct and try again.
            + CategoryInfo : ObjectNotFound: (Install-PackageProvider:String) [], CommandNotFoundException
            + FullyQualifiedErrorId : CommandNotFoundException

            The term ‘install-module’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling
            of the name, or if a path was included, verify that the path is correct and try again.
            + CategoryInfo : ObjectNotFound: (install-module:String) [], CommandNotFoundException
            + FullyQualifiedErrorId : CommandNotFoundException

            And because of these errors it does not move forward.

          • What OS are you running this against? The error messages indicate that the OS is missing Install-PackageProvider, you may need to install WMF 5.1 on the OS if it is older: https://www.microsoft.com/en-us/download/details.aspx?id=50395.

      • psychozoic says:

        hi! any updates?

        • Script has been updated! Multiple servers work now. Check it out: .\install-windowsupdates.ps1 -computer “server1″,”server2″,”server3″,”server4”

      • Daniel says:

        have u updatet your script for many Clients in a system?
        It would be plenty of help!
        Thank You!

        • This script has been updated! You can now run updates against multiple servers by using: .\install-windowsupdates.ps1 -computer “server1″,”server2″,”server3″,”server4”.

  • William says:

    I get a ‘Get-Content : Cannot find path \\computername\c$\pswindowsupdate.log because it does not exist’ error …

    Please advise.

  • Jason says:

    Is there a way to make this all work on the local computer?

    • The script I created is intended to be used from a remote endpoint. However, you could easily do this on a local computer by installing the PSWindowsUpdate module on the local machine:

      Install-Module -Name PSWindowsUpdate

      Then create some sort of scheduled task (either by Powershell or manually) that runs the following PowerShell commands on start up:

      Get-WindowsUpdate -install -acceptall -autoreboot

      This would cause the local machine to run windows updates continuously and reboot after install until there are no more windows updates available.

  • Darren says:

    Hi there,

    I’ve been hacking your script and have it working great for my needs.

    One question.

    I have a few servers in a DMZ (on same domain as script) with ICMP disabled. These servers fail as the script just doesn’t recognise they are online.

    Is there anyway to fix this?

    Cheers

    • If these servers are VMs on a hyper-visor then you are in luck. You would have to edit the script a bit and the VMs in the DMZ would need internet access, but for VMware you can use the invoke-vmscript commandlet that would run the commands straight on the VM regardless of network connectivity, it uses VMware Tools to issue the commands.

      If you are using Hyper-V check out powershell direct.

  • Charlie Cho says:

    Thanks for the script, Luke! It is very helpful. Do you know if the PS module allows for a method to skip installation of Silverlight or Silverlight related updates?

    • You can modify get-windowsupdate to not incllude specific KB’s by using the -NotKBArticleID parameter. So it would look someting like: get-windowsupdate -NotKBArticleID ‘KB982861’

      Look through “help get-windowsupdate -full” to see what else you can do with the windows updates, it’s pretty neat.

  • Daniel Dillard says:

    I am having trouble with the Execution policy. What would be the best practice to use this script and temporarily bypass the policy? Is there no way to do that while running it as a remote command? Most of my research points to this.

    If so, is there a way to run this without changing the executionpolicy to bypass or remotesigned permenantly?

    • You can do this when you execute a PowerShell script by running the following command, just use -file and input the path to the script you want to run without execution policy: powershell.exe -executionpolicy bypass -file “script.ps1”

  • BasWas says:

    This script is gold! Was trying to make the -RunNow work with -Autoreboot. But your script does it all including progress.
    Thanks for sharing!!

  • suraj shinde says:

    Hey,

    The script runs fine till searching for updates but it gives error while invoke-WUJob section.

    “The term Invoke-WUJob is not recognized as the name of cmdlet, function, script file…….”

    PSWindowsUpdate version is 2.1.1.2

    • Looks like your having an issue with the PSWindowsUpdate module not being present inside the Powershell session that the script is running in. Try a Get-Command invoke-wujob to see if the module is there.

  • Chris says:

    Hi – Many thanks for the awesome script. Unfortunately when I try to update multiple computers the retrieval of the WindowsUpdate.log fails (looks like the script is appending each computer name to the UNC path, as below:

    Currently processing the following update:
    Get-Content : Cannot find path ‘\\alcyone dardanos kerberos proteus\c$\PSWindowsUpdate.log’ because it does not exist.

    How can I go about fixing this?

    Kind Regards
    Chris

  • ahmed jehanzeb says:

    Hi Luke,
    Thanks for the script! Is it possible to modify this script to include ‘Important’ or Critical’ Windows updates only ?
    Thanks!

    • I believe you can. If you use Get-Windowsupdate -severity you can specify the patches according to their severity types. Use help get-windowsupdate -full to look at all the different options.

  • Kye says:

    Hi,

    This module works great but it doesn’t seem to restart automatically on a local machine?

    Any help would be appreciated.

  • Diego says:

    Is it possible to install a individual update in multiple remote servers?

    • Yes, you just have to modify the Get-WindowsUpdate -AcceptAll -Install to Get-WindowsUpdate -KBArticleID KB890830 -install and change the -KBArticleID to whatever KB you want to install.

  • Enigma says:

    HI,

    Is there any way, we can choose specific update to install. Instead of installing all the updates ?

  • Donnie says:

    If the remote machine is on the domain, can we use a domain admin account to execute the script?

  • vishal Dhavale says:

    Hi sir its good script but can i run workgroup PC.

  • Gustavo Lopes says:

    I get a access denied but my machines are in the same domain and it’s running under the administrator account, follow the details:

    + CategoryInfo : NotSpecified: (:) [Invoke-WUJob], UnauthorizedAccesException
    + FullyQualifiedErrorId : System.UnauthorizedAccesException,PSWindowsUpdate.InvokeWUJob

    • That error looks like your getting an unauthorized access error from the local machine your running the script on. Make sure your running the script through an administrative Powershell console.

  • josh says:

    When I run the following script, I don’t see the kb as having been installed even though the script runs without error?

    Set-ExecutionPolicy Bypass -Scope Process -Force
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force # https://dzone.com/articles/install-nuget-packages-in-visual-studio
    Set-PSRepository -Name “psgallery” -InstallationPolicy Trusted # https://www.addictivetips.com/windows-tips/add-a-trusted-repository-in-powershell-windows-10/
    Install-Module -Name PSWindowsUpdate -RequiredVersion 2.1.0.1
    Import-Module PSWindowsUpdate
    Get-WUInstall -KBArticleID KB4512501 -AcceptAll -IgnoreReboot

    • You have -IgnoreReboot at the end of Get-WUInstall, this may be causing the KB to not show as installed until the machine is completely rebooted. Test on a machine without the -IgnoreReboot parameter and see if that fixes it.

  • Rohit Kumar says:

    Still waiting for the multiple servers script 🙂 I have been editing mine but its not looking and running the best.
    Would really appreciate it if you could work on the multiple server script. I tried the foreach with the parallel option but it didnt work. After reading more about it, it only works for ForEach-Object command 🙁

    Thanks

    • ani says:

      This is a function, So you can create a variable which takes multiple servers and use in -computername

  • Jeremy McGee says:

    I’m having an issue when I try to use Get-WUInstall via a WINRM session, when using Get-WUInstall -computername or when using Invoke-WUJob -computer. I am getting an 0x80070005 error. The script does work on some of the servers that I run it on, they are for the most part configured the same. Get-WUInstall -Install -AutoReboot -AcceptAll does work on the machines that have this error, if I run the command locally in RDP. Any thoughts? Thanks a ton for this!

    DEBUG: 10/14/2019 3:44:26 PM CmdletStart: Get-WUInstall
    DEBUG: 10/14/2019 3:44:29 PM ParameterSetName: Default
    DEBUG: 10/14/2019 3:44:30 PM Set pre search criteria: IsInstalled = 0
    DEBUG: 10/14/2019 3:44:31 PM Set pre search criteria: IsHidden = 0
    DEBUG: 10/14/2019 3:44:32 PM Search criteria is: IsInstalled = 0 and IsHidden = 0
    DEBUG: 10/14/2019 3:44:33 PM SERVERNAME: Connecting…
    DEBUG: 10/14/2019 3:44:34 PM Module version: 2.1.1.2
    DEBUG: 10/14/2019 3:44:35 PM Dll version: 2.0.6995.28496
    DEBUG: 10/14/2019 3:44:36 PM UpdateSessionObj mode: Local
    DEBUG: 10/14/2019 3:44:38 PM ServiceManagerObj mode: Local
    DEBUG: 10/14/2019 3:44:39 PM Try Default. Set source of updates to Windows Server Update Service
    VERBOSE: SERVERNAME (10/14/2019 3:44:40 PM): Connecting to Windows Server Update Service (http://wsus.COMPANY.org:8530) server. Please wait…
    VERBOSE: Found [3] Updates in pre search criteria
    DEBUG: 10/14/2019 3:44:41 PM 2019-10 Servicing Stack Update for Windows Server 2012 R2 for x64-based Systems (KB4521864)
    DEBUG: 10/14/2019 3:44:42 PM Update was not filtered
    DEBUG: 10/14/2019 3:44:44 PM 2019-10 Security Only Quality Update for Windows Server 2012 R2 for x64-based Systems (KB4519990)
    DEBUG: 10/14/2019 3:44:45 PM Update was not filtered
    DEBUG: 10/14/2019 3:44:46 PM 2019-10 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB4520005)
    DEBUG: 10/14/2019 3:44:47 PM Update was not filtered
    VERBOSE: Found [3] Updates in post search criteria
    DEBUG: 10/14/2019 3:44:48 PM Show update to accept: 2019-10 Servicing Stack Update for Windows Server 2012 R2 for x64-based Systems (KB4521864)
    DEBUG: 10/14/2019 3:44:49 PM Accepted
    DEBUG: 10/14/2019 3:44:50 PM Show update to accept: 2019-10 Security Only Quality Update for Windows Server 2012 R2 for x64-based Systems (KB4519990)
    DEBUG: 10/14/2019 3:44:51 PM Accepted
    DEBUG: 10/14/2019 3:44:52 PM Show update to accept: 2019-10 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB4520005)
    DEBUG: 10/14/2019 3:44:53 PM Accepted

    X ComputerName Result KB Size Title
    – ———— —— — —- —–
    1 SERVERNAME Accepted KB4521864 10MB 2019-10 Servicing Stack Update for Windows Server 2012 R2 for x64-based Systems (KB4521864)
    1 SERVERNAME Accepted KB4519990 32MB 2019-10 Security Only Quality Update for Windows Server 2012 R2 for x64-based Systems (KB4519990)
    1 SERVERNAME Accepted KB4520005 478MB 2019-10 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB4520005)
    VERBOSE: Accepted [3] Updates ready to Download
    DEBUG: 10/14/2019 3:44:55 PM Show update to download: 2019-10 Servicing Stack Update for Windows Server 2012 R2 for x64-based Systems (KB4521864)
    Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
    + CategoryInfo : NotSpecified: (:) [Get-WindowsUpdate], UnauthorizedAccessException
    + FullyQualifiedErrorId : System.UnauthorizedAccessException,PSWindowsUpdate.GetWindowsUpdate

    • Jeremy,

      The error message is showing “Access is denied” which indicates that there is either some sort of permissions issue with the account running the script or some sort of security block. Since the command works locally, it could also be some sort of 3rd party Anti Virus as well blocking remote execution.

  • Ilia says:

    I run the script against windows 2016 server inside our domain. It runs without any errors. It found 4 updates. Then for 24 hours the script is only saying

    “Currently processing the following update:
    1 IGBGSOFTE… Accepted KB2267602 113MB Security Intelligence Update for …”

    If I check the log file, it says:

    X ComputerName Result KB Size Title
    – ———— —— — —- —–
    1 IGBGSOFTE… Accepted KB4103720 1GB 2018-05 Cumulative Update for Win…
    1 IGBGSOFTE… Accepted KB890830 37MB Windows Malicious Software Remova…
    1 IGBGSOFTE… Accepted KB4521858 12MB 2019-10 Servicing Stack Update fo…
    1 IGBGSOFTE… Accepted KB2267602 113MB Security Intelligence Update for …

    Furthermore, the get-wujob returns:
    ComputerName Name Action
    ———— —- ——
    Servern… PSWindowsUpdate import-module PSWindowsUpdate; Get-WindowsUpdate -AcceptAll -Install | Out-File C:\PSWindowsUpdate.log
    It seems that the updates are accepted but the download and installation process didn`t start.
    Do you have idea what`s wrong?

    • This is most likely a limitation of Windows Updates. Sometimes their download servers have issues. When running into this, are you able to manually run windows updates and install the updates still?

      • Neztach says:

        I would bet this is a firewall issue. On Windows Server 2016 and up, if Windows firewall service is stopped, updates will timeout and never work.

        implement some variant of this at the beginning of your script

        $firewallStatus = Get-Service -Name ‘MpsSvc’
        If ($firewallStatus.Status -eq ‘Running’){
        <>
        } else {
        (Get-WmiObject -Class win32_service -Filter “name=’MpsSvc'”).startservice()
        start-sleep 1
        $check = Get-Service -Name ‘MpsSvc’
        If ($check.Status -eq ‘Running’){
        <>
        } else {
        Write-Host “Can’t start firewall service. Start svc and try again”
        Break
        }
        }

        then somewhere in the script wrap-up, if the script enabled the firewall, then it should disable as a standard cleanup task.

  • Robert Olsén says:

    Hi!
    A question about the

    do {
    [..]
    }until ( $installednumber -eq $updatenumber)

    What if a update fails to install? Then you will never get out of the loop, right?

    • Hi Robert,

      I’ve added in a timeout counter that can be modified. Right now after 2 hours it will time out if stuck inside the loop. Also added “failed number” of updates to count for updates failing. Thanks!

  • Andrew O'Brien says:

    Luke, brilliant article and great script – thank you for sharing this. I’m running this successfully across our domain, but frequently get errors at the end of the script “No MSFT_ScheduledTask found with property ‘TaskName’ equal to ‘PSWindowsUpdate’. I can see that line 82 is where the unregister command is, but can’t see where the scheduled task is actually created – lines 44 to 48 seem to be where it should be?

    • Hi Andrew,

      The scheduled task gets created from the Invoke-WUjob command. There are limitations with the Windows OS that prevent remotely running windows updates, so the only way to get some sort of remote windows updates is to remotely create a scheduled task with PowerShell to run the windows updates locally. Not sure why that task is erroring sometimes, could be that there were no windows updates needed.

  • Xavier says:

    Hello Luke,

    I really love the concept of that script and I thank you for sharing it with us. Unfortunately, I have been unsuccessful running it in my environment. We have computers on our domain as well as computers belonging to a workgroup. However, all computers are all on the same subnet.

    Whenever, I try to run the script, either to update the Domain computers or the workgroup ones, I get the following error: Connecting to remote server machinename failed with the following error message: The client cannot connect to the destination specified in the request. Verify that the service on the destination is running and is accepting requests.

    Is there anything that I am doing wrong. I have tried several things such as using the following statements:
    winrm set winrm/config/client/auth ‘@{Basic=”true”}’
    winrm set winrm/config/client ‘@{AllowUnencrypted=”true”}’
    on both the server in charge of launching the updates and the destination machines. I have also tried Set-ExecutionPolicy unrestricted
    but all my attempts remained unsuccessful.
    Would you have any idea what I am doing wrong?

    • Hi Xavier, you may be running into WSMan trusted host issues with your non domain joined workstations. You can try adding the computers your updating to your server’s trusted hosts like this:
      #set trusted host on Source Endpoint to remotely connect to servers
      $trustedhosts = (Get-item WSMan:\localhost\Client\TrustedHosts).value
      If($trustedhosts)
      {
      (Get-item WSMan:\localhost\Client\TrustedHosts).value + “,$computer”
      }
      else
      {
      $trustedhosts = $computer
      }
      Set-Item WSMan:\localhost\Client\TrustedHosts –Value $trustedhosts -Force

      Then add a cleanup step at the end to remove that computer from trusted hosts for security:
      #remove computer from trusted hosts for cleanup
      $trustedhosts = ((Get-item WSMan:\localhost\Client\TrustedHosts).value).replace(“$computer”,””).TrimEnd(“,”)
      Set-Item WSMan:\localhost\Client\TrustedHosts –Value $trustedhosts -Force

      Let me know if that helps. Thanks!

  • Mike Pinkston says:

    noob here. I am getting an error when running the script and not having much luck troubleshooting the error.

    At C:\temp\NewWindowsUpdate.ps1:7 char:1
    + [CmdletBinding()]
    + ~~~~~~~~~~~~~~~~~
    Unexpected attribute ‘CmdletBinding’.
    At C:\temp\NewWindowsUpdate.ps1:8 char:1
    + param (
    + ~~~~~
    Unexpected token ‘param’ in expression or statement.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedAttribute

  • James says:

    This script is really useful, thanks for sharing.

    How would you advise dealing with errors, a lot of the VMs I run this on are always close to being completely full and this stops updates from working a lot of the time. It looks like the script just carries on ad infinitum, what’s going to be the best way to make the script bail out and try the next VM?

    • Hi James,

      It might be worth it to just make sure your VMs have adequate space to run Windows updates before running this script instead of trying to modify the script to work around this issue. But, you could add a portion in there to check for available disk space on the VM and if its under 2GB do nothing.

      -Thanks!

  • Andrew Carey says:

    Hi there,

    I’m having issues installing the module. I receive the following error.


    PS C:\WINDOWS\system32> Install-Module -Name PSWindowsUpdate
    WARNING: Source Location ‘https://www.powershellgallery.com/api/v2/package/PSWindowsUpdate/2.1.1.2’ is not valid.
    PackageManagement\Install-Package : Package ‘PSWindowsUpdate’ failed to download.
    At C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.0.0.1\PSModule.psm1:1809 char:21
    + … $null = PackageManagement\Install-Package @PSBoundParameters
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : ResourceUnavailable: (C:\Users\andrew…owsUpdate.nupkg:String) [Install-Package], Exception
    + FullyQualifiedErrorId : PackageFailedInstallOrDownload,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage”

    Can you advise whats going wrong?

    Thanks in advance

    • Hey Andrew, try updating your NuGet to the latest version. There were some faulty releases of NuGet that caused this error. Open up an administrative PowerShell console and run: install-packageprovider nuget -force. Then re-launch the PowerShell console.

  • Ed says:

    Hi Thanks for the script Luke, not sure if by design or not but the log gets wiped as it doesn’t have the append switch. This meant I could only see the last update applied, not sure if this happens every time, or just if there is an update after a reboot.
    I also found I had to add some registry key creation to the script to enable tls 1.2 on older .net installs so I could install nuget. I added checks for the reg keys, nuget already being installed, as well as the pswindowsupdate module to avoid unnecessary steps on future runs

  • Kenneth says:

    Hi,
    If I use the script for multiple computers, will it run in parallel, or finish each server before going to the next?

  • Gilbert says:

    Love you script Luke. Thank you so much for this tutorial!
    Is there a way to install only important updates and not the optional ones?

  • Bryan Hatt says:

    Hi Luke, thanks for writing this script.

    I have been using it for a little over a year now and it has been really helpful for automating the preparation of Citrix Gold Image VMs.

    I have noticed a couple of problems that I have managed to find fixes for, so I thought I would feed back here.

    The main problem I had was that the script was never finishing. The ” }until($updates -eq $null)” on line 101 was not triggering. I changed this line to “}until($updatenumber -eq 0)” and that seems to have resolved the problem for me.

    I also added a check for the Task scheduler task so that the script doesn’t try to remove it if it doesn’t exist.

  • Ole says:

    Very good script!
    On some servers I get this error message after the script has searched for updates:

    (10,44):StartBoundary:2021-05-19T15.35.38
    + CategoryInfo : NotSpecified: (:) [Invoke-WUJob], COMException
    + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException,PSWindowsUpdate.InvokeWUJob

    I can’t find out what the issue is. Do you know?

    • Is there anything in the Windows Update Log files? Typically this type of error is caused by a firewall, connectivity issues, or the Windows Update service itself being in an inconsistent state. Do updates run manually with no issues on these systems?

  • Adam Sanderson says:

    I’m trying to use this on standalone laptops. I was able to manually run the “Get-WindowsUpdate -install -acceptall -autoreboot” command in Powershell and it rebooted after the first round of were updates installed. However, after rebooting nothing happens since I manually ran the commands. I need to have a script that resumes checking for updates when using a standalone PC. How can I make this work on a standalone computer so that it continually runs and reboots until all the updates have been installed? I have minimal experience with scripting so please forgive me question if it’s not clear. Thank

  • cyberdott says:

    Hi there,

    the script installs PSWindowsUpdate on the remote machine every time, regardless of whether it is already installed or not. Is this correct? This slows down the hole update-prccess. Can this be turned off? THX

    • Hi Cyberdott! Good feedback here. Yes, the -force parameter on the lines that install the PSWindowsUpdate package on the remote machine causes the package to be installed each time. You could add logic to the script to check for the PSWindowsUpdate Package first if needed. I’ll reach out to the script author with the feedback so it gets updated during the next update. Thanks!

  • Wayne says:

    Line |
    7 | [CmdletBinding()]
    | ~~~~~~~~~~~~~~~~~
    | Unexpected attribute ‘CmdletBinding’.

    Any idea why this fails? Have Nuget, PowershellGet, PackageManager…

    Regards 🙂

  • Alex says:

    Randomly came across this script and decided to give it ago, seems to work fine but is extremely slow, would be amazing if it ran on several servers from a text file in parallel, its taking me around 20mins per server with only 3 small updates.

    unregister scheduled task is not present on windows 2008, you need a Get-wmiobject check for operating system link Microsoft Windows Server 201* and only unregister if on OS higher than 2008.

    2008 you need to schtask delete.

    unsure if its the timers but it takes far longer to run this script than it does to run the command locally.

  • Robin says:

    Hi Luke,

    First of all Thanks for the script it works when i install nuget and pswindowsupdate on the machine by hand.
    But installing Nuget and pswindowsupdate over the script does not work and it’s just throwing errors.
    i also need this command to make the installation work manually
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    PS C:\Windows\system32> PowerShell “C:\Users\xx.xx-adm\Documents\install-windowsupdates.ps1″ -computer bs01
    WARNING: MSG:UnableToDownload «https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409» «»
    WARNING: Unable to download the list of available providers. Check your internet connection.
    WARNING: Unable to download from URI ‘https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409’ to ”.
    No match was found for the specified search criteria for the provider ‘NuGet’. The package provider requires
    ‘PackageManagement’ and ‘Provider’ tags. Please check if the specified package has the tags.
    + CategoryInfo : InvalidArgument: (Microsoft.Power…PackageProvider:InstallPackageProvider) [Install-Pac
    kageProvider], Exception
    + FullyQualifiedErrorId : NoMatchFoundForProvider,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackagePro
    vider
    + PSComputerName : bs01

    NuGet provider is required to continue
    PowerShellGet requires NuGet provider version ‘2.8.5.201’ or newer to interact with NuGet-based repositories. The NuGet
    provider must be available in ‘C:\Program Files\PackageManagement\ProviderAssemblies’ or
    ‘C:\Users\xx.xx-adm\AppData\Local\PackageManagement\ProviderAssemblies’. You can also install the NuGet provider
    by running ‘Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force’. Do you want PowerShellGet to install
    and import the NuGet provider now?
    [Y] Yes [N] No [?] Help (default is “Y”): Y
    WARNING: Unable to download from URI ‘https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409’ to ”.
    WARNING: Unable to download the list of available providers. Check your internet connection.
    No match was found for the specified search criteria for the provider ‘NuGet’. The package provider requires
    ‘PackageManagement’ and ‘Provider’ tags. Please check if the specified package has the tags.
    + CategoryInfo : InvalidArgument: (Microsoft.Power…PackageProvider:InstallPackageProvider) [Install-Pac
    kageProvider], Exception
    + FullyQualifiedErrorId : NoMatchFoundForProvider,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackagePro
    vider
    + PSComputerName : bs01

    No match was found for the specified search criteria and provider name ‘NuGet’. Try ‘Get-PackageProvider
    -ListAvailable’ to see if the provider exists on the system.
    + CategoryInfo : InvalidData: (NuGet:String) [Import-PackageProvider], Exception
    + FullyQualifiedErrorId : NoMatchFoundForCriteria,Microsoft.PowerShell.PackageManagement.Cmdlets.ImportPackageProv
    ider
    + PSComputerName : bs01

    If i install nuget manually i get this:

    PS C:\Windows\system32> PowerShell “C:\Users\xx.xx-adm\Documents\install-windowsupdates.ps1″ -computer bs01
    WARNING: MSG:UnableToDownload «https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409» «»
    WARNING: Unable to download the list of available providers. Check your internet connection.
    WARNING: Unable to download from URI ‘https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409’ to ”.
    No match was found for the specified search criteria for the provider ‘NuGet’. The package provider requires ‘PackageManagement’ and ‘Provider’ tags. Please check if the
    specified package has the tags.
    + CategoryInfo : InvalidArgument: (Microsoft.Power…PackageProvider:InstallPackageProvider) [Install-PackageProvider], Exception
    + FullyQualifiedErrorId : NoMatchFoundForProvider,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackageProvider
    + PSComputerName : bs01

    No match was found for the specified search criteria and module name ‘pswindowsupdate’. Try Get-PSRepository to see all available registered module repositories.
    + CategoryInfo : ObjectNotFound: (Microsoft.Power….InstallPackage:InstallPackage) [Install-Package], Exception
    + FullyQualifiedErrorId : NoMatchFoundForCriteria,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage
    + PSComputerName : bs01

    The specified module ‘PSWindowsUpdate’ was not loaded because no valid module file was found in any module directory.
    + CategoryInfo : ResourceUnavailable: (PSWindowsUpdate:String) [Import-Module], FileNotFoundException
    + FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand
    + PSComputerName : bs01

  • Gordon Mac says:

    Thank you for this, great bit of work.
    I’ve got a minor issue wonder if you may have an idea of why?
    Only sometimes the Out-File C:\PSWindowsUpdate.log fails to create the file and I’ll then get
    VERBOSE: Found [18] Updates in pre search criteria
    VERBOSE: Found [18] Updates in post search criteria
    Get-Content : Cannot find path ‘\\Server1\c$\PSWindowsUpdate.log’ because it does not exist.
    At C:\SLCScripts\WindowsUpdates\InstallWindowsUpdates.ps1:125 char:33
    + … do {$updatestatus = Get-Content \\$c\c$\PSWindowsUpdate.log
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : ObjectNotFound: (\\Server1\c$\PSWindowsUpdate.log:String) [Get-Content], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand

    As I say this only happens occasionally.

    I have also taken the liberty of adding a Pending Reboot check, prior to checking for updates, which thought may be of interest.

    #check for pending reboot
    Invoke-command -computername $c -scriptblock {Get-WUIsPendingReboot | Out-File C:\PendingReboot.log}

    $pending = Get-Content \\$c\c$\PendingReboot.log
    $check = ([regex]::Matches($pending, “True” ))

    If ($pending -eq $check) {

    Write-Output “Updates are pending rebooting $c …”
    Restart-Computer -ComputerName $c -Force
    Break

    }

    Else
    {

    Write-Output “No Reboot Pending on $c”

    }

    • Hey Gordan! Thanks for the feedback on this! I’ll make sure Luke sees this for the next time he updates this article! Regarding the error you’re getting. Is that on the same machines every time? Can you confirm the WindowsUpdate.log is present or not when the error is thrown?

  • Rizwan says:

    Hi Luke
    Can i run this script on a looged on machine. so the reboot can be included

  • David says:

    Hi! I need the script not to reconnect after reboot. Do you have any idea what I can skip?

    regards!

  • Aps says:

    Hi, Thanks for a great script. wondering if i can use it for 600 virtual servers, and then the output of each server installed updates in excel or csv file ?
    Will be very much easier for reporting (For that particular month, i mean how many patches got installed / not applicable / or failed status) ? Any possibilities?

  • Alex R says:

    Total PS noob here. I’ve got PSWindowsUpdate installed on various Windows Server 2016 VMs and a Windows 10 VM I am executing their commands remotely from. I was able to update all of my server VMs successfully using PSWU commands. But, after restarting, a few of the VMs no longer respond to remote commands though the same commands work from the VMs’ consoles.

    The problems some servers experience are one of two issues:
    1) The command seemingly executes but nothing is returned.
    For instance, on my Win10 VM I enter “get-wuhistory -computername SERVER -verbose” and get “Connecting to default for SERVER. Please wait…” No data is returned. I just come back to the PS prompt within a couple of seconds. Executing the command in PS at the server’s console results in the full history of applied Windows updates.
    2) I enter the command “get-wuhistory -computername SERVER -verbose” and get the following results

    —————————————————–
    VERBOSE: Performing the operation “(5/19/2020 8:58:35 AM) Get Windows Update History” on target “SERVER”.
    get-wuhistory : CORP-SYS-DC-01: Unknown failure.
    At line:1 char:1
    + get-wuhistory -computername SERVER -verbose
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : PermissionDenied: (:) [Get-WUHistory], Exception
    + FullyQualifiedErrorId : Unknown,PSWindowsUpdate.GetWUHistory
    —————————————————–

    Again, these commands worked fine until I restarted some of the servers. Oddly, the problem affects only a few of my servers.

Leave a comment

Your email address will not be published.