Ultimately it becomes a choice of trading deployment speed for system performance but with Group Manager it doesn’t have to be this way…

The Challenge:-

The majority of our customers and the environments we work in, leverage an Active Directory Group based deployment method within Configuration Manager.

Specifically this configuration consists of:-

  • A Domain Local or Global Security Group in Active Directory
  • Configuration Manager Active Directory Group, System and / or User Discovery enabled / Delta Discovery enabled.
  • A User or Device Collection with a Query Rule configured for all members of the Active Directory Security Group.
  • A collection schedule of some sort.

When you break this down the following Activities and defaults can be expected:-

  1. Delta Active Directory Group Discovery (Every 5 Minutes)
  2. Collection Evaluation  (See note below)
  3. Machine Policy Update (Every 60 minutes by default)

Note: When considering the use of “Dynamic” collections please refer to this best practice statement:- https://technet.microsoft.com/en-us/library/gg699372.aspx

CM-1

The Limitations:-

How the solution works:-

  1. A PowerShell Script running as a scheduled task on any custom schedule.
  2. Execute a Delta Group modification query for changes to the Active Directory Security Group.
  3. Return any modified Group(s) membership and the associated Configuration Manager Collection ID.
  4. Add or remove Direct Membership rules from the associated Configuration Manager Collection as required.
  5. Where a Resource has been added or removed from a Collection, attempt a remote WMI connection to the relevant Resource to invoke a machine policy refresh and evaluation cycle.
CM-3

The Quick Demonstration:-

The Installation and Configuration:-

The Source:-

# Name:            SCCM 2012 Collection Manager
# Author:        Victor Meyer (Esebenza) | victor.meyer@esebenza.com
# Copyright:    
# Purpose:        Delta AD Group Discovery, SCCM 2012 R2 Collection Update and Target Client Machine Policy Update
#
# Pre-Reqs:        Make sure the AD PowerShell cmdlets are installed (RSAT)
#                Decide which Group attribute will host the corresponding SCCM Collection (Default is Info) and
#                    populate the Collection ID in Active Directory
#
# Permissions:     Domain Users / Authenticated Users - Read Active Drectory Groups
#                Update Collections - Add / remove / read Collections.
#                Remote Enable / Connect From Network - Execute WMI Class.
 
# // Constants
# Core Configuration
[string]$GroupsBaseDN   = "OU=Software Distribution,OU=Groups,DC=lab,DC=local" # Active Directory Group OU DN
[string]$SCCMSiteCode   = "LP1" # Configuration Manager 3 Character Site Code
[string]$SCCMSiteServer = "CFG-1" # Configuration Manager Site Server
# Scheduling Configuration
[int]$CheckInterval        = 6 # Number of Minutes back in time looking for groups changes
[int]$SleepInterval        = 60 # Number of Seconds the script should wait for collection refresh before waking up machines.
# Log Configuration
[string]$DefaultLogFile    = "E:\Group Manager for Configuration Manager 2012\Collection.Manager.LOG" # Path and name of Log File.
[string]$LogLevel        = "VERBOSE" # Accepted Values = NONE | STD | VERBOSE
 
 
# // Functions
Function Log
{
    <# 
    .Synopsis 
        Write to an event Log
    .DESCRIPTION 
        A very simple script logger
    .NOTES
        To Do:
    #>     
    PARAM($LogMsg, $LogType="Information", $LogFile = $DefaultLogFile)
    END
    {
    
        # check the size of the current log file.
        if ((Get-Item $LogFile).length -gt 10240kb) 
            { 
                $NewLogFile = $true 
            } 
        else 
            { 
                $NewLogFile = $false 
            }
    
        # Check whether a new log should be created or existing log appended.
        If($NewLogFile -eq $true -and $LogLevel -ne "NONE")
            {
                # Test Path [Assume the executing account can make a new logpath]
                if((Test-Path $LogFile) -eq $true)
                    {
 
                        # Check and delete the previous OLD file
                        $LogFileOld = $LogFile.Replace("LOG","OLD")
                        if((Test-Path $LogFileOld) -eq $true)
                            {
                                Remove-Item $LogFileOld -force
                            }
                        else
                            {
                                # Rename the old log file
                                Rename-Item $LogFile "Collection.Manager.OLD"
                            }
                            
                        # Write the Event Message
                        $Date = Get-Date
                        Add-Content $LogFile "$Date :: $LogType :: $LogMsg"    
                    }
                else
                    {
                        # Create a new log file
                        New-Item $LogFile -type file -force
                        
                        # Write the Event Message
                        $Date = Get-Date
                        Add-Content $LogFile "$Date :: $LogType :: $LogMsg"    
                    }
            }
        else
            {
                # Write the Event Message
                $Date = Get-Date
                Add-Content $LogFile "$Date :: $LogType :: $LogMsg"                            
            }
    }
}
Function Get-ChangedGroups
{
    <# 
    .Synopsis 
        Retrieves all groups with newer than the current date minus the check interval
    .DESCRIPTION 
        Check for all group changes that have occured since the Last Run date Under the base DN
    .NOTES
        To Do:
    #>     
    PARAM($BaseDN = $GroupsBaseDN)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 1 - Get-ChangedGroups ($BaseDN) Function has been invoked"
            }
 
        # Ensure correct data type for AD filter
        $Date = ((get-date).addminutes(-$CheckInterval))
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - The following Filter has been used(whenchanged -ge $Date -and objectClass -eq 'Group')"
            }
        # Create a Collection of Changed Groups
        $Groups = Get-ADObject -SearchBase $BaseDN -Filter {whenchanged -ge $Date -and objectClass -eq 'Group'} -Properties member,info
        # Check Groups were returned
        if($Groups.Count -ge 1 -or $Groups -ne $null)
            {
                # Write to Log
                if($LogLevel -eq "VERBOSE")
                    {
                        LOG -LogMsg "$LogLevel :: Stage 2 - Changed Detected. There are ("$Groups.Count") Modified Groups"
                    }
                RETURN $Groups
            }
        else
            {
                # Write to Log
                if($LogLevel -eq "VERBOSE")
                    {
                        LOG -LogMsg "$LogLevel :: Stage 2 - No Changed Detected. There are ("$Groups.Count") Groups"
                    }
                # No Groups Returned
                RETURN $null
            }
    }
}
 
