If you work with SCCM and you use AD Forest Discovery to automatically create boundaries from AD Sites or Subnets, you know how important it is for AD to stay up to date with the current information. And when something is changed in Sites or Subnets, you need to be made aware of it so you can reflect the change in your SCCM boundaries and boundary groups. Unfortunately, communication between IT teams is not always what it should be, so I wrote this script to run as a scheduled task and keep an eye on any changes made in AD Sites and IP subnets.
The script works by retrieving the current site and subnet information and exporting it to cache files. The next time the script runs, it will compare the current information with the information in the cached files, and if anything has changed, a report will be sent by email detailing the changes.
It’s one way of ensuring you’re keeping SCCM in sync with your AD!
###################################################################################### | |
## ## | |
## This script compares the current list of AD sites and subnets with a cached list ## | |
## If anything has changed, the cached list will be updated and the changes emailed ## | |
## ## | |
###################################################################################### | |
################ | |
## PARAMETERS ## | |
################ | |
# Location of cache files | |
$ADSitesFile = "G:\Scheduled Task Scripts\Cache Files\ADSites.csv" | |
$ADSubnetsFile = "G:\Scheduled Task Scripts\Cache Files\ADSubnets.csv" | |
# Email parameters | |
$EmailParams = @{ | |
smtpserver = "contoso-com.mail.protection.outlook.com" | |
To = "SCCMAdmins@contoso.com" | |
From = "SCCMReports@contoso.com" | |
Subject = "Active Directory Site and Subnet Changes" | |
} | |
# Html CSS style | |
$Style = @" | |
<style> | |
table { | |
border-collapse: collapse; | |
} | |
td, th { | |
border: 1px solid #ddd; | |
padding: 8px; | |
} | |
th { | |
padding-top: 12px; | |
padding-bottom: 12px; | |
text-align: left; | |
background-color: #4286f4; | |
color: white; | |
} | |
</style> | |
"@ | |
################# | |
## MAIN SCRIPT ## | |
################# | |
# ArrayLists to hold the data | |
$ADSites = [System.Collections.ArrayList]::new() | |
$ADSubnets = [System.Collections.ArrayList]::new() | |
# Retrieve the list of AD sites for the current forest | |
$Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites | |
# Retrieve the AD subnets for each AD site and convert to a custom object | |
foreach ($ADSite in $Sites) | |
{ | |
[void]$ADSites.Add( | |
[PSCustomObject]@{ | |
'AD Site' = $ADSite.Name | |
} | |
) | |
Foreach ($Subnet in $ADSite.Subnets) | |
{ | |
[void]$ADSubnets.Add( | |
[pscustomobject]@{ | |
Name = $Subnet.Name | |
Site = $Subnet.Site | |
Location = $Subnet.Location | |
} | |
) | |
} | |
} | |
# Test whether the cached lists exist, if not create them assuming first run | |
If (!(Test-Path $ADSitesFile)) | |
{ | |
$ADSites | Sort 'AD Site' | Export-Csv –Path $ADSitesFile –NoTypeInformation –Force | |
} | |
If (!(Test-Path $ADSubnetsFile)) | |
{ | |
$ADSubnets | Sort Name | Export-Csv –Path $ADSubnetsFile –NoTypeInformation –Force | |
} | |
# Load in the cached lists | |
$ADSitesCached = Import-Csv –Path $ADSitesFile | |
$ADSubnetsCached = Import-Csv –Path $ADSubnetsFile | |
# More ArrayLists to hold the data | |
$ADSitesAdded = [System.Collections.ArrayList]::new() | |
$ADSitesRemoved = [System.Collections.ArrayList]::new() | |
$ADSubnetsAdded = [System.Collections.ArrayList]::new() | |
$ADSubnetsRemoved = [System.Collections.ArrayList]::new() | |
$ADSubnetsModified = [System.Collections.ArrayList]::new() | |
# New AD sites | |
Foreach ($Item in $ADSites) | |
{ | |
If($Item.'AD Site' -notin $ADSitesCached.'AD Site') | |
{ | |
[void]$ADSitesAdded.Add($Item) | |
} | |
} | |
# Removed AD Sites | |
Foreach ($Item in $ADSitesCached) | |
{ | |
If($Item.'AD Site' -notin $ADSites.'AD Site') | |
{ | |
[void]$ADSitesRemoved.Add($Item) | |
} | |
} | |
# IP subnet where AD Site has changed, or new IP subnet added | |
Foreach ($Item in $ADSubnets) | |
{ | |
$Sub = $ADSubnetsCached.Where({$_.Name -eq $Item.Name}) | |
If ($Sub) | |
{ | |
If ($Sub.Site -ne $Item.Site) | |
{ | |
[void]$ADSubnetsModified.Add( | |
[PSCustomObject]@{ | |
Name = $Item.Name | |
OldSite = $Sub.Site | |
NewSite = $Item.Site | |
} | |
) | |
} | |
} | |
Else | |
{ | |
[void]$ADSubnetsAdded.Add($Item) | |
} | |
} | |
# IP subnet removed | |
Foreach ($Item in $ADSubnetsCached) | |
{ | |
$Sub = $ADSubnets.Where({$_.Name -eq $Item.Name}) | |
If ($Sub){} | |
Else | |
{ | |
[void]$ADSubnetsRemoved.Add($Item) | |
} | |
} | |
# Prepare the HTML | |
If ($ADSitesAdded.Count -ge 1) | |
{ | |
$HTML1 = $ADSitesAdded | | |
ConvertTo-Html –Head $Style –Property 'AD Site' –Body "<h2>The following AD Sites have been ADDED in Active Directory</h2>" –CssUri "http://www.w3schools.com/lib/w3.css" | | |
Out-String | |
} | |
If ($ADSitesRemoved.Count -ge 1) | |
{ | |
$HTML2 = $ADSitesRemoved | | |
ConvertTo-Html –Head $Style –Property 'AD Site' –Body "<h2>The following AD Sites have been REMOVED from Active Directory</h2>" –CssUri "http://www.w3schools.com/lib/w3.css" | | |
Out-String | |
} | |
If ($ADSubnetsAdded.Count -ge 1) | |
{ | |
$HTML3 = $ADSubnetsAdded | | |
ConvertTo-Html –Head $Style –Property Name,Site,Location –Body "<h2>The following IP Subnets have been ADDED in Active Directory</h2>" –CssUri "http://www.w3schools.com/lib/w3.css" | | |
Out-String | |
} | |
If ($ADSubnetsRemoved.Count -ge 1) | |
{ | |
$HTML4 = $ADSubnetsRemoved | | |
ConvertTo-Html –Head $Style –Property Name,Site,Location –Body "<h2>The following IP Subnets have been REMOVED from Active Directory</h2>" –CssUri "http://www.w3schools.com/lib/w3.css" | | |
Out-String | |
} | |
If ($ADSubnetsModified.Count -ge 1) | |
{ | |
$HTML5 = $ADSubnetsModified | | |
ConvertTo-Html –Head $Style –Property Name,OldSite,NewSite –Body "<h2>The AD Site for the following IP Subnets has been MODIFIED in Active Directory</h2>" –CssUri "http://www.w3schools.com/lib/w3.css" | | |
Out-String | |
} | |
$HTML = $HTML1 + $HTML2 + $HTML3 + $HTML4 + $HTML5 | |
# Send the email report and update the cached lists if required | |
If ($HTML.Length -ge 1) | |
{ | |
Try | |
{ | |
Send-MailMessage –Body $HTML @EmailParams –BodyAsHtml –Priority High –ErrorAction Stop | |
} | |
Catch | |
{ | |
$_ | |
Break | |
} | |
$ADSites | Sort 'AD Site' | Export-Csv –Path $ADSitesFile –NoTypeInformation –Force | |
$ADSubnets | Sort Name | Export-Csv –Path $ADSubnetsFile –NoTypeInformation –Force | |
} |
Do you know of any automated methods (ideally using Powershell) to run a forest discovery sync immediately? It would seem using a temp schedule is not feasible because the minimum poll is 1 day. The closest I got was a suggestion to restart the SMS Components service. However this doesn’t appear to kick-start a forest discovery. Any ideas or suggestions welcomed. Thanks.