Jul 12, 2017

Automating WSUS Tasks with PowerShell Scripts


The scripts explained in this guide allow you to automate several Windows Server Update Services (WSUS) tasks such as synchronization, approvals, cleanups, and scheduled update installations.





Note: Partially these scripts  are not our own, we will provide link to the original sources where we have taken from.

Syncing WSUS with PowerShell and Task Scheduler

This article assume you are familiar with WSUS administration. Right after installing WSUS, you have to configure periodic synchronization. Unfortunately, as you can see in the screenshot below, the synchronization options are somewhat limited.


Since we don't need to sync every day, we select Synchronize manually and use the script below along with Task Scheduler to synchronize WSUS at the times we prefer.

$wsusserver = "wsus"
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($wsusserver, $False,8530);
$wsus.GetSubscription().StartSynchronization();


Load the .NET Update Services object into the $wsusserver variable and use the StartSynchronization() method to start manual synchronization. The screenshot below shows the Task Scheduler task we are using to launch the PowerShell script.


You will see the synchronization results in the WSUS console as if you synced manually:


Automating WSUS update approval

The next task is to automate the approval of updates. WSUS offers automatic approval. However, it is quite inflexible, so we will be using the PowerShell script below:

[string[]]$recipients = admins@contoso.com #Email address where you want to send the notification after the script completes

$wsusserver = "wsus" #WSUS server name

$log = "C:\Temp\Approved_Updates_{0:MMddyyyy_HHmm}.log" -f (Get-Date) #Log file name

new-item -path $log -type file -force #Creating log file

[void][reflection.assembly]::LoadWithPartialName ("Microsoft.UpdateServices.Administration") #Loading the WSUS .NET classes

$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($wsusserver, $False,8530) #Storing the object into the variable

$UpdateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope #Loading WSUS Update scope object into variable

$groups = "All Computers" #Setting up groups for updates approval

$Classification = $wsus.GetUpdateClassifications() | ?{$_.Title -ne 'Service Packs' ‑and $_.Title -ne 'Drivers' -and $_.Title -ne 'Upgrades'} #Setting up update classifications for approval

$Categories = $wsus.GetUpdateCategories() | ? {$_.Title -notmatch "SQL" -and $_.Title -notmatch "Skype"} #Setting up update categories for approval

$UpdateScope.FromCreationDate = (get-date).AddMonths(-1) #Configuring starting date for UpdateScope interval

$UpdateScope.ToCreationDate = (get-date) #Configuring ending date for UpdateScope interval

$UpdateScope.Classifications.Clear() #Clearing classification object before assigning new value to it

$UpdateScope.Classifications.AddRange($Classification) #Assigning previously prepared classifications to the classification object

$UpdateScope.Categories.Clear() #Clearing the categories object before assigning a new value to it

$UpdateScope.Categories.AddRange($Categories) #Assigning previously prepared categories to the classification object

$updates = $wsus.GetUpdates($UpdateScope) | ? {($_.Title -notmatch "LanguageInterfacePack" -and $_.Title -notmatch "LanguagePack" -and $_.Title -notmatch "FeatureOnDemand" -and $_.Title -notmatch "Skype" -and $_.Title -notmatch "SQL" -and $_.Title -notmatch "Itanium" -and $_.PublicationState -ne "Expired" -and $_.IsDeclined -eq $False )} #Storing all updates in the previously defined UpdateScope interval to the $updates variable and filtering out those not required

foreach ($group in $groups) #Looping through groups
  {
   $wgroup = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $group} #Storing the current group into the $wgroup variable
   foreach ($update in $updates) #Looping through updates
     {
      $update.Approve(“Install”,$wgroup) #Approving each update for the current group
     }
  }

$date = Get-Date #Storing the current date into the $date variable

"Aproved updates (on " + $date + "): " | Out-File $log -append #Updating the log file

"Updates have been approved for following groups: (" + $groups + ")" | Out-File $log ‑append #Updating log file

"Folowing updates have been approved:" | Out-File $log -append #Updating the log file

$updates | Select Title,ProductTitles,KnowledgebaseArticles,CreationDate | ft -Wrap | Out-File $log -append #Updating log file

Send-MailMessage -From "WSUS@contoso.com" -To $recipients -Subject "New updates have been approved" -Body "Please find the list of approved updates enclosed" -Attachments $log -SmtpServer "smtp-server" -DeliveryNotificationOption OnFailure #Sending the log file by email.


I'll just explain briefly how the script works. First I load the Windows Update Assembly, so I can use the WSUS .NET object. Then I'm preparing the variables that I need to work with the WSUS object:
  • $wsus: is the WSUS object.
  • $UpdateScope: Defines the time interval for the $wsus.GetUpdates() method.
  • $groups: Defines all WSUS groups I'd like to approve updates for.
  • $Classification: Defines updates classifications for the $wsus.GetUpdates() method. I'm filtering out service packs, drivers, and upgrades.
  • $Categories: Defines updates categories or products for the $wsus.GetUpdates() method. I'm filtering out SQL and Skype updates. SQL gets updated manually, and I don't have Skype installations in my environment.
