|
| 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