SCCM – Duplicate Device Records

A few days back I was investigating duplicate device records in SCCM. Basically, there are two major reasons for duplicate device records:

  1. Reinstalling a device
  2. Active Directory delta discovery

For the first reason SCCM has some built-in solutions. In the Hierarchy Settings there’s an option for conflicting client records. This option looks at the hardware ID’s to detect duplicates and gives you the option to automatically resolve the conflict or to do it manually.

There’s an old Microsoft article that describes how SCCM (SMS) handles duplicate ID’s and how you can find them.

If you look at the image you can see that in my test environment the option to manually resolve conflicts is selected. One way of resolving the conflicts manually is to create a Status Filter Rule.

Event 2642:

Configuration Manager has detected a record that might be conflicting with the following client record in the site database: <GUID> . Possible cause: <GUID> has been imaged, recovered from backup, or migrated to a new computer. Solution: In the Configuration Manager console, under Computer Management, in the Conflicting Records node, right click <GUID> and then choose one of the following options: Merge to match the conflicting record with the existing record, New to create a new client record, or Block to block this record from being a client.

BTW the location in SCCM 1702 is Monitoring, System Status, Conflicting Records node.

MergeConflictingRecords.ps1

####################################################################################
#                                                                                  #
# Configuration                                                                    #
#                                                                                  #
####################################################################################

# SCCM Servername
Set-Variable serverName "servername" -Option Constant

# test = $true: shows information about conflicting records without merging them.
# test = $false: shows information about conflicting records and merges them.
Set-Variable test $false -Option Constant

####################################################################################
#                                                                                  #
# Functions                                                                        #
#                                                                                  #
####################################################################################

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

