Public/activedirectory/Get-ADSiteTopology.ps1
|
#Requires -Version 5.1 function Get-ADSiteTopology { <# .SYNOPSIS Retrieves Active Directory site topology including sites, subnets, and site links .DESCRIPTION Returns one object per AD site with associated subnets, site links, replication cost/interval, and domain controllers hosted in each site. Provides a complete picture of the AD physical topology for capacity planning, replication analysis, and subnet auditing. .PARAMETER Server Specifies the Active Directory Domain Services instance to connect to. When omitted, the current domain is used. .PARAMETER Credential Specifies the credentials to use for the Active Directory queries. .EXAMPLE Get-ADSiteTopology Returns topology information for all sites in the current forest. .EXAMPLE Get-ADSiteTopology -Server 'dc01.contoso.com' Returns site topology from a specific domain controller. .EXAMPLE Get-ADSiteTopology -Credential (Get-Credential) | Where-Object SubnetCount -eq 0 Finds sites with no subnets assigned, which may indicate configuration issues. .OUTPUTS PSWinOps.ADSiteTopology Returns one object per site with site name, description, subnets, site links, replication details, and domain controller list. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-04-04 Requires: PowerShell 5.1+ / Windows only Requires: ActiveDirectory module (RSAT) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adreplicationsite #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$Server, [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential]$Credential ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" try { Import-Module -Name 'ActiveDirectory' -ErrorAction Stop -Verbose:$false } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] ActiveDirectory module is not available: $_" return } $adSplat = @{} if ($PSBoundParameters.ContainsKey('Server')) { $adSplat['Server'] = $Server } if ($PSBoundParameters.ContainsKey('Credential')) { $adSplat['Credential'] = $Credential } } process { try { # --- Sites --- Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying replication sites" $sites = Get-ADReplicationSite -Filter * -Properties 'Description' -ErrorAction Stop @adSplat # --- Subnets (pre-fetch all, map to site) --- Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying replication subnets" $allSubnets = Get-ADReplicationSubnet -Filter * -Properties 'Site', 'Location', 'Description' -ErrorAction Stop @adSplat $subnetBySite = @{} foreach ($subnet in $allSubnets) { $siteDN = if ($subnet.Site) { $subnet.Site.ToString() } else { '' } if (-not $subnetBySite.ContainsKey($siteDN)) { $subnetBySite[$siteDN] = [System.Collections.Generic.List[PSCustomObject]]::new() } $subnetBySite[$siteDN].Add([PSCustomObject]@{ Name = $subnet.Name Location = $subnet.Location Description = $subnet.Description }) } # --- Site Links (pre-fetch all, map to sites) --- Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying replication site links" $allSiteLinks = Get-ADReplicationSiteLink -Filter * -Properties 'Cost', 'ReplicationFrequencyInMinutes', 'SitesIncluded', 'Description' -ErrorAction Stop @adSplat $siteLinkBySite = @{} foreach ($link in $allSiteLinks) { foreach ($siteDN in $link.SitesIncluded) { $siteDNStr = $siteDN.ToString() if (-not $siteLinkBySite.ContainsKey($siteDNStr)) { $siteLinkBySite[$siteDNStr] = [System.Collections.Generic.List[PSCustomObject]]::new() } $siteLinkBySite[$siteDNStr].Add([PSCustomObject]@{ Name = $link.Name Cost = $link.Cost ReplicationInterval = $link.ReplicationFrequencyInMinutes Description = $link.Description }) } } # --- Domain Controllers (pre-fetch all, map to site) --- Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying domain controllers" $allDCs = Get-ADDomainController -Filter * -ErrorAction Stop @adSplat $dcBySite = @{} foreach ($dc in $allDCs) { $siteName = $dc.Site if (-not $dcBySite.ContainsKey($siteName)) { $dcBySite[$siteName] = [System.Collections.Generic.List[string]]::new() } $dcBySite[$siteName].Add($dc.HostName) } # --- Build output per site --- $timestamp = Get-Date -Format 'o' foreach ($site in $sites | Sort-Object -Property 'Name') { $siteDN = $site.DistinguishedName $siteName = $site.Name $siteSubnets = if ($subnetBySite.ContainsKey($siteDN)) { $subnetBySite[$siteDN] } else { @() } $siteLinks = if ($siteLinkBySite.ContainsKey($siteDN)) { $siteLinkBySite[$siteDN] } else { @() } $siteDCs = if ($dcBySite.ContainsKey($siteName)) { $dcBySite[$siteName] } else { @() } $subnetNames = ($siteSubnets | Select-Object -ExpandProperty 'Name' | Sort-Object) -join ', ' $linkNames = ($siteLinks | Select-Object -ExpandProperty 'Name' -Unique | Sort-Object) -join ', ' $dcNames = ($siteDCs | Sort-Object) -join ', ' [PSCustomObject]@{ PSTypeName = 'PSWinOps.ADSiteTopology' SiteName = $siteName Description = $site.Description SubnetCount = @($siteSubnets).Count Subnets = $subnetNames SubnetDetails = $siteSubnets SiteLinkCount = @($siteLinks).Count SiteLinks = $linkNames SiteLinkDetails = $siteLinks DomainControllers = $dcNames DCCount = @($siteDCs).Count DistinguishedName = $siteDN Timestamp = $timestamp } } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to retrieve site topology: $_" } Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |