Installing and Configuring WSUS with Powershell

In setting up our SCCM 2012 infrastructure, I decided to patch our OS deployments using WSUS instead of SCCM Software Updates.  Since we have multiple distribution points in different geographical areas, I decided to install a WSUS server in each location where we do deployments.  Granted, installing and configuring WSUS is not the most technically challenging thing in the world, but when you have to do it multiple times, it begs for automation!  So I fired up my trusty Powershell ISE to see what could be done.

I wrote this script for my own environment, but it should be flexible enough to be used by anyone.  It’s tested for use on Windows Server 2012 R2 and It’s designed to run in Powershell ISE, so it doesn’t take any parameters, just set the variables as required.  Also you can change any of the WSUS configuration, such as Products and Classifications, just edit the relevant section of the script.

Download from Technet Gallery.

What does the script do?

First, we install .Net Framework 3.5 if it isn’t already installed, this is a requirement for WSUS.  Next, we download and install Microsoft Report Viewer 2008 SP1, which is required for viewing WSUS reports.  If you chose the ‘SQLExpress’ installation, we download SQL Server 2012 Express SP1 with tools and run an unattended installation using default parameters.  Then we install WSUS and run the post-installation tasks with wsusutil.exe.

Now, we do a basic configuration, which is equivalent to running the WSUS Configuration Wizard.  We set the location to sync updates from, the update language/s, run a metadata sync to get available Products and Classifications, set which Products and Classifications we want to sync, and enable the automatic sync schedule.  Then we do a full sync.

Once the sync is completed, we decline certain updates that we don’t want, such as all ‘itanium’ updates, configure and enable the Default Automatic Approval Rule, then run it so the updates will start downloading.

Most of these activities are optional and are activated using variables which you must set before you run the script, so if you want to use WID, or an existing SQL instance you can.  You can skip the configuration entirely and do it manually, or just do the bare minimum, and of course you can customise the configuration in the script.

Step by Step Walkthrough

First, we set the variables, such as the WSUS installation type, the location for Updates, things to configure etc.


###############
## Variables ##
###############

##//INSTALLATION//##

# Do you want to install .NET FRAMEWORK 3.5? If true, provide a location for the Windows OS media in the next variable
    $DotNet = $True
# Location of Windows sxs for .Net Framework 3.5 installation
    $WindowsSXS = "D:\sources\sxs"
# Do you want to download and install MS Report Viewer 2008 SP1 (required for WSUS Reports)?
    $RepViewer = $True
# WSUS Installation Type.  Enter "WID" (for WIndows Internal Database), "SQLExpress" (to download and install a local SQLExpress), or "SQLRemote" (for an existing SQL Instance).
    $WSUSType = "SQLRemote"
# If using an existing SQL server, provide the Instance name below
    $SQLInstance = "MyServer\MyInstance"
# Location to store WSUS Updates (will be created if doesn't exist)
    $WSUSDir = "C:\WSUS_Updates"
# Temporary location for installation files (will be created if doesn't exist)
    $TempDir = "C:\temp"

##//CONFIGURATION//##

# Do you want to configure WSUS (equivalent of WSUS Configuration Wizard, plus some additional options)?  If $false, no further variables apply.
# You can customise the configurations, such as Products and Classifications etc, in the "Begin Initial Configuration of WSUS" section of the script.
    $ConfigureWSUS = $True
# Do you want to decline some unwanted updates?
    $DeclineUpdates = $True
# Do you want to configure and enable the Default Approval Rule?
    $DefaultApproval = $True
# Do you want to run the Default Approval Rule after configuring?
    $RunDefaultRule = $False

We install .Net Framework 3.5 if required


# Install .Net Framework 3.5 from media
if($DotNet -eq $true)
{
write-host 'Installing .Net Framework 3.5'
Install-WindowsFeature -name NET-Framework-Core -Source $WindowsSXS
}

We install the Report Viewer from Microsoft for viewing WSUS reports. We start a bits job to download it, the we do a silent install.

# Download MS Report Viewer 2008 SP1 for WSUS reports

if ($RepViewer -eq $True)
{
write-host "Downloading Microsoft Report Viewer 2008 SP1...please wait"
$URL = "http://download.microsoft.com/download/3/a/e/3aeb7a63-ade6-48c2-9b6a-d3b6bed17fe9/ReportViewer.exe"
Start-BitsTransfer $URL $TempDir -RetryInterval 60 -RetryTimeout 180 -ErrorVariable err
if ($err)
{
write-host "Microsoft Report Viewer 2008 SP1 could not be downloaded!" -ForegroundColor Red
write-host 'Please download and install it manually to use WSUS Reports.' -ForegroundColor Red
write-host 'Continuing anyway...' -ForegroundColor Magenta
}

# Install MS Report Viewer 2008 SP1

write-host 'Installing Microsoft Report Viewer 2008 SP1...'
$setup=Start-Process "$TempDir\ReportViewer.exe" -verb RunAs -ArgumentList '/q' -Wait -PassThru
if ($setup.exitcode -eq 0)
{
write-host "Successfully installed"
}
else
{
write-host 'Microsoft Report Viewer 2008 SP1 did not install correctly.' -ForegroundColor Red
write-host 'Please download and install it manually to use WSUS Reports.' -ForegroundColor Red
write-host 'Continuing anyway...' -ForegroundColor Magenta
}
}

I prefer to use WSUS with a local SQL Express installation so I have some access to the database if I need to.  If chosen, we download and install SQL Server Express 2012 SP1 with admin tools using an unattended installation. We use the ‘ALLFEATURES_WITHDEFAULTS’ role, and add the local administrators group to the SQL sysadmin accounts.

# Download SQL 2012 Express SP1 with tools

if ($WSUSType -eq 'SQLExpress')
{
write-host "Downloading SQL 2012 Express SP1 with Tools...please wait"
Start-Sleep -Seconds 10 # wait 10 seconds in case of BITS overload error
$URL = "http://download.microsoft.com/download/5/2/9/529FEF7B-2EFB-439E-A2D1-A1533227CD69/SQLEXPRWT_x64_ENU.exe"
Start-BitsTransfer $URL $TempDir -RetryInterval 60 -RetryTimeout 180 -ErrorVariable err
if ($err)
{
write-host "Microsoft SQL 2012 Express SP1 could not be downloaded!  Please check internet availability." -ForegroundColor Red
write-host 'The script will stop now.' -ForegroundColor Red
break
}

# Install SQL 2012 Express with defaults

write-host 'Installing SQL Server 2012 SP1 Express with Tools...'
$setup=Start-Process "$TempDir\SQLEXPRWT_x64_ENU.exe" -verb RunAs -ArgumentList '/QUIETSIMPLE /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /ROLE=ALLFEATURES_WITHDEFAULTS /INSTANCENAME=SQLEXPRESS /SQLSYSADMINACCOUNTS="BUILTIN\ADMINISTRATORS" /UPDATEENABLED=TRUE /UPDATESOURCE="MU"' -Wait -PassThru

if ($setup.exitcode -eq 0)
{
write-host "Successfully installed"
}
else
{
write-host 'SQL Server 2012 SP1 Express did not install correctly.' -ForegroundColor Red
write-host 'Please check the Summary.txt log at C:\Program Files\Microsoft SQL Server\110\Setup Bootstrap\Log' -ForegroundColor Red
write-host 'The script will stop now.' -ForegroundColor Red
break
}
}

Next we install WSUS

# Install WSUS (WSUS Services, SQL Database, Management tools)

if ($WSUSType -eq 'WID')
{
write-host 'Installing WSUS for WID (Windows Internal Database)'
Install-WindowsFeature -Name UpdateServices -IncludeManagementTools
}
if ($WSUSType -eq 'SQLExpress' -Or $WSUSType -eq 'SQLRemote')
{
write-host 'Installing WSUS for SQL Database'
Install-WindowsFeature -Name UpdateServices-Services,UpdateServices-DB -IncludeManagementTools
}

Then we run the post-install configuration tasks using the wsusutil.exe

# Run WSUS Post-Configuration

if ($WSUSType -eq 'WID')
{
sl "C:\Program Files\Update Services\Tools"
.\wsusutil.exe postinstall CONTENT_DIR=$WSUSDir
}
if ($WSUSType -eq 'SQLExpress')
{
sl "C:\Program Files\Update Services\Tools"
.\wsusutil.exe postinstall SQL_INSTANCE_NAME="%COMPUTERNAME%\SQLEXPRESS" CONTENT_DIR=$WSUSDir
}
if ($WSUSType -eq 'SQLRemote')
{
sl "C:\Program Files\Update Services\Tools"
.\wsusutil.exe postinstall SQL_INSTANCE_NAME=$SQLInstance CONTENT_DIR=$WSUSDir
}

Now we begin to configure WSUS. We connect to the WSUS server and get the configuration. We tell it to sync from Microsoft Update, then set the updates language to English.

# Get WSUS Server Object
$wsus = Get-WSUSServer

# Connect to WSUS server configuration
$wsusConfig = $wsus.GetConfiguration()

# Set to download updates from Microsoft Updates
Set-WsusServerSynchronization –SyncFromMU

# Set Update Languages to English and save configuration settings
$wsusConfig.AllUpdateLanguagesEnabled = $false
$wsusConfig.SetEnabledUpdateLanguages("en")
$wsusConfig.Save()

We do an initial sync to get the available products and categories from Microsoft Update

# Get WSUS Subscription and perform initial synchronization to get latest categories
$subscription = $wsus.GetSubscription()
$subscription.StartSynchronizationForCategoryOnly()
write-host 'Beginning first WSUS Sync to get available Products etc' -ForegroundColor Magenta
write-host 'Will take some time to complete'
While ($subscription.GetSynchronizationStatus() -ne 'NotProcessing') {
    Write-Host "." -NoNewline
    Start-Sleep -Seconds 5
}
write-host ' '
Write-Host "Sync is done." -ForegroundColor Green

We tell WSUS which Products we want to sync. It’s very important to get these right, otherwise you will download a lot of updates that you don’t need and fill up your disk space! Obviously you’ll want to customise these for your environment.

# Configure the Platforms that we want WSUS to receive updates
write-host 'Setting WSUS Products'
Get-WsusProduct | where-Object {
    $_.Product.Title -in (
    'Report Viewer 2005',
    'Report Viewer 2008',
    'Report Viewer 2010',
    'Visual Studio 2005',
    'Visual Studio 2008',
    'Visual Studio 2010 Tools for Office Runtime',
    'Visual Studio 2010',
    'Visual Studio 2012',
    'Visual Studio 2013',
    'Microsoft Lync 2010',
    'Microsoft SQL Server 2008 R2 - PowerPivot for Microsoft Excel 2010',
    'Dictionary Updates for Microsoft IMEs',
    'New Dictionaries for Microsoft IMEs',
    'Office 2003',
    'Office 2010',
    'Office 2013',
    'Silverlight',
    'System Center 2012 - Orchestrator',
    'Windows 7',
    'Windows 8.1 Drivers',
    'Windows 8.1 Dynamic Update',
    'Windows 8',
    'Windows Dictionary Updates',
    'Windows Server 2008 R2',
    'Windows Server 2008',
    'Windows Server 2012 R2',
    'Windows Server 2012',
    'Windows XP 64-Bit Edition Version 2003',
    'Windows XP x64 Edition',
    'Windows XP')
} | Set-WsusProduct

We do the same for the Update Classifications

# Configure the Classifications
write-host 'Setting WSUS Classifications'
Get-WsusClassification | Where-Object {
    $_.Classification.Title -in (
    'Critical Updates',
    'Definition Updates',
    'Feature Packs',
    'Security Updates',
    'Service Packs',
    'Update Rollups',
    'Updates')
} | Set-WsusClassification

I guess it’s a bug, but it seems WSUS sometimes enables the entire parent Product when adding them by script this way, so we pause the script and prompt to check in the WSUS console that the correct Products are selected before continuing.

# Prompt to check products are set correctly
write-host 'Before continuing, please open the WSUS Console, cancel the WSUS Configuration Wizard,' - -ForegroundColor Red
write-host 'Go to Options > Products and Classifications, and check that the Products are set correctly.' - -ForegroundColor Red
write-host 'Pausing script' -ForegroundColor Yellow
$Shell = New-Object -ComObject "WScript.Shell"
$Button = $Shell.Popup("Click OK to continue.", 0, "Script Paused", 0) # Using Pop-up in case script is running in ISE

We set the automatic sync schedule to once per day at midnight, then start the first full synchronisation.

# Configure Synchronizations
write-host 'Enabling WSUS Automatic Synchronisation'
$subscription.SynchronizeAutomatically=$true

# Set synchronization scheduled for midnight each night
$subscription.SynchronizeAutomaticallyTimeOfDay= (New-TimeSpan -Hours 0)
$subscription.NumberOfSynchronizationsPerDay=1
$subscription.Save()

# Kick off a synchronization
$subscription.StartSynchronization()

We monitor the progress of the sync in the Powershell console as it can take some time.

