Skip to content

Commit fad62fa

Browse files
committed
0.5 First public version
1 parent 1e24098 commit fad62fa

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#requires -version 5.1
2+
#requires -modules activedirectory
3+
4+
<#
5+
.SYNOPSIS
6+
Checks the DFSR backlog and generates replication reports for DFS Replication groups.
7+
.DESCRIPTION
8+
This script analyzes the DFS Replication status on the local server and within the Active Directory environment. It creates propagation tests and reports, determines backlog values between DFSR members, optionally exports detailed results to CSV, and can also compare file hashes between replication partners.
9+
10+
DISCLAIMER
11+
This script is provided "as is" without any warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
12+
Use of this script is at your own risk. The author assumes no responsibility for any damage or data loss caused by the use of this script.
13+
14+
(c) 2026 Fabian Niesen, www.infrastrukturhelden.de - License: GNU General Public License v3 (GPLv3), see notes for details
15+
.EXAMPLE
16+
get-DSFRBacklog.ps1 -LogPath "C:\Temp\DFSRMonitor" -Verbose
17+
This will execute the script and create logfiles and CSV exports in C:\Temp\DFSRMonitor. The -Verbose switch will show additional information about the DFSR replication groups and folders.
18+
.INPUTS
19+
none
20+
.OUTPUTS
21+
none
22+
.PARAMETER CompareHashes
23+
Compares file hashes between replication partners after the backlog analysis to help identify content mismatches.
24+
25+
.PARAMETER ReplicationGroupList
26+
Limits the backlog analysis to the specified DFS Replication groups. If not specified, all available replication groups are processed.
27+
28+
.PARAMETER LogPath
29+
Defines the path where log files, HTML reports, and CSV exports are created. Default: C:\Temp\DFSRMonitor
30+
31+
.PARAMETER CSVFilename
32+
Defines the file name used for the CSV backlog export. The current timestamp is prefixed automatically. Default: DFSR-Backlog.csv
33+
34+
.NOTES
35+
Author : Fabian Niesen
36+
Filename : get-DFSRBacklog.ps1
37+
Requires : PowerShell Version 5.1
38+
License : GNU General Public License v3 (GPLv3)
39+
(c) 2026 Fabian Niesen, www.infrastrukturhelden.de
40+
This script is licensed under the GNU General Public License v3 (GPLv3).
41+
You can redistribute it and/or modify it under the terms of the GPLv3 as published by the Free Software Foundation.
42+
This script is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
43+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
44+
See https://www.gnu.org/licenses/gpl-3.0.html for the full license text.
45+
46+
Version : 0.5 FN 28.03.2026 First public version
47+
History : 0.5 FN 28.03.2026 First public version
48+
49+
50+
.LINK
51+
https://github.com/InfrastructureHeroes/Scipts/blob/master/ActiveDirectory/get-DFSRBacklog.ps1
52+
#>
53+
54+
[cmdletbinding()]
55+
Param (
56+
[Switch]$CompareHashes,
57+
[String[]]$ReplicationGroupList = (""),
58+
$LogPath = "C:\Temp\DFSRMonitor",
59+
$CSVFilename = "DFSR-Backlog.csv"
60+
)
61+
if (((Get-ComputerInfo).WindowsInstallationType) -like "Server Core") {$CoreVersion=$true} else {$CoreVersion = $false}
62+
Write-Verbose "Detect Server Core: $CoreVersion"
63+
$ScriptVersion = "0.5"
64+
$ScriptName = $($myInvocation.MyCommand.Name).Replace('.ps1', '')
65+
"Get-DFSRBacklog.ps1 by Fabian Niesen, www.infrastrukturhelden.de - License: GNU General Public License v3 (GPLv3), see notes for details" | Write-Output
66+
"Start $ScriptName $ScriptVersion - Executed on $($Env:COMPUTERNAME) by $($Env:USERNAME) at $(get-date -format 'HH:mm dd.MM.yyyy' )" | Write-Output
67+
if ($CoreVersion -eq $True)
68+
{
69+
Write-Output "Core Installation Setup..."
70+
Try { $InAD = Install-WindowsFeature -Name FS-DFS-Namespace,FS-DFS-Replication -IncludeManagementTools -ErrorAction Stop }
71+
Catch { Write-Warning "Something went wrong..." ; break }
72+
Set-SConfig -AutoLaunch $false
73+
}
74+
else
75+
{
76+
Write-Output "Desktop Experience Installation Setup..."
77+
Try { $InAD = Install-WindowsFeature -Name RSAT-DFS-Mgmt-Con -IncludeManagementTools -ErrorAction Stop }
78+
Catch { Write-Warning "Something went wrong..." ; break }
79+
}
80+
Import-Module -Name DFSR -Verbose:$false
81+
Import-Module -Name ActiveDirectory -Verbose:$false
82+
IF ($LogPath.EndsWith("\") -like "False") { $LogPath =$LogPath+"\" }
83+
IF (!(Test-Path $LogPath)) { new-item -Path $LogPath -ItemType directory | out-null }
84+
Write-Output "Logpath is set to: $LogPath"
85+
Write-Output "SysVol is only visable in the last test!"
86+
Write-Output "=========================="
87+
$date = get-date -format yyyyMMdd-HHmm
88+
$CSVExport = $LogPath +"\"+$date + $CSVFilename
89+
$DFSRServers = Get-ADDomain | Select-Object -ExpandProperty ReplicaDirectoryServers
90+
Write-Verbose "*********"
91+
If ( $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { $DFSRServers | format-Table -AutoSize }
92+
Write-Verbose "*********"
93+
Write-Output "Start DFSR Propagation Test"
94+
ForEach ( $DFSRFolder in $(Get-DfsReplicationGroup -IncludeSysvol | Get-DfsReplicatedFolder))
95+
{
96+
ForEach ($DFSRMember in $(Get-DfsReplicationGroup -GroupName $DFSRFolder.GroupName | Get-DfsrMember))
97+
{
98+
Start-DfsrPropagationTest -FolderName $DFSRFolder.FolderName -ReferenceComputerName $DFSRMember.ComputerName -Verbose
99+
}
100+
}
101+
Write-Output "Wait 60 Seconds for DFS-R Replication"
102+
Start-sleep -Seconds 60
103+
Write-Output "Create DFSR Propagation Test Reports"
104+
$DFSRFolders = Get-DfsReplicationGroup -IncludeSysvol | Get-DfsReplicatedFolder
105+
Write-Verbose "*********"
106+
If ( $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { $DFSRFolders | Format-Table -AutoSize }
107+
Write-Verbose "*********"
108+
ForEach ( $DFSRFolder in $DFSRFolders)
109+
{
110+
ForEach ($DFSRMember in $(Get-DfsReplicationGroup -GroupName $DFSRFolder.GroupName | Get-DfsrMember))
111+
{
112+
IF (!(Test-Path $($LogPath+"\"+$DFSRMember.ComputerName))) { new-item -Path $($LogPath+"\"+$DFSRMember.ComputerName) -ItemType directory | out-null }
113+
Write-DfsrPropagationReport -FolderName $DFSRFolder.FolderName -GroupName $DFSRFolder.GroupName -ReferenceComputerName $DFSRMember.ComputerName -Path $($LogPath+"\"+$DFSRMember.ComputerName) -FileCount 5
114+
Start-Sleep -Seconds 2
115+
$Report = (Get-ChildItem -Path $($LogPath+"\"+$DFSRMember.ComputerName) -Filter *.html | Sort-Object -Descending -Property LastWriteTime)[0].FullName
116+
Write-Output "Check DFS-R Propagation Report: $Report"
117+
if ($CoreVersion -eq $false) {.$Report}
118+
}
119+
}
120+
Write-Warning "DFSRState - this output might not be reliable! This shows only normal backlog when everthing is smooth"
121+
ForEach ($DFSRServer in $DFSRServers)
122+
{
123+
Write-Output "Server: $DFSRServer"
124+
Try { Get-DfsrState -ComputerName $DFSRServer -Verbose -ErrorAction Stop } CATCH { Write-Output "Get-DfsrState needed WinRM" }
125+
}
126+
Try {
127+
Write-Output "=========================="
128+
Write-Output "Running deep backlog analyses - Including conflict files"
129+
IF ( Test-Path $CSVExport -ErrorAction SilentlyContinue ) { Clear-Content $CSVExport }
130+
$RGroups = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query "SELECT * FROM DfsrReplicationGroupConfig" -ErrorAction Stop
131+
#If replication groups specified, use only those.
132+
if($ReplicationGroupList)
133+
{
134+
$SelectedRGroups = @()
135+
foreach($ReplicationGroup IN $ReplicationGroupList)
136+
{
137+
$SelectedRGroups += $rgroups | Where-Object {$_.ReplicationGroupName -eq $ReplicationGroup}
138+
}
139+
if($SelectedRGroups.count -eq 0)
140+
{
141+
Write-Error "None of the group names specified were found, exiting"
142+
exit
143+
}
144+
else
145+
{
146+
$RGroups = $SelectedRGroups
147+
}
148+
}
149+
150+
$ComputerName=$env:ComputerName
151+
$Succ=0
152+
$Warn=0
153+
$Err=0
154+
155+
foreach ($Group in $RGroups)
156+
{
157+
$RGFoldersWMIQ = "SELECT * FROM DfsrReplicatedFolderConfig WHERE ReplicationGroupGUID='" + $Group.ReplicationGroupGUID + "'"
158+
$RGFolders = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $RGFoldersWMIQ
159+
$RGConnectionsWMIQ = "SELECT * FROM DfsrConnectionConfig WHERE ReplicationGroupGUID='"+ $Group.ReplicationGroupGUID + "'"
160+
$RGConnections = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $RGConnectionsWMIQ
161+
foreach ($Connection in $RGConnections)
162+
{
163+
$ConnectionName = $Connection.PartnerName#.Trim()
164+
if ($Connection.Enabled -eq $True)
165+
{
166+
#if (((New-Object System.Net.NetworkInformation.ping).send("$ConnectionName")).Status -eq "Success")
167+
#{
168+
foreach ($Folder in $RGFolders)
169+
{
170+
$RGName = $Group.ReplicationGroupName
171+
$RFName = $Folder.ReplicatedFolderName
172+
173+
if ($Connection.Inbound -eq $True)
174+
{
175+
$SendingMember = $ConnectionName
176+
$ReceivingMember = $ComputerName
177+
$Direction="inbound"
178+
}
179+
else
180+
{
181+
$SendingMember = $ComputerName
182+
$ReceivingMember = $ConnectionName
183+
$Direction="outbound"
184+
}
185+
186+
$BLCommand = "dfsrdiag Backlog /RGName:'" + $RGName + "' /RFName:'" + $RFName + "' /SendingMember:" + $SendingMember + " /ReceivingMember:" + $ReceivingMember
187+
$Backlog = Invoke-Expression -Command $BLCommand
188+
189+
$BackLogFilecount = 0
190+
foreach ($item in $Backlog)
191+
{
192+
if ($item -ilike "*Backlog File count*")
193+
{
194+
$BacklogFileCount = [int]$Item.Split(":")[1].Trim()
195+
}
196+
}
197+
198+
if ($BacklogFileCount -eq 0)
199+
{
200+
$Color="white"
201+
$Succ=$Succ+1
202+
}
203+
elseif ($BacklogFilecount -lt 10)
204+
{
205+
$Color="yellow"
206+
$Warn=$Warn+1
207+
}
208+
else
209+
{
210+
$Color="red"
211+
$Err=$Err+1
212+
}
213+
Write-Host "$BacklogFileCount files in backlog $SendingMember->$ReceivingMember for $RGName" -fore $Color
214+
IF ( $BacklogFileCount -ne 0)
215+
{
216+
Write-Warning -Message "Please Check Log for File List: $CSVExport"
217+
Get-DfsrBacklog -DestinationComputerName $ReceivingMember -SourceComputerName "$SendingMember" -GroupName $RGName -FolderName $RFName | Export-Csv -Path $CSVExport -Append -UseCulture -NoClobber
218+
}
219+
220+
} # Closing iterate through all folders
221+
#} # Closing If replies to ping
222+
} # Closing If Connection enabled
223+
} # Closing iteration through all connections
224+
} # Closing iteration through all groups
225+
226+
227+
Write-Host "$Succ successful, $Warn warnings and $Err errors from $($Succ+$Warn+$Err) replications."
228+
IF ( $Err -ne 0 ) { Write-Warning "Please wait 5 minutes and check if numbers are reducing. If not execute Repair-DFSR.ps1"}
229+
} Catch { Write-Warning "Error: $($_.Exception.Message)" ; Write-Warning "Detail Analysis work only on DFSR member servers" }
230+
IF ($CompareHashes)
231+
{
232+
[String[]]$DFSHashFiles = $null
233+
Write-Output "Compare Hashes ... this may take a while ..."
234+
ForEach ($DFSRMembership in $($DFSRFolder| Get-DfsrMembership))
235+
{
236+
$ContentPath = $DFSRMembership.ContentPath
237+
$UNCPref = "\\" + $DFSRMembership.ComputerName + "\c$\"
238+
Write-Verbose "UNCPath : $UNCPref"
239+
$ContentPath = $ContentPath.replace("C:\",$UNCPref)
240+
$DFSHashFile = $LogPath+$date+"-DFSHash-"+$DFSRMembership.ComputerName+".txt"
241+
Write-Verbose "Hashfile : $DFSHashFile"
242+
IF (Test-Path $DFSHashFile ) { Clear-Content $DFSHashFile}
243+
[String[]]$DFSHases = "Path;FileHash;Server"
244+
Get-DfsrFileHash -Path (Get-ChildItem -Path $ContentPath -Recurse -file ).fullname | ForEach-Object {
245+
$DFSHash = (($_.Path -split "\\",4)[3])+";"+$_.FileHash + ";" + $DFSRMembership.ComputerName
246+
$DFSHases += $DFSHash
247+
}
248+
##If ( $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { $DFSHases }
249+
$DFSHases | Out-File $DFSHashFile
250+
$DFSHashFiles += $DFSHashFile
251+
}
252+
Write-Verbose "*********"
253+
If ( $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { $DFSHashFiles | format-List }
254+
Write-Verbose "*********"
255+
$Server1Hashes = Import-Csv -Path $DFSHashFiles[0] -Delimiter ";"
256+
$Server2Hashes = Import-Csv -Path $DFSHashFiles[1] -Delimiter ";"
257+
If ( $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { Write-Output "List of all scanned files and Hashes - Missmatch will be presented as Warning"} Else { Write-Output "Only Missmatch are presented - To show matches aslo run again with >-Verbose<"}
258+
Write-Output "Found $($Server1Hashes.count) entries for Server 1 and $($Server2Hashes.count) for Server 2."
259+
ForEach ( $HashValue in $Server1Hashes)
260+
{
261+
$NotMatched = $Server2Hashes | Where-Object { $_.Path -eq $HashValue.Path -and $_.FileHash -ne $HashValue.FileHash }
262+
$Matched = $Server2Hashes | Where-Object { $_.Path -eq $HashValue.Path -and $_.FileHash -eq $HashValue.FileHash }
263+
#$Hash1 = ($Server1Hashes | where { $_.Path -eq $HashValue.Path } ).FileHash
264+
IF ($NotMatched ) { Write-Warning "$($HashValue.Path) Not Match - 1: $($HashValue.FileHash) - 2: $($NotMatched.FileHash)"} Else {Write-Verbose "$($HashValue.Path) Match - 1: $($HashValue.FileHash) - 2: $($Matched.FileHash)" }
265+
#| IF ( $_.FileHash -notlike $HashValue.FileHash ) { Write-Warning "$($_.Path) - Hash not match "} Else {Write-Output "$($_.Path) - Hash match "}
266+
}
267+
IF ( $($DFSRFolder| Get-DfsrMembership).count -ne 2 ) {
268+
Write-Warning "The Comparison will only happen between the first 2 Files. Please manually compare the files:"
269+
$DFSHashFiles | format-List
270+
}
271+
}

0 commit comments

Comments
 (0)