Automate Patching With PoshWSUS and PowerShell Scheduled Jobs

Introduction

Hello there! If you’re a sysadmin, chances are you have experienced both the joy and pain (but mostly pain) of a wonderful Microsoft product known as Windows Server Update Services or WSUS. My current role involves heavy use of WSUS, patching fleets of servers and workstations. That requires short maintenance windows and a number of tasks that must be performed before reboots. When you have more than one customer environment, being able to perform those tasks automatically, uniformly, and reliably is important. I want to show you how to make working with WSUS a little less painful.

Let’s get started!

PoshWSUS

PoshWSUS is a PowerShell module created by Boe Prox that manages WSUS. This module contains a plethora of useful cmdlets that allow you to manage & maintain clients and Windows Updates. There’s even a cmdlet to help you clean up WSUS.

PoshWSUS gives you a little more control than the built-in WSUS cmdlets available in Windows Server 2016 and above do.

PowerShell Scheduled Jobs

A scheduled job is a background job. Starting with PowerShell 3.0, a new module called PSScheduledJob was introduced with an additional 16 cmdlets for managing these jobs. You only need to know when, how often, and what command (a script) to run. When you have specific maintenance windows and a litany of maintenance tasks to complete, scheduled jobs can enhance your efficiency in performing those tasks.

Example Scenario

You have a WSUS server and configured computer groups based on your three target groups: Production (PROD), User Acceptance Testing, (UAT), and Development (DEV). To add a little more complication, you also have Primary (PRI) and Secondary (SEC) servers as subgroups within each target group. On top of all this, each target group has a strict maintenance window and can’t be done at the same time. Conservatively, you plan out the monthly patch cycle for each environment to occur on the weekends. Developers want patches to be deployed on Saturday between 8 AM and 12 PM. UAT testing can only be performed Monday through Tuesday during normal business hours of 9 AM to 5 PM. The customer production environment must be patched between 7 AM and 11 PM on the last Sunday of each month according to the Service Level Agreement (SLA).

Because you don’t like working weekends and have some PowerShell knowledge, you decide to build some scripts that will do this task for you. The only manual thing you will do is schedule a maintenance window in your monitoring solution to prevent extraneous alert emails during these times.

Getting Started

The following code example will download the latest version from the https://powershellgallery.com/ repository.

1 # It's good practice to put modules in your user context, not the system's.
2 Install-Module -Name PoshWSUS -Scope CurrentUser

Alternatively, you can also clone from the source using Git:

1 # Places the module directly in your user context
2 Set-Location -Path "C:\users\$env:USERNAME\Documents\WindowsPowerShell\Modules\"
3 
4 git clone https://github.com/proxb/PoshWSUS/

Once installed, verify that the module installed correctly and can be imported.

1 <#
2     If you cloned from the source directly, you may be able to skip this part
3     as long as you restarted your console session.
4 #>
5 Import-Module -Name PoshWSUS
6 
7 # Show the possibilities!
8 Get-Command -Module PoshWSUS -All

Now you are ready to begin working with WSUS through the PoshWSUS module.

Connect And Poke Around

Now that you have the correct module loaded, you need to connect to your WSUS instance. To do so you’ll need the Connect-PSWSUSServercmdlet.

1 Get-Help Connect-PSWSUSServer -ShowWindow

The two parameters you will need are -WsusServer and the -Port WSUS is operating on. By default, WSUS uses HTTP 8530 and for HTTPS/SSL 8531. If your WSUS instance is different, please consult docs.microsoft.com for more information.

Now that you know how to connect to your WSUS instance using the Connect-PSWSUSServer cmdlet and a few parameters, consider the following example. Here the lab server’s Fully Qualified Domain Name (FQDN) is used and the default port of 8530. The -Verbose parameter issued only to show you the connection messages:

1 Connect-PSWSUSServer -WsusServer 'WSUS.kindlelab.int' -Port 8530 -Verbose

Output:

1 VERBOSE: Connecting to WSUS.kindlelab.int <8530>
2 
3 Name                 Version              PortNumber      ServerProtocolVersion
4 ----                 -------              ----------      ---------------------
5 WSUS.kindlelab.int   6.3.9600.18838       8530            1.8

As you can see, you now have an active connection and can begin issuing other cmdlets supplied by the module. Start by using another cmdlet, Get-PSWSUSServer with the parameter -ShowConfiguration to gather some documentation.

1 Get-PSWSUSConfig

Output:

 1 UpdateServer                  : Microsoft.UpdateServices.Internal.BaseApi.Up...
 2 LastConfigChange              : 5/26/2019 2:22:27 PM
 3 ServerId                      : 00000000-0000-0000-0000-000000000000
 4 SupportedUpdateLanguages      : {he, cs, fr, es...}
 5 TargetingMode                 : Server
 6 SyncFromMicrosoftUpdate       : True
 7 IsReplicaServer               : False
 8 HostBinariesOnMicrosoftUpdate : False
 9 UpstreamWsusServerName        :
10 UpstreamWsusServerPortNumber  : 8530
11 UpstreamWsusServerUseSsl      : False
12 UseProxy                      : False
13 ProxyName                     :
14 ProxyServerPort               : 8080
15 UseSeparateProxyForSsl        : False
16 SslProxyName                  :
17 SslProxyServerPort            : 443
18 AnonymousProxyAccess          : True

That’s just a small sampling of the available properties. There’s no need to go into further details as that would be out of the scope of this chapter. It’s just a little something to keep in mind while working with any WSUS server using this module.

Building A Solution For Automated Patch Management

At this point, it is strongly suggested that you have a documented plan in place for your patching cycle. If you recall in our example scenario, you have known maintenance windows to work with. This makes building a script a lot easier in the long run.

Here’s a little pseudo code to map out the initial process:

  1. Connect to WSUS instance.
  2. Set a few variables for groups, Knowledge Base (KB) list, and date/time.
  3. Read in KB’s that need deployed / installed.
  4. Set approval flag and deadline to designated groups.
  5. Disconnect from WSUS instance.

The first script will be called Deploy-PriSecUpdates.ps1 and will be for the PRI and SEC groups. Now you can code!

 1 # Import required tooling. Should already be there but make sure.
 2 Import-Module -Name PoshWSUS
 3 
 4 # Begin connection.
 5 # Can use '[IP]' or '[FQDN]' here instead of environment variable.
 6 Connect-PSWSUSServer -WsusServer $env:COMPUTERNAME -Port 8530 -Verbose
 7 
 8 # Process
 9 # Set the groups
10 $PRI = Get-PSWSUSGroup -Name 'PRI'
11 $SEC = Get-PSWSUSGroup -Name 'SEC'
12 
13 # Set the deadlines. 2 hours is a good timeframe.
14 $PRIDeadline = (Get-Date).addHours(2)
15 $SECDeadline = (Get-Date)
16 
17 # text file containing KB's
18 $Updates = Get-Content -Path 'C:\PScripts\Maintenance\updates.txt'
19 <#
20     The text file will look like this:
21 
22     KB4509000
23     KB4509001
24     KB4509002
25     etc for each KB released that month.
26 
27     There's a way to eliminate this step which will be discussed later.
28 #>
29 
30 # Profit!
31 # Just as you would select and click on updates to install in the WSUS
32 # management console, this is simply doing the same thing with the
33 # below lines of code:
34 
35 Get-PSWSUSUpdate -Update $Updates |
36     Approve-PSWSUSUpdate -Group $PRI -Action Install -Deadline $PRIDeadline
37 
38 Get-PSWSUSUpdate -Update $Updates |
39     Approve-PSWSUSUpdate -Group $SEC -Action Install -Deadline $SECDeadline
40 
41 # Cleanup. It's best practice to always close connections when they are
42 # no longer needed in a script.
43 Disconnect-PSWSUSServer
44 
45 <#
46     Added bonus of having an email alert to let you know the script ran.
47     You'll need an SMTP server in your environment or access to one elsewhere.
48 #>
49 
50 #Region Email Alert
51 # building a simple multiple line body here.
52 $Body       = @()
53 $Body       += 'The following updates were released:'
54 $Body       += "$Updates"
55 
56 $Body       = $Body | out-string
57 
58 $EmailSplat = @{
59     From        = "Automated Patching With WSUS<Strongbad@homestarrunner.com>"
60     To          = "<homestar@homestarrunner.com>"
61     Subject     = 'WSUS Release Status - Updates Deployed'
62     Body        = "$Body"
63     Priority    = 'High'
64     SMTPServer  = '1.2.3.4'
65     ErrorAction = 'SilentlyContinue'
66 }
67 
68 # Now just take the splatted array values and pass them along to the cmdlet
69 Send-MailMessage @EmailSplat
70 
71 #EndRegion

