The PowerShell script described here enables you to install Windows updates more flexibly than with Windows Server Update Services (WSUS) or Group Policy. To this end, it allows you to determine precisely when to install certain updates on different types of computers in your network.

Alex Chaika

Alex Chaika is a Microsoft Certified Solution Expert (MCSE) with more than 15 years of experience in IT systems engineering. He currently focuses on PowerShell and VMware PowerCLI.

WSUS and Group Policy work well for updating Windows if it doesn't really matter much when to install updates. If you look at the standard Group Policy settings below for Automatic Updates, you realize the schedule options are very limited. All you can determine is the day on which to install updates.

Default updates schedule

Default updates schedule

However, I needed to create a more precise schedule for my update tasks. For instance, I wanted to install updates every second Thursday of the month on my pre-production systems, every third Monday on my test computers, and every third Thursday on my production system. The standard tools from Microsoft don't provide these options.

The PowerShell script below uses the Windows Update Agent API that you can access through the Microsoft.Update.Searcher COM object.

First, I need to determine the criteria that I have to supply to the Microsoft.Update.Searcher object. There are actually a lot of choices for the criteria. For example, I could get the updates already installed, or get updates marked “hidden,” or get a particular update by its ID. For my purpose, I only need software updates not yet installed. I exclude all other updates, such as driver updates.

I create the Searcher object with the New-Object cmdlet using its search method with the criteria I established in the previous step. Then I create the Microsoft.Update.Session object because I'd like to make sure to download the updates selected for installation.

Next, I use the CreateUpdateDownloader method of the Session object to create the UpdateDowloader interface. In the next step, with the help of the $SearchResult value, I determine the updates to download. I then call the Download() method, which initiates the actual download of the updates.

The last four lines are for installing the updates. First, I create the Installer object, and then I assign the value of $SearchResult to the object's $Installer.Updates property to ensure installation of only the updates that meet my criteria.

Finally, the Install() method installs the updates. I'm saving the result of the installation into the $Result variable, so I can check whether to require a reboot after the installation. The last line of the script accomplishes this.

There is only one significant caveat of this script: It must run locally. Some Windows Update Agent tasks can run remotely, but tasks for downloading and installing updates have to run locally. To solve this problem, I created a scheduled task with the desired run time on my test computer and exported this task to an XML file. Then I wrote a short PowerShell script to import this task to all machines on which to install updates:

The script executes the "schtasks" command remotely. It uses the Invoke-Expression cmdlet and the imported XML file that contains the scheduled task on a list of computers imported from a CSV file.

If you want to learn more about how to work with scheduled tasks in Windows, you can read the article I wrote some time ago.

Win the monthly 4sysops member prize for IT pros

Share
8+

Users who have LIKED this post:

  • avatar
  • avatar

Related Posts

