Create a Software Update Group defined by date in ConfigMgr with PowerShell

190_0

As a ConfigMgr administrator, one of the main functionality that many of us are working with is Software Updates. Over time, I’ve eventually decided to automate many of the tasks involved in managing Software Updates, like my console extension to automatically clean Software Update Groups which I’ve shared on this blog. Another tasks that I often have to perform is to combine a set of Software Update Groups into a single group, resulting in a better overview. An example would be to combine the Software Update Groups automatically created by an Automatic Deployment Rule for each month during 2015 into a single Software Update Group for all the Software Updates released (and deployed in your environment) in 2015.

You may be thinking that this can easily be done by creating a Saved search in the ConfigMgr console, executing that and simply just set the dates correctly. Although, you’re correct, I’ve seen many occasions where this is forgotten and it’s not being taken care of by the admins responsible. In my opinion, this particular tasks that you perform for instance once a year, is something that we could simply just automate.

To automate this whole process, there are several tasks that you’d need for a full automated workflow:

  • Scheduled task/runbook to execute a script
  • PowerShell script creating a Software Update Group
  • PowerShell script to deploy the newly created Software Update Group

In this post, I’ll be sharing the PowerShell script to create the Software Update Group, like explained in the steps above. I’ve chosen not to embed the last step in the code for this script, mainly due to that there’s not really any generic way that organizations configure their deployments. However, with the logic of creating the Software Update Group from the script in this post, and if you fully want to automate the whole process, you should be well on your way.

Script

Just like all of my scripts, I’ve uploaded it to my script repository on GitHub. In addition, you’ll also find the script below.

<# 
.SYNOPSIS 
    Create a Software Update Group containing Software Updates defined by Product and Update Classification between a range of years. 

.DESCRIPTION 
    This script will create a Software Update Group containing Software Updates defined by Product and Update Classification between a range of years. 

.PARAMETER SiteServer 
    Site server where the SMS Provider is installed 

.PARAMETER Name 
    Name of the Software Update Group. 

.PARAMETER Products 
    Define a set of Products. 

.PARAMETER UpdateClassifications 
    Define a set of Update Classifications. 

.PARAMETER StartYear 
    Set the start year from which released or revised patches will be evaluated against when adding to Software Update Group. 

.PARAMETER EndYear 
    Set the end year when released or revised patches will be evaluated against when adding to Software Update Group. 

.EXAMPLE 
    Create a Software Update Groups containing Security Updates and Critical Updates for Windows 7 and Windows 8.1 between 2014 and 2015: 
    .\New-CMSoftwareUpdateGroupBetweenYear.ps1 -SiteServer CM01 -Name "SUM - Windows Clients - 2014-2015" -Products "Windows 7","Windows 8.1" -UpdateClassifications "Critical Updates","Security Updates" -StartYear 2014 -EndYear 2015 -Verbose 
    Create a Software Update Groups containing Security Updates and Critical Updates for Windows Server 2012 R2 between 2015 and 2015: 
    .\New-CMSoftwareUpdateGroupBetweenYear.ps1 -SiteServer CM01 -Name "SUM - Windows Clients - 2014-2015" -Products "Windows Server 2012 R2" -UpdateClassifications "Critical Updates","Security Updates" -StartYear 2015 -EndYear 2015 -Verbose 