Function Update-Collections
{
    <# 
    .Synopsis 
        Adds or removes static query rules.
    .DESCRIPTION 
        Attempt a collection update based on the AD Groups CollectionID and Member ID changes.
    .NOTES
        To Do:
            - Potentially break this function up into smaller more managable blocks.
            - Add better error handling.
    #>         
    PARAM($Groups,$SiteCode = $SCCMSiteCode, $SiteServer = $SCCMSiteServer)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - Update-Collections ($Groups|$SiteCode|$SiteServer) Function has been invoked"
            }
        # Store an Array of Computer Names which need a policy update
        $NotifySystems = @()    
            
        # Ensure Group is not null
        foreach($Group in $Groups)
            {
            
                # Write to Log
                if($LogLevel -eq "VERBOSE")
                    {
                        LOG -LogMsg "$LogLevel :: Stage 2 --------------------------------------------------------------------"
                        LOG -LogMsg "$LogLevel :: Stage 2 - Processing ($Group)"
                    }
            
                # Only Process groups which have a valid Collection ID
                if($Group.Info -ne $null -and $Group.Info -match "^[A-Z0-9]{8}")
                    {
                    
                        # Store the Collection ID
                        [string]$CollectionID = $Group.Info
                        # Write to Log
                        if($LogLevel -eq "VERBOSE")
                            {
                                LOG -LogMsg "$LogLevel :: Stage 2 - There was a valid ($CollectionID) value set on the Group Notes Attribute and the contents of this attribute matched the SCCM Site Code Regular expression."
                            }
                        # Write to Log
                        if($LogLevel -eq "VERBOSE")
                            {
                                LOG -LogMsg "$LogLevel :: Stage 2 - Attempting retrieve existing Direct Rules by Connecting to Site Server($SiteServer) | Namespace(root\sms\site_$SiteCode) | Query(select * from SMS_Collection Where SMS_Collection.CollectionID='$CollectionID')"
                            }
                        # Store existing collection rules
                        $Collection = Get-WmiObject -Computername $SiteServer -Namespace "root\sms\site_$SiteCode" -Query "select * from SMS_Collection Where SMS_Collection.CollectionID='$CollectionID'"
                        # Only proceed if a valid collection is found to work with
                        If($Collection -ne $null)
                            {
                                # Write to Log
                                if($LogLevel -eq "VERBOSE")
                                    {
                                        LOG -LogMsg "$LogLevel :: Stage 2 - The WMI Query was able to connect SCCM Server WMI and retrive information for collection ($CollectionID)"
                                    }
                                # Get all properties
                                $Collection.Get()
                                # Process Membership
                                $GroupMembers = $Group | Get-ADGroupMember
                                # Write to Log
                                if($LogLevel -eq "VERBOSE")
                                    {
                                        LOG -LogMsg "$LogLevel :: Stage 2 - Beginning Group Membership Processing. Members for group($Group) are($GroupMembers)"
                                    }
                                If($GroupMembers.Count -eq 0)
                                    {
                                        # Write to Log
                                        if($LogLevel -eq "VERBOSE")
                                            {
                                                LOG -LogMsg "$LogLevel :: Stage 2 - There are no Members in the Group($Group). All static collection rules in the corresponding collection ($CollectionID) will be removed."
                                            }
                                        # Empty Group Handling
                                        ForEach($CollectionRule in $Collection.CollectionRules)
                                            {
                                                # Write to Log
                                                if($LogLevel -eq "VERBOSE")
                                                    {
                                                        LOG -LogMsg "$LogLevel :: Stage 2 - The Following Collection rule ($CollectionRule) will be removed"
                                                    }
                                                # Remove Collection Rule
                                                Remove-CollectionMember -CollectionID $CollectionID -CollectionRule $CollectionRule
                                            }
                                    }
                                else
                                    {
                                        # Write to Log
                                        if($LogLevel -eq "VERBOSE")
                                            {
                                                LOG -LogMsg "$LogLevel :: Stage 3 - There are Members in the Group($Group)."
                                            }
                                        # Group Handling / Adding New Query Rules
                                        ForEach($GroupMember in $GroupMembers)
                                            {
                                                # Get and Check Resource ID
                                                [int]$ResourceID = Get-ResourceID -DN $GroupMember
                                                [string]$ComputerName = Get-ComputerName -DN $GroupMember
                                                # Write to Log
                                                if($LogLevel -eq "VERBOSE")
                                                    {
                                                        LOG -LogMsg "$LogLevel :: Stage 2 - Processing Group Member($ComputerName) who has an SCCM ResourceID($ResourceID)"
                                                    }
                                                # Ensure there is a valid Resource ID
                                                if($ResourceID -ne $null)
                                                    {
                                                        # If necessary add a new rule
                                                        if($Collection.CollectionRules.RuleName -notcontains $ComputerName)
                                                            {    
                                                                # Write to Log
                                                                if($LogLevel -eq "VERBOSE")
                                                                    {
                                                                        LOG -LogMsg "$LogLevel :: Stage 2 - There was no pre-existing Direct Membership Rule for ($ComputerName) so a new one will be created."
                                                                    }
                                                                Add-CollectionMember -ResourceID $ResourceID -CollectionID $CollectionID -ComputerName $ComputerName
                                                                $NotifySystems += $ComputerName
                                                            }
                                                        else
                                                            {
                                                                # A rule already exists
                                                                # Write to Log
                                                                if($LogLevel -eq "VERBOSE")
                                                                    {
                                                                        LOG -LogMsg "$LogLevel :: Stage 2 - There was a pre-existing Direct Membership Rule for ($ComputerName) so a new one is not required."
                                                                    }
                                                            }
                                                    }
                                                else
                                                    {
                                                        #No Resource ID returned / No Record in SCCM
                                                        # Write to Log
                                                        if($LogLevel -eq "VERBOSE")
                                                            {
                                                                LOG -LogMsg "$LogLevel :: Stage 2 - Group Member($ComputerName) had no SCCM Resource ID so no Collection Change occured."
                                                            }
                                                    }
                                            }
                                        # Write to Log
                                        if($LogLevel -eq "VERBOSE")
                                            {
                                                LOG -LogMsg "$LogLevel :: Stage 2 - Will See if any Collection members should be removed."
                                            }
                                        # Collection Rules Handling / Removing Query Rules
                                        $GroupMembers = @($GroupMembers.Name)
                                        ForEach($CollectionRule in $Collection.CollectionRules)
                                            {
                                                If($GroupMembers -notcontains $CollectionRule.RuleName)
                                                    {
                                                        # Write to Log
                                                        if($LogLevel -eq "VERBOSE")
                                                            {
                                                                LOG -LogMsg "$LogLevel :: Stage 2 - The "$CollectionRule.RuleName " does not match the group members and will be removed."
                                                            }
                                                        Remove-CollectionMember -CollectionID $CollectionID -CollectionRule $CollectionRule
                                                    }
                                            }
                                        # Write to Log
                                        if($LogLevel -eq "VERBOSE")
                                            {
                                                LOG -LogMsg "$LogLevel :: Stage 2 - Returning the array of systems which may need to be notified($NotifySystems)"
                                            }
                                        # Return any systems to be notified
                                        RETURN $NotifySystems
                                    }        
                            }
                        else
                            {
                                # Write to Log
                                LOG -LogMsg "$LogLevel :: Stage 2 - There is no collection which matched the Collection ID ($CollectionID) or the SCCM Server WMI provider could not be contacted."
                                # No collection rules.
                            }
                    }
                else
                    {
                        # Write to Log
                        LOG -LogMsg "$LogLevel :: Stage 2 - No Collection ID / Group Notes (Info) Attribute was returned for the group. Please check and set this information."
                        # No Collection ID
                    }
            }
    }
}
Function Get-ResourceID
{ 
    <# 
    .Synopsis 
        Query SCCM For a Resource ID
    .DESCRIPTION 
        Will return the SCCM resource ID based on a systems Active Directory DN.
    .NOTES
        To Do:
    #>     
    PARAM($DN,$SiteCode = $SCCMSiteCode, $SiteServer = $SCCMSiteServer)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - The Get-ResourceID($DN|$SiteCode|$SiteServer) Function has been invoked"
            }
        $Resource = Get-WmiObject -Computername $SiteServer -Namespace "Root\SMS\Site_$($SiteCode)" -Class 'SMS_R_SYSTEM' -Filter "DistinguishedName='$DN'"
        $ResourceID = $Resource.ResourceId
        if($ResourceID -match "^[0-9]{8,10}")
            {
                # Valid Resource ID found
                RETURN $ResourceID
            }
        else
            {
                # No Resource ID
                RETURN $null
            }
 
    }
}
Function Get-ComputerName
{
    <# 
    .Synopsis 
        Returns just the CN name portion of a Distinguished Name
    .DESCRIPTION 
        Take a DN and Return just the CN name portion.
    .NOTES
        To Do:
    #>     
    PARAM($DN)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - The Get-ComputerName($DN) Function has been invoked"
            }
        $CN = ($DN -split ",*..=")[1]
        RETURN $CN
    }
}
Function Add-CollectionMember
{
    <# 
    .Synopsis 
        Add a Direct Membership Rule to a Collection
    .DESCRIPTION 
        Create a new direct membership rule within an SCCM Collection.
    .NOTES
        To Do:
    #>     
    PARAM($ResourceID,$CollectionID,$ComputerName,$SiteCode = $SCCMSiteCode, $SiteServer = $SCCMSiteServer)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - The Add-CollectionMember($ResourceID|$CollectionID|$ComputerName|$SiteCode|$SiteServer) Function has been invoked"
            }
        $Collection = [wmi]"\\$SiteServer\Root\SMS\Site_$($SiteCode):SMS_Collection.CollectionID='$CollectionID'" 
        $ruleClass = [wmiclass]"\\$SiteServer\Root\SMS\Site_$($SiteCode):SMS_CollectionRuleDirect"
        $newRule = $ruleClass.CreateInstance()
        $newRule.RuleName = $ComputerName
        $newRule.ResourceClassName = "SMS_R_System"
        $newRule.ResourceID = $ResourceID 
        $Collection.AddMembershipRule($newRule)
    }
}
Function Remove-CollectionMember
{
    <# 
    .Synopsis 
        Removes a Direct Membership Rule to a Collection
    .DESCRIPTION 
        Remove a Direct Membership Rule from a Collection
    .NOTES
        To Do:
    #> 
    PARAM($CollectionRule, $CollectionID, $SiteCode = $SCCMSiteCode, $SiteServer = $SCCMSiteServer)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 2 - The Remove-CollectionMember($CollectionRule|$CollectionID|$SiteCode|$SiteServer) Function has been invoked"
            }
        $Collection = [wmi]"\\$SiteServer\Root\SMS\Site_$($SiteCode):SMS_Collection.CollectionID='$CollectionID'"
        $Collection.DeleteMemberShipRule($CollectionRule)
    }
}
Function Notify-System
{
    <# 
    .Synopsis 
        Will attempt to execute a remote SCCM Machine Policy Refresh
    .DESCRIPTION 
        Will attempt to ping a computer and then envoke a WMI method remotely.
    .NOTES
        To Do:
    #> 
    PARAM($ComputerName)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 3 - The Notify-System($ComputerName) Function has been invoked"
            }
        $PingResult = Ping-System -ComputerName $ComputerName
        if($PingResult -eq $true)
            {
                LOG -LogMsg "$LogLevel :: Stage 3 - Computer ($ComputerName) was online and a remote Policy Update was attempted."
                Invoke-MachinePolicyUpdate -ComputerName $ComputerName
            }
    }
}
Function Ping-System
{
    <# 
    .Synopsis 
        Performs a ping.
    .DESCRIPTION 
        Attempts to ping a machine once in order to confirm network connectivity.
    .NOTES
        To Do:
    #> 
    PARAM($ComputerName)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 3 - The Ping-System($ComputerName) Function has been invoked"
            }
        $PingResult = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 3 - Returning Ping Result($PingResult) for computer ($ComputerName)"
            }
        RETURN $PingResult
    }
}
Function Invoke-MachinePolicyUpdate
{    
    <# 
    .Synopsis 
        Attempt Machine Policy Update
    .DESCRIPTION 
        Will attempt to envoke a WMI method remotely.
    .NOTES
        To Do:
            - There is currently no confirmation of Machine Update (Guarenteed Delivery)
    #> 
    PARAM($ComputerName)
    END
    {
        # Write to Log
        if($LogLevel -eq "VERBOSE")
            {
                LOG -LogMsg "$LogLevel :: Stage 3 - The Invoke-MachinePolicyUpdate($ComputerName) Function has been invoked"
            }
        # Connect to WMI Class
        $SMSCli = [wmiclass] "\\$ComputerName\root\ccm:SMS_Client"  
        if($SMSCli)
            {  
            
                # Write to Log
                LOG -LogMsg "$LogLevel :: Stage 3 - Was able to connect to remote WMI (\\$ComputerName\root\ccm:SMS_Client) and will attempt to invoke the Request and Evaluate Machine Policy Methods."                
                # Envoke Methods.
                $check = $SMSCli.RequestMachinePolicy()              
                $check = $SMSCli.EvaluateMachinePolicyInvoke
           }
        else
            {
                # Write to Log
                if($LogLevel -eq "VERBOSE")
                    {
                        # Write to Log
                        LOG -LogMsg "$LogLevel :: Stage 3 - Was unable to connect to remote WMI (\\$ComputerName\root\ccm:SMS_Client) and did not invoke the Request and Evaluate Machine Policy Evaluation Methods."
                        LOG -LogMsg "$LogLevel :: Stage 3 - The most likely cause of this was. (Access Denied | WMI Firewall rules not enabled) as the system responded to PING and has a SCCM Resource Record"
                    }            
            }
    }
}
 