20 Comments
  1. Lars Panzerbjørn 8 months ago

    Even though this must run locally, I feel like it should be possible to control centrally via DSC...

    0

    • Author
      Alex Chaika 8 months ago

      Well, I guess it might be possible to use the DISC script resource to run it, but I haven't tried it.

      1+

  2. Tom Hughes 6 months ago

    Hi!

    this looks amazing and exactly what i'm looking for! however i'm receiving this error:

    Exception calling "Download" with "0" argument(s): "Exception from HRESULT: 0x80240044"
    At C:\installupdates.ps1:10 char:1
    + $Downloader.Download()
    + ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation

    Exception calling "Install" with "0" argument(s): "Exception from HRESULT: 0x80240044"
    At C:\installupdates.ps1:14 char:1
    + $Result = $Installer.Install()
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation

    not sure what i'm doing wrong but have tested it on a computer thats ready for search and download updates, i've also tested it on one that already has updates ready to install and i get the same error.

    0

    • Author
      Alex Chaika 6 months ago

      Check if you are running your PowerShell session on behalf of administrator (Run as administrator). The error you are getting might suggest that the user running this script doesn't have enough permissions.

      0

  3. James 5 months ago

    Thanks for the insight - it is very helpful.

    Just so you know, the reason the download function doesn't work in a remote session is because of the 'double-hop' restriction in the default configuration of powershell endpoints.

    If you properly configure a Just-Enough-Administration (JEA) endpoint, you can do the whole thing without resorting to scheduled tasks. The JEA endpoint needs to be configured to RunAsVirtualAccount and would need either the "FullLanguage" LanguageMode, or have the commands wrapped in a module/visible functions.

     

    2+

    Users who have LIKED this comment:

    • avatar
    • avatar
    • Author
      Alex Chaika 5 months ago

      Good point!

      I just tested it using JEA configuration and it worked great!

      Thank you!

      1+

  4. Brian 4 months ago

    Thanks for the script, working well for the most part.

    I am having an issue running it on Server 2016. The script works fine as long as it is run as administrator on my 2008/2012 servers but still get the permission error Tom Hughes referenced on 2016. Has anyone seen this behavior?

    I'm running scripts as administrator on all server versions and my logon ID is a member of the local admins group.

    0

    • Author
      Alex Chaika 4 months ago

      Could you paste the text of the error you're getting here?

      1+

      • Brian 4 months ago

        PS C:\Windows\system32> C:\Support\WSUS\wsus_install_updates.ps1
        Exception from HRESULT: 0x80240024
        At C:\Support\WSUS\wsus_install_updates.ps1:15 char:1
        + $Downloader.Download()
        + ~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OperationStopped: (:) [], COMException
        + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException

        Exception from HRESULT: 0x80240024
        At C:\Support\WSUS\wsus_install_updates.ps1:19 char:1
        + $Result = $Installer.Install()
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OperationStopped: (:) [], COMException
        + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException

        Unless there is something easily identifiable I may just rebuild the server since it isn't in production. I've tested on another 2016 server and it worked fine there.

        0

        • Author
          Alex Chaika 4 months ago

          I just ran it on the Win 2016 server and it worked.

          And as you said it worked on on another server for you as well.

          So, its probably that particular server issue. I've seen similar behavior once, except that in my case I haven't been able to install updates at all. It was Windows 2016 RTM fresh install. So I ended up with downloading and installing manually following update:

          http://www.catalog.update.microsoft.com/Search.aspx?q=kb4015217

          After that it started working.

          I can't say if it is your case or not, though.

          1+

        • John 4 months ago

          I got this on several of my Windows 2016 servers. For me it turned out to be that I had a proxy set. Once I removed that it worked fine.

          So in general I think this error is saying that it is unable to talk to the specified WSUS server. In my case it was a local WSUS server but I think you would get the same error if you were trying to use Microsoft but did not have internet access.

          0

  5. senthil 4 months ago

    Hi ,

    Thank you for your wonderful work.

    I need to save the downloaded updates into local disk the i will trigger the updates whenever i require could you please tell me how to do it in powershell. i am very new for this..

    Thanking in advance

    0

  6. John Thayer Jensen 4 months ago

    Alex - I am trying to work out how to force the searcher to access Microsoft directly even though the GPO for our machines tells them to use SCCM. It looks like the ServerSelection property is the one to set but I don't understand how to use it.

    Any thoughts?

    John Thayer Jensen
    Applications Specialist
    University of Auckland Business School

    0

  7. John Thayer Jensen 4 months ago

    And - sorry, I should have tried this first! - it looks like I just set the value of ServerSelection to 2, which appears to mean the Windows Update service.  I'll try that.

    jj

    0

  8. John 4 months ago

    Great scrip. I expanded it a little to give a bit more feedback and control

    2+

  9. Tommy Becker 3 months ago

    You need to add a hook to run the *.AcceptEula() method on updates that require EULA.

    0

  10. senthil 2 months ago

    could you please help me to modify this code using patch id to download only particular patch and save it in to local disk.

    0

  11. david puckett 1 month ago

    modified for my purpose

    added: date filter (patch tuesday, same patches for everyone)

    added: scan / install

    added: reboot force
    ###################
    ### example: run script -> powershell.exe -file {scriptname} 2017-OCT-09 Action=Install ForceReboot=Yes
    ### example: run script -> powershell.exe -file {scriptname} 2050-JAN-01 Action=ScanOnly
    ### fwiw: really should add fuctionions instead of long script
    #$args.count
    $DateIn=$args[0]
    $ActionIn=$args[1]
    $bScanOnly=$true
    $bForceReboot=$false

    if ($args.Length -gt 2) {$ForceRebootIn=$args[2]} else { $ForceRebootIn="no" }

    #needed: Add acceptance for EULA

    #If no date passed to the script, quit
    if ( $DateIn -eq $null ){write-host "Date parameter missing, script exiting."; Exit}
    #If no action passed to the script, quit
    if ( $ActionIn -eq $null ){write-host "Action parameter missing, script exiting."; Exit}

    #If "Action=Install" value passed, scan only and exit.
    if ( $ActionIn.ToLower() -eq "action=install" ){ $bScanOnly=$false }

    #If "ForceReboot=True" value passed, reboot at end of install if required.
    if ( $ForceRebootIn.ToLower() -eq "forcereboot=yes" ){ $bForceReboot=$true }

    ###added for limiting installs by before date###
    $enus = "en-US" -as [Globalization.CultureInfo]
    $LastDeploymentChangeTime = $DateIn + " 12:00:00 AM"
    $BeforeCheckDate = [datetime]::ParseExact($LastDeploymentChangeTime,"yyyy-MMM-dd hh:mm:ss tt", $enus)
    #$BeforeCheckDate

    #Initialize global variables
    $INSTALLFAILED = $false
    $REBOOTREQUIRED = $false

    #Initialize objects that are needed for the script
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    $Session = New-Object -ComObject Microsoft.Update.Session
    $Installer = New-Object -ComObject Microsoft.Update.Installer
    $CheckInfo = $Session.CreateUpdateDownloader()
    $Downloader = $Session.CreateUpdateDownloader()
    $UpdatesToCheckInfo = New-Object -ComObject Microsoft.Update.UpdateColl
    $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
    $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl

    #Clear-Host

    #This set the criteria used for searching for new updates
    $Criteria = "IsInstalled=0 and Type='Software'"

    #This searches for new needed updates and stores them as a list in $SearchResult
    Write-Host "Searching for updates. Please wait... (Date criteria: before $BeforeCheckDate)"
    $SearchResult = $Searcher.Search($Criteria).Updates | Where-Object { $_.LastDeploymentChangeTime -lt $BeforeCheckDate }

    #This gets the number of updates that are currently needed by the given server
    $NumberOfUpdates = $SearchResult.Count

    #If $NumberOfUpdates is zero then there are no needed updates so we just exit
    #Otherwise inform the user of how many updates are required then proceed
    if ($NumberOfUpdates -gt 0) {
    Write-Host "There are "$SearchResult.Count" updates to install"
    Write-Host
    }
    else {
    #Write-Host "There are no updates to install released before $BeforeCheckDate"
    Write-Host "There are no updates released before $BeforeCheckDate to install."
    Exit
    }

    ######################################################################################################################
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #Show the updates
    Write-Host "Applying date criteria: before $BeforeCheckDate"
    #$SearchResult | Select-Object -Property @{N="Patch Release Date";E={$_.LastDeploymentChangeTime}}, `
    $SearchResult | Select-Object -Property @{N="PatchReleaseDate";E={$_.LastDeploymentChangeTime}}, `
    @{N="Severity";E={$_.MsrcSeverity}}, Title | `
    Where-Object { $_.PatchReleaseDate -lt $BeforeCheckDate } | Format-Table -AutoSize

    #Exit if not "Action=Install"
    if ($bScanOnly) { exit }

    ######################################################################################################################

    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #This steps through each of the discovered updates and downloads them one at a time
    #These are done individually to provide more feedback on the overall progress of the script
    ForEach ($Update in $SearchResult) {
    #This adds the current Update to the collection and sends the return value to null to mask bogus output
    $null=$UpdatesToDownload.Add($Update)

    #This creates a temporary download task
    $Downloader.Updates = $UpdatesToDownload

    if ($Downloader.Updates.Item(0).LastDeploymentChangeTime -lt $BeforeCheckDate){
    #Update the end user with which patch is being downloaded
    $DownloadSize = "{0:N1}" -f ((($Downloader.Updates.Item(0).MaxDownloadSize)/1024)/1000)
    Write-Host "Downloading ->"$Downloader.Updates.Item(0).Title" ---Please wait..."
    Write-Host "Estimated Size: "$DownloadSize"MB"

    #This checks to see if the current update has already been downloaded at some point
    #If it already exists then the download is skipped. Otherwise it starts the download
    if ($Downloader.Updates.Item(0).IsDownloaded) {
    Write-Host "Update has already been downloaded -> Skipped"
    }else{
    #Process the actual download
    $null=$Downloader.Download()
    }

    #This verifies that the update was successfully downloaded. If not it alerts the user and aborts the script
    if (-not ($Downloader.Updates.Item(0).IsDownloaded)) {
    Write-Host "Download failed for unknown reason. Script abort."
    Exit
    }
    }else{
    #Write-Host "skipping update -> "$Downloader.Updates.Item(0).Title " Released:" $Downloader.Updates.Item(0).LastDeploymentChangeTime
    }

    #This clears the update collection to prepare for the next update
    $UpdatesToDownload.Clear()
    Write-Host

    }

    Write-Host "All updates have been downloaded. Proceeding with installation steps..."
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #This copies the list of updates from above to a list that should be installed on the computer
    #$Installer.Updates = $SearchResult

    #This steps through each of the downloaded updates and installs them one at a time
    #These are done individually to provide more feedback on the overall progress of the script
    ForEach ($Update in $SearchResult) {
    #This adds the current Update to the collection and sends the return value to null to mask bogus output
    $null=$UpdatesToInstall.Add($Update)

    #This creates a temporary install task
    $Installer.Updates = $UpdatesToInstall

    if ($Installer.Updates.Item(0).LastDeploymentChangeTime -lt $BeforeCheckDate){

    #Update the end user with which patch is being installed
    Write-Host "Installing ->"$Installer.Updates.Item(0).Title" ---Please wait..."

    #This checks to see if the current update has already been installed at some point
    #If it is already installed then the install is skipped. Otherwise it starts the install
    if ($Installer.Updates.Item(0).IsInstalled) {
    Write-Host "Update has already been installed -> Skipped"
    }else{
    #Process the actual install
    $result=$Installer.Install()
    }

    #This verifies that the update was successfully installed. If not it alerts the user then continues with the next update
    if (-not ($result.ResultCode -eq 2)) {
    Write-Host "WARNING!!! - Installation failed for unknown reason. Continuing to next update."
    $INSTALLFAILED = $true
    }
    }else{
    #Write-Host "skipping install -> "$Installer.Updates.Item(0).Title " Released:" $Downloader.Updates.Item(0).LastDeploymentChangeTime
    }

    #This clears the update collection to prepare for the next update
    $UpdatesToInstall.Clear()
    Write-Host

    #This checks to see if the current update requires a reboot
    if ($result.rebootRequired) {
    Write-Host "INFO!!! - Installation requires reboot."
    $REBOOTREQUIRED = $true
    }
    }

    Write-Host
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"
    Write-Host "Update Results:"
    Write-Host

    #Alert the user about installation success
    if ($INSTALLFAILED) {
    Write-Host "WARNING!!! - At least one update failed to install."
    }
    else {
    Write-Host "All updates have been installed."
    }

    #Alert the user if reboot is required
    if ($REBOOTREQUIRED) {
    if ($bForceReboot){
    Write-host "Rebooting. ForceReboot=$bForceReboot"
    Start-Sleep -s 10
    Restart-Computer -Force
    }else{
    Write-Host "INFO!!! - At least one update requires a reboot. Please reboot now."
    }
    }
    else {
    Write-Host "Script Finished"
    }

    Exit

     

    0

  12. david puckett 1 month ago

    want to call it from a patch file?

    assumes patching.ps1 is the file name

    powershell.exe -file "%~dp0patching.ps1" 2017-oct-17 Action=Install

    1+

    • David Puckett 1 month ago

      I spent some time trying to think around the double hop issue and not being able to download patches when invoking the script remotely.
      Imo, the workaround to force patch installs on a remote computer is launch an RDP session with a PatchingAccount that runs a command at login. (disable the account when patching is completed until the next patch tuesday)
      Another option is to remotely schedule a task and have it launch in ~x minutes.

      0

Leave a reply

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

*

CONTACT US

Please ask IT administration questions in the forum. Any other messages are welcome.

Sending
© 4sysops 2006 - 2017

Log in with your credentials

or    

Forgot your details?

Create Account