Tuesday, June 17, 2014

PowerShell + SCCM 2012 : Automate Patching

This is an evolving post and is related to a project aiming to automate Software Updates in our environment. 

The environment is a little complex as we have to work with other teams to get the list of Bulletin-IDs of S/W Updates (or patches) which will be deployed to our environment. So we can't probably go for ADR in CM 2012.

So we will look at automating this Patching scenario from the very beginning where we get a list of Bulletin-ID's from outside source.

Note - This post is inspired by Steve Rachui's post on Automating Software Updates, where he has shown some very cool stuff (link at the end check that one out).
Thanks to Stephane >:D< for sharing his WMI Functions which helped me a lot, along with cool B-) people like Kaido,Peter & David who have shared there awesome work :). All the resources link are at the end of the page.

Below are the steps we will follow, which should get someone started who is looking to achieve the same:

  1. Get list of patches to be deployed
  2. Create a Software Update Group
  3. Download the Software Updates
  4. Create  deployment Package
  5. Deploy them
Note - that this is just groundwork for my project....so finer details like logging, error handling will be taken care of later.  I went with the WMI way of doing this as encountered that the cmdlets shipped with CM are little buggy right now but if it suits you go ahead and try those too.

1. Get list (CI_ID) of Patches to be deployed

This is the first and it's important to put in the logic to only select the patches you are looking for.
There is a patchList.txt file containing the approved Bulletin-ID's which are to be deployed to the Machines.


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
# Use Splatting
$hash = @{
            #open a CIM sesison to the ConfigMgr Server
            cimsession = New-CimSession -ComputerName dexsccm
            NameSpace = 'Root\SMS\Site_DEX' #point to the SMS Namespace
            ErrorAction = 'Stop'

        }

$BulletinIDList = get-content C:\temp\BulletinIDList.txt

# loop through the contents of the text file to build you a query
for ( $i = 0 ; $i -lt $BulletinIDList.count;$i++)
{
    if ($i -eq 0)
    {
        $query = "Select * from SMS_SoftwareUpdate WHERE (BulletinID='$($BulletinIDList[$i])')"
    }
    else
    {
        $query = $query + " OR (BulletinID='$($BulletinIDList[$i])')"

    }
}

#Get the Software Updates corresponding to the Bulletin-IDs
$BulletinIDPatches  = Get-CimInstance -Query $query @hash

Now we have list of all the S/W Updates as per the Bulletin-ID's , we need to filter out all the Software Updates applicable to my Environment (NumMissing property tells me on how many machines it is missing)


001
002
#Filter out not Applicable patches
$BulletinIDPatches = $BulletinIDPatches | where NumMissing -ne 0

One can also filter on a lot of parameters here like the Product, OS etc.
For my environment want to only select the patches applicable to Server OS (server 2008 R2 & Server 2012 R2 )


001
002
#Select Products you are targeting
$productcategories = Get-CimInstance -ClassName  SMS_UpdateCategoryInstance -Filter 'CategoryTypeName="Product"' | Out-GridView -Title "Slect the Products to target" -PassThru

Above will give an out-gridview  window for me to select the products am targeting...I will select Windows Server 2008 and Windows Server 2012 & 2012 R2



Now once I have the Products selected I will filter out all the earlier Software Updates I have that apply on these Products :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
# will use a HashTable for filtering later
$ProductIDHash = @{}

#create a Hash Table used to filter out later
$productcategories | ForEach-Object -Process {$ProductIDHash.Add("$($($_.CategoryInstance_UniqueID) -replace 'Product:')","$($_.LocalizedCategoryInstanceName)")}

#Filter out S/W updates which do not apply to the Products we want
 $DeployPatches = $BulletinIDPatches |
    ForEach-Object -Process {
         # Check if the Software Update is applicable to the list of Products we selected and doesn't target Itanium Architecture
         if ( $ProductIDHash.Contains($([System.Xml.XmlDocument]$_.ApplicabilityCondition).ApplicabilityRule.ProductId) -and ( $_.LocalizedDisplayName -notlike "*Itanium*") )
         {
            # Adding a note property to the S/W update Object in Pipeline to know which product it applies to
            $_ | Add-Member -MemberType NoteProperty -Name AppliesTo -Value "$($ProductIDHash.$($([System.Xml.XmlDocument]$_.ApplicabilityCondition).ApplicabilityRule.ProductId))"

            Write-Output -InputObject $_
         }
    }