# // Main Routine
#        The purpose of this routine is to run 4 key stages of a workflow
#            which attempts to take the load off the SCCM Collection evaluator / Management Points
#                and ultimately generate the best possible processing performance within Configuration Manager
#
#        For a more detailed understanding and for more information goto http://wp.me/p5GMrl-CG 
#
#        1) Getting back modified groups from AD
#        2) Adding and removing devices from a SCCM 2012 Collection
#        3) Remote-Invoke of the Machine Policy Refresh Action on any "added" resources.
 
# Write to clean log
LOG -LogMsg " ************************************* // COLLECTION MANAGER STARTED"
LOG -LogMsg "The Logging Level is ($LogLevel)"
 
# // 1) Check for Changed Groups and make sure a value was returned
$Groups = Get-ChangedGroups
if($Groups -ne $null)
    {
        # Write to Log
        LOG -LogMsg "$LogLevel :: Stage 1 - The following modified groups were found ($Groups)."
        # // 2) Add or remove Devices from collection
        $NotifySystems = Update-Collections -Groups $Groups
        if($NotifySystems -ne $null)
            {
                # Write to Log
                LOG -LogMsg "$LogLevel :: Stage 2 - There are systems to be notified of collection changes (Policy Changes)."
                LOG -LogMsg "$LogLevel :: Stage 2 - Sleeping for ($SleepInterval) seconds to allow relevant collections to refresh."
                        
                # Sleep for a short period. This is to give the collection
                #    evaluator a chance to finish.
                Start-Sleep -s $SleepInterval
                        
                # Write to Log
                LOG -LogMsg "$LogLevel :: Stage 3 - Waking up from sleep."                        
                        
                # Loop through any systems which need notification.
                foreach($System in $NotifySystems)
                    {
                        # Write to Log
                        LOG -LogMsg "$LogLevel :: Stage 3 - Will attempt to invoke a policy refresh on system ($System)."    
                        # // 4) Invoke-MachinePolicyUpdate
                        Notify-System -ComputerName $System
                    }
                # Write to Log
                LOG -LogMsg "$LogLevel :: Stage 3 - Exiting."
                # Exit
                        
            }
        else
            {
                # Write to Log
                LOG -LogMsg "$LogLevel :: Stage 3 - There are no systems to be notified (Policy Changes)."
            }
    }        
else
    {
        # No Group Changes
        # Write to Log
        LOG -LogMsg "$LogLevel :: Stage 2 - No Groups have changed since the check interval Last ($CheckInterval) Minutes."
    }
# Write to Log
LOG -LogMsg " ************************************* // COLLECTION MANAGER ENDED"

download-button-orange