|
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