Uncategorized

Downloading a Software Update Group with Disconnected WSUS (With Bonus ADR version)

April 9, 2020

I have run in to the situation a few times where a customer has a CM instance on an air gapped network and need to get update content. In the case they are the ones responsible to transfer the data over from an internet facing network, they follow the instructions here. Assuming they downloaded all the updates they need on the internet facing system, they just tell the air gapped CM instance to download from \\<SUP>\WSUSContent and all is good. In some cases, the people I work with are not the ones responsible for moving the data to the air gapped network, but instead must use the WSUS server of someone else as their upstream (non-replica). In this case, they must go in to WSUS and approve each update they need to get it to download from the upstream WSUS to the local WSUSContent. Being one that prefers to work smarter, not harder, I wrote a couple scripts.

Script 1: Manually triggered

In the event you have an existing Software Update Group (SUG) or manually create one, this script can be given the name of that SUG and the WSUS server name to handle the approvals for you. One shortcoming of the script is that it must be run on your CM server. This is to require fewer parameters and to ensure the CM cmdlets are available.

The first step is to create a computer group in WSUS to approve the updates to, this group will remain empty. For that reason, I opted to name mine “empty”.

Now you can call the script giving it the name of the SUG and the name of the group you created in WSUS. If you run without the -approve switch, you will get a list of what updates would be approved.

If all looks good, you can run again with the -approve switch and the updates will be approved.

Depending on how many updates and how much data they require, now may be a good time to grab a drink. Once the downloads are complete in WSUS, you can “download” in Configuration Manager from the WSUSContent folder.

# Microsoft provides programming examples for illustration only, 
# without warranty either expressed or implied, including, but not 
# limited to, the implied warranties of merchantability and/or 
# fitness for a particular purpose. 
#
# This sample assumes that you are familiar with the programming 
# language being demonstrated and the tools used to create and debug 
# procedures. Microsoft support professionals can help explain the 
# functionality of a particular procedure, but they will not modify 
# these examples to provide added functionality or construct 
# procedures to meet your specific needs. If you have limited 
# programming experience, you may want to contact a Microsoft 
# Certified Partner or the Microsoft fee-based consulting line at 
# (800) 936-5200. 
#
# For more information about Microsoft Certified Partners, please 
# visit the following Microsoft Web site:
# https://partner.microsoft.com/global/30000104

Param(
    [Parameter(Mandatory=$True)][string]$SUG,
    [Parameter()][string]$WSUSGroupname,
    [switch]$approve
)

if((Get-Module ConfigurationManager) -eq $null) {Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"}

$psdrive = Get-PSDrive -PSProvider CMSite
Set-Location "$(($psdrive).Name):\"
$WSUSServer = (Get-CMSoftwareUpdatePoint | ?{$_.Type -eq 2}).NetworkOSPath.trim('\')

$states = @{
'NotNeeded'='Not Approved'
'NotReady'='Approved, waiting content'
'Ready'='Downloaded'
}

write-output "Connecting to WSUS Server $($WSUSServer)"
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WSUSServer,$False, 8530)
$group = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $WSUSGroupname}

