Sync Configuration Manager Application Collections in Service Manager

This post is the 11th in a series focused on making common administrative tasks in System Center and Azure available via the Service Manager Self-Service Portal. The Configuration Manager and Operations Manager Connectors pull a lot of information into Service Manager but not everything necessary to manage clients, agents, and other settings. This solution allows for the synchronizing of Configuration Manager Collections, with deployments targeted at them, with Service Manager. Specifically, it provides for the ability to deploy software with Configuration Manager via the Service Manager Self-Service Portal. Note, this will only bring collections into Service Manager with the word "Portal:" in the comment of the collection and I’m calling these collections Application Collections for the purposes of this scenario.

Series

Using the Service Manager Self-Service Portal for Common Tasks in Configuration Manager, Operations Manager, and Azure

Prerequisites

The scenarios were designed using the following

  • System Center Service Manager 2012 R2
    • Self-Service Portal configured and working
    • Active Directory Connector configured and working
    • Configuration Manager Connector configured and working
    • Orchestrator Connector configured and working
  • System Center Configuration Manager 2012 R2
    • Discovery configured and working
  • System Center Orchestrator 2012 R2
    • SC 2012 Configuration Manager Integration Pack configured and working
    • SC 2012 Service Manager Integration Pack configured and working
    • Configuration Manager Console installed on runbook servers (open the console, make sure you can connect to your site server)
    • Service Manager Console installed on runbook servers
    • Runbook servers configured to allow PowerShell scripts to run

Create a service account or use the one created in the previous blog post

  1. Give the account admin rights to Service Manager
  2. Give the account admin rights to Configuration Manager

Create a share to store scripts and logs or use the one created in the previous blog post

  1. Create a share that the service account you created and authenticated users will have access to on the Runbook Servers that will be used for this scenario.
  2. In the share, create a folder called "Automation" and give the service account access to it.
  3. Copy SyncApplicationCollections.ps1 into the Automation Folder
  4. In the share, create a sub-folder called "Logs" in the Automation Folder and give the applicable administrators access to it. Orchestrator will write logs to this folder and admins can use these logs for troubleshooting.
  5. In the Logs folder, create a sub-folder called "SRLogs" and give authenticated users access to it. Users of the Service Manager Portal will use these to see the status of the Collection Sync task so they will need rights to this folder.
 param
(
  [Parameter(Mandatory=$true)]
  $CMSiteCode,
  [Parameter(Mandatory=$true)]
  $CMSiteServer,
  [Parameter(Mandatory=$true)]
  $SMManagementServer,
  [Parameter(Mandatory=$true)]
  $VerboseLogging,
  [Parameter(Mandatory=$false)]
  $Collection,
  [Parameter(Mandatory=$false)]
  $ServiceRequest
)

#Functions
function LogIt
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath $Global:LogFile
    $Global:LogBuffer = $Global:LogBuffer + $toLog + "`r`n"
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath $Global:LogFile
    $Global:LogBuffer = $Global:LogBuffer + $toLog + "`r`n"
    Write-Host $message
  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }
}

function CreateServiceRequestLog
{
  param($serviceRequest, $srLogPath)

  LogIt -message ("Full Log File Path:" + $Global:LogFile) -component "Main()" -type 1
  if ($serviceRequest)
  {
    $srLog = Join-Path $srLogPath ("Logs\SRLogs\" + $serviceRequest + ".log")
    LogIt -message ("Service Request Log File Path:" + $srLog) -component "Main()" -type 1
    $Global:LogBuffer | Out-File -Append -Encoding UTF8 -FilePath $srLog
  }
}

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
}