# Monitor Progress of Synchronisation

write-host 'Starting WSUS Sync, will take some time' -ForegroundColor Magenta
Start-Sleep -Seconds 60 # Wait for sync to start before monitoring
while ($subscription.GetSynchronizationProgress().ProcessedItems -ne $subscription.GetSynchronizationProgress().TotalItems) {
    Write-Progress -PercentComplete (
    $subscription.GetSynchronizationProgress().ProcessedItems*100/($subscription.GetSynchronizationProgress().TotalItems)
    ) -Activity "WSUS Sync Progress"
}
Write-Host "Sync is done." -ForegroundColor Green

After the sync is complete, we decline some updates that we don’t want. In my example, we are declining IE10 and the Microsoft Browser Choice EU updates, which we don’t want (I used the KB article number in the ‘TextIncludes’ parameter to find them), then we decline all ‘itanium’ updates because we don’t have any itanium servers. Do you?

# Decline Unwanted Updates

if ($DeclineUpdates -eq $True)
{
write-host 'Declining Unwanted Updates'
$approveState = 'Microsoft.UpdateServices.Administration.ApprovedStates' -as [type]

# Declining All Internet Explorer 10
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope -Property @{
    TextIncludes = '2718695'
    ApprovedStates = $approveState::Any
}
$wsus.GetUpdates($updateScope) | ForEach {
    Write-Verbose ("Declining {0}" -f $_.Title) -Verbose
    $_.Decline()
}

# Declining Microsoft Browser Choice EU
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope -Property @{
    TextIncludes = '976002'
    ApprovedStates = $approveState::Any
}
$wsus.GetUpdates($updateScope) | ForEach {
    Write-Verbose ("Declining {0}" -f $_.Title) -Verbose
    $_.Decline()
}

# Declining all Itanium Update
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope -Property @{
    TextIncludes = 'itanium'
    ApprovedStates = $approveState::Any
}
$wsus.GetUpdates($updateScope) | ForEach {
    Write-Verbose ("Declining {0}" -f $_.Title) -Verbose
    $_.Decline()
}
}

Then we enable the Default Automatic Approval Rule and configure it with the classifications we want.

# Configure Default Approval Rule

