Functions/Test-FSMORoleHealth.ps1
|
<# .SYNOPSIS Checks the health and reachability of Active Directory FSMO role holders. .DESCRIPTION This function identifies which domain controllers hold each of the five FSMO roles (Schema Master, Domain Naming Master, PDC Emulator, RID Master, Infrastructure Master) and verifies each role holder is online and reachable. Helps catch situations where a FSMO role holder has been decommissioned or is unreachable, which can silently break AD operations like password resets, schema updates, or new object creation. .PARAMETER OutputPath Optional path to export an HTML report. If omitted, results are only shown in console. .EXAMPLE Test-FSMORoleHealth .EXAMPLE Test-FSMORoleHealth -OutputPath "C:\Reports\FSMOHealth.html" .NOTES Requires: ActiveDirectory PowerShell module Requires: Read access to forest and domain configuration #> function Test-FSMORoleHealth { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$OutputPath ) if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { Write-Error "ActiveDirectory module not found. Install RSAT-AD-PowerShell and try again." return } Import-Module ActiveDirectory -ErrorAction Stop Write-Verbose "Retrieving forest and domain FSMO role information..." try { $forest = Get-ADForest -ErrorAction Stop $domain = Get-ADDomain -ErrorAction Stop } catch { Write-Error "Failed to retrieve forest/domain information: $($_.Exception.Message)" return } $fsmoRoles = @( [PSCustomObject]@{ RoleName = "Schema Master"; Holder = $forest.SchemaMaster; Scope = "Forest" } [PSCustomObject]@{ RoleName = "Domain Naming Master"; Holder = $forest.DomainNamingMaster; Scope = "Forest" } [PSCustomObject]@{ RoleName = "PDC Emulator"; Holder = $domain.PDCEmulator; Scope = "Domain" } [PSCustomObject]@{ RoleName = "RID Master"; Holder = $domain.RIDMaster; Scope = "Domain" } [PSCustomObject]@{ RoleName = "Infrastructure Master"; Holder = $domain.InfrastructureMaster; Scope = "Domain" } ) $results = @() foreach ($role in $fsmoRoles) { Write-Verbose "Checking $($role.RoleName) held by $($role.Holder)..." $status = "OK" $detail = "Role holder is reachable" if (-not $role.Holder) { $status = "MISSING" $detail = "No role holder found for this role" } else { $isReachable = Test-Connection -ComputerName $role.Holder -Count 1 -Quiet -ErrorAction SilentlyContinue if (-not $isReachable) { $status = "UNREACHABLE" $detail = "Role holder '$($role.Holder)' did not respond to ping. It may be offline or improperly decommissioned." } else { # Confirm it's still a valid, current domain controller try { $null = Get-ADDomainController -Identity $role.Holder -ErrorAction Stop } catch { $status = "REVIEW" $detail = "Role holder '$($role.Holder)' responded to ping but could not be validated as an active domain controller." } } } $results += [PSCustomObject]@{ Role = $role.RoleName Scope = $role.Scope Holder = $role.Holder Status = $status Detail = $detail } } # --- Console summary --- $issues = $results | Where-Object { $_.Status -in @("MISSING", "UNREACHABLE", "REVIEW") } Write-Host "`n=== AD FSMO Role Health Summary ===" -ForegroundColor Cyan Write-Host "Forest: $($forest.Name)" Write-Host "Domain: $($domain.DNSRoot)" Write-Host "Roles checked: $($results.Count)" Write-Host "Issues found: $($issues.Count)`n" if ($issues.Count -gt 0) { Write-Host "Issues detected:" -ForegroundColor Yellow $issues | Format-Table Role, Scope, Holder, Status, Detail -AutoSize } else { Write-Host "All FSMO roles are healthy and reachable." -ForegroundColor Green } $results | Format-Table Role, Scope, Holder, Status -AutoSize # --- Optional HTML export --- if ($OutputPath) { $htmlHeader = @" <style> body { font-family: Segoe UI, Arial, sans-serif; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; } th { background-color: #2c3e50; color: white; } tr.OK { background-color: #eafaf1; } tr.MISSING, tr.UNREACHABLE { background-color: #fdecea; } tr.REVIEW { background-color: #fff8e1; } </style> "@ $htmlBody = $results | ConvertTo-Html -Head $htmlHeader -Title "AD FSMO Role Health Report - $(Get-Date)" -PreContent "<h2>AD FSMO Role Health Report</h2><p>Forest: $($forest.Name)</p><p>Domain: $($domain.DNSRoot)</p><p>Generated: $(Get-Date)</p>" $htmlBody | Out-File -FilePath $OutputPath -Encoding UTF8 Write-Host "`nHTML report saved to: $OutputPath" -ForegroundColor Cyan } return $results } |