function GetCMSiteConnection
{
  param ($siteCode, $siteServer)
  try { $CMModulePath = Join-Path -Path (Split-Path -Path "${Env:SMS_ADMIN_UI_PATH}" -ErrorAction Stop) -ChildPath "ConfigurationManager.psd1" }
  catch 
  { 
    LogIt -message ("Cannot get path to CM console, will try default path: " + $_.Exception.Message) -component "GetCMSiteConnection()" -type 4
    LogIt -message ("Trying static path: C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1") -component "GetCMSiteConnection()" -type 4
    $CMModulePath = 'C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1'
  }
  Import-Module $CMModulePath -ErrorAction Stop
  try { $CMProvider = Get-PSDrive -PSProvider 'CMSite' -Name $siteCode -ErrorAction Stop }
  catch
  {
    LogIt -message ("Cannot connect to CM site by Site Code, will retry: " + $siteCode + " Error: " + $_.Exception.Message) -component "GetCMSiteConnection()" -type 4
    try { $CMProvider = New-PSDrive -PSProvider 'AdminUI.PS.Provider\CMSite' -Name $siteCode -Root $siteServer -Description 'SCCM Site' -ErrorAction Stop}
    catch
    {
      LogIt -message ("Cannot connect to CM site by Server Name, exiting: " + $siteServer + " Error: " + $_.Exception.Message) -component "GetCMSiteConnection()" -type 3
      exit
    }
  }
  LogIt -message ("Connected to CM Site: " + $siteCode) -component 'GetCMSiteConnection()' -type 1 
  CD "$($CMProvider.SiteCode):\"
  return $CMProvider
}

function GetSMManagementGroupConnection
{
  param ($computerName)
  $smDir = (Get-ItemProperty 'hklm:/software/microsoft/System Center/2010/Service Manager/Setup').InstallDirectory
  try { Import-Module ($smDir + "\Powershell\System.Center.Service.Manager.psd1") -ErrorAction Stop }
  catch
  {
    LogIt -message ("Cannot import SM PowerShell module, Error: " + $_.Exception.Message) -component "GetSMManagementGroupConnection()" -type 3
  }
  try { $SM = New-SCManagementGroupConnection -computerName $computerName -ErrorAction Stop }
  catch
  {
    LogIt -message ("Cannot connect to SM management group: " + $computerName + " Error: " + $_.Exception.Message) -component "GetSMManagementGroupConnection()" -type 3
    exit
  }
  LogIt -message ("Connected to SM management group: " + $computerName) -component "GetSMManagementGroupConnection()" -type 1
  return $SM
}

function GetSCClass
{
  param($name)

  try { $scClass = Get-SCClass -Name $name }
  catch { LogIt -message ("Cannot get SM class: " + $name) -component "GetSCClass()" -type 3 }

  LogIt -message ("Retrieved SM class: " + $name) -component "GetSCClass()" -type 4
  return $scClass
}

function GetSCClassInstance
{
  param($class, $filter)

  if ($filter)
  {
    try { $scClassInstance = Get-SCClassInstance -Class $class -Filter $filter }
    catch { LogIt -message ("Cannot get SM class instance: " + $class + " with filter: " + $filter) -component "GetSCClassInstance()" -type 3 }
  }
  else
  {
    try { $scClassInstance = Get-SCClassInstance -Class $class }
    catch { LogIt -message ("Cannot get SM class instance: " + $class) -component "GetSCClassInstance()" -type 3 }
  }
  LogIt -message ("Retrieved SM class instance of type: " + $class.Name) -component "GetSCClassInstance()" -type 4
  return $scClassInstance
}

function GetSCRelationship
{
  param($name)

  try { $scRelationship = Get-SCRelationship -Name $name }
  catch { LogIt -message ("Cannot get SM relationship: " + $name) -component "GetSCRelationship()" -type 3 }

  LogIt -message ("Retrieved SM relationship: " + $name) -component "GetSCRelationship()" -type 4
  return $scRelationship
}

function GetSCRelationshipInstance
{
  param($sourceInstance, $targetInstance)

  if ($sourceInstance)
  {
    try { $scRelationshipInstance = Get-SCRelationshipInstance -SourceInstance $sourceInstance }
    catch { LogIt -message ("Cannot get source SM relationship instance") -component "GetSCRelationshipInstance()" -type 3 }
  }
  else
  {
    try { $scRelationshipInstance = Get-SCRelationshipInstance -TargetInstance $targetInstance }
    catch { LogIt -message ("Cannot get target SM relationship instance") -component "GetSCRelationshipInstance()" -type 3 }
  }
  LogIt -message ("Retrieved SM relationship instance") -component "GetSCRelationshipInstance()" -type 4
  return $scRelationshipInstance
}

function NewSCRuleRelationshipInstance
{
  param ($relationshipClass, $source, $target)
  try { New-SCRelationshipInstance -RelationshipClass $relationshipClass -Source $source -Target $target -ErrorAction Stop }
  catch { LogIt -message ($target.DisplayName + " cannot be added to " + $source.CollectionID) -component "NewSCRuleRelationshipInstance()" -type 3 }
  LogIt -message ($target.DisplayName + " added to " + $source.CollectionID) -component "NewSCRuleRelationshipInstance()" -type 1
}

function RemoveSCRuleRelationshipInstance
{
  param ($relationship, $collection, $rule)

  try { Remove-SCRelationshipInstance -Instance $relationship -ErrorAction Stop }
  catch { LogIt -message ($rule.CollectionID + " cannot be removed from " + $collection.CollectionID) -component "RemoveSCRuleRelationshipInstance()" -type 3 }
  LogIt -message ($rule.CollectionID + " removed from " + $collection.CollectionID) -component "RemoveSCRuleRelationshipInstance()" -type 1
}

function GetCMDeviceCollections
{
  $ret = @()
  try { $cmDeviceCollections = Get-CMDeviceCollection -ErrorAction Stop }
  catch 
  { 
    LogIt -message ("Cannot get CM device collections") -component "GetCMDeviceCollections()" -type 3
    exit
  }
  LogIt -message ("Retrieved CM device collections") -component "GetCMDeviceCollections()" -type 4

  #Filter on only application collections
  foreach ($cmDeviceCollection in $cmDeviceCollections)
  {
    if($cmDeviceCollection.Comment -like 'Portal:*')
    {
      $ret+= $cmDeviceCollection
    }
  }
  return $ret
}

function GetCMUserCollections
{
  $ret = @()
  try { $cmUserCollections = Get-CMUserCollection -ErrorAction Stop }
  catch 
  { 
    LogIt -message ("Cannot get CM user collections") -component "GetCMUserCollections()" -type 3
    exit
  }
  LogIt -message ("Retrieved CM user collections") -component "GetCMUserCollections()" -type 4
  
  #Filter on only application collections
  foreach ($cmUserCollection in $cmUserCollections)
  {
    if($cmUserCollection.Comment -like 'Portal:*')
    {
      $ret+= $cmUserCollection
    }
  }
  return $ret
}

function GetSMCollections
{
  $smCollectionDefinition = GetSCClass -name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo'
  $smCollections = GetSCClassInstance -class $smCollectionDefinition
  LogIt -message ("Retrieved SM Collections") -component "GetSMCollections()" -type 4
  return $smCollections
}

function GetSMCollection
{
  param($collectionID)

  $smCollectionDefinition = GetSCClass -name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo'
  $smCollection = GetSCClassInstance -class $smCollectionDefinition -filter ('CollectionID -eq {0}' -f $collectionID)
  LogIt -message ("Retrieved SM Collection: " + $collectionID) -component "GetSMCollection()" -type 4
  return $smCollection
}

function GetCMDeviceCollection
{
  param($collectionID)
  
  try { $collection = Get-CMDeviceCollection -CollectionID $collectionID -ErrorAction Stop }
  catch { LogIt -message ("Cannot get CM Device Collection: " + $collectionID) -component "GetCMDeviceCollection" -type 3 }
  return $collection
}

function GetCMUserCollection
{
  param($collectionID)

  try { $collection = Get-CMUserCollection -CollectionID $collectionID }
  catch { LogIt -message ("Cannot get CM User Collection: " + $collectionID) -component "GetCMUserCollection()" -type 3 }
  return $collection
}