write-output "Getting Updates from Software Update Group (SUG) `"$($SUG)`""
$SUGupdates = Get-CMSoftwareUpdate -UpdateGroupName $SUG -Fast
$SUGCount = $SUGupdates.count
write-output "SUG has $($SUGCount) updates"
$Updates = @()
$Count = 0
$approved = 0
ForEach($SUGupdate in $SUGupdates){
    $Count ++
    write-output "Gathering info for update $($count) of $($SUGCount)"
    $WSUSUpdate = $wsus.GetUpdate([GUID]($SUGupdate.CI_UniqueID))
    $update = New-Object -TypeName PSObject
    Add-Member -InputObject $update -MemberType NoteProperty -Name Name -Value $SUGupdate.LocalizedDisplayName
    Add-Member -InputObject $update -MemberType NoteProperty -Name WSUSState -Value $states.item([string]$WSUSUpdate.State)
    if($approve -and ($WSUSUpdate.State -ne "Ready" -or $WSUSUpdate.State -ne "NotReady")){
        if($WSUSUpdate.HasLicenseAgreement){$WSUSUpdate.AcceptLicenseAgreement() | out-null}
        $WSUSUpdate.ApproveForOptionalInstall($group) | out-null
        $approved ++
    }
    $Updates += $update
}

write-output "Approved $($approved) updates on WSUS server $($WSUSServer)"
if($approved -gt 0){
    write-output "Reloading update status to reflect approvals"
    $Updates = @()
    $Count = 0
    ForEach($SUGupdate in $SUGupdates){
        $Count ++
        write-output "Gathering info for update $($count) of $($SUGCount)"
        $WSUSUpdate = $wsus.GetUpdate([GUID]($SUGupdate.CI_UniqueID))
        $update = New-Object -TypeName PSObject
        Add-Member -InputObject $update -MemberType NoteProperty -Name Name -Value $SUGupdate.LocalizedDisplayName
        Add-Member -InputObject $update -MemberType NoteProperty -Name WSUSState -Value $states.item([string]$WSUSUpdate.State)
        $Updates += $update
    }
}

$Updates

Script 2: The Electric Boogaloo ADR Fun

Just like script 1, a group to use for approvals is required in WSUS. For this script we also need to modify the script itself. Before I get there, I need to explain how I setup Automatic Deployment Rules (ADRs). I create a monthly ADR (I create one per product, but that is not required), and give it multiple deployments. Normally I create two levels of testing then one production deployment that. In a connected environment, by testing deployments are enabled at ADR run time and then I have to manually enable the production deployment once I am satisfied with testing results. For this disconnected version, I set all deployments to disabled. In the script I set my testing collectionIDs in an array and then my ADR names matched up to deployment packages in a hashtable. I then set the name of the WSUS group.

Now I create a Status Filter Rule to watch for the ADRs to run. The Status Filter Rule UI is on the Sites portion of Site Configuration in the Administration workspace.

You will create a new rule that watches for Message ID 5800

You will set the rule to execute the command line:
“C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe” -executionpolicy bypass -file “C:\temp\SUGApprovalAutomated.ps1” -desc “%msgdesc”

The command line is included in the script for reference.

The script gets passed the test of the message and parses the deployment name from it. Since I have three deployments for each ADR, when they are created one has the base name, then one gets a _2_ and another gets a _3_. I watch for the _3_ and only continue if it is in the deployment name. This prevents the approval trying to happen multiple times for a single ADR.

The steps the script takes if it continues:

  1. wait 15 seconds just to make sure all deployments are created
  2. Load CM cmdlets
  3. Verifies CM PSDrive exists, creates if needed
  4. Gets SUG name from the deployment name
  5. Gets Software Update Point (SUP) name
  6. Connects to WSUS on SUP
  7. Gets WSUS computer group
  8. Gets list of updates in the SUG
  9. Approves updates to the WSUS group
  10. Checks each update for download status, if downloaded, removes from name array. Loops through until the array is empty
  11. Trigger download in CM from WSUSContent
  12. Approves SUG deployments targeting test collections
# Microsoft provides programming examples for illustration only, 
# without warranty either expressed or implied, including, but not 
# limited to, the implied warranties of merchantability and/or 
# fitness for a particular purpose. 
#
# This sample assumes that you are familiar with the programming 
# language being demonstrated and the tools used to create and debug 
# procedures. Microsoft support professionals can help explain the 
# functionality of a particular procedure, but they will not modify 
# these examples to provide added functionality or construct 
# procedures to meet your specific needs. If you have limited 
# programming experience, you may want to contact a Microsoft 
# Certified Partner or the Microsoft fee-based consulting line at 
# (800) 936-5200. 
#
# For more information about Microsoft Certified Partners, please 
# visit the following Microsoft Web site:
# https://partner.microsoft.com/global/30000104

Param(
    [Parameter(Mandatory=$True)][string]$desc
)

#Create Status Filter Rule to run a program
#look for message ID 5800
#cmd line is "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -executionpolicy bypass -file "C:\temp\SUGApprovalAutomated.ps1" -desc "%msgdesc"
#change path to ps1

####################################MODIFY BELOW########################################


$testcollections = @(
'PFE00043',
'PFE00044'
)

$packages = @{
#ADRName = PackageName
"Server 2012 R2 Monthly" = "Server2012R2Updates"
"Server 2016 Monthly" = "Server 2016 Updates"
}

$WSUSGroupname = "empty"

####################################MODIFY ABOVE########################################

$states = @{
'NotNeeded'='Not Approved'
'NotReady'='Approved, waiting content'
'Ready'='Downloaded'
}

$deployment = ($desc -replace "CI Assignment Manager successfully processed new CI Assignment ","").trimend('.')

if($deployment -like "*_3_*"){

    #15 second sleep to allow script iterations for deployments 1&2 to exit
    start-sleep -Seconds 15

    #load CM cmdlets
    if((Get-Module ConfigurationManager) -eq $null) {Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"}
    if(Get-Module ConfigurationManager){write-output "CM cmdlets loaded"}
    else{write-error "CM cmdlets failed to load"}

    #check for, create if needed, and change to CM drive
    $SiteCode = (Get-WmiObject -Namespace ROOT\sms -class SMS_ProviderLocation).SiteCode
    if ((Get-PSDrive -Name $SiteCode -ErrorAction SilentlyContinue | Measure-Object).Count -ne 1) {
        New-PSDrive -Name $SiteCode -PSProvider "AdminUI.PS.Provider\CMSite" -Root $SiteCode -ErrorAction Stop -Verbose:$false | Out-Null
    }
    $psdrive = Get-PSDrive -PSProvider CMSite
    Set-Location "$(($psdrive).Name):\"
    write-output "Set location to $(get-location)"

    #Get SUG Name
    $SUGName = (Get-CMSoftwareUpdateGroup -Id (Get-CMUpdateGroupDeployment -DeploymentName $deployment).AssignedUpdateGroup).LocalizedDisplayName
    write-output "Found Software Update Group (SUG): $($SUGName)"

    #Find WSUS
    $WSUSServer = (Get-CMSoftwareUpdatePoint | ?{$_.Type -eq 2}).NetworkOSPath.trim('\')
    write-output "Found WSUS Server: $($WSUSServer)"

    #Connect to WSUS
    write-output "Connecting to WSUS"
    [reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
    $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WSUSServer,$False, 8530)
    if($wsus){write-output "Successfully connected to WSUS"}
    else{write-error "Failed to connect to WSUS"; exit 30}

    #load WSUS computer group
    $group = $wsus.GetComputerTargetGroups() | where {$_.Name -eq $WSUSGroupname}
    if($group){write-output "Connected to WSUS computer group"}
    else{write-error "Failed to Connect to WSUS computer group";exit 31}

    #get updates in SUG
    $SUGupdates = Get-CMSoftwareUpdate -UpdateGroupName $SUGName -Fast

    #approve SUG updates within WSUS
    $Updates = @()
    $downloaded = @()
    ForEach($SUGupdate in $SUGupdates){
        $WSUSUpdate = $wsus.GetUpdate([GUID]($SUGupdate.CI_UniqueID))
        $update = New-Object -TypeName PSObject
        Add-Member -InputObject $update -MemberType NoteProperty -Name Name -Value $SUGupdate.LocalizedDisplayName
        Add-Member -InputObject $update -MemberType NoteProperty -Name WSUSState -Value $states.item([string]$WSUSUpdate.State)
        if($WSUSUpdate.State -ne "Ready" -or $WSUSUpdate.State -ne "NotReady"){
            if($WSUSUpdate.HasLicenseAgreement){$WSUSUpdate.AcceptLicenseAgreement() | out-null}
            $WSUSUpdate.ApproveForOptionalInstall($group) | out-null
            write-output "Approved `"$($SUGupdate.LocalizedDisplayName)`" in WSUS"
        }
        $Updates += $update
    }

    #wait for WSUS to download updates
    while($SUGupdates.count -gt 0){
        start-sleep -Seconds 60
        ForEach($SUGupdate in $SUGupdates){
            $WSUSUpdate = $wsus.GetUpdate([GUID]($SUGupdate.CI_UniqueID))
            if($WSUSUpdate.State -eq "Ready"){
                write-output "Download of `"$($SUGupdate.LocalizedDisplayName)`" in WSUS COMPLETE"
                $update = New-Object -TypeName PSObject
                Add-Member -InputObject $update -MemberType NoteProperty -Name Name -Value $SUGupdate.LocalizedDisplayName
                Add-Member -InputObject $update -MemberType NoteProperty -Name WSUSState -Value $states.item([string]$WSUSUpdate.State)
                $downloaded += $update
                $SUGupdates = $SUGupdates | Where-Object{$_ -ne $SUGupdate}
            }
        }
    }

    #Tell CM to download updates from WSUSContent
    Write-Output "Downloading Updates to CM Package from WSUSConcent"
    Save-CMSoftwareUpdate -SoftwareUpdateGroupName $SUGName -DeploymentPackageName $packages.item($deployment.split('_')[0]) -Location "\\$($WSUSServer)\WsusContent"

    #Find SUG deployments targeting test collections    
    $SUGS = Get-CMUpdateGroupDeployment -name $SUGName | Where-Object{$_.TargetCollectionID -in $testcollections}

    #enable SUG deployments
    write-output "Enabling the testing deployments"
    ForEach($S in $SUGS){Set-CMUpdateGroupDeployment -UpdateGroupDeployment $S -Enable}
}