Creating ADR Deployments in SCCM with PowerShell

Today I needed to create a number of deployments for Software Update Automatic Deployment Rules in SCCM, so I turned to PowerShell and used the New-CMAutoDeploymentRuleDeployment cmdlet available in the ConfigurationManager module. It works well enough, however there are a couple of options that the cmdlet cannot set, namely:

  • If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
  • If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart

Turns out that these can easily be set though by manipulating the XML deployment template in the object returned by the cmdlet. You can actually set all the deployment properties that way if you wanted, so long as you know the parameters and values from the deployment template XML.

Here is an example that creates the ADR deployments for an array of collections and also sets the two options above:

# ADR name
$ADRName = "Windows 10 Updates"

# Collections to create deployments for
$Collections = @(
    'SUP - Pilot - ABC - All'
    'SUP - Pilot - XYZ - All'
    'SUP - Production - ABC - All'
    'SUP - Production - XYZ - All'

)

# Import ConfigMgr Module
Import-Module $env:SMS_ADMIN_UI_PATH.Replace('i386','ConfigurationManager.psd1')
$SiteCode = (Get-PSDrive -PSProvider CMSITE).Name
Set-Location ("$SiteCode" + ":")

# Get the ADR
$ADR = Get-CMAutoDeploymentRule -Name $ADRName

# Create the deployments
Foreach ($Collection in $Collections)
{
    # Create the deployment
    $Params = @{
        CollectionName = $Collection
        EnableDeployment = $true
        SendWakeupPacket = $false
        VerboseLevel = 'OnlySuccessAndErrorMessages'
        UseUtc = $true
        AvailableTime = 2
        AvailableTimeUnit = 'Days'
        DeadlineImmediately = $true
        UserNotification = 'DisplaySoftwareCenterOnly'
        AllowSoftwareInstallationOutsideMaintenanceWindow = $true
        AllowRestart = $false
        SuppressRestartServer = $true
        SuppressRestartWorkstation = $true
        WriteFilterHandling = $true
        NoInstallOnRemote = $false 
        NoInstallOnUnprotected = $false
        UseBranchCache = $true
    }
    $null = $ADR | New-CMAutoDeploymentRuleDeployment @Params

    # Update the deployment with some additional params not available in the cmdlet
    $ADRDeployment = Get-CMAutoDeploymentRuleDeployment -Name $ADRName -Fast | where {$_.CollectionName -eq $Collection}
    [xml]$DT = $ADRDeployment.DeploymentTemplate
    # If software updates are not available on distribution point in current, neighbour or site boundary groups, download content from Microsoft Updates
    $DT.DeploymentCreationActionXML.AllowWUMU = "true" 
    # If any update in this deployment requires a system restart, run updates deployment evaluation cycle after restart
    If ($DT.DeploymentCreationActionXML.RequirePostRebootFullScan -eq $null)
    {
        $NewChild = $DT.CreateElement("RequirePostRebootFullScan")
        [void]$DT.SelectSingleNode("DeploymentCreationActionXML").AppendChild($NewChild)
    }
    $DT.DeploymentCreationActionXML.RequirePostRebootFullScan = "Checked" 
    $ADRDeployment.DeploymentTemplate = $DT.OuterXml
    $ADRDeployment.Put()
}

ConfigMgr OS Upgrade TS W10 1709 Does Not Care About Windows Edition

Today I ran a ConfigMgr OS Upgrade task sequence configured to use the Enterprise edition of Windows 10 1709 on a workstation that had Windows 10 Pro 1703 installed. Since the VLC media for 1709 contains the various editions in different indexes, you are supposed to choose the relevant one in the Upgrade Operating System step.

OSUpgrade

Of course, I expected the TS to fail because the configured edition is different to the edition that TS was being run on – but to my surprise, it didn’t care and the TS succeeded! Windows 10 Pro 1703 was upgraded to Windows 10 Pro 1709.

Not sure if that’s a bug or a feature, but it’s actually quite convenient!

Find the Compliance State for Software Updates on Remote Computers with PowerShell

