Sync Configuration Manager Collections in Service Manager

This post is the 3rd in a series of posts focused on making common administrative tasks in System Center and Azure available via the Service Manager Self-Service Portal. The Configuration Manager Connector pulls a lot of information into Service Manager but not everything necessary to manage collections and collection membership. This solution allows for the synchronizing of Configuration Manager Collections and Collection Members into Service Manager on a schedule and on-demand.

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)
    • Operations Manager Console installed on runbook servers
    • Service Manager Console installed on runbook servers
    • Runbook servers configured to allow PowerShell scripts to run

Create a service account

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

Create a share to store scripts and logs

  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 SyncCollections.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=$true)]
  $FullUpdate,
  [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 NewSCDeviceRelationshipInstance
{
  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 "NewSCDeviceRelationshipInstance()" -type 3 }
  LogIt -message ($target.DisplayName + " added to " + $source.CollectionID) -component "NewSCDeviceRelationshipInstance()" -type 1
}

function RemoveSCDeviceRelationshipInstance
{
  param ($relationship, $collection, $device)

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

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

  Write-Host "ADD"$collection.CollectionID"|"$user.UserName
  New-SCRelationshipInstance -RelationshipClass $relationship -Source $collection -Target $user
}

function RemoveSCUserRelationshipInstance
{
  param ($relationship, $collection, $user)

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

function GetCMDeviceCollections
{
  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
  return $cmDeviceCollections
}

function GetCMUserCollections
{
  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
  return $cmUserCollections
}

function GetSMCollections
{
  $smCollectionDefinition = GetSCClass -name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo'
  $smCollections = GetSCClassInstance -class $smCollectionDefinition
  LogIt -message ("Retrieved SM Collections") -component "GetSMCollections()" -type 4
  return $smCollections
}

function GetSMCollection
{
  param($collectionID)

  $smCollectionDefinition = GetSCClass -name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo'
  $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 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo'
  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 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo'
  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($fullUpdate, $collection, $cmDeviceCollections, $cmUserCollections)

  if ($collection)
  {
    UpdateSingleCollection -collectionID (GetCollectionID -collectionName $collection)
  }
  else
  {
    if ($fullUpdate) { FullUpdate -smCollections (GetSMCollections) -cmDeviceCollections $cmDeviceCollections -cmUserCollections $cmUserCollections }
    else { DeltaUpdate -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 DeltaUpdate
{
  param ($cmDeviceCollections, $cmUserCollections, $smCollections)

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

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

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

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

  #Find collections with mismatched MemberCount
  foreach ($collection in $htCM.GetEnumerator())
  {
    if ($collection.Value -ne ($htSM.Get_Item($collection.Key)).Value)
    {
      $htToUpdate.Add($collection.Key, $collection.Key)
    }
  }

  #Update collections
  LogIt -message "Starting Delta Sync" -component "DeltaUpdate()" -type 1
  SyncCollections -collectionList $htToUpdate
  LogIt -message "Done with Delta Sync" -component "DeltaUpdate()" -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 UpdateMemberCounts
{
  LogIt -message ("Updating SM Collection Member Counts") -component "UpdateMemberCounts()" -type 1
  foreach ($collection in (GetSMCollections))
  {
    try { $count = $collection.GetRelatedObjectsWhereSource((GetSCRelationship -name 'Microsoft.SystemCenter.ConfigurationManager.CollectionHasConfigItem')).Count }
    catch { LogIt -message ("Cannot get related objects for " + $collection.CollectionID) -component "UpdateMemberCounts()" -type 3 }
    $collection.MemberCount = $count
    try { Update-SCClassInstance -Instance $collection -ErrorAction Stop }
    catch { LogIt -message ("Cannot update SM collection member count for collection: " + $collection.CollectionID + " to member count of: " + $count) -component "UpdateMemberCounts()" -type 3 }
    LogIt -message ("Updated member count for collection " + $collection.CollectionID + " to count of " + $count) -component "UpdateMemberCounts()" -type 4
  }
  LogIt -message ("Done Updating SM Collection Member Counts") -component "UpdateMemberCounts()" -type 1
}

function SyncCollections
{
  param($collectionList)
  
  LogIt -message ("Entering SyncCollections()") -component "SyncCollections()" -type 4
  #Get SM Definitions
  $collectionDefinition = GetSCClass -Name 'Microsoft.SystemCenter.ConfigurationManager.CollectionInfo'
  $relationshipDefinition = GetSCRelationship -name 'Microsoft.SystemCenter.ConfigurationManager.CollectionHasConfigItem'
  $deviceDefinition = GetSCClass -name 'Microsoft.Windows.Computer'
  $userDefinition = GetSCClass -name 'Microsoft.AD.User'

  #Put all SM devices in hash table
  $htAllSMDevices = @{}
  $deviceInstances = GetSCClassInstance -class $deviceDefinition
  foreach ($deviceInstance in $deviceInstances)
  { $htAllSMDevices.Add($deviceInstance.NetbiosComputerName.ToUpper() + "." + $deviceInstance.NetbiosDomainName.ToUpper(), $deviceInstance.NetbiosComputerName + "." + $deviceInstance.NetbiosDomainName) }

  #Put all SM users in hash table
  $htAllSMUsers = @{}
  $userInstances = GetSCClassInstance -class $userDefinition
  foreach ($userInstance in $userInstances)
  { $htAllSMUsers.Add($userInstance.Domain.ToUpper() + "\" + $userInstance.UserName.ToUpper(), $userInstance.Domain + "\" + $userInstance.UserName ) }

  #Loop through collections that have changed
  foreach ($collection in $collectionList.GetEnumerator())
  {
    if ((GetCollectionType -collectionID $collection.Key) -eq 'DEVICE')
    {
      #Put CM collection devices in hash table
      $htCMCollectionDevices = @{}
      $cmDevices = Get-CMDevice -CollectionId ((Get-CMDeviceCollection -CollectionId $collection.Key).CollectionID)
      foreach ($cmDevice in $cmDevices)
      { $htCMCollectionDevices.Add(($cmDevice.Name + "." + $cmDevice.Domain).ToUpper(), $cmDevice.Name + "." + $cmDevice.Domain) }

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

      #See what members need to be added to SM collection
      foreach ($cmCollectionDevice in $htCMCollectionDevices.GetEnumerator())
      {
        if (!($htSMCollectionDevices.Contains($cmCollectionDevice.Key)))
        {
          if ($htAllSMDevices.Contains($cmCollectionDevice.Key))
          {
            $filter = 'NetbiosComputerName -eq "{0}"' -f $cmCollectionDevice.Value.ToString().Split(".")[0]
            $smDevice = GetSCClassInstance -class $deviceDefinition -filter $filter
            if ($smDevice.NetbiosDomainName -eq $cmCollectionDevice.Value.ToString().Split(".")[1])
            {
              AddDeviceToCollection -relationship $relationshipDefinition -collection (GetSMCollection($collection.Key)) -device $smDevice
            }
          }
          else
          {
            LogIt -message ($cmCollectionDevice.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 ($smCollectionDevice in $htSMCollectionDevices.GetEnumerator())
      {
        if (!($htCMCollectionDevices.Contains($smCollectionDevice.Key)))
        {
          $filter = 'NetbiosComputerName -eq "{0}"' -f $smCollectionDevice.Value.ToString().Split(".")[0]
          $smDevice = GetSCClassInstance -class $deviceDefinition -filter $filter
          if ($smDevice.NetbiosDomainName -eq $smCollectionDevice.Value.ToString().Split(".")[1])
          {
            RemoveDeviceFromCollection -relationship (GetSCRelationshipInstance -targetInstance $smDevice) -collection (GetSMCollection($collection.Key)) -device $smDevice
          }
        }
      }
    }
    else
    {
      #Put CM collection users in hash table
      $htCMCollectionUsers = @{}
      $cmUsers = Get-CMUser -CollectionId ((Get-CMUserCollection -CollectionId $collection.Key).CollectionID)
      foreach ($cmUser in $cmUsers)
      { $htCMCollectionUsers.Add($cmUser.SMSID.ToUpper(), $cmUser.SMSID) }

      #Put SM collection users in hash table
      $htSMCollectionUsers = @{}
      $smCollectionInstance = GetSCClassInstance -class $collectionDefinition -filter ('CollectionID -eq "{0}"' -f $collection.Key)
      $smRelationships = GetSCRelationshipInstance -sourceInstance $smCollectionInstance
      foreach ($smRelationship in $smRelationships)
      { 
        if (!($smRelationship.IsDeleted))
        {
          $key = ("{0}\{1}" -f $smRelationship.TargetObject.Values[6].ToString().ToUpper(), $smRelationship.TargetObject.Values[7]).ToUpper()
          $value = ("{0}\{1}" -f $smRelationship.TargetObject.Values[6], $smRelationship.TargetObject.Values[7])
          $htSMCollectionUsers.Add($key, $value)
        }
      }

      #See what members need to be added to SM collection
      foreach ($cmCollectionUser in $htCMCollectionUsers.GetEnumerator())
      {
        if (!($htSMCollectionUsers.Contains($cmCollectionUser.Key)))
        {
          if ($htAllSMUsers.Contains($cmCollectionUser.Key))
          {
            $filter = 'UserName -eq "{0}"' -f $cmCollectionUser.Value.ToString().Split("\")[1]
            $smUser = GetSCClassInstance -class $userDefinition -filter $filter
            AddUserToCollection -relationship $relationshipDefinition -collection (GetSMCollection($collection.Key)) -user $smUser
          }
          else
          {
            LogIt -message ($cmCollectionUser.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 ($smCollectionUser in $htSMCollectionUsers.GetEnumerator())
      {
        if (!($htCMCollectionUsers.Contains($smCollectionUser.Key)))
        {
          $filter = 'UserName -eq "{0}"' -f $smCollectionUser.Value.ToString().Split("\")[1]
          $smUser = GetSCClassInstance -class $UserDefinition -filter $filter
          RemoveUserFromCollection -relationship (GetSCRelationshipInstance -targetInstance $smUser) -collection (GetSMCollection($collection.Key)) -user $smUser
        }
      }
    }
  }
  LogIt -message ("Leaving SyncCollections()") -component "SyncCollections()" -type 4
}

function AddDeviceToCollection
{
  param ($relationship, $collection, $device)
  NewSCDeviceRelationshipInstance -relationshipClass $relationship -source $collection -target $device
}

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

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]$FullUpdate = [System.Convert]::ToBoolean($FullUpdate)
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
$Global:LogFile = Join-Path (GetScriptDirectory) 'Logs\SyncCollections.log'
$Global:ScriptName = 'SyncCollections.ps1'
$Global:LogBuffer = ''
$Global:ScriptStatus = 'Success'
LogIt -message ("Sync 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 -fullUpdate $FullUpdate -collection $Collection -cmDeviceCollections $CMDeviceCollections -cmUserCollections $CMUserCollections

#Update Collection Member Counts in SM
UpdateMemberCounts

#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 Collections Management Pack into Service Manager

This Management Pack contains a Type Projection (aka Combination Class) that allows the members of a collection to be viewable when selecting a collection in the Service Manager Self-Service Portal.

  1. Open the Service Manager Console
  2. Select Administration
  3. Right-Click Management Packs and select Import
  4. Select the Custom.Example.DataCenter.Automation.Collections.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.Collections</ID>
      <Version>1.0.0.0</Version>
    </Identity>
    <Name>DataCenter Automation: Configuration Manager Collections</Name>
    <References>
      <Reference Alias="CM">
        <ID>Microsoft.SystemCenter.ConfigurationManager</ID>
        <Version>7.5.3079.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="System">
        <ID>System.Library</ID>
        <Version>7.5.8501.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
    </References>
  </Manifest>
  <TypeDefinitions>
    <EntityTypes>
      <TypeProjections>
        <TypeProjection ID="Custom.Example.DataCenter.Automation.Collections.CollectionProjection" Accessibility="Public" Type="CM!Microsoft.SystemCenter.ConfigurationManager.CollectionInfo">
          <Component Path="$Target/Path[Relationship='CM!Microsoft.SystemCenter.ConfigurationManager.CollectionHasConfigItem']$" Alias="CollectionProjection" />
        </TypeProjection>
      </TypeProjections>
    </EntityTypes>
  </TypeDefinitions>
  <LanguagePacks>
    <LanguagePack ID="ENU" IsDefault="true">
      <DisplayStrings>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.Collections">
          <Name>DataCenter Automation: Configuration Manager Collections</Name>
          <Description>Management Pack for bringing collection information from SCCM into SM</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Example.DataCenter.Automation.Collections.CollectionProjection">
          <Name>Collection Projection</Name>
        </DisplayString>
      </DisplayStrings>
      <KnowledgeArticles></KnowledgeArticles>
    </LanguagePack>
  </LanguagePacks>
</ManagementPack>

Create the Sync 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\SyncCollections.ps1 -CMSiteCode CAS -CMSiteServer sccm2012r2cas.contoso.com -SMManagementServer sm2012r2.contoso.com -VerboseLogging false -FullUpdate false -Collection "{CollectionName from "Initialize Data"}" -ServiceRequest {ServiceRequest from "Initialize Data"}
    • Working Folder: <local sharepath>\Automation
    • 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 Collections (Full - 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\SyncCollections.ps1 -CMSiteCode CAS -CMSiteServer sccm2012r2cas.contoso.com -SMManagementServer sm2012r2.contoso.com -VerboseLogging false -FullUpdate true
    • 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

 

Summary

The on-demand runbook will be used in a future post for synchronizing Configuration Manager collections with Service Manager, including collection members and member count. The scheduled runbook will start working on the schedule to synchronize collections, collection members, and member counts every hour thereby picking up changes made outside of the Service Manager Portal.

 

Continue to the 4th post in this series: Sync Configuration Manager Client and Operations Manager Agent State in Service Manager

SyncCollections.zip