function GetCMDeviceCollectionByName
{
  param($collectionName)
  
  try { $collection = Get-CMDeviceCollection -Name $collectionName -ErrorAction Stop }
  catch { LogIt -message ("Cannot get CM Device Collection: " + $collectionName) -component "GetCMDeviceCollectionByName" -type 3 }
  return $collection
}

function GetCMUserCollectionByName
{
  param($collectionName)

  try { $collection = Get-CMUserCollection -Name $collectionName }
  catch { LogIt -message ("Cannot get CM User Collection: " + $collectionName) -component "GetCMUserCollectionByName()" -type 3 }
  return $collection
}

function GetCollectionType
{
  param($collectionID)
  
  $collection = GetCMDeviceCollection -collectionID $collectionID
  if ($collection) { $collectionType = 'DEVICE' }
  else 
  {
    $collection = GetCMUserCollection -collectionID $collectionID
    if ($collection) { $collectionType = 'USER' }
    else { $collectionType = 'NEW'  }
  }
  LogIt -message ("Retrieved CM Collection: " + $collectionID + " of type: " + $collectionType) -component "GetCollectionType()" -type 4
  return $collectionType
}

function GetCollectionTypeByName
{
  param($collectionName)
  
  $collection = GetCMDeviceCollectionByName -collectionName $collectionName
  if ($collection) { $collectionType = 'DEVICE' }
  else 
  {
    $collection = GetCMUserCollectionByName -collectionName $collectionName
    if ($collection) { $collectionType = 'USER' }
    else { $collectionType = 'NEW'  }
  }
  LogIt -message ("Retrieved CM Collection: " + $collectionName + " of type: " + $collectionType) -component "GetCollectionTypeByName()" -type 4
  return $collectionType
}

function GetCollectionID
{
  param($collectionName)

  $type = GetCollectionTypeByName -collectionName $collectionName
  if ($type -eq 'DEVICE')
  {
    $collection = GetCMDeviceCollectionByName -collectionName $collectionName
  }
  else
  {
    $collection = GetCMUserCollectionByName -collectionName $collectionName
  }
  return $collection.CollectionID
}

function GetCollIDs
{
  param ($smCollections)

  $collIDs = @{}
  $collIDs.Add(0,0)
  foreach ($smCollection in $smCollections)
  {
    try {$collIDs.Add($smCollection.CollID, $smCollection.CollID)} catch {}
  }
  LogIt -message ("Retrieved SM Collection Ids") -component "GetCollIDs()" -type 4
  return $collIDs
}

function GetNextCollID
{
  $collIDs = $Global:CollIDs.GetEnumerator() | Sort-Object Name
  foreach ($id in $collIDs) { $i = $id.Key }
  $i++
  $Global:CollIDs.Add($i, $i)
  LogIt -message ("Retrieved Next Collection ID: " + $i) -component "GetNextCollID()" -type 4
  return $i
}

function UpdateCollections
{
  param ($cmDeviceCollections, $cmUserCollections, $smCollections)

  LogIt -message ("Entering UpdateCollections()") -component "UpdateCollections()" -type 4
  $htCM = @{}
  $htSM = @{}
  $htToUpdate = @{}

  #Put CM device collections in hash table
  foreach ($cmDeviceCollection in $cmDeviceCollections)
  { $htCM.Add($cmDeviceCollection.CollectionID, $cmDeviceCollection.Name) }

  #Add CM user collections to hash table
  foreach ($cmUserCollection in $cmUserCollections)
  { $htCM.Add($cmUserCollection.CollectionID, $cmUserCollection.Name) }

  #Put SM collections in hash table
  foreach ($smCollection in $smCollections)
  { $htSM.Add($smCollection.CollectionID, $smCollection.Name) }

  #Find collection to add
  foreach ($cmColl in $htCM.GetEnumerator())
  {
    if (!($htSM.ContainsKey($cmColl.Key))) { $htToUpdate.Add($cmColl.Key, $cmColl.Value) }
  }

  #Find collections to remove
  foreach ($smColl in $htSM.GetEnumerator())
  {
    if (!($htCM.ContainsKey($smColl.Key))) { $htToUpdate.Add($smColl.Key, 'REMOVE') }
  }

  #Update collections
  foreach ($collection in $htToUpdate.GetEnumerator())
  {
    if ($collection.Value -eq 'REMOVE') { DeleteCollection -collectionID $collection.Key }
    else { CreateCollection -collectionID $collection.Key -collectionName $collection.Value }
  }
  LogIt -message ("Leaving UpdateCollections()") -component "UpdateCollections()" -type 4
}