Since the recent WannaCrypt ransomware attacks, many organisations have wanted to get the patch status of their systems to see if they are protected. Several reports and SQL queries for ConfigMgr were quickly posted online to help enterprises identify at-risk machines. But what for those who don’t use ConfigMgr, or what if you want to get the real-time status for a patch on a particular system instead of relying on deployment results or hardware inventory data?

For this, I wrote a couple of PowerShell scripts that will connect to a remote system, or group of systems, and check the status of any number of patches based on information from WMI. The scripts will use the Win32_QuickFixEngineering class on all targeted systems, as well as the SCCM WMI classes for those that are using ConfigMgr.

There are two scripts, one is single-threaded which is fine if you want to just check the local system, or a couple of remote systems.  If you want to check many remote systems at the same time, then use the multi-threaded version which will give you significantly quicker results.

Both scripts work the same way, but bear in mind the multi-threaded version will use more system resources, especially CPU, so set the throttle-limit (number of simultaneous threads) to something sensible for your CPU. The default limit is 32. The multi-threaded version uses my [BackgroundJob] custom class which will only work in PowerShell 5 +, and you will need to run the code for that class in your PowerShell session first.

To use the scripts, simply pass the computer name/s and Article ID/s (KB number) for the patch.

To run against the local machine:


Get-PatchStatus -ArticleID 4019264

Against a remote machine:


Get-PatchStatus -ComputerName PC001 -ArticleID 4019264

Against several remote machines:


Get-PatchStatus -ComputerName PC001,PC002,PC003,SRV001,SRV002 -ArticleID 4019264

Using several Article IDs


Get-PatchStatus -ComputerName PC001,PC002,PC003 -ArticleID @(4019264,4019265,4016871)

Set the throttle limit on the multi-threaded version, and using verbose output:


Get-PatchStatus -ComputerName $Computers -ArticleID $ArticleIDs -ThrottleLimit 64 -Verbose

Result

result

You might want to output to GridView if you have a lot of results for better filtering.

Note that if no results are returned in the (SCCM) or (QFE) columns then the patch is not installed, or if there was an error connecting to the remote system, this will be returned in the ‘Comments’ column.

The Scripts

Single-threaded version

Multi-threaded version

Parse the WindowsUpdate.log on Local and Remote Computers with PowerShell

The WindowsUpdate.log, which logs activities of the Windows Update client, is not the easiest of log files to parse through but it’s handy one for finding details about update installation successes and failures.  To make searching this log file easier both on the local computer, a remote computer (where PS remoting is enabled) or groups of computers, I wrote a simple but handy PowerShell script.

The script will retrieve the most important elements of the log – the date, time, component and entry text – and put them into a PS object.  This allows us to summarise the number of entries in the log by either component or date, for example, or provide some search filters to find specific entries and in a specific time period, for example all the software updates installed in the last 3 days.

I haven’t tried this with Windows 10 yet, where the WU logging mechanism has changed, but it will work for older operating systems that have PowerShell installed.

Download

Download the script from the Technet Gallery.

Examples

Note: due to the number of results returned when parsing log entries, I recommend piping to Gridview for easier viewing.

First, let’s find the number of entries in the log for each component:


Parse-WindowsUpdateLog.ps1 -GroupBy Component

 

Capture

So there are 54 entries for the “Content Install” component, lets read those:


Parse-WindowsUpdateLog.ps1 -Component "Content Install" | Out-Gridview

Capture

Now let’s search for the keyword “warning” anywhere in the log, in the last 3 days, on a remote computer:


Parse-WindowsUpdateLog.ps1 -ComputerName SRV001 -Days 3 -Text warning | Out-GridView

Capture

A benefit of using PowerShell’s Gridview is that you can also filter the results dynamically using the criteria:

capture

Let’s wrap the script in some additional code to find the number of updates successfully installed in the last 7 days across an array of computers:


