A Dickens of a DNS Puzzle: How to clean up those stale AD site DNS records (with PowerShell of course)

Typically DNS scavenging takes care of old DNS records, and that is the recommended path of resolution.  For those who choose not to use scavenging, this post may be of some assistance.

Frequently when I visit customers I find a graveyard down in the DNS tree where stale AD sites live like the ghost of Christmas past.  These sites were once happy and thriving in the AD forest, but they got the ax.  Now their old DNS records are just taking up space and cluttering the zone.

Now you may ask yourself, "Doesn't AD delete those DNS records when I delete the site?"  In most cases that is true, however some site DNS records find a way to haunt you and rattle their chains in the middle of the night.  Here are some examples of the records in question:

_ldap._tcp.Bogus1._sites.wingtiptoys.local. 600 IN SRV 0 100 389 dcA.wingtiptoys.local.
_kerberos._tcp.Bogus1._sites.dc._msdcs.wingtiptoys.local. 600 IN SRV 0 100 88 dcA.wingtiptoys.local.
_ldap._tcp.Bogus1._sites.dc._msdcs.wingtiptoys.local. 600 IN SRV 0 100 389 dcA.wingtiptoys.local.
_kerberos._tcp.Bogus1._sites.wingtiptoys.local. 600 IN SRV 0 100 88 dcA.wingtiptoys.local.
_ldap._tcp.Bogus1._sites.ForestDnsZones.wingtiptoys.local. 600 IN SRV 0 100 389 dcA.wingtiptoys.local.
_ldap._tcp.Bogus1._sites.DomainDnsZones.wingtiptoys.local. 600 IN SRV 0 100 389 dcA.wingtiptoys.local.

Deleting these records manually is quite tedious, and it would be easy to miss one.  It would also be easy to mistakenly delete the good records.  Enter PowerShell...

This script cleans stale AD site DNS records by comparing the current AD site list to each DNS site record and deleting the ones where the site no longer exists. Results are logged to a tab-delimited text file for documentation.
 
If by some chance valid DNS records are purged simply restart the NetLogon service on all DCs in the affected site to re-register the records. Note that this should not happen.
 
If all root, child, and _msdcs zones are hosted on the forest root, then you should only have to run this once. Otherwise you can rerun it for each child domain, targeting a DNS server and zone for each using the switches.
 
Syntax:
.\Remove-SiteDNS.ps1 [-Zone foo.com] [-DNSServer dc1.foo.com] [-LogFile log.txt] [-WhatIf]
Be sure to add the -WhatIf switch the first time you run it to see what will be deleted.
Running without switches will produce the following defaults:
Zone - domain name of the forest root
DNSServer - PDC emulator of the forest root (assuming it is running DNS)
LogFile - DNSSitesLog.txt
WhatIf - Since no WhatIf switch is specified deletes will be active

Once these ghostly records are gone we're back to "It's A Wonderful Life".  Cheers.

PS - I'll say it again. This is a destructive (but elegant) script. Run with the -WhatIf switch in a test lab first and examine the log file before you turn it loose in production.  

#==============================================================================           
# Remove-SiteDNS           
# December 2010           
# Ashley McGlone, Microsoft PFE           
# https://blogs.technet.com/b/ashleymcglone           
#           
# This script cleans stale AD site DNS records by comparing the current AD site           
# list to each DNS site record and deleting the ones where the site no longer           
# exists. Results are logged to a tab-delimited text file for documentation.           
#           
# If by some chance valid DNS records are purged simply restart the NetLogon           
# service on all DCs in the affected site to re-register the records. Note           
# that this should not happen.           
#           
# If all root, child, and _msdcs zones are hosted on the forest root, then you           
# should only have to run this once. Otherwise you can rerun it for each           
# child domain, targeting a DNS server and zone for each using the switches.           
#           
# Syntax:           
# .\Remove-SiteDNS.ps1 -Zone foo.com -DNSServer dc1.foo.com -LogFile log.txt           
# Add the -WhatIf switch the first time you run it to see what will be deleted.           
# Running without switches will use the following defaults:           
# Zone - domain name of the forest root           
# DNSServer - PDC emulator of the forest root (assuming it is running DNS)           
# LogFile - DNSSitesLog.txt           
# WhatIf - Since no WhatIf switch is specified deletes will be active           
#==============================================================================           
Param (           
    $Zone,           
    $DNSServer,           
    $LogFile,           
    [switch]$WhatIf           
)           
           
# Clear errors and stop if any are encountered           
$Error.psbase.clear()           
$ErrorActionPreference = "Stop"           
           