Now let's see what is there in the $DeployPatches which is our Final list of S/W updates which are going to be deployed:
PS> $DeployPatches | format-list -Property localizeddisplayname,appliesto


localizeddisplayname : Security Update for Windows Server 2008 R2 x64 Edition (KB2957503)
AppliesTo            : Windows Server 2008 R2

localizeddisplayname : Security Update for Windows Server 2008 R2 x64 Edition (KB2957189)
AppliesTo            : Windows Server 2008 R2

localizeddisplayname : Security Update for Windows Server 2008 R2 x64 Edition (KB2939576)
AppliesTo            : Windows Server 2008 R2

localizeddisplayname : Cumulative Security Update for Internet Explorer 10 for Windows Server 2008 R2 Service Pack 1
                       for x64-based Systems (KB2957689)
AppliesTo            : Windows Server 2008 R2

localizeddisplayname : Security Update for Windows Server 2008 R2 x64 Edition (KB2957509)
AppliesTo            : Windows Server 2008 R2




Note that In order to Script the entire solution we will have to work with CI_ID as the SDK documentation states :

The CI_ID value identifies the software updates information across several classes. For the purposes of using the Configuration Manager SDK interfaces, the CI_ID value is vital


2. Create a Software Update Group

There is a cmdlet in the Configuration Manager Module by the name New-CMSoftwareUpdateGroup , but I wanted to stick to the WMI way of doing this.
We have to create an instance of the Class SMS_AuthorizationList and for this we will have to switch back to WMI ....can't be done using CIM :(


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
#region create Software Update Group (SMS_AuthorizationList)

#prepare the Default Paramater Values to work with Get-WMIObject
$PSDefaultParameterValues =@{"get-wmiobject:namespace"="Root\SMS\site_DEX";"get-WMIObject:computername"="DexSCCM"}

#Get a reference to the WMI Class SMS_CI_LocalizedProperties
$class = Get-WmiObject -Class SMS_CI_LocalizedProperties -list

#instantiate the Class
$LocalizedProperties = $class.CreateInstance()

$LocalizedProperties.DisplayName="TEST Software Update Group"
$LocalizedProperties.Description="Testing PS Automation"


$class = Get-WmiObject -Class SMS_AuthorizationList -list
$UpdateGroup = $class.CreateInstance()

$UpdateGroup.LocalizedInformation = $LocalizedProperties
$UpdateGroup.Updates = $DeployPatches | select -ExpandProperty CI_ID

$UpdateGroup.put()
$UpdateGroup.get()
#endregion create Software Update Group (SMS_AuthorizationList)

There is an post by David O'Brien which covers this very topic...link at the end.


So after this if you head to the ConfigMgr Console you will see the Update Group created along with the appropriate Updates inside that.




Software Updates inside above Update Group


Note - Compare this with the PowerShell Console Output of $DeployPatches | format-list -Property localizeddisplayname,appliesto

3. Download the Software Updates

Now need to download all the Software Updates in a temporary location, we do this in a UNC path. So we create a new PSDrive pointing to that location. Then we use the relation between the classes SMS_SoftwareUpdate , SMS_CIToContent and SMS_CIContentFiles described in the SDK to get the URL for the download.

Then later BITS is used to download the patches..right now I am doing this in my own LAB and will try this later in the QA  and add support to handle failed downloads cause of network bandwidth costs.

Will be using the ContentID to create a folder where the corresponding patch will be downloaded. You will see in next section why I need to do this.



001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
#region Download the Software Updates

#Create a new PSDrive where the Patches will be downloaded
New-PSDrive -Name M -PSProvider FileSystem -Root "\\dexsccm\patches\2014"
$downloadpath = "M:"