"srvsccm-01","srvsccm-02","srvsccm-03v" | ForEach-Object {
    $UpdatesInstalled = (Parse-WindowsUpdatesLog -ComputerName $_ -Days 7 -Text "Installation successful").Count
    New-Object -TypeName PSObject -Property @{
        ComputerName = $_
        UpdatesInstalled = $UpdatesInstalled
        }
    } | ft -AutoSize

capture

Finally, I want to find out if KB3092627 was installed on a group of servers in an AD group in the last 30 days:


Get-ADGroupMember -Identity SCCM2012_Secondary_WSUS_Servers | 
    Sort Name | 
    Select -ExpandProperty Name |
    ForEach-Object {
        $KB = Parse-WindowsUpdatesLog -ComputerName $_ -Days 30 -Text "KB3092627" | 
            where {$_.Details -match "Installation Successful"} | 
            Select -ExpandProperty Date
        If ($KB -ne $null)
            {$Installed = "True"}
        Else {$Installed = "False"}
        New-Object -TypeName PSObject -Property @{
            ComputerName = $_
            KBInstalled = $Installed
            InstallDate = $KB
            }
    } | ft -AutoSize

capture

Pretty handy 🙂

There is some (outdated) advice for searching the WindowsUpdate.log in the following MS KB: http://support.microsoft.com/en-us/kb/902093

The list of parameters you can use are:

-ComputerName (optional, to get results from a remote computer)
-Days (mandatory, the number of days past to search the log)
-Component (optional, choose from the list of available components to filter results)
-Text (optional, search for a specific keyword or phrase)
-GroupBy (optional, group results by either date or component to find the number of log entries)

Calculate the Size of Multiple Packages in ConfigMgr with PowerShell

Have you ever wanted to select a group of applications or packages in ConfigMgr and find out the total size of the content files for all those packages?  Or maybe you have your packages organised into folders in the ConfigMgr console, and you want to find out the total content size of all the packages in that folder?  Well with PowerShell you can!

I wrote this script to help me in calculating the content size for multiple packages of any type I choose – applications, standard packages, driver packages, boot images etc, so that I know how much data I am sending across the wire when distributing packages to a new distribution point. If your network bandwidth to a distribution point is limited, then it is helpful to know how much data you are sending so you know whether you need to set rate limits, or distribute out of hours etc.

This script uses PowerShell’s handy Out-GridView to display the list of available packages for the package types found in the Software Library (you choose) and allow you to select multiple packages. It will then calculate the total size of all the packages you selected.  If you use the -Verbose option, it will also report the size of each individual package.

For Applications, Standard Packages and Driver Packages, which are often organised into sub-folders in the ConfigMgr console, you can also specify the folder name, and the script will return the total content size of all packages in that folder.

The following package types are supported:

  • Applications (Specific console folder supported)
  • Standard Packages (Specific console folder supported)
  • Driver Packages (Specific console folder supported)
  • Software Update Packages
  • OS Image Packages
  • Boot Image Packages

Get the script

Download from the Technet Gallery here.

Configure the script

The script has two parameters that you might want to set the defaults for, otherwise you will need to enter them each time:

  • $SiteServer – the name of the site server you are running the script on
  • $SiteCode – your Site code

Edit the following part of the script:


[Parameter(
Mandatory=$False,
HelpMessage="The Site Server name"
)]
[string]$SiteServer="mysccmserver",

[Parameter(
Mandatory=$False,
HelpMessage="The Site Code"
)]
[string]$SiteCode="ABC"

The script also has comment-based help.

Examples

Get the total content size of all Adobe Flash Player Applications

I run the script with the -Applications switch:

Get-CMSelectedSoftwareContentSizes.ps1 -Applications

Select the Adobe Flash Player applications and click OK:

1

Total content size is returned:

2

Get the total size of all Intel packages and also report the size of each package

I run the script with the -Packages and the -Verbose switches:

Get-CMSelectedSoftwareContentSizes.ps1 -Packages -Verbose

Select the Intel packages and click OK:

3

Total size and individual sizes are returned:

4

Get the total size of all Dell driver packages in the “Dell System Cab Driver Packages” console folder and report the size of each package

In the ConfigMgr console, I have Dell driver packages organised into a subfolder:

5