.NOTES 
    FileName: New-CMSoftwareUpdateGroupBetweenYear.ps1 
    Author:   Nickolaj Andersen 
    Contact:  @NickolajA 
    Created:  2016-03-22 
    Updated:  2016-03-22 
    Version:  1.0.0 
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [parameter(Mandatory=$true, HelpMessage="Site server where the SMS Provider is installed.")]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({Test-Connection -ComputerName $_ -Count 1 -Quiet})]
    [string]$SiteServer,

    [parameter(Mandatory=$true, HelpMessage="Name of the Software Update Group.")]
    [ValidateNotNullOrEmpty()]
    [string]$Name,

    [parameter(Mandatory=$true, HelpMessage="Define a set of Products.")]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Windows 7", "Windows 8.1", "Windows 10", "Windows 10 LTSB", "Windows Server 2008", "Windows Server 2008 R2", "Windows Server 2012", "Windows Server 2012 R2", "Office 2007", "Office 2010", "Office 2013", "Office 2016", "Office 365 Client")]
    [string[]]$Products,

    [parameter(Mandatory=$true, HelpMessage="Define a set of Update Classifications.")]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Critical Updates", "Security Updates", "Feature Packs", "Updates", "Update Rollups")]
    [string[]]$UpdateClassifications,

    [parameter(Mandatory=$true, HelpMessage="Set the start year from which released or revised patches will be evaluated against when adding to Software Update Group.")]
    [ValidateNotNullOrEmpty()]
    [string]$StartYear,

    [parameter(Mandatory=$true, HelpMessage="Set the end year when released or revised patches will be evaluated against when adding to Software Update Group.")]
    [ValidateNotNullOrEmpty()]
    [string]$EndYear
)
Begin {
    # Determine SiteCode from WMI
    try {
        Write-Verbose -Message "Determining Site Code for Site server: '$($SiteServer)'"
        $SiteCodeObjects = Get-WmiObject -Namespace "root\SMS" -Class SMS_ProviderLocation -ComputerName $SiteServer -ErrorAction Stop
        foreach ($SiteCodeObject in $SiteCodeObjects) {
            if ($SiteCodeObject.ProviderForLocalSite -eq $true) {
                $SiteCode = $SiteCodeObject.SiteCode
                Write-Verbose -Message "Site Code: $($SiteCode)"
            }
        }
    }
    catch [System.UnauthorizedAccessException] {
        Write-Warning -Message "Access denied" ; break
    }
    catch [System.Exception] {
        Write-Warning -Message "Unable to determine Site Code" ; break
    }
}
Process {
    # Functions
    function New-SoftwareUpdateGroupList {
        param(
            [parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]$SoftwareUpdateGroupName,

            [parameter(Mandatory=$false)]
            [System.Collections.ArrayList]$UpdatesList
        )
        # Create a new SMS_CI_LocalizedProperties instance (embedded object)
        $LocalizedProperties = ([WmiClass]"\\$($SiteServer)\root\SMS\site_$($SiteCode):SMS_CI_LocalizedProperties").CreateInstance()
        $LocalizedProperties.DisplayName = $SoftwareUpdateGroupName
        $LocalizedProperties.Description = "Automatically generated by script"
        $LocalizedProperties.LocaleID = 1033

        # Create a new SMS_AuthorizationList instance
        $AuthorizationListArguments = @{
            LocalizedInformation = [array]$LocalizedProperties
        }
        try {
            Set-WmiInstance -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_AuthorizationList -ComputerName $SiteServer -Arguments $AuthorizationListArguments -ErrorAction Stop | Out-Null
            Write-Verbose -Message "Successfully created '$($SoftwareUpdateGroupName)' software update group"
        }
        catch [System.Exception] {
            Write-Warning -Message "Unable to create '$($SoftwareUpdateGroupName)' software update group, breaking build operation. Line: $($_.InvocationInfo.ScriptLineNumber)" ; break
        }

        # Add list of CI_ID's to Software Update Group
        $SoftwareUpdateGroup = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_AuthorizationList -ComputerName $SiteServer -Filter "LocalizedDisplayName like '$($SoftwareUpdateGroupName)'" -ErrorAction Stop
        if ($SoftwareUpdateGroup -ne $null) {
            $SoftwareUpdateGroup.Get()
            $SoftwareUpdateGroup.Updates = $UpdatesList
            $SoftwareUpdateGroup.Put() | Out-Null
            Write-Verbose -Message "Successfully added '$($UpdatesList.Count)' software updates to '$($SoftwareUpdateGroupName)' software update group"
        }
    }

    # Build Software Udates updates classification hash-table
    $UpdateClassificationsTable = @{}
    $UpdateClassificationObjects = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_CategoryInstance -Filter "CategoryTypeName like 'UpdateClassification'"
    foreach ($UpdateClassificationObject in $UpdateClassificationObjects) {
        $UpdateClassificationsTable.Add($UpdateClassificationObject.LocalizedCategoryInstanceName, $UpdateClassificationObject.CategoryInstance_UniqueID)
    }

    # Build Software Udates products hash-table
    $ProductsTable = @{}
    $ProductObjects = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_CategoryInstance -Filter "CategoryTypeName like 'Product' AND LocalizedCategoryInstanceName not like 'Windows Live' AND LocalizedCategoryInstanceName not like 'Visual Studio 2010 Tools for Office Runtime'"
    foreach ($ProductObject in $ProductObjects) {
        $ProductsTable.Add($ProductObject.LocalizedCategoryInstanceName, $ProductObject.CategoryInstance_UniqueID)
    }

    # Defince beginning of Software Updates query
    $SoftwareUpdatesQuery = "SELECT SMS_SoftwareUpdate.* FROM SMS_SoftwareUpdate WHERE (SMS_SoftwareUpdate.CI_ID NOT IN (SELECT CI_ID FROM SMS_CIAllCategories WHERE CategoryInstance_UniqueID='UpdateClassification:3689bdc8-b205-4af4-8d4a-a63924c5e9d5'))"
    
    # Construct year part of query
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, " AND (DateRevised >='01/01/$($StartYear) 00:00:00' AND DateRevised <='12/31/$($EndYear) 00:00:00' )")

    # Construct expired updates part of query
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, " AND (IsExpired ='0' ) AND ")
    
    # Construct Products part of query
    $ProductsCount = 0
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "( ")
    foreach ($Product in $Products) {
        $ProductsCount++
        if ($ProductsCount -lt $Products.Count) {
            $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "CI_ID in (select CI_ID from SMS_CIAllCategories where CategoryInstance_UniqueID='$($ProductsTable[$Product])') OR ")
        }
        else {
            $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "CI_ID in (select CI_ID from SMS_CIAllCategories where CategoryInstance_UniqueID='$($ProductsTable[$Product])')")
        }
    }
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, " )")

    # Construct superseded part of query
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, " AND (IsSuperseded ='0' ) AND ")

    # Construct classification part of query
    $UpdateClassificationsCount = 0
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "( ")
    foreach ($UpdateClassification in $UpdateClassifications) {
        $UpdateClassificationsCount++
        if ($UpdateClassificationsCount -lt $UpdateClassifications.Count) {
            $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "CI_ID in (select CI_ID from SMS_CIAllCategories where CategoryInstance_UniqueID='$($UpdateClassificationsTable[$UpdateClassification])') OR ")
        }
        else {
            $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, "CI_ID in (select CI_ID from SMS_CIAllCategories where CategoryInstance_UniqueID='$($UpdateClassificationsTable[$UpdateClassification])')")
        }
    }
    $SoftwareUpdatesQuery = -join @($SoftwareUpdatesQuery, " )")

    # Create Software Update Group
    $SoftwareUpdates = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Query $SoftwareUpdatesQuery
    if ($SoftwareUpdates -ne $null) {
        New-SoftwareUpdateGroupList -SoftwareUpdateGroupName $Name -UpdatesList $SoftwareUpdates.CI_ID
    }
    else {
        Write-Warning -Message "Specified search for Software Updates between '$($StartYear)' and '$($EndYear)' did not return any Software Updates"
    }
}