Then I'm setting up the Update Scope interval to get only updates created within the last month. I know I'm approving my updates every month, so I only need to get recently released updates.

After that, I'm assigning the $Classification and $Categories variables to the corresponding objects. And with the help of the $wsus.GetUpdates($UpdateScope) method, I am saving all updates that match my scope to the $updates variable. Then I'm adding some filtering to remove updates such as LanguagePack, FeatureOnDemand, and Itanium from the results because I don't have these kinds of updates in my environment.

Now I have all updates I want to approve. Next, I'm looping through the WSUS groups to which I want to assign the updates. Then I loop through the updates, approving every update for every group. In this particular case, there is only one group. However, I use a loop here, just to be able to add more groups later.

After approving all updates, I only need to update the log file and send this file by email to myself. This way, I am sure I've approved the updates, and I receive brief information about them.
Like before, I'm using Task Scheduler to run the script:


Declining superseded updates

As you know, Microsoft frequently replaces single updates with packages of multiple updates. They call the replaced update a "superseded update," which is no longer needed. Thus, it makes sense to decline those updates. For this purpose, I modified the PowerShell script below, which I found here.

My changes are in the lines 57–59, 99–100, and 242. I added the transcript file, so when the script ran via the Task Scheduler, I could see the number of declined updates. And after I ran the script the first time, I changed the update scope. So it'll check and decline only updates within the last six months.

# ===============================================
# Script to decline superseeded updates in WSUS.
# ===============================================
# It's recommended to run the script with the -SkipDecline switch to see how many superseded updates are in WSUS and to TAKE A BACKUP OF THE SUSDB before declining the updates.
# Parameters:

# $UpdateServer             = Specify WSUS Server Name
# $UseSSL                   = Specify whether WSUS Server is configured to use SSL
# $Port                     = Specify WSUS Server Port
# $SkipDecline              = Specify this to do a test run and get a summary of how many superseded updates we have
# $DeclineLastLevelOnly     = Specify whether to decline all superseded updates or only last level superseded updates
# $ExclusionPeriod          = Specify the number of days between today and the release date for which the superseded updates must not be declined. Eg, if you want to keep superseded updates published within the last 2 months, specify a value of 60 (days)


# Supersedence chain could have multiple updates.
# For example, Update1 supersedes Update2. Update2 supersedes Update3. In this scenario, the Last Level in the supersedence chain is Update3.
# To decline only the last level updates in the supersedence chain, specify the DeclineLastLevelOnly switch

# Usage:
# =======

# To do a test run against WSUS Server without SSL
# Decline-SupersededUpdates.ps1 -UpdateServer SERVERNAME -Port 8530 -SkipDecline

# To do a test run against WSUS Server using SSL
# Decline-SupersededUpdates.ps1 -UpdateServer SERVERNAME -UseSSL -Port 8531 -SkipDecline

# To decline all superseded updates on the WSUS Server using SSL
# Decline-SupersededUpdates.ps1 -UpdateServer SERVERNAME -UseSSL -Port 8531

# To decline only Last Level superseded updates on the WSUS Server using SSL
# Decline-SupersededUpdates.ps1 -UpdateServer SERVERNAME -UseSSL -Port 8531 -DeclineLastLevelOnly

# To decline all superseded updates on the WSUS Server using SSL but keep superseded updates published within the last 2 months (60 days)
# Decline-SupersededUpdates.ps1 -UpdateServer SERVERNAME -UseSSL -Port 8531 -ExclusionPeriod 60


[CmdletBinding()]
Param(
    [Parameter(Mandatory=$True,Position=1)]
    [string] $UpdateServer,
   
    [Parameter(Mandatory=$False)]
    [switch] $UseSSL,
   
    [Parameter(Mandatory=$True, Position=2)]
    $Port,
   
    [switch] $SkipDecline,
   
    [switch] $DeclineLastLevelOnly,
   
    [Parameter(Mandatory=$False)]
    [int] $ExclusionPeriod = 0
)

$file = "c:\temp\WSUS_Decline_Superseded_{0:MMddyyyy_HHmm}.log" -f (Get-Date)

Start-Transcript -Path $file


if ($SkipDecline -and $DeclineLastLevelOnly) {
    Write-Output "Using SkipDecline and DeclineLastLevelOnly switches together is not allowed."
    Write-Output ""
    return
}

$outPath = Split-Path $script:MyInvocation.MyCommand.Path
$outSupersededList = Join-Path $outPath "SupersededUpdates.csv"
$outSupersededListBackup = Join-Path $outPath "SupersededUpdatesBackup.csv"
"UpdateID, RevisionNumber, Title, KBArticle, SecurityBulletin, LastLevel" | Out-File $outSupersededList

