Wednesday, March 5, 2014

Automate the Roll out of Windows Updates in an available state through SCCM ADR's, Powershell and Orchestrator


I've been really busy with OpsMgr and ConfigMgr lately so I haven't had much time to add content to this blog. This post is about the final step in automating Windows Updates Deployments when they need to be deployed through SCCM 2012 (R2) Automatic Deployment Rules in an available deployment type instead of in a required deployment type. I used Orchestrator to schedule the script.

Ok... Our Enterprise Administrator wanted me to deploy Windows Updates through Automatic Deployment Rules in an available state for our server environment. Problem here is that in ADR's you cannot set the deployment type... they are always deployed in a required type (with a deadline). Personally I would have deployed them with a deadline waaaay in the future and suppress system restart but that wasn't an option (don't ask me why).

The only way you can achieve this is not to enable the deployment after the rule is run (can be configured in the ADR) and then either manually set the deployment type to available and enable the deployment each time the ADR's run or... automate these steps :-)

I'm not going to go through the process of creating the ADR so let's get started with the script.

In the script I use a cmdlet from the Powershell SCCM module and 3 functions.
Since we use an SCCM cmdlet we need to register the ConfigurationManager Snap-in

$SiteCode = "YourSccmSitecode"

$SiteServer = "YourSccmSiteserver"

Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"

Set-Location $SiteCode":"

Cmdlet to set the DeploymentType to Available:

Set-CMSoftwareUpdateDeployment -SoftwareUpdateGroupName $SoftwareUpdateGroupName -DeploymentName $DeploymentName -CollectionName $CollectionName -DeploymentType Available

A function to set the DeploymentStatus to enabled, one to retrieve the members of the targeted collection and one to combine everything and send a notification:
Function Set-CMSUPDeploymentStatus
{
    Param(
         $SiteCode,
         $SiteServer,
         $DeployMentName,
         $TargetCollectionID
         )
    Try{
        $SUPDeployment = Get-WmiObject -Namespace "Root\SMS\Site_$($SiteCode)" -Class SMS_UpdatesAssignment -Filter "AssignmentName='$DeployMentName' and TargetCollectionID='$TargetCollectionID'" -ComputerName $SiteServer -ErrorAction STOP
        $SUPDeployment.Enabled = $True
        $SUPDeployment.Put() }
    Catch{
        $_.Exception.Message }
 
Function Get-CMCollectionMembers
{
    Param(
         $CollectionName
         )
    Try{
        $Collection = get-wmiobject -ComputerName $siteServer -NameSpace "ROOT\SMS\site_$SiteCode" -Class SMS_Collection | where {$_.Name -eq "$CollectionName"}
        $SMSClients = Get-WmiObject -ComputerName $SiteServer -Namespace  "ROOT\SMS\site_$SiteCode" -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='$($Collection.CollectionID)' order by name" | select Name }
    Catch{
        $_.Exception.Message }
   
    Return $SMSClients
}
Function Automate-CMSoftwareUpdateDeployment
{
Param(
$CollectionName,
$SoftwareUpdateGroupName,
$DeploymentName,
$TargetCollectionID
)

$SUPDeployment = Get-WmiObject -Namespace "Root\SMS\Site_$($SiteCode)" -Class SMS_UpdatesAssignment -Filter "AssignmentName='$DeployMentName' and TargetCollectionID='$TargetCollectionID'" -ComputerName $SiteServer -ErrorAction STOP
If ($SUPDeployment.Enabled -eq $false)
{
Try{
#Set to Available and enable deployment
Set-CMSoftwareUpdateDeployment -SoftwareUpdateGroupName $SoftwareUpdateGroupName -DeploymentName $DeploymentName -CollectionName $CollectionName -DeploymentType Available
Start-Sleep -Seconds 10
Set-CMSUPDeploymentStatus -SiteCode $SiteCode -SiteServer $SiteServer -DeployMentName $DeploymentName -TargetCollectionID $TargetCollectionID

#Notifications
$TargetServers = Get-CMCollectionMembers -CollectionName $CollectionName
$members = foreach ($member in $TargetServers) { $member.Name }
$HTMLmessage = @"
<html>
<head>
<style type="text/css">
<!--
body {
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
font-size: 12px;
}
table{
border-collapse: collapse;
border: none;
font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
color: black;
margin-bottom: 10px;
}
table td{
font-size: 12px;
padding-left: 0px;
padding-right: 20px;
text-align: left;
}
table th {
font-size: 12px;
font-weight: bold;
padding-left: 0px;
padding-right: 20px;
text-align: left;
}
-->
</style>
</head>

<body>
New required Critical and Security Updates from the SUG <b>'$SoftwareUpdateGroupName'</b> have been made available to all members of the ConfigMgr Collection <b>'$CollectionName'</b>
<br><br>
<h5>Members of '$SoftwareUpdateGroupName'</h5>
$members
<br><br><hr noshade size=3 width="100%">
<small>This automated report is scheduled using Orchestrator and ran on $env:computername at $((get-date).ToString())</small>
</body>
</html>
"@
Send-MailMessage -From "Windows Update Deployment<youremail@yourdomain>" -To "Recipient<youremail@yourdomain>"`
-Subject "New Software Updates are available" -Smtpserver yoursmtpserver -body $HTMLmessage -BodyAsHtml }
Catch{
$_.Exception.Message }
} #END If ($SUPDeployment.Enabled = $false)
}  
Next we simply need to fill in the parameters and run the Automate-CMSoftwareUpdateDeployment for each Software Update Deployment you need to set to Available and Enabled.

Automate-CMSoftwareUpdateDeployment -CollectionName 'Patch Test Group - Servers' -SoftwareUpdateGroupName "Server Updates - Pilot Rollout Patches" -DeploymentName "Server Updates - Pilot Rollout Patches" -TargetCollectionID "BE100118"

So this was the fun part... making this work in Orchestrator was less fun!

The script runs fine under my user credentials but when I tried to run this under a service account I had to deal with a few issues.

These are the things you need to be aware of:
  • There seems to be an incompatibility with the SCCM integration pack for Orchestrator and the SCCM adminconsole. If the integration pack is installed you cannot use the adminconsole or register the snap-in. As a workaround you can invoke the script on the SCCM server.
  • The Service Account used needs to be a local admin on the SCCM server (to remote powershell into it) and needs to be Full Administrator in SSCM.
  • To run a runbook under alternative credentials you need to invoke the runbook using the "Invoke runbook" activity. The Service Account needs to be member of the local OrchestratorSystemGroup on the Orchestrator server to be able to invoke the runbook.
  • And last but not least... you need to rdp into the SCCM server with the Service Account credentials and launch the SCCM adminconsole! Apparently the user receives a certificate in doing so. Else you will receive following error: Command execution stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find drive. A drive with the name 'yoursitecode' does not exist.

Complete script I used in a runbook below (modify the lines in bold to match your environment).

<#
Author: Baldwin D.
Description: Script to set an SCCM SoftwareUpdate Deployment to available and enabled.
#>
Invoke-Command -Computername "YourSCCMServer" {
$SiteCode = "YourSccmSitecode"
$SiteServer = "YourSccmSiteserver"
Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"
Set-Location $SiteCode":"
#region Function Definitions
Function Set-CMSUPDeploymentStatus
{
    Param(
         $SiteCode,
         $SiteServer,
         $DeployMentName,
         $TargetCollectionID
         )
    Try{
        $SUPDeployment = Get-WmiObject -Namespace "Root\SMS\Site_$($SiteCode)" -Class SMS_UpdatesAssignment -Filter "AssignmentName='$DeployMentName' and TargetCollectionID='$TargetCollectionID'" -ComputerName $SiteServer -ErrorAction STOP
        $SUPDeployment.Enabled = $True
        $SUPDeployment.Put() }
    Catch{
        $_.Exception.Message }
}
Function Get-CMCollectionMembers
{
    Param(
         $CollectionName
         )
    Try{
        $Collection = get-wmiobject -ComputerName $siteServer -NameSpace "ROOT\SMS\site_$SiteCode" -Class SMS_Collection | where {$_.Name -eq "$CollectionName"}
        $SMSClients = Get-WmiObject -ComputerName $SiteServer -Namespace  "ROOT\SMS\site_$SiteCode" -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='$($Collection.CollectionID)' order by name" | select Name }
    Catch{
        $_.Exception.Message }
   
    Return $SMSClients
}
Function Automate-CMSoftwareUpdateDeployment
{
    Param(
        $CollectionName,
        $SoftwareUpdateGroupName,
        $DeploymentName,
        $TargetCollectionID
        )
   
    $SUPDeployment = Get-WmiObject -Namespace "Root\SMS\Site_$($SiteCode)" -Class SMS_UpdatesAssignment -Filter "AssignmentName='$DeployMentName' and TargetCollectionID='$TargetCollectionID'" -ComputerName $SiteServer -ErrorAction STOP
    If ($SUPDeployment.Enabled -eq $false)
    {
    Try{
        #Set to Available and enable deployment
        Set-CMSoftwareUpdateDeployment -SoftwareUpdateGroupName $SoftwareUpdateGroupName -DeploymentName $DeploymentName -CollectionName $CollectionName -DeploymentType Available
        Start-Sleep -Seconds 10
        Set-CMSUPDeploymentStatus -SiteCode $SiteCode -SiteServer $SiteServer -DeployMentName $DeploymentName -TargetCollectionID $TargetCollectionID
       
        #Notifications
        $TargetServers = Get-CMCollectionMembers -CollectionName $CollectionName
        $members = foreach ($member in $TargetServers) { $member.Name }
$HTMLmessage = @" 
    <html>
    <head>
    <style type="text/css">
    <!--
    body {
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
    font-size: 12px;
    }
        table{
           border-collapse: collapse;
           border: none;
           font: 10pt Verdana, Geneva, Arial, Helvetica, sans-serif;
           color: black;
           margin-bottom: 10px;
    }
        table td{
           font-size: 12px;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }
        table th {
           font-size: 12px;
           font-weight: bold;
           padding-left: 0px;
           padding-right: 20px;
           text-align: left;
    }   
    -->
    </style>
    </head>
    <body>
    New required Critical and Security Updates from the SUG <b>'$SoftwareUpdateGroupName'</b> have been made available to all members of the ConfigMgr Collection <b>'$CollectionName'</b>
    <br><br>
    <h5>Members of '$SoftwareUpdateGroupName'</h5>
    $members
    <br><br><hr noshade size=3 width="100%">
    <small>This automated report is scheduled using Orchestrator and ran on $env:computername at $((get-date).ToString())</small>
    </body>
    </html>
"@
        Send-MailMessage -From "Windows Update Deployment<youremail@yourdomain>" -To "Recipient <recipientemail@yourdomain>"`
        -Subject "New Software Updates are available" -Smtpserver yoursmtpserver -body $HTMLmessage -BodyAsHtml }
    Catch{
        $_.Exception.Message }
    } #END If ($SUPDeployment.Enabled = $false)
}
#endregion
Automate-CMSoftwareUpdateDeployment -CollectionName 'Patch Test Group - Servers' -SoftwareUpdateGroupName "Server Updates - Pilot Rollout Patches" -DeploymentName "Server Updates - Pilot Rollout Patches" -TargetCollectionID "BE100118"
Automate-CMSoftwareUpdateDeployment -CollectionName 'Patch Acceptance Group - Servers' -SoftwareUpdateGroupName "Server Updates - Acceptance Rollout Patches" -DeploymentName "Server Updates - Acceptance Rollout Patches" -TargetCollectionID "BE100119"
Automate-CMSoftwareUpdateDeployment -CollectionName 'Patch Production Group - Servers' -SoftwareUpdateGroupName "Server Updates - Production Rollout Patches" -DeploymentName "Server Updates - Production Rollout Patches" -TargetCollectionID "BE10011A"
}

I wrote this in a hurry so I might have gone to easily over a few things. If you have any questions or remarks please let me know.

Grts!


My Blog List