function Log
{
    param
    (
        [Parameter(Mandatory=$true)]
        [String]
        $message
    ,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Info","Debug","Warn","Error")]
        [String]
        $type
    ,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Console","LogFile","Both")]
        [String]
        $outputMode
    )
    
    $dateTimeString = Get-Date -Format "yyyy-MM-dd HH:mm:sszz"
    $output = ($dateTimeString + "`t" + $type.ToUpper() + "`t" + $message)
    
    if ($outputMode -eq "Console" -OR $outputMode -eq "Both")
    {
        Write-Host $output
    }
    
    if ($outputMode -eq "LogFile" -OR $outputMode -eq "Both")
    {
        try
        {
            Add-Content $logFile -Value $output -ErrorAction Stop
        }
        catch
        {
			Log ("Failed to write to log file: """ + $logFile + """.") -OutputMode Console -Type Error
        	Log ("[" + $_.Exception.GetType().FullName + "] " + $_.Exception.Message) -OutputMode Console -Type Error
        }
    }
}

function GetSCCMSiteCode
{
    try
    {    
        $sccmProvider = Get-WMIObject -ComputerName $serverName -Namespace "root\SMS" -Class "SMS_ProviderLocation"
        $sccmProvider | foreach-object{if ($_.ProviderForLocalSite -eq $true){$siteCode=$_.sitecode}}
    }
    catch
    {
        Log ("[" + $_.Exception.GetType().FullName + "] " + $_.Exception.Message) -OutputMode Both -Type Error
        exit 1
    }
    
    if (!$siteCode)
    {
        Log ("Failed to determine site code.") -OutputMode Both -Type Error
        exit 1
    }
    
    return $siteCode
}

function MergeRecords
{
	param
    (
        [Parameter(Mandatory=$true)]
        [String]
        $SMSID
	)
    
    $pendingRegClass = [WmiClass]("\\$serverName\ROOT\SMS\Site_" + $sccmSiteCode + ":SMS_PendingRegistrationRecord")
    
    $inParams = $pendingRegClass.PSBase.GetMethodParameters("ResolvePendingRegistrationRecord")
    $inParams.SMSID = $SMSID
    $inParams.Action = 1 # Action 1 = Merge
	
    Log ("Merging records...") -OutputMode Both -Type Info
	
	try
    {
		$result = $pendingRegClass.PSBase.InvokeMethod("ResolvePendingRegistrationRecord", $inParams, $Null)
		if ($result["returnValue"] -eq "0")
		{
			Log ("Successfully merged records. Return value: " + $result["returnValue"]) -OutputMode Both -Type Info
		}
		else
		{
			Log ("Failed to merge records! Return value: " + $result["returnValue"]) -OutputMode Both -Type Error
		}
	}
    catch
    {
        Log ("Failed to merge records!") -OutputMode Both -Type Error
        Log ("[" + $_.Exception.GetType().FullName + "] " + $_.Exception.Message) -OutputMode Both -Type Error
    }
}

####################################################################################
#                                                                                  #
# Main Code                                                                        #
#                                                                                  #
####################################################################################

# Logfile
Set-Variable logFile ((Get-ScriptDirectory) + "\MergeConflictingRecords.log") -Option Constant -ErrorAction SilentlyContinue

# Determine SCCM Site Code
Set-Variable sccmSiteCode (GetSCCMSiteCode) -Option Constant -ErrorAction SilentlyContinue

# Get conflicting records
$pendingRegistrations = Get-WmiObject -class "SMS_PendingRegistrationRecord" -namespace ("root\sms\site_" + $sccmSiteCode) -impersonation 3 -computername $serverName

# Merge conflicting records
if ($pendingRegistrations)
{
    Log ("Found conflicting records.") -OutputMode Both -Type Info
    
    foreach ($item in $pendingRegistrations)
    {
        Log ("**************************************************************************************************************************************") -OutputMode Both -Type Info
        
        Log ("NetBIOS Name: " + $item.NetBiosName) -OutputMode Both -Type Info
        Log ("SMSID: " + $item.SMSID) -OutputMode Both -Type Info
        Log ("Conflicting SMSID: " + $item.ConflictSMSID) -OutputMode Both -Type Info
        Log ("Hardware ID: " + $item.HardwareID) -OutputMode Both -Type Info
        
		if (!$test)
    	{
        	MergeRecords -SMSID $item.SMSID
		}
        
        Log ("**************************************************************************************************************************************") -OutputMode Both -Type Info
    }
}
else
{
    Log ("No conflicting records found.") -OutputMode Both -Type Info
}

However, for the second reason there is no built-in solution. So I had to create one.

The first step was getting some background information about the issue. There’s a Microsoft article about the issue.

As you can see the article is old (2011) and it’s not using powershell. So I rewrote the script from VB to powershell. One other change is that the VB script kicks of a full AD discovery but I will use the regular scheduled delta discovery method. For this to work the AD object must be changed. I found one blog that uses the same approach but uses an EXE file for this.  I decided to incorporate this into the powershell script.

DeleteDuplicateRecords.ps1

<# .SYNOPSIS Deletes duplicate device records .DESCRIPTION Use this script if you need to delete duplicate device records because of a timing issue between OSD and AD delta discovery .PARAMETER SiteServer Site server name with SMS Provider installed .PARAMETER MachineName Name of the duplicate device .EXAMPLE .\DeleteDuplicateRecords.ps1 -SiteServer "SERVER" -MachineName "MACHINE" Deletes duplicate records of the device with the name "MACHINE" .NOTES Script name: DeleteDuplicateRecords.ps1 Author: Jeroen Buren DateCreated: 02-06-2017 #>

[CmdletBinding(SupportsShouldProcess=$true)]

param(
    [parameter(Mandatory=$true,HelpMessage="Site server where the SMS Provider is installed")]
    [ValidateScript({Test-Connection -ComputerName $_ -Count 1 -Quiet})]
    [string]$SiteServer,
    [parameter(Mandatory=$true,HelpMessage="Name of the device")]
    [string]$MachineName
)

####################################################################################
#                                                                                  #
# Functions                                                                        #
#                                                                                  #
####################################################################################

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

function Log {
    param
    (
        [Parameter(Mandatory=$true)]
        [String]
        $message
    ,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Info","Debug","Warn","Error")]
        [String]
        $type
    ,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Console","LogFile","Both")]
        [String]
        $outputMode
    )
    
    $dateTimeString = Get-Date -Format "yyyy-MM-dd HH:mm:sszz"
    $output = ($dateTimeString + " " + $type.ToUpper() + " " + $message)
    
    if ($outputMode -eq "Console" -OR $outputMode -eq "Both")
    {
        Write-Host $output
    }
    
    if ($outputMode -eq "LogFile" -OR $outputMode -eq "Both")
    {
        try
        {
            Add-Content $logFile -Value $output -ErrorAction Stop
        }
        catch
        {
			Log ("Failed to write to log file: """ + $logFile + """.") -OutputMode Console -Type Error
        	Log ("[" + $_.Exception.GetType().FullName + "] " + $_.Exception.Message) -OutputMode Console -Type Error
        }
    }
}

function GetSCCMSiteCode {
    try
    {    
        $sccmProvider = Get-WMIObject -ComputerName $SiteServer -Namespace "root\SMS" -Class "SMS_ProviderLocation"
        $sccmProvider | foreach-object{if ($_.ProviderForLocalSite -eq $true){$siteCode=$_.sitecode}}
    }
    catch
    {
        Log ("[" + $_.Exception.GetType().FullName + "] " + $_.Exception.Message) -OutputMode Both -Type Error
        exit 1
    }
    
    if (!$siteCode)
    {
        Log ("Failed to determine site code.") -OutputMode Both -Type Error
        exit 1
    }
    
    return $siteCode
}

####################################################################################
#                                                                                  #
# Main Code                                                                        #
#                                                                                  #
####################################################################################

# Logfile
Set-Variable logFile ((Get-ScriptDirectory) + "\DeleteDuplicateRecords.log") -Option Constant -ErrorAction SilentlyContinue

# Determine SCCM Site Code
Set-Variable sccmSiteCode (GetSCCMSiteCode) -Option Constant -ErrorAction SilentlyContinue

# If you give the computer a new machine name, you need to query for the new machine name
$StatusMessageQuery = "select RecordID from SMS_StatMsg where MessageID = 11171 and MachineName = '" + $MachineName + "' order by RecordID desc"
$StatusMessages = gwmi -Query $StatusMessageQuery -Namespace ("root\sms\site_" + $sccmSiteCode) -ComputerName $SiteServer

If ($StatusMessages.Count -lt 1) {
    Log ("No Status Message with ID = 11171 and MachineName = $MachineName found. Exiting...") -OutputMode LogFile -Type Info
    #Exit   
}
Else {
    $RecordID = $StatusMessages[0].RecordID
    Log ("Status Message RecordID = $RecordID") -outputMode LogFile -type Info
    $StatusMessageAttributeQuery = "select AttributeValue from SMS_StatMsgAttributes where RecordID = '" + $RecordID + "' and AttributeID = 408"
    $StatusMessageAttributes = gwmi -Query $StatusMessageAttributeQuery -Namespace ("root\sms\site_" + $sccmSiteCode) -ComputerName $SiteServer
    If ($StatusMessageAttributes.Count -lt 1) {
        Log ("No Status Message Attribute with AttributeID = 408 and RecordID = $RecordID found. Exiting...") -outputMode LogFile -type Info
    }
    Else {
        $GUID = $StatusMessageAttributes[0].AttributeValue
        Log ("SMS Client GUID = $GUID") -outputMode LogFile -type Info
        $MachineNameQuery = "select NetbiosName from SMS_R_System where SMSUniqueIdentifier = '" + $GUID + "'"
        $MachineNames = gwmi -Query $MachineNameQuery -Namespace ("root\sms\site_" + $sccmSiteCode) -ComputerName $SiteServer
        If ($MachineNames.Count -lt 1) {
            Log ("No Systems with SMSGUID = $GUID found. Using the Machine Name in the status message.") -outputMode LogFile -type Info
        }
        Else {
            $MachineName = $MachineNames[0].NetbiosName
            Log ("New MachineName = $MachineName" ) -outputMode LogFile -type Info
        }
    }
}

# Find the system with the specific machine name.

$duplicateRecordsQuery = "select * from SMS_R_System where NetBIOSName = '" + $MachineName + "'"
$duplicateRecords = gwmi -Query $duplicateRecordsQuery -Namespace ("root\sms\site_" + $sccmSiteCode) -ComputerName $SiteServer

If ($duplicateRecords.Count -lt 1) {
    Log ("Didn't find a duplicate record for the machine, exiting...") -outputMode LogFile -type Info
}
Else {
    # Delete if the Client, Client Type, Hardware ID, SMBIOSGUID, SMSUniqueIdentifier is null
    Foreach ($item in $duplicateRecords) {
        $Active = $item.Active
        $Client = $item.Client
        $ClientType = $item.ClientType
        $HardwareID = $item.HardwareID
        $ResourceId = $item.ResourceId
        $SMBIOSGUID = $item.SMBIOSGUID
        $SMSUniqueIdentifier = $item.SMSUniqueIdentifier
        If (($Active -eq $null) -and ($Client -eq $null) -and ($ClientType -eq $null) -and ($HardwareID -eq $null) -and ($SMBIOSGUID -eq $null) -and ($SMSUniqueIdentifier -eq $null)) {
            Log ("Delete this one: ResourceId = $ResourceId") -outputMode LogFile -type Info
            # Delete Record when there's duplicate and it's active/SMBIOSGUID, etc is null
            $item.Delete()
            Log ("Deleted item: ResourceId = $ResourceId") -outputMode LogFile -type Info
        }
    }
}

# Modify AD computer object so delta discovery will pick this up.
Get-ADComputer -Identity $MachineName | Set-ADComputer -Replace @{adminDescription="Touched by SCCM - $dateTimeString"}