$DownloadInfo =foreach ($CI_ID in  $UpdateGroup.Updates)
{
    $contentID = Get-CimInstance -Query "Select ContentID,ContentUniqueID,ContentLocales from SMS_CITOContent Where CI_ID='$CI_ID'"  @hash
    #Filter out the English Local and ContentID's not targeted to a particular Language
    $contentID = $contentID  | Where {($_.ContentLocales -eq "Locale:9"-or ($_.ContentLocales -eq "Locale:0") }

    foreach ($id in $contentID)
    {
        $contentFile = Get-CimInstance -Query "Select FileName,SourceURL from SMS_CIContentfiles WHERE ContentID='$($ID.ContentID)'" @hash
        [pscustomobject]@{Source = $contentFile.SourceURL ;
                            Destination = "$downloadpath\$($id.ContentID)\$($contentFile.FileName)";
                             }

    }
}

#test and create the Destination Folders if needed
$DownloadInfo.destination |
    ForEach-Object -Process {
            If (! (test-path -Path "filesystem::$(Split-Path -Path $_)"))
             {
                New-Item -ItemType directory -Path "$(Split-Path -Path $_)"
            }
        }

$DownloadInfo | Start-BitsTransfer
#endregion Download the Software Updates


Now the the UNC path where am storing all the downloaded patches looks like below :


Note - There are caveats around running the BitsTransfer as a job. I am testing it and once done will post it

4. Create Deployment Package

Need to create the deployment package which will eventually get distributed to the Distribution Points and then  deployed to the Clients.


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
#region Create the Deployment Package

#Get the Class
$class = Get-WmiObject -Class SMS_SoftwareUpdatesPackage -List

#Instantiate the Class Object
$DeployPackage = $class.CreateInstance()

#set the appropriate properties on the Instance
$DeployPackage.Name = "Test Deploy Package"
$DeployPackage.SourceSite = "DEX"
$DeployPackage.PkgSourcePath = "\\dexsccm\patches\2014"
$DeployPackage.Description = "testing PS Automation"
$DeployPackage.PkgSourceFlag = [int32]2 #Value of 2 -->Stores Software Updates

#persist the changes
$DeployPackage.put()

#get the latest WMI Instance back
$DeployPackage.get()

#endregion Create the Deployment Package

The deployment package is created and now to tell it to where to get/add the patches from we will invoke a method on the WMI Object.

Using the old trick of asking PowerShell the Method Overloaddefinition (where I just dot reference the Method name) , you will get below:
PS> $DeployPackage.AddUpdateContent

OverloadDefinitions
-------------------
System.Management.ManagementBaseObject AddUpdateContent(System.UInt32[] ContentIDs, System.String[] ContentSourcePath,
System.Boolean bRefreshDPs)


So it tells need to pass an array of ContentIDs this deployment package will store and then the array of where these are stored (note it's an array) and then a Boolean value telling if we want to refresh DP's.

Below is how we can get this information. Note how using the ContenID as the folder name helps here to get the information we need:
001
002
003
004
005
006
#get the Array of content source path
$contentsourcepath = Get-ChildItem  -path 'M:' | select -ExpandProperty Fullname

#get the array of ContentIDs
$allContentIDs =  $contentsourcepath | foreach {Split-Path  -Path $_ -Leaf}

Make sure that the number of elements in both the arrays are same ;)
Below is what it should be like:



Now ready to invoke the method :

001
002
003
# call the Method...May the force be with me ;)
$DeployPackage.AddUpdateContent($allContentIDs,$contentsourcepath,$true)
If you have done everything correct this should give you something like below:



Meanwhile if you get any error or even success I urge you to take a look at the SMSProv.log which is your best friend if you are looking to automate ConfigMgr.



After all is done you can verify the same in the console too :

Deploy Package is created :

Have a look inside the Deployment Package...Updates do reflect:



After all is done if we take a look back to the UNC path :



It needs cleanup period:


001
002
003
#final cleanup
$DownloadInfo.destination | ForEach-Object -Process { Remove-Item -Path (split-path -path $_-Recurse -verbose}       

5. Deploy them

Leaving this for now one can achieve this either using WMI or the Cmdlet.

CM Cmdlet is  Start-CMSoftwareUpdateDeployment.
The WMI Class you would be interested in is SMS_UpdatesAssignment

Go ahead explore & automate <:-P

========================================================================
[UPDATE]
I am trying this in my environment and learning the challenges faced as I progress. The process am following for this might not be the prefect one but I would love to take any inputs/feedback.

P.S. - Have a very cool & awesome idea in my mind but will post about it when it's done.

========================================================================
Resources :

Automating Software Updates - Steve Rachui
http://blogs.msdn.com/b/steverac/archive/2014/06/12/automating-software-updates.aspx

S/W Update WMI Functions - Stephane Van Gulick
http://powershelldistrict.com/?p=340

Add Update Content to a deployment Package - Peter van der Woude
www.petervanderwoude.nl/post/add-update-content-to-a-deployment-package-via-powershell-in-configmgr-2012/

Create a New Software update group in ConfigMgr - David O'Brien
http://www.david-obrien.net/2012/12/02/create-a-new-software-update-group-in-configmgr/

ConfigMgr PowerShell SDK - Kaido
http://cm12sdk.net/?page_id=10

11 comments:

  1. Great post. Looking forward to future updates. We are in the middle of changing our SCCM to R2 and need to rebuild our Software Updates area. This is going to help out a ton. One item that I am doing a little different is that I am querying the system for the CI_IDs based on two dates (start and end). For the most part we don't exclude many updates so this makes things go a little faster. Here is the snippet I use to collect the CI_IDs.


    # Put the date info into a format that can be used in different ways
    $DRStart = Get-Date $DateRangeStart
    $DREnd = Get-Date $DateRangeEnd

    # This query is specific to my group - adjust as needed. Right now if filters out definitions for SCEP, and Itanium Updates.
    $queryUpdates = "SELECT * FROM SMS_SoftwareUpdate WHERE IsExpired = 'False' AND LocalizedDisplayName NOT LIKE 'Definition Update for Microsoft Endpoint Protection%' AND LocalizedDisplayName NOT LIKE 'Definition Update for Microsoft Forefront%' AND LocalizedDisplayName NOT LIKE '%Itanium%' AND LocalizedDisplayName NOT LIKE '%IA-64%' AND DateRevised between '" + $DRStart.year + "/" + "{0:D2}" -f ($DRStart.month) + "/" + "{0:D2}" -f ($DRStart.Day) + "' and '" + $DREnd.year + "/" + "{0:D2}" -f ($DREnd.month) + "/" + "{0:D2}" -f ($DREnd.Day) + "'"

    $KnowledgeBaseIDs = (Get-WmiObject -ComputerName $SiteServer -namespace "root\sms\site_$SiteCode" -Query $queryUpdates).ci_id

    ReplyDelete
    Replies
    1. Thanks for sharing the Query ! looks promising and this will surely help someone out there.
      Cheers !

      Delete
  2. Is it possible to configure the SUP Products from Powershell/WMI/etc... ? I've looked all over and can't find anything.

    ReplyDelete
    Replies
    1. Matt,

      Not doing much of ConfigMgr these days, Maybe you can shout at twitter and ask MVPs there.

      Regards

      Delete
    2. I can suggest one thing, try doing the actions from Console and observer the SMSProv.log to see which WMI class is being used.
      Maybe that can give you pointers. I did a little presentation for Singapore PS Group and did mention it (towards the end) the recording is over here -> http://poshbreakfast.com/?p=51

      Delete
  3. Hi,
    Sometimes getting this error during download process: Start-BitsTransfer : HTTP status 503: The service is temporarily overloaded.
    How can I solve it?

    ReplyDelete
    Replies
    1. Hey there,

      The HTTP Status code beginning with 5 signify that it is a server errror.
      HTTP Status code of 503 -> means the server is simply overloaded at the moment.
      You can put a retry logic in your script to keep polling until the server serves you the content.

      http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

      Regards,
      Dex

      Delete
  4. Quick Question, How does this compare to Desired State Configuration in POSH 4?

    ReplyDelete
    Replies
    1. DSC is more of a Configuration Management tool, baked into the Windows OS.
      I am not sure, what aspects are you trying to compare here ?

      Patching Servers using DSC ?

      Delete
  5. I was wondering if that was an option. We have several servers that need to maintain the same patch levels. I would think that DSC could achieve that. But I'm a complete newbie with DSC.

    ReplyDelete
    Replies
    1. There is an xUpdate PowerShell DSC module which has a xHotfix resource using which you can install hotfixes on the Server.

      But you will have to automate generation of these DSC resources (maybe partial configs), dynamically based on applicability of patches to the Server, copy the hotfixes to the network share and then push/pull DSC configs to the nodes.

      IMO awful lot of work, In theory this can be done.
      It would be interesting to see this being used, but WSUS comes free of cost.

      Most certainly you can use DSC to configure WSUS settings on the Server nodes.
      Fellow MVP Fabien wrote few of them or maybe you can search GitHub for something similar.
      https://pwrshell.net/powershell-desired-state-configuration-resources-for-wsus/

      Delete