I run the script and use the -DriverPackages, -FolderName and -Verbose switches:

Get-CMSelectedSoftwareContentSizes.ps1 -DriverPackages -FolderName “Dell System Cab Driver Packages” -Verbose

The individual size and total size of all driver packages in that console folder is returned:

7

WSUS Database Maintenance for SQL Express

To perform database maintenance (defragmentation, re-indexing) on a WSUS database that is installed using SQL Express, I use the following solution.  Because SQL Express has no SQL Server Agent, you cannot create jobs or maintenance plans, but we can use Powershell with a Scheduled Task to perform regular maintenance.  I am using SQL Express 2012.

Download the WSUSDBMaintenance script from the Technet Gallery, and save it to a local folder on your WSUS server/s.

Then use the following Powershell script as a scheduled task to call the sql maintenance script and send an email to yourself with the output of the sql script as a log file.


$WSUSServers = @(
    "wsussrv-01v",
    "wsussrv-02v",
    "wsussrv-03v",
    "wsussrv-04",
    "wsussrv-05v",
    "wsussrv-06v",
    "wsussrv-07v"
    "wsussrv-08",
    "wsussrv-09v",
    "wsussrv-10v",
    "wsussrv-11v"
    )

foreach ($server in $WSUSServers)
    {
        $S = New-PSSession -ComputerName $server
        Invoke-Command -Session $S -ArgumentList $server -ScriptBlock {
        param ($server)
        Invoke-SQLCmd -InputFile C:\LocalScripts\SUSDB_Maintenance.sql -ServerInstance $env:COMPUTERNAME -OutputSqlErrors $True -Verbose *>$env:TEMP\SUSDB_Maintenance_$Server.log
        Send-MailMessage -To Trevor.jones@contoso.com -From Powershell@contoso.com -SmtpServer mysmtpserver -Subject "WSUS DB Maintenance log for $server" -Attachments $env:TEMP\SUSDB_Maintenance_$Server.log
        }
        Remove-PSSession $S
    }


Note that I am performing this maintenance on multiple WSUS servers in succession using an array.

In the Invoke-SQLCmd command, change the inputfile to the location of your sql script.

In the Send-MailMessage command, use the particulars for your environment

Powershell remoting must be available as the command will be run on the WSUS server itself.

If you are using an older version of SQL Express, you may need to add the SQL snapins first as described here.  In SQL Express 2012, the cmdlet is a part of a module that will be imported when you call it (Powershell 3.0 +).

WSUS Server Cleanup Report

There are a few scripts out there that will perform a cleanup of your WSUS server/s, but here’s my contribution 🙂  It uses the Invoke-WSUSServerCleanup cmdlet only available in Windows Server 2012 / Windows 8 onwards, so for previous versions try something like Kaido Järvemets’ script.  This script will perform the WSUS cleanup for any number of WSUS servers, then send you a summary html email report.  It will also give you the time taken for each WSUS server to perform the cleanup, convert the ‘Freed diskpace’ value into a more useful GB value, and report any errors that occurred trying to perform the cleanup.  An error can sometimes occur when maintenance has not been performed for a long time on the server, and the cleanup can timeout.

Simply add your WSUS server names to the $WSUSservers variable, and add the mail settings for your environment. If you are using upstream/downstream WSUS servers, perform the cleanup on the lowermost downstream server first, and work up the heirarchy, as recommended by Microsoft.

You can then run the script as a scheduled task to perform regular maintenance.

Capture


$WSUSServers = @(
    "wsussrv-01v",
    "wsussrv-02v",
    "wsussrv-03v",
    "wsussrv-04v",
    "wsussrv-05v",
    "wsussrv-06v",
    "wsussrv-07",
    "wsussrv-08v",
    "wsussrv-09",
    "wsussrv-10v",
    "wsussrv-11v",
    "wsussrv-12v"
    )

# Mail settings
$smtpserver =  "mysmtpserver"
$MailSubject = "WSUS Cleanup Report"
$MailRecipients = "trevor.jones@contoso.com"
$FromAddress = "WSUS@contoso.com"

