|
3 | 3 | PowerShell Script for Synchronizing Domain Controllers Across an AD Forest.
|
4 | 4 |
|
5 | 5 | .DESCRIPTION
|
6 |
| - This script automates the synchronization of all Domain Controllers (DCs) across an Active Directory |
7 |
| - (AD) forest, ensuring that all changes are properly replicated and up-to-date. |
| 6 | + Automates the synchronization of all Domain Controllers (DCs) across an Active Directory (AD) forest. |
| 7 | + Ensures replication is triggered and up-to-date. |
8 | 8 |
|
9 | 9 | .AUTHOR
|
10 | 10 | Luiz Hamilton Silva - @brazilianscriptguy
|
11 | 11 |
|
12 | 12 | .VERSION
|
13 |
| - Last Updated: October 22, 2024 |
| 13 | + UX Enhanced Edition – July 24, 2025 |
14 | 14 | #>
|
15 | 15 |
|
16 |
| -# Hide the PowerShell console window |
| 16 | +#region ── Hide Console Window ── |
17 | 17 | Add-Type @"
|
18 | 18 | using System;
|
19 | 19 | using System.Runtime.InteropServices;
|
20 | 20 | public class Window {
|
21 |
| - [DllImport("kernel32.dll", SetLastError = true)] |
22 |
| - static extern IntPtr GetConsoleWindow(); |
23 |
| - [DllImport("user32.dll", SetLastError = true)] |
24 |
| - [return: MarshalAs(UnmanagedType.Bool)] |
25 |
| - static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); |
26 |
| - public static void Hide() { |
27 |
| - var handle = GetConsoleWindow(); |
28 |
| - ShowWindow(handle, 0); // 0 = SW_HIDE |
29 |
| - } |
30 |
| - public static void Show() { |
31 |
| - var handle = GetConsoleWindow(); |
32 |
| - ShowWindow(handle, 5); // 5 = SW_SHOW |
33 |
| - } |
| 21 | + [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); |
| 22 | + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); |
34 | 23 | }
|
35 | 24 | "@
|
36 |
| -[Window]::Hide() |
| 25 | +[Window]::ShowWindow([Window]::GetConsoleWindow(), 0) |
| 26 | +#endregion |
37 | 27 |
|
38 |
| -# Import necessary modules |
| 28 | +#region ── Load Required Types ── |
39 | 29 | Add-Type -AssemblyName System.Windows.Forms
|
40 | 30 | Add-Type -AssemblyName System.Drawing
|
| 31 | +#endregion |
41 | 32 |
|
42 |
| -# Determine the script name and set up the logging path |
| 33 | +#region ── Logging Setup ── |
43 | 34 | $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
|
44 | 35 | $logDir = 'C:\Logs-TEMP'
|
45 |
| -$logFileName = "${scriptName}.log" |
46 |
| -$logPath = Join-Path $logDir $logFileName |
| 36 | +$logFile = Join-Path $logDir "${scriptName}.log" |
47 | 37 |
|
48 |
| -# Ensure the log directory exists |
49 | 38 | if (-not (Test-Path $logDir)) {
|
50 |
| - $null = New-Item -Path $logDir -ItemType Directory -ErrorAction SilentlyContinue |
51 |
| - if (-not (Test-Path $logDir)) { |
52 |
| - Write-Error "Failed to create log directory at $logDir. Logging will not be possible." |
53 |
| - return |
54 |
| - } |
| 39 | + try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch {} |
55 | 40 | }
|
56 | 41 |
|
57 |
| -# Enhanced logging function with error handling |
58 | 42 | function Log-Message {
|
59 | 43 | param (
|
60 |
| - [Parameter(Mandatory=$true)] |
61 |
| - [string]$Message, |
62 |
| - [Parameter(Mandatory=$false)] |
63 |
| - [string]$MessageType = "INFO" |
| 44 | + [Parameter(Mandatory)] [string]$Message, |
| 45 | + [ValidateSet('INFO','ERROR','WARN')] [string]$Type = 'INFO' |
64 | 46 | )
|
65 | 47 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
66 |
| - $logEntry = "[$timestamp] [$MessageType] $Message" |
| 48 | + $entry = "[$timestamp] [$Type] $Message" |
| 49 | + |
67 | 50 | try {
|
68 |
| - Add-Content -Path $logPath -Value "$logEntry`r`n" -ErrorAction Stop |
69 |
| - $global:logBox.Items.Add($logEntry) |
70 |
| - $global:logBox.TopIndex = $global:logBox.Items.Count - 1 |
| 51 | + Add-Content -Path $logFile -Value $entry |
| 52 | + $global:logBox.SelectionStart = $global:logBox.TextLength |
| 53 | + $global:logBox.SelectionColor = switch ($Type) { |
| 54 | + 'ERROR' { 'Red' } |
| 55 | + 'WARN' { 'DarkOrange' } |
| 56 | + 'INFO' { 'Black' } |
| 57 | + } |
| 58 | + $global:logBox.AppendText("$entry`r`n") |
| 59 | + $global:logBox.ScrollToCaret() |
71 | 60 | } catch {
|
72 |
| - Write-Error "Failed to write to log: $_" |
| 61 | + Write-Error "Log error: $_" |
73 | 62 | }
|
74 | 63 | }
|
| 64 | +#endregion |
75 | 65 |
|
76 |
| -# Function to force synchronization on all DCs |
| 66 | +#region ── Core Functions ── |
77 | 67 | function Sync-AllDCs {
|
78 |
| - # Import the Active Directory module |
79 |
| - Import-Module ActiveDirectory |
80 |
| - |
81 |
| - Log-Message "Starting Active Directory synchronization process: $(Get-Date)" |
82 |
| - |
83 |
| - # Get a list of all domains in the forest |
| 68 | + Log-Message "Sync process started" |
84 | 69 | try {
|
| 70 | + Import-Module ActiveDirectory -ErrorAction Stop |
85 | 71 | $forest = Get-ADForest
|
86 |
| - $allDomains = $forest.Domains |
| 72 | + $domains = $forest.Domains |
87 | 73 | } catch {
|
88 |
| - Log-Message "Error retrieving forest domains: $_" -MessageType "ERROR" |
89 |
| - [System.Windows.Forms.MessageBox]::Show("Error retrieving forest domains. See log for details.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
| 74 | + Log-Message "Failed to load forest info: $_" -Type 'ERROR' |
| 75 | + [System.Windows.Forms.MessageBox]::Show("Could not retrieve domains. See log.", "Error", "OK", "Error") |
90 | 76 | return
|
91 | 77 | }
|
92 | 78 |
|
93 |
| - # Collect all domain controllers from all domains |
94 | 79 | $allDCs = @()
|
95 |
| - foreach ($domain in $allDomains) { |
| 80 | + foreach ($domain in $domains) { |
96 | 81 | try {
|
97 |
| - $domainDCs = Get-ADDomainController -Filter * -Server $domain |
98 |
| - $allDCs += $domainDCs |
| 82 | + $allDCs += Get-ADDomainController -Filter * -Server $domain |
99 | 83 | } catch {
|
100 |
| - Log-Message "Error retrieving domain controllers from ${domain}: $_" -MessageType "ERROR" |
| 84 | + Log-Message "Error retrieving DCs for ${domain}: $_" -Type 'ERROR' |
101 | 85 | }
|
102 | 86 | }
|
103 | 87 |
|
104 |
| - # Force synchronization on all domain controllers |
105 | 88 | foreach ($dc in $allDCs) {
|
106 |
| - $dcName = $dc.HostName |
107 |
| - Write-Output "Forcing synchronization on $dcName" |
108 |
| - Log-Message "Forcing synchronization on ${dcName}: $(Get-Date)" |
| 89 | + $name = $dc.HostName |
| 90 | + Log-Message "Syncing $name" |
109 | 91 | try {
|
110 |
| - # Perform the synchronization |
111 |
| - $syncResult = & repadmin /syncall /e /A /P /d /q $dcName |
112 |
| - # Log the result of the synchronization |
113 |
| - Log-Message "Synchronization result for ${dcName}: $syncResult" |
| 92 | + $output = & repadmin /syncall /e /A /P /d /q $name |
| 93 | + Log-Message "Result: $output" |
114 | 94 | } catch {
|
115 |
| - # Log any errors that occur |
116 |
| - Log-Message "Error synchronizing ${dcName}: $_" -MessageType "ERROR" |
| 95 | + Log-Message "Sync error for ${name}: $_" -Type 'ERROR' |
117 | 96 | }
|
118 | 97 | }
|
119 | 98 |
|
120 |
| - Log-Message "Active Directory synchronization process completed: $(Get-Date)" |
121 |
| - [System.Windows.Forms.MessageBox]::Show("Synchronization process completed.", "Info", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information) |
| 99 | + Log-Message "Sync completed" |
| 100 | + [System.Windows.Forms.MessageBox]::Show("Sync completed. See log for details.", "Info", "OK", "Information") |
122 | 101 | }
|
123 | 102 |
|
124 |
| -# Function to display the log file |
125 | 103 | function Show-Log {
|
126 |
| - notepad $logPath |
| 104 | + Start-Process notepad.exe $logFile |
127 | 105 | }
|
128 | 106 |
|
129 |
| -# Function to run Repadmin.exe /replsummary and display output in the list box |
130 | 107 | function Show-ReplSummary {
|
131 |
| - Log-Message "Starting Repadmin.exe /replsummary: $(Get-Date)" |
| 108 | + Log-Message "Running replsummary" |
132 | 109 | try {
|
133 |
| - $replSummaryResult = & repadmin /replsummary |
134 |
| - |
135 |
| - # Split the output into individual lines and add them to the list box |
136 |
| - $replSummaryResultLines = $replSummaryResult -split "`r`n" |
137 |
| - foreach ($line in $replSummaryResultLines) { |
138 |
| - $global:logBox.Items.Add($line) |
| 110 | + $summary = & repadmin /replsummary |
| 111 | + $summary -split "`r`n" | ForEach-Object { |
| 112 | + $global:logBox.AppendText("$_`r`n") |
139 | 113 | }
|
140 |
| - |
141 |
| - $global:logBox.TopIndex = $global:logBox.Items.Count - 1 |
142 |
| - Log-Message "Repadmin.exe /replsummary completed" |
| 114 | + $global:logBox.ScrollToCaret() |
| 115 | + Log-Message "replsummary complete" |
143 | 116 | } catch {
|
144 |
| - Log-Message "Error executing Repadmin.exe /replsummary: $_" -MessageType "ERROR" |
145 |
| - [System.Windows.Forms.MessageBox]::Show("Error executing Repadmin.exe /replsummary. See log for details.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
| 117 | + Log-Message "Error running replsummary: $_" -Type 'ERROR' |
| 118 | + [System.Windows.Forms.MessageBox]::Show("Error running replsummary. See log.", "Error", "OK", "Error") |
146 | 119 | }
|
147 | 120 | }
|
| 121 | +#endregion |
148 | 122 |
|
149 |
| -# Create the form |
150 |
| -$form = New-Object System.Windows.Forms.Form |
151 |
| -$form.Text = "AD Forest Sync Tool" |
152 |
| -$form.Size = New-Object System.Drawing.Size(800, 620) # Increased size to fit content |
153 |
| -$form.StartPosition = "CenterScreen" |
154 |
| - |
155 |
| -# Create a ListBox to display log messages |
156 |
| -$global:logBox = New-Object System.Windows.Forms.ListBox |
157 |
| -$logBox.Location = New-Object System.Drawing.Point(10, 10) |
158 |
| -$logBox.Size = New-Object System.Drawing.Size(760, 500) # Adjusted size to fit form |
159 |
| -$form.Controls.Add($logBox) |
160 |
| - |
161 |
| -# Create a button to start synchronization |
162 |
| -$syncButton = New-Object System.Windows.Forms.Button |
163 |
| -$syncButton.Location = New-Object System.Drawing.Point(50, 520) |
164 |
| -$syncButton.Size = New-Object System.Drawing.Size(150, 50) |
165 |
| -$syncButton.Text = "Sync All Forest DCs" |
166 |
| -$syncButton.Add_Click({ |
167 |
| - Sync-AllDCs |
168 |
| -}) |
169 |
| -$form.Controls.Add($syncButton) |
170 |
| - |
171 |
| -# Create a button to view the log |
172 |
| -$logButton = New-Object System.Windows.Forms.Button |
173 |
| -$logButton.Location = New-Object System.Drawing.Point(250, 520) |
174 |
| -$logButton.Size = New-Object System.Drawing.Size(150, 50) |
175 |
| -$logButton.Text = "View Output Logs" |
176 |
| -$logButton.Add_Click({ |
177 |
| - Show-Log |
| 123 | +#region ── GUI Setup ── |
| 124 | + |
| 125 | +$form = New-Object Windows.Forms.Form -Property @{ |
| 126 | + Text = "AD Forest Sync Tool" |
| 127 | + Size = '800,660' |
| 128 | + StartPosition = 'CenterScreen' |
| 129 | + FormBorderStyle = 'FixedDialog' |
| 130 | + MaximizeBox = $false |
| 131 | +} |
| 132 | + |
| 133 | +# Status bar |
| 134 | +$statusStrip = New-Object Windows.Forms.StatusStrip |
| 135 | +$statusLabel = New-Object Windows.Forms.ToolStripStatusLabel |
| 136 | +$statusLabel.Text = "Ready" |
| 137 | +$statusStrip.Items.Add($statusLabel) |
| 138 | +$form.Controls.Add($statusStrip) |
| 139 | + |
| 140 | +# RichTextBox for logs |
| 141 | +$global:logBox = New-Object Windows.Forms.RichTextBox -Property @{ |
| 142 | + Location = '10,10' |
| 143 | + Size = '760,500' |
| 144 | + ReadOnly = $true |
| 145 | + Font = New-Object Drawing.Font("Consolas", 9) |
| 146 | + WordWrap = $false |
| 147 | + ScrollBars = "Vertical" |
| 148 | +} |
| 149 | +$form.Controls.Add($global:logBox) |
| 150 | + |
| 151 | +# Button: Sync |
| 152 | +$syncBtn = New-Object Windows.Forms.Button -Property @{ |
| 153 | + Text = "Sync All Forest DCs" |
| 154 | + Location = '50,520' |
| 155 | + Size = '150,50' |
| 156 | +} |
| 157 | +$syncBtn.Add_Click({ |
| 158 | + $syncBtn.Enabled = $false |
| 159 | + $statusLabel.Text = "Syncing domain controllers..." |
| 160 | + try { |
| 161 | + Sync-AllDCs |
| 162 | + $statusLabel.Text = "Sync completed" |
| 163 | + } finally { |
| 164 | + $syncBtn.Enabled = $true |
| 165 | + } |
178 | 166 | })
|
179 |
| -$form.Controls.Add($logButton) |
180 |
| - |
181 |
| -# Create a button to show Repadmin.exe /replsummary |
182 |
| -$replSummaryButton = New-Object System.Windows.Forms.Button |
183 |
| -$replSummaryButton.Location = New-Object System.Drawing.Point(450, 520) |
184 |
| -$replSummaryButton.Size = New-Object System.Drawing.Size(250, 50) |
185 |
| -$replSummaryButton.Text = "Show Replication Summary" |
186 |
| -$replSummaryButton.Add_Click({ |
187 |
| - Show-ReplSummary |
| 167 | +$form.Controls.Add($syncBtn) |
| 168 | + |
| 169 | +# Button: View Logs |
| 170 | +$logBtn = New-Object Windows.Forms.Button -Property @{ |
| 171 | + Text = "View Output Logs" |
| 172 | + Location = '250,520' |
| 173 | + Size = '150,50' |
| 174 | +} |
| 175 | +$logBtn.Add_Click({ Show-Log }) |
| 176 | +$form.Controls.Add($logBtn) |
| 177 | + |
| 178 | +# Button: Show Replication Summary |
| 179 | +$replBtn = New-Object Windows.Forms.Button -Property @{ |
| 180 | + Text = "Show Replication Summary" |
| 181 | + Location = '450,520' |
| 182 | + Size = '250,50' |
| 183 | +} |
| 184 | +$replBtn.Add_Click({ |
| 185 | + $replBtn.Enabled = $false |
| 186 | + $statusLabel.Text = "Running replication summary..." |
| 187 | + try { |
| 188 | + Show-ReplSummary |
| 189 | + $statusLabel.Text = "Replication summary complete" |
| 190 | + } finally { |
| 191 | + $replBtn.Enabled = $true |
| 192 | + } |
188 | 193 | })
|
189 |
| -$form.Controls.Add($replSummaryButton) |
| 194 | +$form.Controls.Add($replBtn) |
190 | 195 |
|
191 |
| -# Show the form |
192 |
| -$form.Add_Shown({$form.Activate()}) |
193 |
| -[void] $form.ShowDialog() |
| 196 | +$form.Add_Shown({ $form.Activate() }) |
| 197 | +[void]$form.ShowDialog() |
| 198 | +#endregion |
194 | 199 |
|
195 |
| -# End of script |
| 200 | +# ── End of Script ── |
0 commit comments