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