#==============================================================================           
function ParseSiteNameFromDNSRecord {           
# Take a DNS record string as a parameter           
# Return just the site name portion of the data in lower case           
    Param (           
        $DNSRecord           
    )           
           
    # _kerberos._tcp.Bogus1._sites.wingtiptoys.local IN SRV 0 100 88 dca.wingtiptoys.local.           
    $x = ($DNSRecord -split "._sites.")[0]           
    # _kerberos._tcp.Bogus1           
    $y = $x.split(".")           
    $z = $y[-1]  # Last index of array           
    # bogus1           
    Return $z.toLower()           
}           
#==============================================================================           
           
#==============================================================================           
function CleanDNSSites {           
# Take a zone name and DNS server as a parameter (required by WMI)           
# Delete the invalid DNS entries           
# Return the number of invalid site records in DNS           
    Param (           
        $Zone,           
        $DNSServer           
    )           
           
    $DeleteCount = 0           
           
    # This WMI query will return all DNS records on the server as long as the           
    # end of the FQDN matches the parent zone. In other words, even though           
    # parentzone.com and child.parentzone.com may be stored in separate zones           
    # on the server, this one WMI query will return all records that match           
    # *.parentzone.com. We filter these results for only the AD site DNS           
    # records.           
    $src = Get-WMIObject -ComputerName $DNSServer -Namespace 'root\MicrosoftDNS' `
           -Class MicrosoftDNS_ResourceRecord | Where-Object { `
           ($_.ContainerName -like "*$Zone") -and `
           ($_.TextRepresentation -like "*._sites.*")}           
           
    ForEach ($srcRec in $src) {           
           
        $DNSRecordText = $srcRec.TextRepresentation           
        $DNSRecordSiteName = ParseSiteNameFromDNSRecord $DNSRecordText           
           
        # If DNSRecordSiteName is not in the list of forest sites then delete.           
        # Use lowercase and the semicolon delimiters to make sure we get an           
        # exact match.           
        If ($ADSiteList.Contains(";$DNSRecordSiteName;"))           
            {           
                "Valid`t$DNSRecordSiteName`t$DNSRecordText" | `
                    Out-File -FilePath $LogFile -Append           
            }           
        Else           
            {           
                ++$DeleteCount           
                "Invalid`t$DNSRecordSiteName`t$DNSRecordText" | `
                    Out-File -FilePath $LogFile -Append           
                If ($WhatIf)           
                    # Leave the record           
                    {   }           
                Else           
                    # Delete the record           
                    { $srcRec.Delete()  }           
            }           
    }           
           
    Return $DeleteCount           
}           
#==============================================================================           
           
           
# Import AD cmdlets           
# Normally we would put this on the first line of the script,           
# but PARAM has to be on the first line.           
Import-Module ActiveDirectory           
           
# Normally we would specify parameter default values in the PARAM block,           
# but we cannot initialize the AD cmdlets before the PARAM block.           
# Default DNS zone name is the forest root domain name.           
If ($Zone -eq $NULL) { $Zone = (Get-ADForest).RootDomain }           
# Default DNS server name is the PDC emulator of the forest root.           
# We assume it is running DNS, since we didn't get a parameter passed in.           
If ($DNSServer -eq $NULL) { $DNSServer = (Get-ADDomain -Identity $Zone).PDCEmulator }           
# Set default log file name.           
If ($LogFile -eq $NULL) { $LogFile = "DNSSitesLog.txt" }           
           
# Populate header data in log file.           
"Start time: $(Get-Date)" | Out-File -FilePath $LogFile           
"Zone: $Zone" | Out-File -FilePath $LogFile -Append           
"DNSServer: $DNSServer" | Out-File -FilePath $LogFile -Append           
"LogFile: $LogFile" | Out-File -FilePath $LogFile -Append           
"WhatIf: $WhatIf" | Out-File -FilePath $LogFile -Append           
           
# One-liner to put all of the forest AD sites into an array.           
# We don't use the default collection, because it won't let us modify the           
# values to lower case to guarantee an exact match in case the DNS records           
# come back in all lower case.           
# We create a single string list of all sites semicolon delimited for           
# quicker matching later.           
$ADSiteArray = @((Get-ADForest).Sites)           
$ADSiteList = ";"           
For ($i=0; $i -lt $ADSiteArray.length; $i++) {           
    $ADSiteList += $ADSiteArray[$i].ToLower() + ";"           
}           
"AD Sites: $ADSiteList" | Out-File -FilePath $LogFile -Append           
           
# Call the main function and return the number of stale DNS entries.           
$Total = CleanDNSSites $Zone $DNSServer           
           
"Total Invalid: $Total" | Out-File -FilePath $LogFile -Append           
If ($WhatIf)           
    { "Invalid site DNS entries logged only." | `
        Out-File -FilePath $LogFile -Append }           
Else           
    { "Invalid site DNS entries DELETED." | `
        Out-File -FilePath $LogFile -Append }           
           
"End time: $(Get-Date)" | Out-File -FilePath $LogFile -Append           

 

 

Remove-SiteDNS.p-s-1.txt