In System Center Configuration Manager there are 2 Site Maintenance tasks that help take care of stale or obsolete client records: Delete Aged Discovery Data and Delete Inactive Client Discovery Data. However in some cases some records can remain in SCCM and are not removed by these tasks, for example, when a system is no longer active but the computer account has not been deleted or disabled in Active Directory. AD System Discovery will continue to pick this system up and create a record for it, so maintenance of AD computer accounts is essential for a healthy ConfigMgr environment.
As long as a client is disabled or deleted from Active Directory and does not get picked up by any of the discovery methods, it will eventually get deleted from SCCM according to the schedule defined in these maintenance tasks.
For computer accounts that are disabled however, you might not want to wait for the maintenance tasks to remove them since you know they are no longer active and can safely be deleted from the SCCM database. Removing them sooner can improve compliance figures for deployment reporting, for example. For this scenario, I prepared a PowerShell script that can run as a scheduled task and remove any system that is marked as inactive – or does not have the SCCM client installed – and is also disabled or not present in active directory.
I’ll run through the script quickly here to explain what it does.
First we define some variables to be used in the script, such as the site code, the site server, and the email information (the script will email the SCCM admin with the list of systems it deletes):
$SiteCode = "ABC" $SiteServer = "SCCMSERVER-01" $EmailRecipient = "joe.blow@contoso.com" $EmailSender = "PowerShell@contoso.com" $SMTPServer = "SMTPServer.contoso.com"
Then we create a list of discovered systems in SCCM that are either inactive (determined by client health data) or have no SCCM client installed, using WMI on the site server:
try { $DevicesToCheck = Get-WmiObject -ComputerName $SiteServer -Namespace root\SMS\Site_$SiteCode -Query "SELECT SMS_R_System.Name FROM SMS_R_System left join SMS_G_System_CH_ClientSummary on SMS_G_System_CH_ClientSummary.ResourceID = SMS_R_System.ResourceID where (SMS_G_System_CH_ClientSummary.ClientActiveStatus = 0 or SMS_R_System.Active = 0 or SMS_R_System.Active is null)" -ErrorAction Stop | Select -ExpandProperty Name | Sort } Catch { $_ }
Next we check active directory for each system to find the status of the computer account and filter those that are either disabled or not present in AD using a custom function. Note that this function uses .Net to search AD so it is not dependent on having the RSAT tools installed.
$NotEnabledDevices = ($DevicesToCheck | foreach { Get-IsComputerAccountDisabled -Computername $_ }) | where {$_.IsDisabled -ne $False}
Then we will attempt to remove each resulting system from SCCM using another custom function. This function uses WMI rather than the ConfigMgr PS cmdlets, again to remove the dependency.
$DeletedRecords = New-Object System.Collections.ArrayList $Output = $NotEnabledDevices| foreach { Remove-CMRecord -Computername $_.ComputerName -DeviceStatus $_.IsDisabled -SiteCode $SiteCode -SiteServer $SiteServer } $DeletedRecords.AddRange(@($Output))
Finally we send an HTML formatted email to the SCCM administrator with the list of systems that were deleted from SCCM:
If ($DeletedRecords.Count -ne 0) { $Body = $DeletedRecords | ConvertTo-Html -Head $style -Body " <h2>The following computer accounts are either disabled or not present in active directory and have been deleted from SCCM</h2> " | Out-String Send-MailMessage -To $EmailRecipient -From $EmailSender -Subject "Disabled Computer Accounts Deleted from SCCM ($(Get-Date -format 'yyyy-MMM-dd'))" -SmtpServer $SMTPServer -Body $body -BodyAsHtml }
To verify in SCCM the systems that were deleted you can view the All Audit Status Messages from a Specific Site status message query.
IMPORTANT: As this script deletes records from the SCCM database, please be sure to understand what it does and to test it first before using it in any production environment!
Here is the full script:
$SiteCode = "ABC" $SiteServer = "SCCMSERVER-01" $EmailRecipient = "joe.blow@contoso.com" $EmailSender = "PowerShell@contoso.com" $SMTPServer = "SMTPServer.contoso.com" #region Functions # Function to check with the computer account is disabled function Get-IsComputerAccountDisabled { param($Computername) $root = [ADSI]'' $searcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList ($root) $searcher.filter = "(&(objectClass=Computer)(Name=$Computername))" $Result = $searcher.findall() If ($Result.Count -ne 0) { $Result | ForEach-Object { $Computer = $_.GetDirectoryEntry() [pscustomobject]@{ ComputerName = $Computername IsDisabled = $Computer.PsBase.InvokeGet("AccountDisabled") } } } Else { [pscustomobject]@{ ComputerName = $Computername IsDisabled = "Not found in AD" } } } # Function to remove the device record from SCCM via WMI function Remove-CMRecord { [CmdletBinding()] Param ( [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] [string] $Computername, [Parameter(Mandatory = $True)] [string] $SiteCode, [Parameter(Mandatory = $True)] [string] $SiteServer, [Parameter(Mandatory = $True)] [string] $DeviceStatus ) Begin { $ErrorAction = 'Stop' } Process { # Check if the system exists in SCCM Try { $Computer = [wmi] (Get-WmiObject -ComputerName $SiteServer -Namespace "root\sms\site_$SiteCode" -Class sms_r_system -Filter "Name = `'$Computername`'" -ErrorAction Stop).__PATH } Catch { $Result = $_ Continue } # Delete it Try { $Computer.psbase.delete() } Catch { $Result = $_ Continue } # Check that the delete worked Try { $Computer = [wmi] (Get-WmiObject -ComputerName $SiteServer -Namespace "root\sms\site_$SiteCode" -Class sms_r_system -Filter "Name = `'$Computername`'" -ErrorAction Stop).__PATH } Catch { } # Report result If ($Computer.PSComputerName) { $Result = "Tried to delete but record still exists" } Else { $Result = "Successfully deleted" } } End { [pscustomobject]@{ DeviceName = $Computername IsDisabled = $DeviceStatus Result = $Result } } } #endregion # Let's get the list of inactive systems, or systems with no SCCM client, from WMI try { $DevicesToCheck = Get-WmiObject -ComputerName $SiteServer -Namespace root\SMS\Site_$SiteCode -Query "SELECT SMS_R_System.Name FROM SMS_R_System left join SMS_G_System_CH_ClientSummary on SMS_G_System_CH_ClientSummary.ResourceID = SMS_R_System.ResourceID where (SMS_G_System_CH_ClientSummary.ClientActiveStatus = 0 or SMS_R_System.Active = 0 or SMS_R_System.Active is null)" -ErrorAction Stop | Select -ExpandProperty Name | Sort } Catch { $_ } # Now let's filter those systems whose AD account is disabled or not present $NotEnabledDevices = ($DevicesToCheck | foreach { Get-IsComputerAccountDisabled -Computername $_ }) | where {$_.IsDisabled -ne $False} # Then we will delete each record from SCCM using WMI $DeletedRecords = New-Object System.Collections.ArrayList $Output = $NotEnabledDevices| foreach { Remove-CMRecord -Computername $_.ComputerName -DeviceStatus $_.IsDisabled -SiteCode $SiteCode -SiteServer $SiteServer } $DeletedRecords.AddRange(@($Output)) # Finally we will send the list of affected systems to the administrator $style = @" <style> body { color:#333333; font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;} } h1 { text-align:center; } table { border-collapse: collapse; font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif; } th { font-size: 10pt; text-align: left; padding-top: 5px; padding-bottom: 4px; background-color: #1FE093; color: #ffffff; } td { font-size: 8pt; border: 1px solid #1FE093; padding: 3px 7px 2px 7px; } </style> "@ If ($DeletedRecords.Count -ne 0) { $Body = $DeletedRecords | ConvertTo-Html -Head $style -Body " <h2>The following systems are either disabled or not present in active directory and have been deleted from SCCM</h2> " | Out-String Send-MailMessage -To $EmailRecipient -From $EmailSender -Subject "Disabled Computer Accounts Deleted from SCCM ($(Get-Date -format 'yyyy-MMM-dd'))" -SmtpServer $SMTPServer -Body $body -BodyAsHtml }
Thanks for the script, I ran this in my environment and it purged a few computers as expected. When I re-ran the script I get an error below…
Exception calling “AddRange” with “1” argument(s): “Collection cannot be null.
Parameter name: c”
At C:\Users\administrator.VAND1\Desktop\PowerShell Scripts\CM-ComputerCleanup.ps1:134 char:28
+ $DeletedRecords.AddRange($($NotEnabledDevices| foreach {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentNullException
This is probably happening because there are no entries returned the second time the script is invoked. Is there a way to trap this error and have the execution of the code exit out without errors if there are no hits?
Sure, after line 129, wrap the rest of the code in an IF statement, like this:
if ($NotEnabledDevices)
{
}
Then it will only execute if there are devices to process.
Thanks for the nice script. Since I am not master in PowerShell if you could help me on the below error I will be thankful
this error after adding below suggested line
if ($NotEnabledDevices)
{
}
Cannot convert argument “c”, with value: “@{DeviceName=(Hostanme showing ); IsDisabled=Not found in AD; Result=Successfully deleted}”, for “AddRange” to type “System.Collections.ICollection”: “Cannot convert the “@{DeviceName=Hostanme showing;
IsDisabled=Not found in AD; Result=Successfully deleted}” value of type “System.Management.Automation.PSCustomObject” to type “System.Collections.ICollection”.”
At line:136 char:28
+ $DeletedRecords.AddRange($($NotEnabledDevices| foreach {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
This could be because only one result is being returned therefore the AddRange() method is not recognizing it as an array, therefore not technically a range. You could try casting it into an array like this:
$DeletedRecords.AddRange([array]$($NotEnabledDevices …
If we support Workgroup machines too (Which in my case is) would this end up deleting those machines from SCCM?
Hi Trevor,
nice script, how we can just compare sccm and ad but not delete
just create report
Thanks
Just comment out the following line:
Remove-CMRecord -Computername $_.ComputerName -DeviceStatus $_.IsDisabled -SiteCode $SiteCode -SiteServer $SiteServer
Thanks
If i just comment this line, the script return an error :
Exception calling “AddRange” with “1” argument(s): “Collection cannot be null.
Parameter name: c”
At C:\Tools\CM-ComputerCleanup.ps1:133 char:28
+ $DeletedRecords.AddRange($($NotEnabledDevices| foreach {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentNullException
Try adding the code:
if ($NotEnabledDevices)
{
}
that I mentioned in a previous comment
Hi. Very nice work. Would there be a way to specify the domain in the case of multiple trusted / Untrusted forests?
Does it work on SCCM 2012 1610 ?
I dont receive email and no error appart ?
It will only send an email if it actually deleted any records.
thanks for this script… I get an empty return on $DeletedRecords if I do an write-host $DeletedRecords right after the following line:
$DeletedRecords = New-Object System.Collections.ArrayList
So it seems that New-Object instructions doesn’t work in my case. Script is ran with admin rights directly on the SCCM site server. $NotEnabledDevices does actually return the list of devices that are disabled or not found.
it seems that the pscustomobject part isn’t doing what it’s supposed to do :s
I changed the script a bit so that an array is always read into the $DeletedRecords list. Does that work for you?
thanks! Indeed that works better in my case 🙂
I’m having a little bit of difficulty. I’m running the script, but not getting any output in the console nor am I getting the email. Is this confirmed working on 1710?
How would I change the script to save to a file instead of emailing results?