How to use the script

If you’ve paid attention to the script above, I’ve added validation to only allow a set of Products and Update Classifications in the param block. If you need any other items, simply just extend the ValidateSet() function. Executing the script is pretty straight forward, follow the steps below:

1. Open an elevated PowerShell prompt and browse to where you’ve saved the script.
2. Run the following command:

.\New-CMSoftwareUpdateGroupBetweenYear.ps1 -SiteServer CM01 -Name "SUM - Windows Clients - 2015" -Products "Windows 7","Windows 8.1" -UpdateClassifications "Critical Updates","Security Updates" -StartYear 2015 -EndYear 2015 -Verbose

190_2
I hope this helps on your journey for automating your everything.

5 Comments

  1. John

    Hi,

    Thank you for the script. How would I make a group for all products that are synct by the SUP?

    Thanks,

    Reply
    1. NickolajNickolaj (Post author)

      Hi John,

      Have a look at the tool that I just released this week, it gives you more options without amending the script.

      http://www.scconfigmgr.com/2016/05/10/create-software-update-group-tool-console-extension-for-configmgr/

      Regards,
      Nickolaj

      Reply
      1. John

        Hi Nickolaj,

        I already did, but I am not able to select all products. Besides I’m trying to automate my deployment 🙂

        Regards,
        John

        Reply
  2. Matt

    Nickolaj,

    Thank you for once again sharing your fantastic work. I’m still a novice when it comes to Powershell, so it’s helpful to see how it should be done. I actually grabbed your entire Powershell GitHub folder, and look forward to incorporating pieces into some of our automation and orchestration processes. You are a gentleman and a scholar!

    Reply
    1. NickolajNickolaj (Post author)

      Hi Matt,

      I’m glad that you appreciate my work. Let me know if you run into any issues or need some help/advice on implementing some automation to ConfigMgr.

      Regards,
      Nickolaj

      Reply

Leave a Comment

Your email address will not be published. Required fields are marked *