try {
   
    if ($UseSSL) {
        Write-Output "Connecting to WSUS server $UpdateServer on Port $Port using SSL... " -NoNewLine
    } Else {
        Write-Output "Connecting to WSUS server $UpdateServer on Port $Port... " -NoNewLine
    }
   
    [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
    $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($UpdateServer, $UseSSL, $Port);
}
catch [System.Exception]
{
    Write-Output "Failed to connect."
    Write-Output "Error:" $_.Exception.Message
    Write-Output "Please make sure that WSUS Admin Console is installed on this machine"
    Write-Output ""
    $wsus = $null
}

if ($wsus -eq $null) { return }

Write-Output "Connected."

$UpdateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope

(get-date).AddMonths(-6)
$UpdateScope.FromArrivalDate = (get-date).AddMonths(-6)
$UpdateScope.ToArrivalDate = (get-date)

$countAllUpdates = 0
$countSupersededAll = 0
$countSupersededLastLevel = 0
$countSupersededExclusionPeriod = 0
$countSupersededLastLevelExclusionPeriod = 0
$countDeclined = 0

Write-Output "Getting a list of all updates... " -NoNewLine

try {
    $allUpdates = $wsus.GetUpdates($UpdateScope)
}

catch [System.Exception]
{
    Write-Output "Failed to get updates."
    Write-Output "Error:" $_.Exception.Message
    Write-Output "If this operation timed out, please decline the superseded updates from the WSUS Console manually."
    Write-Output ""
    return
}

Write-Output "Done"

Write-Output "Parsing the list of updates... " -NoNewLine
foreach($update in $allUpdates) {
   
    $countAllUpdates++
   
    if ($update.IsDeclined) {
        $countDeclined++
    }
   
    if (!$update.IsDeclined -and $update.IsSuperseded) {
        $countSupersededAll++
       
        if (!$update.HasSupersededUpdates) {
            $countSupersededLastLevel++
        }

        if ($update.CreationDate -lt (get-date).AddDays(-$ExclusionPeriod))  {
            $countSupersededExclusionPeriod++
            if (!$update.HasSupersededUpdates) {
                $countSupersededLastLevelExclusionPeriod++
            }
        }       
       
        "$($update.Id.UpdateId.Guid), $($update.Id.RevisionNumber), $($update.Title), $($update.KnowledgeBaseArticles), $($update.SecurityBulletins), $($update.HasSupersededUpdates)" | Out-File $outSupersededList -Append      
       
    }
}

Write-Output "Done."
Write-Output "List of superseded updates: $outSupersededList"

Write-Output ""
Write-Output "Summary:"
Write-Output "========"

Write-Output "All Updates = $countAllUpdates"
$AnyExceptDeclined = $countAllUpdates - $countDeclined
Write-Output "Any except Declined = $AnyExceptDeclined"
Write-Output "All Superseded Updates = $countSupersededAll"
$SuperseededAllOutput = $countSupersededAll - $countSupersededLastLevel
Write-Output "    Superseded Updates (Intermediate) = $SuperseededAllOutput"
Write-Output "    Superseded Updates (Last Level) = $countSupersededLastLevel"
Write-Output "    Superseded Updates (Older than $ExclusionPeriod days) = $countSupersededExclusionPeriod"
Write-Output "    Superseded Updates (Last Level Older than $ExclusionPeriod days) = $countSupersededLastLevelExclusionPeriod"

$i = 0
if (!$SkipDecline) {
   
    Write-Output "SkipDecline flag is set to $SkipDecline. Continuing with declining updates"
    $updatesDeclined = 0
   
    if ($DeclineLastLevelOnly) {
        Write-Output "  DeclineLastLevel is set to True. Only declining last level superseded updates."
       
        foreach ($update in $allUpdates) {
           
            if (!$update.IsDeclined -and $update.IsSuperseded -and !$update.HasSupersededUpdates) {
              if ($update.CreationDate -lt (get-date).AddDays(-$ExclusionPeriod))  {
                $i++
                $percentComplete = "{0:N2}" -f (($updatesDeclined/$countSupersededLastLevelExclusionPeriod) * 100)
                Write-Progress -Activity "Declining Updates" -Status "Declining update #$i/$countSupersededLastLevelExclusionPeriod - $($update.Id.UpdateId.Guid)" -PercentComplete $percentComplete -CurrentOperation "$($percentComplete)% complete"
               
                try
                {
                    $update.Decline()                   
                    $updatesDeclined++
                }
                catch [System.Exception]
                {
                    Write-Output "Failed to decline update $($update.Id.UpdateId.Guid). Error:" $_.Exception.Message
                }
              }            
            }
        }       
    }
    else {
        Write-Output "  DeclineLastLevel is set to False. Declining all superseded updates."
       
        foreach ($update in $allUpdates) {
           
            if (!$update.IsDeclined -and $update.IsSuperseded) {
              if ($update.CreationDate -lt (get-date).AddDays(-$ExclusionPeriod))  {  
                 
                $i++
                $percentComplete = "{0:N2}" -f (($updatesDeclined/$countSupersededAll) * 100)
                Write-Progress -Activity "Declining Updates" -Status "Declining update #$i/$countSupersededAll - $($update.Id.UpdateId.Guid)" -PercentComplete $percentComplete -CurrentOperation "$($percentComplete)% complete"
                try
                {
                    $update.Decline()
                    $updatesDeclined++
                }
                catch [System.Exception]
                {
                    Write-Output "Failed to decline update $($update.Id.UpdateId.Guid). Error:" $_.Exception.Message
                }
              }             
            }
        }  
       
    }
   
    Write-Output "  Declined $updatesDeclined updates."
    if ($updatesDeclined -ne 0) {
        Copy-Item -Path $outSupersededList -Destination $outSupersededListBackup -Force
        Write-Output "  Backed up list of superseded updates to $outSupersededListBackup"
    }
   
}
else {
    Write-Output "SkipDecline flag is set to $SkipDecline. Skipped declining updates"
}

Write-Output ""
Write-Output "Done"
Write-Output ""

Stop-Transcript


The screenshot below shows a sample log file:


Deleting declined updates from the WSUS database

After you decline the updates, they are still residing inside the WSUS database and taking up disk space. To remove them completely, you have to run the WSUS cleanup wizard. This is another task you can automate:

$file = "c:\temp\WSUS_CleanUp_Wiz_{0:MMddyyyy_HHmm}.log" -f (Get-Date)
Start-Transcript -Path $file
$wsusserver = "wsus"
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")` | out-null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($wsusserver, $False,8530);
$cleanupScope = new-object Microsoft.UpdateServices.Administration.CleanupScope;
$cleanupScope.DeclineSupersededUpdates    = $true
$cleanupScope.DeclineExpiredUpdates       = $true
$cleanupScope.CleanupObsoleteUpdates      = $true
$cleanupScope.CompressUpdates             = $false
$cleanupScope.CleanupObsoleteComputers    = $true
$cleanupScope.CleanupUnneededContentFiles = $true
$cleanupManager = $wsus.GetCleanupManager();
$cleanupManager.PerformCleanup($cleanupScope);
Stop-Transcript


All I'm doing in the script above is defining a cleanup scope using the CleanUpScope object and then running CleanUpManager using the corresponding object against that scope. I'm not compressing updates because this operation takes a long time and doesn't save much space.

The script also runs as scheduled task and produces the log file you can see below:



Because all of these procedures are making many changes in the WSUS database, it is good idea to re-index the database occasionally. To do that, I'm using this SQL query from the Microsoft Script Center. You can use the sqlcmd utility you find there to run the SQL query. Just create a scheduled task and run it once a month.

Arranging the maintenance scripts

Here is how I scheduled the maintenance scripts:
  1. Synchronize WSUS every Tuesday.
  2. Decline superseded updates after every WSUS synchronization.
  3. Run the WSUS cleanup wizard script after declining superseded updates finishes.
  4. Re-index the WSUS database after WSUS cleanup.
  5. Approve updates every Wednesday. This way I know I'm approving updates after removal of all superseded, outdated, and expired updates.

 

Scheduling updates

At this point I'm done with maintenance. However, I still need to install the updates. Unfortunately, WSUS also only offers poor choices when it comes to scheduling update installations. Basically, I can only pick the day of the week and the time. Of course, this is not always what you want. Because I have several environments, I created a Group Policy Object (GPO) for each of them and assigned them to the appropriate Active Directory organizational units (OUs).


As you can see, I configured this GPO to install updates every Friday at 7 p.m. The thing is, I just need to do this on a particular Friday every month. Thus, I wrote a tiny script for enabling this GPO and a second one for disabling it. Then I configured a scheduled task to run the first script a couple days before the update day and the second one after installing the updates.

Enabling GPO
$GPO = Get-GPO -Name "WSUS DEV OU - Automatic Updates"
$GPO.GpoStatus = "AllSettingsEnabled"

Disabling GPO
$GPO = Get-GPO -Name "WSUS DEV OU - Automatic Updates"
$GPO.GpoStatus = "AllSettingsDisabled"







Conclusion

After automation configuration using PowerShell scripts, you are advised to keep eyes on WSUS server every once in a while to make sure everything perfect as intended.

Credit: 4Sysops (Original Publisher) 

Post a Comment

Note: Only a member of this blog may post a comment.

 
TECH SUPPORT © 2012-2017