function CreateCollection
{
  param($collectionID, $collectionName)

  #Populate Collection
  $ht = @{}
  $ht.Add('Name', $collectionName)
  $ht.Add('DisplayName', $collectionName)
  $ht.Add('MemberCount', 0)
  $ht.Add('CollectionID', $collectionID)
  $ht.Add('CollID', (GetNextCollID))
 
  #Add Collection Instance
  $collection = GetSCClass -name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo'
  try { New-SCClassInstance -Class $collection -Property $ht -ErrorAction Stop}
  catch { LogIt -message ("Cannot create new SM Collection: " + $collectionName + "|" + $collectionID) -component "CreateCollection()" -type 3 }
  LogIt -message ("SM Collection Created: " + $collectionName + "|" + $collectionID) -component "CreateCollection()" -type 1
}

function DeleteCollection
{
  param($collectionID)

  $collectionDefinition = GetSCClass -name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo'
  try { Remove-SCClassInstance -Instance (GetSCClassInstance -class $collectionDefinition -filter ('CollectionID -eq {0}' -f $collectionID)) -ErrorAction Stop }
  catch { LogIt -message ("Cannot delete SM Collection: " + $collectionID) -component "DeleteCollection()" -type 3 }
  LogIt -message ("SM Collection Deleted: " + $collectionID) -component "DeleteCollection()" -type 1
}

function UpdateCollectionMembers
{
  param($collection, $cmDeviceCollections, $cmUserCollections)

  if ($collection)
  {
    UpdateSingleCollection -collectionID (GetCollectionID -collectionName $collection)
  }
  else
  {
    FullUpdate -smCollections (GetSMCollections) -cmDeviceCollections $cmDeviceCollections -cmUserCollections $cmUserCollections
  }
}

function UpdateSingleCollection
{
  param($collectionID)

  LogIt -message "Entering UpdateSingleCollection()" -component "UpdateSingleCollection()" -type 4
  $htToUpdate = @{ $collectionID = $collectionID }
  LogIt -message "Starting Single Collection Sync" -component "UpdateSingleCollection()" -type 1
  SyncCollections -collectionList $htToUpdate
  LogIt -message "Done with Single Collection Sync" -component "UpdateSingleCollection()" -type 1
}

function FullUpdate
{
  param ($cmDeviceCollections, $cmUserCollections, $smCollections)

  LogIt -message "Entering FullUpdate()" -component "FullUpdate()" -type 4
  $htCM = @{}

  #Put CM device collections in hash table
  foreach ($cmDeviceCollection in $cmDeviceCollections)
  { $htCM.Add($cmDeviceCollection.CollectionID, $cmDeviceCollection.CollectionID) }

  #Add CM user collections to hash table
  foreach ($cmUserCollection in $cmUserCollections)
  { $htCM.Add($cmUserCollection.CollectionID, $cmUserCollection.CollectionID) }

  #Update collections
  LogIt -message "Starting Full Sync" -component "FullUpdate()" -type 1
  SyncCollections -collectionList $htCM
  LogIt -message "Done with Full Sync" -component "FullUpdate()" -type 1
}