The email splat is completely optional, but it’s nice to have.

If you open up your WSUS management console now, you’ll notice that updates that were previously unapproved are now approved and deadlined.

The next script, Deploy-DevUatUpdates.ps1, will much of the same code. However, you’re going to change couple of lines. To save time, make a copy of the first script and rename it.

 1 # Import required tooling. Should already be there but make sure.
 2 Import-Module -Name PoshWSUS
 3 
 4 # Begin connection.
 5 # Can use '[IP]' or '[FQDN]' here instead of environment variable.
 6 Connect-PSWSUSServer -WsusServer $env:COMPUTERNAME -Port 8530 -Verbose
 7 
 8 # Process
 9 # Set the groups
10 $PRI = Get-PSWSUSGroup -Name 'DEV'
11 $SEC = Get-PSWSUSGroup -Name 'UAT'
12 
13 # Set the deadlines. 2 hours is a good timeframe.
14 $DEVDeadline = (Get-Date)
15 $UATDeadline = (Get-Date).addHours(2)
16 
17 # text file with the KB's
18 $Updates = Get-Content -Path 'C:\PScripts\Maintenance\updates.txt'
19 <#
20     The text file will look like this:
21 
22     KB4509000
23     KB4509001
24     KB4509002
25     etc for each KB released that month.
26 
27     There's a way to eliminate this step which will be discussed later.
28 #>
29 
30 # Profit!
31 # Just as you would select and click on updates to install in
32 # the WSUS management console, this is simply
33 # doing the same thing with the below lines of code:
34 
35 Get-PSWSUSUpdate -Update $Updates |
36     Approve-PSWSUSUpdate -Group $DEV -Action Install -Deadline $DEVDeadline
37 
38 Get-PSWSUSUpdate -Update $Updates |
39     Approve-PSWSUSUpdate -Group $UAT -Action Install -Deadline $UATDeadline
40 
41 # Cleanup. It's best practice to always close connections when
42 # they are no longer needed in a script.
43 Disconnect-PSWSUSServer
44 
45 <#
46     Added bonus of having an email alert to let you know the script ran.
47     You'll need an SMTP server in your environment or access to one elsewhere.
48 #>
49 
50 #Region Email Alert
51 # building a simple multiple line body here.
52 $Body       = @()
53 $Body       += 'The following updates were released:'
54 $Body       += "$Updates"
55 
56 $Body       = $Body | out-string
57 
58 $EmailSplat = @{
59     From        = "Automated Patching With WSUS<Strongbad@homestarrunner.com>"
60     To          = "<homestar@homestarrunner.com>"
61     Subject     = 'WSUS Release Status - Updates Deployed'
62     Body        = "$Body"
63     Priority    = 'High'
64     SMTPServer  = '1.2.3.4'
65     ErrorAction = 'SilentlyContinue'
66 }
67 
68 # Now just take the splatted array values and pass them along to the cmdlet
69 Send-MailMessage @EmailSplat
70 
71 #EndRegion