# Location of temp file for email message body (will be removed after)
$msgfile = "$env:Temp\mailmessage.txt"

$ErrorActionPreference = "Stop"

#region Functions
function New-Table (
$Title,
$Topic1,
$Topic2,
$Topic3,
$Topic4,
$Topic5,
$Topic6,
$Topic7

)
{ 
       Add-Content $msgfile "<style>table {border-collapse: collapse;font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}"
       Add-Content $msgfile "h2 {font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}"
       Add-Content $msgfile "th, td {font-size: 1em;border: 1px solid #87ceeb;padding: 3px 7px 2px 7px;}"
       Add-Content $msgfile "th {font-size: 1.2em;text-align: left;padding-top: 5px;padding-bottom: 4px;background-color: #87ceeb;color: #ffffff;}</style>"
       Add-Content $msgfile "<h2>$Title</h2>"
       Add-Content $msgfile "<p><table>"
       Add-Content $msgfile "<tr><th>$Topic1</th><th>$Topic2</th><th>$Topic3</th><th>$Topic4</th><th>$Topic5</th><th>$Topic6</th><th>$Topic7</th></tr>"
}

function New-TableRow (
$col1, 
$col2,
$col3,
$col4,
$col5,
$col6,
$col7

)
{
Add-Content $msgfile "<tr><td>$col1</td><td>$col2</td><td>$col3</td><td>$col4</td><td>$col5</td><td>$col6</td><td>$col7</td></tr>"
}

function New-TableEnd {
Add-Content $msgfile "</table></p>"}
#endregion


# Create file
New-Item $msgfile -ItemType file -Force | Out-Null

# Add html header
Add-Content $msgfile "<style>h2 {font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}</style>"
Add-Content $msgfile "<p></p>"
        
# Create a new html table
New-Table -Title "WSUS Cleanup Report $(Get-Date -Format f)" -Topic1 "WSUS Server" -Topic2 "Declined Expired Updates" -Topic3 "Declined Superseded Updates" -Topic4 "Cleaned Obsolete Updates" -Topic5 "Compressed Updates" -Topic6 "CleanedUp Unneeded Content Files" -Topic7 "Time Taken"
        

# Run the cleanup on each server
foreach ($WsusServer in $WSUSServers)
    {
        try
            {
            $startDTM = (Get-Date)
            write-host "Doing cleanup on $WSUSServer"
            $Cleanup = Get-WsusServer -Name $WsusServer -PortNumber 8530 | Invoke-WsusServerCleanup -DeclineExpiredUpdates -DeclineSupersededUpdates -CleanupObsoleteUpdates -CompressUpdates -CleanupUnneededContentFiles
            $endDTM = (Get-Date)
            $Time = $endDTM-$startDTM
            $TimeTaken = $Time.Hours.ToString() + " hours, " + $Time.Minutes.ToString() + " minutes, " + $Time.Seconds.ToString() + " seconds"
            $DiskspaceFreed = $cleanup[4].Split(":")[0] + ":" + ([math]::round(($cleanup[4].Split(":")[1] / 1GB),2)).ToString() + " GB" 
            New-TableRow -col1 $WSUSServer -col2 $Cleanup[1] -col3 $Cleanup[0].Replace("Obsolete Updates Deleted", "Superseded Updates Declined") -col4 $Cleanup[2] -col5 $Cleanup[3] -col6 $DiskspaceFreed -col7 $TimeTaken
            }
        catch
            {
            New-TableRow -col1 $WSUSServer -col2 "Failed to perform cleanup." -col3 $($_.Exception.Message)
            }
    }

# Add html table to file
New-TableEnd

# Set email body content
$mailbody = Get-Content $msgfile

Send-MailMessage -Body "$mailbody" -From $FromAddress -to $MailRecipients -SmtpServer $smtpserver -Subject $MailSubject -BodyAsHtml 

# Delete tempfile 
Remove-Item $msgfile

You can follow the cleanup process on each server from the SoftwareDistribution.log located at %ProgramFiles%\Update Services\LogFiles.