Trevor Sullivan's Tech Room

Minding the gap between administration and development

Posts Tagged ‘system center’

PowerShell: Move ConfigMgr Collections

Posted by Trevor Sullivan on 2012/01/12


If you work with Microsoft System Center Configuration Manager (SCCM / ConfigMgr) 2007 in any capacity, you probably are familiar with the concept of "collections" and how painful they can be to work with sometimes. The ConfigMgr console does not provide any method of moving a collection from one parent to another, and the GUI is pretty slow to work with.


So what’s the solution here? PowerShell, of course!

PowerShell Code

Here is a PowerShell function that will allow you to move a ConfigMgr collection either by name or by collection ID.

Note: Select all of the function text top-to-bottom, and you can retrieve the text that is cut off towards the right.

    This function allows you to re-assing the parent for a ConfigMgr collection to a new collection ID

    Trevor Sullivan (

    c:\PS> Move-SccmCollection -SccmServer sccm01 -SiteCode LAB -CollectionID LAB00159 -ParentCollectionID LAB000150;


    This command moves the ConfigMgr collection with ID "LAB000159" to being a child of collection ID "LAB000150".

    c:\PS> Move-SccmCollection -SccmServer sccm01 -SiteCode LAB -CollectionName 'Visual Studio' -ParentCollectionID Microsoft;


    This command moves the ConfigMgr collection named "Visual Studio" to being a child of the collection named "Microsoft". Note that you do not need to specify quotes around the parameter value if it does not contain spaces.

    This function is untested with collection links. It is not known whether or not this will remove existing collection links.
function Move-SccmCollection {
    param (
        [Parameter(Mandatory = $true)] [string] ${SccmServer}
        , [Parameter(Mandatory = $true)] [string] ${SiteCode}
        , [Parameter(ParameterSetName = "ByCollectionID", Mandatory = $true)] [string] ${CollectionID}
        , [Parameter(ParameterSetName = "ByCollectionID", Mandatory = $true)] [string] ${ParentCollectionID}
        , [Parameter(ParameterSetName = "ByCollectionName", Mandatory = $true)] [string] ${CollectionName}
        , [Parameter(ParameterSetName = "ByCollectionName", Mandatory = $true)] [string] ${ParentCollectionName}

    # Set-PSDebug -Strict;

    # Ensure that ConfigMgr site server is available
    if (-not (Test-Connection -ComputerName $SccmServer -Count 1)) {

    # Obtain references to collection and parent collection
    switch ($PSCmdlet.ParameterSetName) {
        # Use the "ByCollectionID" PowerShell parameter set to retrieve collection references by ID
        'ByCollectionID' {
            ${CollectionRelationship} = @(Get-WmiObject -ComputerName $SccmServer -Namespace root\sms\site_$SiteCode -Class SMS_CollectToSubCollect -Filter "subCollectionID = '$CollectionID'")[0];
            ${Collection} = @([wmi]("\\{0}\root\sms\site_{1}:SMS_Collection.CollectionID='{2}'" -f ${SccmServer}, ${SiteCode}, ${CollectionID}))[0];
            ${ParentCollection} = @([wmi]("\\{0}\root\sms\site_{1}:SMS_Collection.CollectionID='{2}'" -f ${SccmServer}, ${SiteCode}, ${ParentCollectionID}))[0];
        # Use the "ByCollectionName" PowerShell parameter set to retrieve collection references by name
        'ByCollectionName' {
            ${Collection} = [wmi](@(Get-WmiObject -ComputerName $SccmServer -Namespace root\sms\site_$SiteCode -Class SMS_Collection -Filter ("Name = '{0}'" -f ${CollectionName}))[0].__PATH);
            ${ParentCollection} = [wmi](@(Get-WmiObject -ComputerName $SccmServer -Namespace root\sms\site_$SiteCode -Class SMS_Collection -Filter ("Name = '{0}'" -f ${ParentCollectionName}))[0].__PATH);
            ${CollectionRelationship} = @(Get-WmiObject -ComputerName $SccmServer -Namespace root\sms\site_$SiteCode -Class SMS_CollectToSubCollect -Filter ("subCollectionID = '{0}'" -f ${Collection}.CollectionID))[0];
    # If references to both the child and [new] parent collection were obtained, then move on
    if (${Collection} -and ${ParentCollection}) {
        Write-Verbose -Message ('Setting parent collection for {0}:{1} to {2}:{3}' -f `
            ${Collection}.CollectionID `
            , ${Collection}.Name `
            , ${ParentCollection}.CollectionID `
            , ${ParentCollection}.Name);
        ${CollectionRelationship}.parentCollectionID = ${ParentCollection}.CollectionID;
        # Create the new collection relationship (this [oddly] spawns a NEW instance of SMS_CollectToSubCollect), so we have to clean up the original one

        # Clean up all other collection relantionships for this collection
        ${OldCollectionRelationshipList} = @(Get-WmiObject -ComputerName $SccmServer -Namespace root\sms\site_$SiteCode -Class SMS_CollectToSubCollect -Filter ("subCollectionID = '{0}' and parentCollectionID <> '{1}'" -f ${Collection}.CollectionID, ${ParentCollection}.CollectionID));
        foreach (${OldCollectionRelationship} in ${OldCollectionRelationshipList}) {
    else {
        Write-Warning -Message 'Please ensure that you have entered a valid collection ID or name';


Here is an example of how to use this function to move a collection based on their collection IDs:

Move-SccmCollection -SccmServer sccm01.mybiz.loc -SiteCode LAB -CollectionID LAB00011 -ParentCollectionID LAB00022;

Here is an example of how to use the function to move a collection based on the collection name:

Move-SccmCollection -SccmServer sccm01.mybiz.loc -SiteCode LAB -CollectionName ‘Visual Studio’ -ParentCollectionID Microsoft;

Posted in configmgr, powershell, scripting, tools, wmi | Tagged: , , , , , , , , , , , , , , , , | Leave a Comment »

PowerShell: Report / Check the Size of ConfigMgr Task Sequences

Posted by Trevor Sullivan on 2012/01/10


In Microsoft System Center Configuration Manager 2007 operating system deployment (OSD), there is a limitation of 4MB for task sequence XML data. This is discussed in a couple of locations:

The Technet document linked to above says the following:

Extremely large task sequences can exceed the 4-MB limit for the task sequence file size. If this limit is exceeded, an error is generated.

Solution: To check the task sequence file size, export the task sequence to a known location and check the size of the resulting .xml file.

Basically, the Technet troubleshooting article is suggesting that you would need to go into the ConfigMgr console, right-click a task sequence, export it to a XML file, and then pull up the file properties. That’s fine for one-off troubleshooting, but what if you had 1000 task sequences and needed to know how large all of them were? Read on to find out how!

Read the rest of this entry »

Posted in configmgr, powershell, scripting, wmi | Tagged: , , , , , , , , , , , , , , , , | 2 Comments »

ConfigMgr Software Updates: Enforcement State Unknown

Posted by Trevor Sullivan on 2011/11/10

There was an interesting thread going on over at the MyITforum MSSMS mailing list. Apparently if certain settings are not properly configured, System Center Configuration Manager (SCCM / ConfigMgr) clients will show a status of “Enforcement state unknown” for certain software updates. One proposed solution was the following:

I had a similar issue some time ago and worked with MS with the following solution (might be worth checking into):

Basically we had “Suppress display notifications on clients” radio button checked on the Display/Time Settings tab of the specific Deployment Management Properties box and in order to do that we also had to set a deadline (on the Schedule tab of the same Properties box).  Without the deadline, I was getting the “Enforcement State Unknown” status.

We set if for some time in the future, but did not check the “Ignore maintenance windows and install immediately at deadline” checkbox, so the workstations will not install until you maintenance window, assuming that’s what you want.

Someone else suggested the following VBscript to force SCCM clients to update their software updates status:

‘ Initialize the UpdatesStore variable.
dim newCCMUpdatesStore
‘ Create the COM object.
set newCCMUpdatesStore = CreateObject ("Microsoft.CCM.UpdatesStore")
‘ Refresh the server compliance state by running the RefreshServerComplianceState method.

Hope this helps, if you’re having the issue.

Posted in configmgr, fixes | Tagged: , , , , , , , , , , | Leave a Comment »

ConfigMgr 2012 Beta 2: WMI Namespace Documentation

Posted by Trevor Sullivan on 2011/08/02

I recently put together some documentation for the Microsoft System Center Configuration Manager 2012 Beta 2 WMI provider. This is a searchable, formatted Excel document that displays all the classes, properties, and methods for the SCCM 2012 provider. Hopefully this will help you to find the proper information for writing custom scripts and so on.

Please provide feedback if this was helpful, or if you’d like to see something else added to it!

Posted in configmgr, ConfigMgr vNext, tools, wmi | Tagged: , , , , , , , , , , | Leave a Comment »

PowerShell / ConfigMgr: Sendsched.vbs Replacement

Posted by Trevor Sullivan on 2011/07/25

Recently, someone posted a PowerShell script, which is intended as a replacement for the SendSched.vbs included in the Microsoft System Center Configuration Manager 2007 Toolkit v2.

I took the liberty of cleaning the code up a little bit, and simplifying it to be more PowerShell friendly. Enjoy.

# Script Name: SendSched_PowerShell_Version.ps1
# Purpose: Serves as a replacement for the sendsched.vbs script included in the Microsoft System Center
#            Configuration Manager 2007 toolkit -- asynchronously invokes SCCM client tasks
# Created By Kaido Jarvemets Http://            
# Configuration Manager MVP            
# 25.07.2011

# Updated by: Trevor Sullivan
#      Email:
#         Blog:
#        Date: 2011-07-25
# Changelog:
#        * Replaced Get-ScheduleID function with a hashtable
#        * Removed custom ping function in favor of using Test-Connection
#        * Replaced default computer name with a period, which represents localhost
function Invoke-SCCMSchedule            
        [String]$ComputerName = ".",
        [Parameter(Mandatory = $true)]            
    $ScheduleIds = @{
        HardwareInventory    = "{00000000-0000-0000-0000-000000000001}"; # Hardware Inventory Collection Task             
        SoftwareInventory    = "{00000000-0000-0000-0000-000000000002}"; # Software Inventory Collection Task             
        HeartbeatDiscovery    = "{00000000-0000-0000-0000-000000000003}"; # Heartbeat Discovery Cycle             
        SoftwareInventoryFileCollection    = "{00000000-0000-0000-0000-000000000010}"; # Software Inventory File Collection Task             
        RequestMachinePolicy    = "{00000000-0000-0000-0000-000000000021}"; # Request Machine Policy Assignments             
        EvaluateMachinePolicy    = "{00000000-0000-0000-0000-000000000022}"; # Evaluate Machine Policy Assignments             
        RefreshDefaultMp    = "{00000000-0000-0000-0000-000000000023}"; # Refresh Default MP Task             
        RefreshLocationServices    = "{00000000-0000-0000-0000-000000000024}"; # Refresh Location Services Task             
        LocationServicesCleanup    = "{00000000-0000-0000-0000-000000000025}"; # Location Services Cleanup Task             
        SoftwareMeteringReport    = "{00000000-0000-0000-0000-000000000031}"; # Software Metering Report Cycle             
        SourceUpdate            = "{00000000-0000-0000-0000-000000000032}"; # Source Update Manage Update Cycle             
        PolicyAgentCleanup        = "{00000000-0000-0000-0000-000000000040}"; # Policy Agent Cleanup Cycle             
        RequestMachinePolicy2    = "{00000000-0000-0000-0000-000000000042}"; # Request Machine Policy Assignments             
        CertificateMaintenance    = "{00000000-0000-0000-0000-000000000051}"; # Certificate Maintenance Cycle             
        PeerDistributionPointStatus    = "{00000000-0000-0000-0000-000000000061}"; # Peer Distribution Point Status Task             
        PeerDistributionPointProvisioning    = "{00000000-0000-0000-0000-000000000062}"; # Peer Distribution Point Provisioning Status Task             
        ComplianceIntervalEnforcement    = "{00000000-0000-0000-0000-000000000071}"; # Compliance Interval Enforcement             
        SoftwareUpdatesAgentAssignmentEvaluation    = "{00000000-0000-0000-0000-000000000108}"; # Software Updates Agent Assignment Evaluation Cycle             
        UploadStateMessage    = "{00000000-0000-0000-0000-000000000111}"; # Send Unsent State Messages             
        StateMessageManager    = "{00000000-0000-0000-0000-000000000112}"; # State Message Manager Task             
        SoftwareUpdatesScan    = "{00000000-0000-0000-0000-000000000113}"; # Force Update Scan            
        AMTProvisionCycle    = "{00000000-0000-0000-0000-000000000120}"; # AMT Provision Cycle            
    if(Test-Connection -Computer $ComputerName) {
        Try {
            $SmsClient = [wmiclass]"\\$ComputerName\root\ccm`:SMS_Client"
        Catch {
            Write-Host "Trigger Schedule Method failed" -ForegroundColor RED            
    else {
        Write-Host "Computer may be offline or please check Windows Firewall settings" -ForegroundColor Red
}# End of Function Invoke-SCCMSchedule

Posted in configmgr, powershell, scripting, tools, wmi | Tagged: , , , , , , , , , | Leave a Comment »

PowerShell / ConfigMgr 2012: Check Client Reboot Pending State

Posted by Trevor Sullivan on 2011/07/25


If you’ve worked with Configuration Manager 2007 for very long, you probably know that clients pending reboots can cause you quite a headache. Determining whether or not a client needs a reboot can be a challenging task, and most folks used desired configuration management rules to detect it.

Well, I’m happy to announce that there’s a new method of figuring out whether or not a SCCM client requires a reboot! There’s a new WMI namespace called root\ccm\ClientSDK, and within it is a WMI class called CCM_ClientUtilities, which has a static method called DetermineIfRebootPending() – the method does not take any input parameters, however it spits out several [out] parameters when it is called.

Read the rest of this entry »

Posted in configmgr, ConfigMgr vNext, powershell, scripting, wmi | Tagged: , , , , , , , , , , , | Leave a Comment »

ConfigMgr 2012: Client Side Software Center

Posted by Trevor Sullivan on 2011/06/15


The System Center Configuration Manager 2012 Beta 2 client agent has been updated significantly. One of the things that has been changed a lot is the client-side interface that allows an end-user to select optional software to install, scripts to run, or operating systems to deploy.

In previous versions of ConfigMgr, there was a Control Panel applet called “Run Advertised Programs.” A user would open this in order to browse the list of optional program made available by a ConfigMgr administrator. There was another control panel applet called “Program Download Monitor” that would allow an end-user to monitor the download status of a program – admittedly, this was probably hardly ever touched by an end-user, realistically.

Read the rest of this entry »

Posted in configmgr, ConfigMgr vNext | Tagged: , , , , , , , , , | 3 Comments »

PowerShell: Cleaning Up Empty ConfigMgr Collections

Posted by Trevor Sullivan on 2010/12/06

Microsoft System Center 50%Someone recently posted on the MyITforum ConfigMgr mailing list, asking how to delete a bunch of old, empty collections in ConfigMgr. I took this opportunity to write a simple PowerShell script that will do just that. The code simply iterates over all collections, looks to see if each collection has members, and if not, then it prompts the user to delete it.

You’ll need to configure a couple things at the top of the script, before you run it:

  • ConfigMgr server name ($SccmServer)
  • ConfigMgr provider namespace (just replace the last 3 characters with your site code)
  • Add any collection IDs to the $ExcludedCollections array, that you want to explicitly exclude

If you’re really daring, and are absolutely confident that you want to remove all empty collections, you can remove the user confirmation. I’ll leave that up to you, if you’re savvy enough 🙂

Here’s a screenshot of the script running in Quest’s free PowerGUI scripting environment:


Here’s the code, with comments in-line:

############################################### # # Author: Trevor Sullivan # Date: 12/6/10 # Purpose: Delete empty ConfigMgr collections # Blog: # ################################################ Your ConfigMgr server name (where the provider sites). Set this to your SCCM server name.$SccmServer='sccm01'# Create an array of excluded collections (add your own here)$ExcludedCollections= ,'COLLROOT'# Exclude these collection IDs (array) # Define the ConfigMgr provider namespace. Set the last three chars to your site code$SccmNamespace='root\sms\site_lab'# OK, house-keeping is out of the way. Now let's delete those collections!# Get a list of ALL collection objects$CollectionList=Get-WmiObject-Namespace$SccmNamespace-ClassSMS_Collection-ComputerName$SccmServer# Iterate over [PowerShell] collection of [ConfigMgr] collections (haha ...)foreach ($Collectionin$CollectionList) { # Define the WMI query language (WQL) query to get [ConfigMgr] collection members for the current [ConfigMgr] collection we're iterating over$WqlQuery="select * from SMS_CollectionMember_a where collectionid = '"+$Collection.CollectionID+"'"# Execute the WQL query, pipe to Measure-Object, and get the Count property from the returned [PowerShell.Commands.GenericMeasureInfo] object$NumCollectionMembers= (Get-WmiObject-Query$WqlQuery-Namespace$SccmNamespace-ComputerName$SccmServer | Measure-Object).Count # If the number of collection members is equal to zero AND the exclusion list doesn't contain the collection ID, then go ahead with deletionif ($NumCollectionMembers-eq0-and$ExcludedCollections-notcontains$Collection.CollectionID) { # Let the user know what collection we're deleting (both ID and name)Write-Host ("Deleting collection ID ("+$Collection.CollectionID+") named: "+$Collection.Name) # Prompt the user to delete the collection. They must type "yes" to delete each collection.if ((Read-Host-Prompt"Really delete? Type `"yes`"") -ieq"Yes") { # Delete the collection object$Collection.Delete() Write-Host"Deleted collection: "+$Collection.Name } } }

Posted in configmgr, powershell, scripting, tools, wmi | Tagged: , , , , , , , , , , , | 5 Comments »

PowerShell: AD / SCCM Workstation Cleanup Script Version 3.0

Posted by Trevor Sullivan on 2010/08/17

I just realized, I still haven’t posted the script that removes SCCM resources, alongside Active Directory cleanup. I had written a version 3.0 of a script I previously posted, but never posted it. So, here it is (I haven’t tested it in a while):

Disclaimer: I am not responsible for what you do with this script.

Update (2010-08-18): Shay Levy (PowerShell MVP) has noted that using Replace(“/”, “-“) will not work in all cultures. Rather, he suggests using this format: (Get-Date -f ‘M-d-yyyy hMMss tt’)

# Author: Trevor Sullivan
# Date: October 28, 2009
# Lessons learned:
# 1. ADSI property names are lower case using DirectorySearcher or DirectoryEntry
# 2. Must explicitly cast 64-bit integers from AD
# 3. The Excel API is terrible (already knew that)
# Change Log:
#    2009-11-06
#        -Added: function to delete objects from SCCM (untested)
#        -Added: User variables at top of script to ease usage
#        -Added: function to auto-detect SCCM site code, based on server name
#        -Added: Windows Vista accounts to search criteria
#        -Fixed: Replaced -bxor operator with -bor to prevent computer accounts
#                from being re-enabled
#        -Fixed: Casted [void] from loading Excel Interop assembly to prevent
#                Assembly object from being written to pipeline

### Populate these variables please. ###
$ExcelLog = $Env:USERPROFILE + '\' + (Get-Date).ToString().Replace("/","-").Replace(":","") + " AD Workstation Cleanup.xlsx" # Full path to save Excel log to
$TargetDn = 'cn=computers,dc=ts,dc=loc' # Top-level distinguishedName 
$DisabledDn    = 'ou=Disabled,ou=Workstations,dc=ts,dc=;pc' # OU to place disabled accounts into
$DisableAge = 60 # Age (in days) of computer account, to be disabled
$DeleteAge = 30 # Age of computer account (<DisabledDate> + X) to delete computer accounts
$SccmServer    = 'sccm01' # The server on which your SMS Provider component is installed
$BreakStuff = $false # If set to $true, the script WILL TAKE ACTION!
$Debug = $false # Enables additional logging to file and stdout
###  ### ############################# ###  ###

function DisableOldAccounts(${TargetDn}, ${DisableAge} = 60)
    ${Computers} = GetComputerList ${TargetDn}

    foreach (${Computer} in ${Computers})
        # PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
        # The FromFileTime method converts a 64-bit integer to datetime
        ${PwdLastSet} = [DateTime]::FromFileTime([Int64]"$(${Computer}.Properties['pwdlastset'])")
        ${CompAge} = ([DateTime]::Now - $PwdLastSet).Days
        if (${CompAge} -gt ${DisableAge})
            LogMessage "$($Computer.Properties['cn']) age is ${CompAge}. Account will be disabled" 2
            WriteDisabledEntry $Computer.Properties['cn'].Item(0) $CompAge $Computer.Properties['distinguishedname'].Item(0) $DisabledDn
            DisableAccount $Computer.Properties['distinguishedname'].Item(0)
            LogMessage "$($Computer.Properties['cn'].Item(0)) age is ${CompAge}, $($Computer.Properties['pwdlastset'].Item(0)), ${PwdLastSet}" 1

# Gets a full list of computer accounts from the target distinguishedName defined at the top of the script
function GetComputerList($TargetDn)
    # Define the LDAP search syntax filter to locate workstation objects.
    # See this link for info:
    ${tFilter} = '(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=*Vista*)(operatingSystem=Windows 7*)))'

    # Create a DirectorySearcher using filter defined above
    ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
    # Set the search root to the distinguishedName specified in the function parameter
    ${Searcher}.SearchRoot = "LDAP://${TargetDn}"
    # Search current container and all subcontainers
    ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
    # See this link for info on why this next line is necessary:
    ${Searcher}.PageSize = 1000
    $Results = $Searcher.FindAll()
    LogMessage "Found $($Results.Count) computer accounts to evaluate for disablement" 1
    return $Results

# Set description on computer account, disable it, and move it to the Disabled OU
function DisableAccount($dn)
#    LogMessage "DisableAccount method called with param: ${dn}" 1
    # Get a reference to the object at <distinguishedName>
    $comp = [adsi]"LDAP://${dn}"
    # Disable the account
#    LogMessage "userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)"
    $comp.userAccountControl = $comp.userAccountControl.Value -bor 2
    # Write the current date to the description field
    if ($comp.Description -ne '') { LogMessage "Description attribute of ($comp.Name) is set to: $($comp.Description)" 2 }
    $comp.Description = "$(([DateTime]::Now).ToShortDateString())"

    # Uncomment these lines to write changes to Active Directory
    if ($BreakStuff)
        [Void] $comp.SetInfo()

# Parameter ($DeleteAge): Days from disable date to delete computer account
function DeleteDisabledAccounts($DeleteAge)
    # Get reference to OU for disabled workstation accounts
    ${DisabledOu} = [adsi]"LDAP://${DisabledDn}"

    ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher '(objectClass=computer)'
    ${Searcher}.SearchRoot = ${DisabledOu}
    ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
    # Page size is used to return result count > default size limit on domain controllers.
    # See:
    ${Searcher}.PageSize = 1000
    LogMessage "Finding computers to evaluate for deletion in container: ${DisabledDn}" 1
    ${Computers} = ${Searcher}.FindAll()

    foreach (${Computer} in ${Computers})
        ${DisableDate} = [DateTime]::Parse(${Computer}.Properties['description'])
        trap {
            LogMessage "Couldn't parse date for $($Computer.Properties['cn'])" 3
        ${CurrentAge} = ([DateTime]::Now - ${DisableDate}).Days
        if (${CurrentAge} -gt ${DeleteAge})
            LogMessage "$(${Computer}.Properties['cn']) age is ${CurrentAge} and will be deleted" 2
            WriteDeletedEntry $Computer.Properties['cn'].Item(0) $CurrentAge $Computer.Properties['distinguishedname'].Item(0) "Note"
            if ($BreakStuff)
                $DisabledOu.Delete('computer', 'CN=' + ${Computer}.Properties['cn'])
            RemoveFromSccm ${Computer}.Properties['cn'] $SccmServer
            LogMessage "$(${Computer}.Properties['cn']) age is ${CurrentAge} and will not be deleted" 1

# Purpose: This function deletes a resource from the Configuration Manager database
function RemoveFromSccm($tPcName, $tSiteServer)
    $tSysQuery = "select * from SMS_R_System where Name = '$tPcName'"
    $tWmiNs = "root\sms\site_" + $Global:SccmSiteCode
    if ($Debug)
        LogMessage "Site code is: $Global:SccmSiteCode" 1
        LogMessage $tSysQuery 1
        LogMessage "tSiteServer is: $tSiteServer" 1
        LogMessage "tWmiNs is: $tWmiNs" 1
    $Resources = Get-WmiObject -ComputerName $tSiteServer -Namespace $tWmiNs -Query $tSysQuery

    if ($Resources -eq $null) { return; }

    foreach ($Resource in $Resources)
        $AgentTime = $($Resource.AgentTime | Sort-Object | Select-Object -Last 1)
        $UserName = $Resource.LastLogonUserDomain + '\' + $Resource.LastLogonUserName
        # Log the deleted SCCM resource to the Excel log
        WriteSccmDeletionEntry $Resource.ResourceID $Resource.Name $AgentTime $UserName
        # This line deletes records from the ConfigMgr database
        if ($BreakStuff)
            # Delete the resource from the ConfigMgr site server

# Purpose: This function looks up the site code for the SMS Provider, given a server name
function GetSiteCode($tSiteServer)
    # Dynamically obtain SMS provider location based only on server name
    $tSiteCode = (Get-WmiObject -ComputerName $tSiteServer -Class SMS_ProviderLocation -Namespace root\sms).NamespacePath
    # Return only the last 3 characters of the NamespacePath property, which indicates the site code
    return $tSiteCode.SubString($tSiteCode.Length - 3).ToLower()

# This function logs a message to the console and a log file.
# Params:
#    $tMessage = A string representing the message to be logged to the file & console
#    $Severity = A integer from 1-3 representing the severity of the message: Info, Warning, Error
function LogMessage(${tMessage}, ${Severity})
        1 {
            $LogPrefix = "INFO"
            $fgcolor = [ConsoleColor]::Blue
            $bgcolor = [ConsoleColor]::White
        2 {
            $LogPrefix = "WARNING"
            $fgcolor = [ConsoleColor]::Black
            $bgcolor = [ConsoleColor]::Yellow
        3 {
            $LogPrefix = "ERROR"
            $fgcolor = [ConsoleColor]::Yellow
            $bgcolor = [ConsoleColor]::Red
        default {
            $LogPrefix = "DEFAULT"
            $fgcolor = [ConsoleColor]::Black
            $bgcolor = [ConsoleColor]::White

    if ($Debug)
        Add-Content -Path "AD-Workstation-Cleanup.log" -Value "$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}"
        Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -Object "$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}"

function SetupExcel()
    LogMessage "Setting up Excel logging" 1
    [void] [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel")
    $Global:Excel = New-Object Microsoft.Office.Interop.Excel.ApplicationClass
    $Excel.Visible = $true
    $Global:Workbook = $Excel.Workbooks.Add()

    # Setup worksheet for deleted SCCM resource records
    $Global:SccmResourceLog = $Workbook.Worksheets.Item("Sheet3")
    $SccmResourceLog.Name = "SCCM Resources"
    $SccmResourceLog.Tab.ThemeColor = [Microsoft.Office.Interop.Excel.XlThemeColor]::xlThemeColorAccent3
    $SccmResourceLog.Cells.Item(1, 1).Value2 = "Date"
    $SccmResourceLog.Cells.Item(1, 2).Value2 = "Resource ID"
    $SccmResourceLog.Cells.Item(1, 3).Value2 = "Name"
    $SccmResourceLog.Cells.Item(1, 4).Value2 = "Last Agent Time"
    $SccmResourceLog.Cells.Item(1, 5).Value2 = "Username"
    $Global:tSccmResRow = 2

    # Setup worksheet for disabled accounts
    $Global:DisabledLog = $Workbook.Worksheets.Item("Sheet2")
    $DisabledLog.Tab.ThemeColor = [Microsoft.Office.Interop.Excel.XlThemeColor]::xlThemeColorAccent2
    $DisabledLog.Name = "Disabled"
    $DisabledLog.Cells.Item(1, 1).Value2 = "Date"
    $DisabledLog.Cells.Item(1, 2).Value2 = "Name"
    $DisabledLog.Cells.Item(1, 3).Value2 = "Age"
    $DisabledLog.Cells.Item(1, 4).Value2 = "Source Container"
    $DisabledLog.Cells.Item(1, 5).Value2 = "Destination Container"
    $Global:tDisabledRow = 2

    # Setup worksheet for deleted accounts log
    $Global:DeletedLog = $Workbook.Worksheets.Item("Sheet1")
    $DeletedLog.Tab.ThemeColor = [Microsoft.Office.Interop.Excel.XlThemeColor]::xlThemeColorAccent5
    $DeletedLog.Name = "Deleted"
    $DeletedLog.Cells.Item(1, 1).Value2 = "Date"
    $DeletedLog.Cells.Item(1, 2).Value2 = "Name"
    $DeletedLog.Cells.Item(1, 3).Value2 = "Age"
    $DeletedLog.Cells.Item(1, 4).Value2 = "DN"
    $DeletedLog.Cells.Item(1, 5).Value2 = "Note"
    $Global:tDeletedRow = 2

# Writes an entry to the global variable used to reference the log for disabled accounts
function WriteDisabledEntry([string] $tName, $tAge, [string] $tSourceDn, [string] $tDestinationDn)
    #LogMessage "Writing disabled computer to Excel log: $tName" 1
    #Write-Host "Value of tname is $tName"
    #Write-Host "Value of tage is $tAge"
    #Write-Host "Value of tsourcedn is $tSourceDn"
    #Write-Host "Value of tDestinationDn is $tDestinationDn"
    $tArrContainer = $tSourceDn.Split(",")
    $tContainer = [string]::Join(",", ($tArrContainer | select -Last ($tArrContainer.Length - 1)))
    $DisabledLog.Cells.Item($tDisabledRow, 1).Value2 = [DateTime]::Now.ToString()
    $DisabledLog.Cells.Item($tDisabledRow, 2).Value2 = $tName
    $DisabledLog.Cells.Item($tDisabledRow, 3).Value2 = $tAge
    $DisabledLog.Cells.Item($tDisabledRow, 4).Value2 = $tContainer
    $DisabledLog.Cells.Item($tDisabledRow, 5).Value2 = $tDestinationDn

# Writes an entry to the global variable used to reference the log for deleted accounts 
function WriteDeletedEntry($tName, $tAge, $tDN, $tNote)
    #LogMessage "Writing deleted computer to Excel log: $tName" 1
    #Write-Host "Value of tName is $tName"
    #Write-Host "Value of tAge is $tAge"
    #Write-Host "Value of tDN is $tDN"
    #Write-Host "Value of tNote is $tNote"
    $DeletedLog.Cells.Item($tDeletedRow,1).Value2 = [DateTime]::Now.ToString()
    $DeletedLog.Cells.Item($tDeletedRow,2).Value2 = $tName.ToString()
    $DeletedLog.Cells.Item($tDeletedRow,3).Value2 = $tAge.ToString()
    $DeletedLog.Cells.Item($tDeletedRow,4).Value2 = $tDN.ToString()
    $DeletedLog.Cells.Item($tDeletedRow,5).Value2 = $tNote.ToString()

function WriteSccmDeletionEntry($tResourceId, $tName, $tLastAgentTime, $tUserName)
    $SccmResourceLog.Cells.Item($tSccmResRow, 1).Value2 = [DateTime]::Now.ToString()
    $SccmResourceLog.Cells.Item($tSccmResRow, 2).Value2 = $tResourceId
    $SccmResourceLog.Cells.Item($tSccmResRow, 3).Value2 = $tName
    $SccmResourceLog.Cells.Item($tSccmResRow, 4).Value2 = $tLastAgentTime
    $SccmResourceLog.Cells.Item($tSccmResRow, 5).Value2 = $tUserName

function CloseExcel()
    # AutoFit the columns

    foreach ($tSheet in $Workbook.Worksheets)
        [Void] $Excel.ActiveCell.CurrentRegion.Columns.AutoFit()
        [Void] $Excel.ActiveCell.CurrentRegion.Select()
        $Global:ListObject = $Excel.ActiveSheet.ListObjects.Add([Microsoft.Office.Interop.Excel.XlListObjectSourceType]::xlSrcRange, $Excel.ActiveCell.CurrentRegion, $null ,[Microsoft.Office.Interop.Excel.XlYesNoGuess]::xlYes)
        $ListObject.Name = "TableData"
        $ListObject.TableStyle = "TableStyleLight9"

    LogMessage "Saving and closing Excel workbook" 1

function Main()
    LogMessage "Beginning workstation account cleanup script" 1

    # Retrieve SCCM site code from site server specified by user
    $Global:SccmSiteCode = GetSiteCode $SccmServer

    # Setup Excel logging

    # Delete accounts that have been disabled for X days
    DeleteDisabledAccounts $DeleteAge

    # Disable accounts that are older than X days
    DisableOldAccounts $TargetDn $DisableAge

    LogMessage "Completed workstation account cleanup script" 1


Posted in configmgr, powershell, scripting, tools, wmi | Tagged: , , , , , , , , , | 11 Comments »

ccmsetup.exe: Trailing slash in “Source” parameter

Posted by Trevor Sullivan on 2010/08/05

Hello everyone!

Tonight, while I’m traveling in Houston, Texas, I wrestled with ccmsetup.exe for a little while. I was working on getting a ConfigMgr vNext client agent installed on a Windows 7 Ultimate virtual machine, and kept getting a message in my ccmsetup.log saying “Source <path> is inaccessible.


A little bit about %~dp0

Now, whenever I build installer packages for software, whether to run manually, or distribute through ConfigMgr, I generally wrap the commands inside a simple batch file, so I don’t have to constantly type out the entire command, or forget what a particular parameter should be set to.

Because I write lots of simple batch files, and use these packages from both UNC paths, as well as local installs, I use a nifty batch trick: %~dp0. Without going into too much detail, if you use %~dp0 in a batch file, it will reference the folder in which the batch file resides, including a trailing slash. For example, if you want to run a MSI package from a local or UNC path, you could write a dynamic batch file like so:

msiexec /i “%~dp0MyPackage.msi” /quiet

In the above command, the double quotes will take care of any spaces in the path, and the %~dp0 will reference the folder path in which the batch file itself resides. This command also assumes that a Windows Installer package called “MyPackage.msi” resides in the same folder alongside the batch file. Using %~dp0 is great, because no matter where I copy an entire package folder, I always know that I can execute a batch file, and it will use appropriate relative pathing to find the supporting files necessary to run the command.

Anyway, that’s enough history about %~dp0 and why I use it!


The Trailing Slash Issue

Installing the ConfigMgr client is generally quite simple: you can simply execute ccmsetup.exe by double-clicking it, or invoking it, with no parameters from a script. In this circumstance, however, I needed to specify the source folder for where to get the files, otherwise ccmsetup.exe was going to point to my ConfigMgr 2007 management point, rather than the ConfigMgr vNext management point that I wanted it to get the files from. Instead of using the /MP parameter, I elected to simply point ccmsetup.exe to the specific folder where the source files resided (on the ConfigMgr network share).

Naturally, since I was building my agent install batch file right inside the client folder on the site server (\\vnext01\sms_vnx\client), I simply used “%~dp0” to point ccmsetup.exe to its own folder (which was a UNC path). The command I placed into the batch file was the following:

“%~dp0ccmsetup.exe” /retry:1 /source:”%~dp0″ FSP=vnext01.ts.loc

Upon execution of this batch file, is when I experienced the “inaccessible source” error message in ccmsetup.log, as described in my opening paragraph. After encountering this issue, I experimented with a number of different permutations of the command line, none of which worked really. I finally realized that, as best I can tell, ccmsetup.exe appeared to be incorrectly parsing the value passed to the /source parameter. If you include a trailing slash on the <path> you pass to /source, then ccmsetup.exe will actually use the remainder of the command line as the <path>. This is very bad!

In short, if you want ccmsetup.exe to succeed at finding its source files, you must not include a trailing slash on the value you feed into the /source parameter!

The only scenario I have not yet tested, is passing a <path> to /source, that has a trailing slash, but does not have double-quotes around the path. I always recommend using double-quotes for any command-line value that may have spaces in it, so I would not really even consider this scenario, other than for the sake of testing.



Well, I hope this has given some of you some insight into a potential problem, and maybe even saves you a few gray hairs by finding this on Google, instead of stressing over what you are doing wrong. It appears to be a small software bug, and I’m somewhat surprised that no one else seems to have encountered it, but there’s a first time for everything, right?

If you have any feedback, or would like to ask a question, feel free to leave a comment on this article, or e-mail me directly at


Update: If you’d like to help out by recreating the issue, posting your comments, upvoting the bug report on Microsoft Connect, that would be fabulous! Make sure you log into Microsoft Connect, join the ConfigMgr vNext Beta Program, and then click the following link:

If you can reproduce the issue, make sure to click the “I can too” link on the above page.

Posted in configmgr, ConfigMgr vNext, fixes | Tagged: , , , , , , , , , , | Leave a Comment »