Now all you have to do is sit back and wait for the reboots to occur! This is great and all, but you still have to manually run these scripts. To fully automate this task, you can use PSScheduledJobs.

Tying It All Together With PSScheduledJobs

Now that you’ve built two fully working scripts that perform all the point and click tasks you once had, it’s time to set a schedule so that you can meet SLA requirements.

For this, you will create a script for a one time task.

 1 # Since this is a throwaway script, didn't go crazy with getting credentials
 2 $Options = New-ScheduledJobOption -RunElevated -RequireNetwork
 3 $Cred = Get-Credential -UserName KindleLab\SB_WSUS
 4 
 5 # Assign the trigger variables
 6 $trigger1 = New-JobTrigger -Once -At "7/13/2019 8:00:00 AM"
 7 $trigger2 = New-JobTrigger -Once -At "7/28/2019 7:00:00 AM"
 8 
 9 # Build the jobs using splatting
10 $Job1 = @{
11     Name                = 'Primary and Secondary Server Maintenance'
12     Trigger             = $Trigger2
13     Credential          = $Cred
14     FilePath            = 'C:\PScripts\Deploy-PriSecUpdates.ps1'
15     ScheduledJobOption  = $Options
16 }
17 
18 $Job2 = @{
19     Name                = 'Development and UAT Server Maintenance'
20     Trigger             = $Trigger1
21     Credential          = $Cred
22     FilePath            = 'C:\PScripts\Deploy-DevUatUpdates.ps1'
23     ScheduledJobOption  = $Options
24 }
25 
26 # Registering the job finishes the script.
27 Register-ScheduledJob @Job1
28 Register-ScheduledJob @Job2

You only need to run this script once. After the jobs have been successfully registered, you can make alterations to them from within the Task Scheduler management interface just as you would any other scheduled task. The only major difference is the location in which these two jobs are stored. You can find the scheduled jobs under Task Scheduler Library > Microsoft > Windows > PowerShell > ScheduledJobs. Now if all goes according to plan, the scheduled jobs will execute the respective script for each of your environments with no interaction required.

Congratulations! You just automated a WSUS update deployment!

How This Script Fitted Into My Process

The driving force behind the creation of this script stemmed from the fact that I have to wake early and sometimes work late to complete a patching schedule on time. The process involves connecting to a VPN, going through two jump servers, and then working with multiple tools and applications.

There is quite a bit of room for error, and a few have been made.

Luckily, the documented procedures to follow which made the creation of scripts for the applications and servers maintenance plan easy. These scripts run as scheduled jobs before and after the Deploy-Updates.ps1 script runs. All I needed to do was to time myself performing the tasks manually. This gave me a rough estimate of how to set the cadence for execution of the scripts. Over time, these schedules were fine tuned to happen in a fraction of the time it used to take to manually perform the task. One environment that used to take 3 hours, now completes a full maintenance cycle in about an hour.

You may find yourself asking, “Why all the separate scripts?”. My reasoning for doing it this way is about separation. Separation allows me to better troubleshoot a failing part of my process. For instance, a shutdown script may run correctly but the deployment script fails. I don’t have to troubleshoot one massive script and can also check the Task Scheduler for timestamps and error codes. You may find yourself not having or wanting that much control.

Next Steps

Now that you can automate update deployments on your own schedule, try something else. Here are some suggestions based on common tasks when working with WSUS:

  • Try setting up an automatic cleanup schedule using Start-PSWSUSCleanup.
  • Try adding and removing groups in WSUS using New-PSWSUSGroup and Remove-PSWSUSGroup.
  • Use Add-PSWSUSClientToGroup and Remove-PSWSUSClientFromGroup to try adding and removing clients in WSUS.

Summary

The PoshWSUS and PSScheduledJobs modules allow you to take control of WSUS instances using PowerShell. Using this module nearly eliminates the need to open another WSUS console again. This will save you time and further automate what can be an annoying, repetitive yet necessary systems administration task. It’s with great hope that this chapter has inspired you to do more with PowerShell.