function SyncCollections
{
  param($collectionList)
  
  LogIt -message ("Entering SyncCollections()") -component "SyncCollections()" -type 4
  
  #Get SM Definitions
  $collectionDefinition = GetSCClass -Name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo'
  $relationshipDefinition = GetSCRelationship -name 'Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionHasConfigItem'

  #Get all SM Collections
  $htAllSMCollections = @{}
  $collectionInstances = GetSCClassInstance -class (GetSCClass -Name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo')
  foreach ($collectionInstance in $collectionInstances)
  { try { $htAllSMCollections.Add($collectionInstance.CollectionID, $collectionInstance.Name) } catch {} }

  #Loop through collections
  foreach ($collection in $collectionList.GetEnumerator())
  {
    if ((GetCollectionType -collectionID $collection.Key) -eq 'DEVICE')
    { $col = GetCMDeviceCollection -collectionID $collection.Key }
    else
    { $col = GetCMUserCollection -collectionID $collection.Key }

    #Get CM Collection Rules
    $htCMCollectionRules = @{}
    foreach ($rule in $col.CollectionRules)
    {
      if ($rule.IncludeCollectionID)
      { try { $htCMCollectionRules.Add($rule.IncludeCollectionID, $rule.RuleName) } catch {} }
    }

    #Get SM Collection Rules
    $htSMCollectionRules = @{}
    $smCollectionInstance = GetSCClassInstance -class $collectionDefinition -filter ('CollectionID -eq "{0}"' -f $collection.Key)
    $smRelationships = GetSCRelationshipInstance -sourceInstance $smCollectionInstance
    foreach ($smRelationship in $smRelationships)
    { 
      if (!($smRelationship.IsDeleted))
      {
        $key = $smRelationship.TargetObject.Values[2].ToString().ToUpper()
        $value = $smRelationship.TargetObject.Values[2]
        $htSMCollectionRules.Add($key, $value)
      }
    }

    #See what members need to be added to SM collection
    foreach ($cmCollectionRule in $htCMCollectionRules.GetEnumerator())
    {
      if (!($htSMCollectionRules.Contains($cmCollectionRule.Key)))
      {
        if ($htAllSMCollections.Contains($cmCollectionRule.Key))
        {
          $filter = 'CollectionID -eq "{0}"' -f $cmCollectionRule.Key.ToString()
          $smCollectionRule = GetSCClassInstance -class (GetSCClass -Name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo') -filter $filter
          if ($smCollectionRule.CollectionID -eq $cmCollectionRule.Key.ToString())
          { AddRuleToCollection -relationship $relationshipDefinition -collection (GetSMCollection($collection.Key)) -rule $smCollectionRule }
        }
        else { LogIt -message ($cmCollectionRule.Key.ToString().Split(".")[0] + " was not found in SM and cannot be added to SM collection: " + $collection.Key) -component "SyncCollections()" -type 2 }
      }
    }
      
    #See what members need to be removed from SM collection
    foreach ($smCollectionRule in $htSMCollectionRules.GetEnumerator())
    {
      if (!($htCMCollectionRules.Contains($smCollectionRule.Key)))
      {
        $filter = 'CollectionID -eq "{0}"' -f $smCollectionRule.Key.ToString()
        $smCollection = GetSCClassInstance -class (GetSCClass -Name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo') -filter $filter
        if ($smCollection.CollectionID -eq $smCollectionRule.Key.ToString())
        {
          RemoveRuleFromCollection -relationship (GetSCRelationshipInstance -targetInstance $smCollection) -collection (GetSMCollection($collection.Key)) -rule $smCollection
        }
      }
    }
  }
  LogIt -message ("Leaving SyncCollections()") -component "SyncCollections()" -type 4
}

function AddRuleToCollection
{
  param ($relationship, $collection, $rule)
  NewSCRuleRelationshipInstance -relationshipClass $relationship -source $collection -target $rule
}

function RemoveRuleFromCollection
{
  param ($relationship, $collection, $rule)
  foreach ($rel in $relationship)
  {
    if ((!($rel.IsDeleted)) -and ($rel.SourceObject.Values[3].ToString() -eq $collection.CollectionID.ToString()))
    {
      RemoveSCRuleRelationshipInstance -relationship $rel -collection $collection -rule $rule
    }
  }
}

function AddUserToCollection
{
  param ($relationship, $collection, $user)
  NewSCUserRelationshipInstance -relationshipClass $relationship -source $collection -target $user
}

function RemoveUserFromCollection
{
  param ($relationship, $collection, $user)
  foreach ($rel in $relationship)
  {
    if ((!($rel.IsDeleted)) -and ($rel.SourceObject.Values[2].ToString() -eq $collection.CollectionID.ToString()))
    {
      RemoveSCUserRelationshipInstance -relationship $rel -collection $collection -user $user
    }
  }
}

#Main
$Version = "1.0"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
$Global:LogFile = Join-Path (GetScriptDirectory) 'Logs\SyncApplicationCollections.log'
$Global:ScriptName = 'SyncApplicationCollections.ps1'
$Global:LogBuffer = ''
$Global:ScriptStatus = 'Success'
LogIt -message ("Sync Application Collections Script v{0}" -f $Version) -type 1 -component "Main()"

#Connect to CM and SM
$CM = GetCMSiteConnection -siteCode $CMSiteCode -siteServer $CMSiteServer
$SM = GetSMManagementGroupConnection -computerName $SMManagementServer

#Get Collections from CM and SM
$CMDeviceCollections = GetCMDeviceCollections
$CMUserCollections = GetCMUserCollections
$SMCollections = GetSMCollections

#Get CollIDs in SM
$Global:CollIDs = GetCollIDs -smCollections $SMCollections

#Update Collections in SM
UpdateCollections -smCollections $SMCollections -cmDeviceCollections $CMDeviceCollections -cmUserCollections $CMUserCollections

#Update Collection Members in SM
UpdateCollectionMembers -collection $Collection -cmDeviceCollections $CMDeviceCollections -cmUserCollections $CMUserCollections

#Log Result
$Ret = $Global:ScriptStatus
LogIt -message ("Script Complete, Result: {0}" -f $Ret) -component "Main()" -type 1

#Create SR Log if needed
CreateServiceRequestLog -serviceRequest $ServiceRequest -srLogPath (GetScriptDirectory)

Import the Configuration Manager Application Collections Management Pack into Service Manager

  1. Open the Service Manager Console
  2. Select Administration
  3. Right-Click Management Packs and select Import
  4. Select the Custom.Example.DataCenter.Automation.ApplicationCollections.mp management pack and choose Open, Import, and OK
 <?xml version="1.0" encoding="utf-8"?>
<ManagementPack SchemaVersion="2.0" ContentReadable="true" xmlns:xsd="https://www.w3.org/2001/XMLSchema">
  <Manifest>
    <Identity>
      <ID>Custom.Example.DataCenter.Automation.ApplicationCollections</ID>
      <Version>1.0.0.0</Version>
    </Identity>
    <Name>Configuration Manager Application Collections</Name>
    <References>
      <Reference Alias="System">
        <ID>System.Library</ID>
        <Version>7.5.8501.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
    </References>
  </Manifest>
  <TypeDefinitions>
    <EntityTypes>
      <ClassTypes>
        <ClassType ID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" Accessibility="Public" Base="System!System.ConfigItem">
          <Property ID="Name" Type="string" />
          <Property ID="Description" Type="string" />
          <Property ID="MemberCount" Type="int" />
          <Property ID="CollectionID" Type="string" />
          <Property ID="CollID" Type="int" Key="true" />
        </ClassType>
      </ClassTypes>
      <RelationshipTypes>
        <RelationshipType ID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionHasConfigItem" Accessibility="Public" Abstract="false" Base="System!System.Reference">
          <Source ID="Collection" Type="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" />
          <Target ID="ConfigItem" Type="System!System.ConfigItem" />
        </RelationshipType>
      </RelationshipTypes>
      <TypeProjections>
        <TypeProjection ID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionProjection" Accessibility="Public" Type="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo">
          <Component Path="$Target/Path[Relationship='Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionHasConfigItem']$" Alias="ApplicationCollectionProjection" />
        </TypeProjection>
      </TypeProjections>
    </EntityTypes>
  </TypeDefinitions>
  <LanguagePacks>
    <LanguagePack ID="ENU" IsDefault="true">
      <DisplayStrings>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections">
          <Name>Configuration Manager Application Collections</Name>
          <Description>Configuration Manager Application Collection Management Pack</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo">
          <Name>Application Collection Info</Name>
          <Description>Application Collection Info</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" SubElementID="Name">
          <Name>Name</Name>
          <Description>Name</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" SubElementID="Description">
          <Name>Description</Name>
          <Description>Description</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" SubElementID="MemberCount">
          <Name>Member Count</Name>
          <Description>Member Count</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" SubElementID="CollectionID">
          <Name>Collection ID</Name>
          <Description>Collection ID</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionInfo" SubElementID="CollID">
          <Name>Coll ID</Name>
          <Description>Coll ID</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionProjection">
          <Name>Application Collection Projection</Name>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.ApplicationCollections.ApplicationCollectionHasConfigItem">
          <Name>Application Collection has configuration item</Name>
          <Description>Relationship between a application collection and the collections that it contains</Description>
        </DisplayString>
      </DisplayStrings>
      <KnowledgeArticles></KnowledgeArticles>
    </LanguagePack>
  </LanguagePacks>
</ManagementPack>

Create the Sync Application Collection (On-Demand) Runbook

This Runbook will launch a PowerShell script when it is triggered via the Service Manager Self-Service Portal.

  1. Open the Orchestrator Runbook Designer

  2. Create a new runbook

  3. Drag the "Runbook Control\Initialize Data" activity into the new runbook

  4. Configure two parameters on the "Initialize Data" activity and click Finish

    • ServiceRequest (data type: String)
    • CollectionName (data type: String)
  5. Drag the "System\Run Program" activity into the new runbook

  6. Link the two activities

  7. Configure the Security of "System\Run Program" to use the service account

  8. Configure three settings under the "Details" section of the "Run Program" activity and click finish

    • Program path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
    • Parameters: -File <local sharepath>\Automation\SyncApplicationCollections.ps1 -CMSiteCode CAS -CMSiteServer sccm2012r2cas.contoso.com -SMManagementServer sm2012r2.contoso.com -VerboseLogging false -Collection "{CollectionName from "Initialize Data"}" -ServiceRequest {ServiceRequest from "Initialize Data"}
    • Working Folder: <local sharepath>\Automatio
    • Note, the data between the curly braces are Published Data from the Data Bus. This is obtained by right-clicking on the white space and selecting the appropriate variable. You can also use Orchestrator variables configured under Global Settings for items such as CMSiteCode, CMSiteServer, and SMManagementServer.
  9. Check In the Runbook

  10. The Runbook should look like this:

clip_image001

Create the Sync Application Collections (Scheduled) Runbook

This Runbook will launch a PowerShell script when it is triggered via a schedule. This will catch Collections and Collection modifications that have occurred outside of the Service Manager Self-Service Portal.

  1. Open the Orchestrator Runbook Designer

  2. Create a new runbook

  3. Drag the "Scheduling\Monitor Date/Time" activity into the new runbook

  4. Configure one setting under Details of the Monitor Date/Time activity and click Finish

    • Every: 1 hour
  5. Drag the "System\Run Program" activity into the new runbook

  6. Link the two activities

  7. Configure the Security of "System\Run Program" to use the service account

  8. Configure three settings under the "Details" section of the "Run Program" activity and click finish

    • Program path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
    • Parameters: -File <sharepath>\Automation\SyncApplicationCollections.ps1 -CMSiteCode CAS -CMSiteServer sccm2012r2cas.contoso.com -SMManagementServer sm2012r2.contoso.com -VerboseLogging false
    • Working Folder: <sharepath>\Automation
    • You can also use Orchestrator variables configured under Global Settings for items such as CMSiteCode, CMSiteServer, and SMManagementServer.
  9. Check in and Start this runbook.

  10. The Runbook should look like this:

clip_image002

Test Solution

  1. Create a new device collection in Configuration Manager
  2. Put "Portal:" without quotes into the comment of the collection
  3. Within an hour it should synchronize with Service Manager and be viewable by searching

clip_image003

Continue to the 12th post in this series: Deploying Software from Configuration Manager using the Service Manager Self-Service Portal

SyncApplicationCollections.zip