if ($DefaultApproval -eq $True)
{
write-host 'Configuring default automatic approval rule'
[void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
$rule = $wsus.GetInstallApprovalRules() | Where {
    $_.Name -eq "Default Automatic Approval Rule"}
$class = $wsus.GetUpdateClassifications() | ? {$_.Title -In (
    'Critical Updates',
    'Definition Updates',
    'Feature Packs',
    'Security Updates',
    'Service Packs',
    'Update Rollups',
    'Updates')}
$class_coll = New-Object Microsoft.UpdateServices.Administration.UpdateClassificationCollection
$class_coll.AddRange($class)
$rule.SetUpdateClassifications($class_coll)
$rule.Enabled = $True
$rule.Save()
}

Finally we run the rule which will approve the updates and begin the file downloads. However, in my testing this always errors with a timeout when activated through Powershell, so I put it in a try-catch-finally block to finish the script successfully. Even if it errors, the rule is actually run as you will be able to see from the WSUS console.


# Run Default Approval Rule

if ($RunDefaultRule -eq $True)
{
write-host 'Running Default Approval Rule'
write-host ' >This step may timeout, but the rule will be applied and the script will continue' -ForegroundColor Yellow
try {
$Apply = $rule.ApplyRule()
}
catch {
write-warning $_
}
Finally {
# Cleaning Up TempDir

write-host 'Cleaning temp directory'
if (Test-Path $TempDir\ReportViewer.exe)
{Remove-Item $TempDir\ReportViewer.exe -Force}
if (Test-Path $TempDir\SQLEXPRWT_x64_ENU.exe)
{Remove-Item $TempDir\SQLEXPRWT_x64_ENU.exe -Force}
If ($Tempfolder -eq "No")
{Remove-Item $TempDir -Force}

write-host 'WSUS log files can be found here: %ProgramFiles%\Update Services\LogFiles'
write-host 'Done!' -foregroundcolor Green
}
}

Monitoring the Update File Downloads

After the Default Approval Rule has been run, you can monitor the ‘Download Status’ of the update files in the WSUS console.  But since it can take a long time, I wrote a little script that will monitor the downloads and email me once they have finished.  It must be run as administrator on the WSUS server.


$Computername = $env:COMPUTERNAME
$ToEmail = "myemailaddress@mydomain.com"
$FromEmail = "WSUS.on.$Computername@mydomain.com"
$smtpServer = "mysmtpServer"
# Polling frequency in seconds
$Seconds = "320"

cls
Write-host 'Monitoring WSUS Update File Downloads...'
write-host 'Will send email when completed.'
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer();
$updateScope = new-object Microsoft.UpdateServices.Administration.UpdateScope;
$updateScope.updateApprovalActions = [Microsoft.UpdateServices.Administration.UpdateApprovalActions]::Install
while (($wsus.GetUpdates($updateScope) | Where {$_.State -eq "NotReady"}).Count -ne 0) {
Start-Sleep -Seconds $Seconds
}
send-mailmessage -To $ToEmail -From $FromEmail -Subject "WSUS Update File Download Completed on $ComputerName" -body "Download of Update Files on $Computername has completed!" -smtpServer $smtpServer

Email Notifications

Finally, if you configure E-mail Notifications in WSUS, you may hit the lovely 5.7.1 error from Exchange:

Mailbox unavailable. The server response was: 5.7.1 Client does not have permissions to send as this sender 

This is because it tries to authenticate with its computer account.  So you have to create a new Receive Connector in Exchange to allow relaying from anonymous users with TLS-authentication to work around the problem.

You can run a command like the following to create it:


New-ReceiveConnector -Name "WSUS Relay" -Bindings 0.0.0.0:25 -RemoteIPRanges 10.x.x.1.,10.x.x.2 -AuthMechanism Tls -Enabled
 $true -PermissionGroups AnonymousUsers -Server MyEdgeServer

Incidentally, you can’t really configure E-mail Notifications with Powershell as you must set the recipient email address for it to work, and this is a read-only property that Powershell can’t change, so better to do it manually.

That’s it!  Feel free to suggest some improvements, or take the code and make something better yourself!

Most of the WSUS code I learned from these great resources, especially the work of Boe Prox

http://blogs.technet.com/b/heyscriptingguy/archive/2013/04/15/installing-wsus-on-windows-server-2012.aspx
http://learn-powershell.net/2010/11/14/wsus-administrator-module/
http://p0w3rsh3ll.wordpress.com/2013/02/05/wsus-on-windows-server-2012-core-from-scratch/
http://community.spiceworks.com/attachments/post/0006/1234/powershell_wsus.ps1
http://learn-powershell.net/2013/11/12/automatically-declining-itanium-updates-in-wsus-using-powershell/
http://poshwsus.codeplex.com/

6 thoughts on “Installing and Configuring WSUS with Powershell

  1. Hey Dude, nice blog. I spent a chunk of time time figuring out how to configure products, and your blog gave me an idea which lead to an answer. I figured it’s only fair to share. So here is the code:

    $ww = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer(“localhost”,$false,8530)
    $Synchronization = $ww.GetSubscription()
    $bla = $Synchronization.GetUpdateCategories() | ? {$_.title -like ‘Windows Server 2008 R2’}
    $test = New-Object Microsoft.UpdateServices.Administration.UpdateCategoryCollection
    $test.AddRange($bla)
    $Synchronization.SetUpdateCategories($test)
    $Synchronization.save()

  2. Thanks Alex. That’s a good alternative to using Get-Wsus Product | Set-Wsus Product, however it only works with the current subscription, so if you’ve already set the subscription you can only filter on Update Categories that have already been set. The Get-Wsus Product | Set-Wsus Product method works independently of the current subscription.

  3. Thanks for the reply, I am dealing with 2008r2, so I don’t have the luxury of get-wsus :/ (Also, I must have missed notification for your reply, so apologies for slow turn around)

    Also, this might be another difference between 2k8 and 2k12:
    On 2k8 to get status, I need to query .phase method – $subscription.GetSynchronizationStatus().phase

    So, below always returns false on 2k8
    While ($subscription.GetSynchronizationStatus() -ne ‘NotProcessing’) {
    Write-Host “.” -NoNewline
    Start-Sleep -Seconds 5
    }

    At any rate, this is my project, after a few week of stagnation, I hope to have it finished and unit tested in a few days: https://github.com/vinyar/wsus (It’s a Chef Cookbook for standing up WSUS and connecting clients to it)

  4. Thanks a lot for this.

    One question though. How can I disable this tick “Yes, I would like to join the Microsoft Update Improvement Program” via powershell command ?

    Raul

Leave a comment

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