├── .gitattributes ├── .gitignore ├── New-DFSMonitor.ps1 └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /New-DFSMonitor.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Monitor your DFS Replication Backlog with a graphical history. 4 | .DESCRIPTION 5 | Use the DFSMonitorWithHistory.ps1 script to monitor your DFS backlog. It 6 | contains a graphical history chart as well as detailed information from 7 | each run. I recommend running from a scheduled task every hour to get the 8 | best information. 9 | 10 | Make sure to edit the PARAM section and update it for your environment. 11 | 12 | Graphical history chart is a Google visualization that requires Flash to run 13 | (it's the same chart you see on Google Finance). Historical data is saved 14 | in an XML file. 15 | .PARAMETER DFSServers 16 | Array of computers to be processed. For backwards compatibility this parameter 17 | will also accept a comma seperated list, i.e.: "server1,server2,server3" and 18 | will automatically split it into an array. Otherwise you can use the 19 | standard array syntax: server1,server2,server3 (without quotes), or 20 | "server1","server2","server3" 21 | .PARAMETER DaysToSaveData 22 | How much of a history do you want the script to keep. I recommend no more then 2 23 | weeks, but 1 week is probably better for larger DFS implementations. 24 | .PARAMETER DataLocation 25 | The full path, or UNC, to where you want the script to keep the historic 26 | data. Logs kept by the script will also be stored here. 27 | .PARAMETER OutputLocation 28 | The full path, or UNC, to where you want the script to save the HTML files 29 | the script creates. I recommend you make this the root folder for a web 30 | server. For IIS, the default location would be: \\servername\c$\inetpub\wwwroot 31 | .PARAMETER MaxThreads 32 | This scripts uses multithreading to improve performance for gathering the 33 | DFS information. You can alter the number of threads that it uses based on 34 | the server you have. In testing I found 10 to about the sweet spot for my 35 | server (less or more was slower). You should not need to modify this 36 | amount. 37 | .INPUTS 38 | Historical data file: $DataLocation\DFSData.XML 39 | .OUTPUTS 40 | Log: $DataLocation\debugMMddyyyyHHmm.log 41 | Data File: $DataLocation\DFSData.XML 42 | Primary HTML Page (use this): $OutputLocation\DFSMonitorGrid.HTML 43 | Placeholder HTML for Detailed Reports (contains iFrame): $OutputLocation\DFSDetailIndex.HTML 44 | Detailed Reports (displayed in iFrame): $OutputLocation\DFSDetailsMMddyyyyHHmm.HTML 45 | .EXAMPLE 46 | .\pathtoscript\DFSMonitorWithHistory.PS1 47 | Runs the script while accepting all default values. Edit PARAM section 48 | of this script to match your environment. 49 | .EXAMPLE 50 | .\pathtoscript\DFSMonitorWithHistory.PS1 -DFSServes server1,server2 -DaysToSave 7 -DataLocation c:\scripts\dfs -OutputLocation \\webserver\c$\inetpub\wwwroot 51 | Runs the script using the servers server1 and server2, it will save the data for 7 days and keep 52 | the logs and datafile (DFSData.XML) at c:\scripts\dfs. All of the HTML pages will be stored on 53 | a server called webserver on the c: drive in \inetpub\wwwroot (the default web root directory 54 | on an IIS server). It will accept the default of 10 threads. 55 | .NOTES 56 | Author: Martin Pugh 57 | Twitter: @thesurlyadm1n 58 | Spiceworks: Martin9700 59 | Blog: www.thesurlyadmin.com 60 | 61 | Change Log: 62 | 2.64 - Renamed to New-DFSMonitor and published on github 63 | 2.63 - Improved error reporting in primary script and in multithreaded sub-scripts. Converted 64 | sub-script output to object for better data handling, and changed the "All Groups" variable 65 | from a string array to hashtable for better performance. Fixed some debug messages that worked 66 | fine in ISE but not on command line. Fixed bug with Alerts not showing up on the 67 | DFSMonitorGrid.HTML start page. 68 | 2.62 - Bug when using $DupeCheck to make sure script doesn't check the same group/folder pair 69 | more then once. Added Cmdletbinding to support -Verbose output. 70 | 2.61 - Bug found by Bhopper577 where test-path was still looking for the old CSV data file. 71 | 2.6 - Changed saving from a CSV file to an XML which will preserve date/time without having 72 | to convert in script. 73 | 2.51 - Increased the # of PING's to 4 so remote server detection over WAN will be a little 74 | more reliable. This required a change in the string detection so 0% loss, 25% loss and 75 | 75% loss would all be acceptable. I fudged on the RegEx a bit so technically 20%, 55% 76 | and 70% are OK too, but if you only ping 4 times you can't get those percentages! 77 | .LINK 78 | http://community.spiceworks.com/scripts/show/1536-dfs-monitor-with-history 79 | .LINK 80 | http://www.thesurlyadmin.com/2012/08/03/dfs-replication-monitoring/ 81 | #> 82 | [CmdletBinding()] 83 | Param ( 84 | [array]$DFSServers = "mffile1,fdfile2,gbfile1", 85 | [int]$DaysToSaveData = 5, 86 | [string]$DataLocation = "d:\blah", 87 | [string]$OutputLocation = "C:\Dropbox\Scripts\PS Scripts\DFS Replication Monitor", 88 | [int]$MaxThreads = 10 89 | ) 90 | 91 | #region Functions 92 | Function Test-ServerAvailability 93 | { [CmdletBinding()] 94 | Param ( 95 | [string]$Name 96 | ) 97 | 98 | Write-Verbose "Test-ServerAvailability: Validating $Name..." 99 | $Ping = $false 100 | $DFS = $false 101 | $DFSr = $false 102 | $Good = $false 103 | 104 | $Result = PING $Name -n 4 105 | Switch -regex ($Result) 106 | { "\(0% loss" { $Ping = $true; Break } 107 | "\(25% loss" { $Err = "Ping response 25%, too low"; Break } 108 | "\((50|75)% loss" { $Ping = $true; Break } 109 | "100% loss" { $Err = "Failed to respond to PING"; Break } 110 | "Destination host unreachable" { $Err = "Destination host unreachable"; Break } 111 | "could not find host" { $Err = "Server Not Found"; Break } 112 | default { $Err = "Unknown Error" } 113 | } 114 | If ($Ping) 115 | { $Err = "" 116 | If ((Get-Service dfsr -ComputerName $Name -ErrorAction SilentlyContinue).Status -eq "Running") 117 | { $DFSr = $true 118 | } 119 | ElseIf ((Get-Service dfs -ComputerName $Name -ErrorAction SilentlyContinue).Status -eq "Running") 120 | { $Err = "DFSr service not running" 121 | $DFS = $true 122 | } 123 | Else 124 | { $Err = "Neither DFSr or DFS services running on server" 125 | Write-Verbose "Test-ServerAvailability: $Err" 126 | } 127 | If ($DFS -or $DFSr) 128 | { Write-Verbose "Test-ServerAvailability: Test WMI communications protocol for $Name" 129 | $Protocol = "WSMAN" 130 | 131 | For ($Loop = 1;$Loop -le 3;$Loop ++) 132 | { $SessionParams = @{ 133 | ComputerName = $Name 134 | SessionOption = New-CimSessionOption -Protocol $Protocol 135 | } 136 | Try { 137 | Write-Verbose "Test-ServerAvailability: Try #$($Loop): Attempting to connect using $Protocol..." 138 | $CimSession = New-CimSession @SessionParams -ErrorAction Stop 139 | Write-Verbose "Test-ServerAvailability: Try #$($Loop): Connected succesfully using $Protocol" 140 | $Good = $true 141 | Break 142 | } 143 | Catch { 144 | If ($Protocol -eq "WSMAN") 145 | { Write-Verbose "Test-ServerAvailability: Try #$($Loop): Unable to connect with WSMAN, attempting DCOM..." 146 | $Protocol = "DCOM" 147 | $Loop -- 148 | Continue 149 | } 150 | If ($Loop -eq 3) 151 | { $Protocol = "" 152 | $Err = "Unable to connect to WMI using WSMAN or DCOM" 153 | Write-Verbose "Test-ServerAvailability: $Err" 154 | } 155 | Else 156 | { $Protocol = "WSMAN" 157 | } 158 | } 159 | } 160 | } 161 | } 162 | Else 163 | { Write-Verbose "Test-ServerAvailability: Did not respond to ping" 164 | } 165 | 166 | $Result = [PSCustomObject]@{ 167 | Name = $Name 168 | Good = $Good 169 | Ping = $Ping 170 | DFS = $DFS 171 | DFSr = $DFSr 172 | Protocol = $Protocol 173 | Error = $Err 174 | } 175 | 176 | Write-Verbose "Test-ServerAvailability: Done with $Name" 177 | Return $Result 178 | } 179 | 180 | Function Get-WQLQuery 181 | { [CmdletBinding()] 182 | Param ( 183 | [string]$ComputerName, 184 | [string]$Query, 185 | #[string]$NameSpace = "root\MicrosoftDFS", 186 | [string]$NameSpace = "root\CIMV2", 187 | [ValidateSet("WSMAN","DCOM")] 188 | [string]$Protocol 189 | ) 190 | 191 | $SessionParams = @{ 192 | ComputerName = $ComputerName 193 | SessionOption = New-CimSessionOption -Protocol $Protocol 194 | } 195 | 196 | For ($Loop = 1;$Loop -le 3;$Loop ++) 197 | { Try { 198 | Write-Verbose "Get-WQLQuery: Try #$Loop - Connecting to $ComputerName using $Protocol" 199 | $CimSession = New-CimSession @SessionParams -ErrorAction Stop 200 | $WMI = Get-CimInstance -CimSession $CimSession -Query $Query -ErrorAction Stop -Namespace $NameSpace 201 | Break 202 | } 203 | Catch { 204 | Write-Verbose "Get-WQLQuery: Failed to connect because $($Error[0])" 205 | Throw $Error[0].ToString() 206 | } 207 | } 208 | Return $WMI 209 | } 210 | 211 | Function Get-WMI 212 | { Param ([String]$WMIQuery, 213 | [String]$Computer) 214 | 215 | $ErrorCount = 0 216 | Do 217 | { $WMIObject = Get-WQLQueryObject -computerName $Computer -Namespace "root\MicrosoftDFS" -Query $WMIQuery -Debug 218 | If ($WMIObject -eq $null) 219 | { $ErrorCount ++ 220 | } 221 | Else 222 | { Return $WMIObject 223 | } 224 | } 225 | While ($ErrorCount -le 2) 226 | $WMIObject = "WMI Error on $Computer" 227 | Return $WMIObject 228 | } 229 | #endregion 230 | 231 | #Here we go! 232 | cls 233 | 234 | #Validate Data path exists, since this is where the log file exists we'll use Write-Host to notify. 235 | If ($DataLocation) 236 | { If (-not (Test-Path $DataLocation -PathType Container)) 237 | { Throw "Data Path: $DataLocation does not exist! Stopping script." 238 | } 239 | } 240 | Else 241 | { $DataLocation = Split-Path $MyInvocation.MyCommand.Path 242 | } 243 | 244 | #Setup the log 245 | $ScriptRunDate = (Get-Date).DateTime 246 | $SaveFormatDate = Get-Date $ScriptRunDate -format yyyyMMddHHmm 247 | Start-Transcript -Path $DataLocation\debug$SaveFormatDate.log 248 | 249 | #Validate Output/Report location exists 250 | If (-not (Test-Path $OutputLocation -PathType Container)) 251 | { Write-Verbose "Output Path: $OutputLocation does not exist! Stopping script." 252 | Exit 253 | } 254 | 255 | #Set some global variables 256 | $NewData = @() 257 | $AllGroupNames = @{} 258 | $DupeCheck = @() 259 | $Servers = @() 260 | $ServerList = @{} 261 | $AlertReport = @() 262 | 263 | #Display parameters 264 | Write-Verbose "Servers to be scanned: $DFSServers" 265 | Write-Verbose "Days to Save Data: $DaysToSaveData" 266 | Write-Verbose "Data Path: $DataLocation" 267 | Write-Verbose "HTML Path: $OutputLocation" 268 | Write-Verbose "Maximum Threads: $MaxThreads" 269 | 270 | Write-Verbose "Loading data..." 271 | 272 | #Parse DFSServers and make a good array 273 | ForEach ($Item in $DFSServers) 274 | { If ($Item.Contains(",")) 275 | { $Servers += $Item.Split(",") 276 | } 277 | Else 278 | { $Servers += $Item 279 | } 280 | } 281 | $SaveDate = (Get-Date).Date.AddDays(-$DaysToSaveData) 282 | 283 | #Check if Data file exists, if so import it, if not create it. 284 | If ((Test-Path $DataLocation\DFSData.xml) -eq $False) 285 | { $Data = @() 286 | } 287 | Else 288 | { $Data = Import-Clixml $DataLocation\DFSData.xml 289 | If ($Data.Count -gt 0) 290 | { $Data = $Data | Where {$_.RunDate -ge $SaveDate} 291 | } 292 | Else 293 | { $Data = @() 294 | } 295 | } 296 | 297 | #Start the main loop. 298 | ForEach ($FileServer in $Servers) 299 | { Write-Verbose "Now working on $FileServer..." 300 | If (-not ($ServerList.ContainsKey($FileServer))) 301 | { $ServerList.Add($FileServer,(Test-ServerAvailability -Name $FileServer)) 302 | } 303 | 304 | If (-not $ServerList[$FileServer].Good) 305 | { Continue 306 | } 307 | 308 | Try { 309 | $WMIQuery = "SELECT * FROM DfsrReplicationGroupConfig" 310 | $GroupGUIDs = Get-WQLQuery -Query $WMIQuery -ComputerName $FileServer -Protocol $ServerList[$FileServer].Protocol 311 | 312 | $WMIQuery = "SELECT * FROM DfsrConnectionConfig WHERE InBound=True" 313 | $RGConnections = Get-WQLQuery -Query $WMIQuery -ComputerName $FileServer -Protocol $ServerList[$FileServer].Protocol 314 | 315 | $WMIQuery = "SELECT * FROM DfsrReplicatedFolderConfig" 316 | $RGFolders = Get-WQLQuery -Query $WMIQuery -ComputerName $FileServer -Protocol $ServerList[$FileServer].Protocol 317 | } 318 | Catch { 319 | $AlertReport += [PSCustomObject]@{ 320 | Server = $FileServer 321 | Query = $WMIQuery 322 | Error = $Error[0] 323 | } 324 | Continue 325 | } 326 | 327 | ForEach ($Group in $GroupGUIDs) 328 | { If (-not $AllGroupNames.ContainsKey($Group.ReplicationGroupGuid)) 329 | { $AllGroupNames.Add($Group.ReplicationGroupGUID,$Group.ReplicationGroupName) 330 | } 331 | $GFolders = $RGFolders | Where {$_.ReplicationGroupGUID -eq $Group.ReplicationGroupGUID} 332 | ForEach ($Folder in $GFolders) 333 | { $GConnection = $RGConnections | Where {$_.ReplicationGroupGUID -eq $Group.ReplicationGroupGUID} 334 | ForEach ($Connection in $GConnection) 335 | { $InServer = $FileServer 336 | $OutServer = $Connection.PartnerName 337 | 338 | #Check if we've already done this 339 | $Found = "No" 340 | ForEach ($Line in $DupeCheck) 341 | { If ($Line[0].ToUpper() -eq $Group.ReplicationGroupName.ToUpper() -and 342 | $Line[1].ToUpper() -eq $Folder.ReplicatedFolderName.ToUpper() -and 343 | $Line[2].ToUpper() -eq $InServer.ToUpper() -and 344 | $Line[3].ToUpper() -eq $OutServer.ToUpper()) 345 | { $Found = "Yes" 346 | Break 347 | } 348 | } 349 | If ($Found -eq "No") 350 | { $DupeCheck += ,@($Group.ReplicationGroupName,$Folder.ReplicatedFolderName,$InServer,$OutServer) 351 | $DupeCheck += ,@($Group.ReplicationGroupName,$Folder.ReplicatedFolderName,$OutServer,$InServer) 352 | } 353 | Else 354 | { Continue 355 | } 356 | 357 | #Now check if partner server is available 358 | If (-not (Test-ServerAvailable $OutServer)) 359 | { Continue 360 | } 361 | 362 | For ($i = 1; $i -le 2; $i++) 363 | { While ($(Get-Job -state "Running").count -ge $MaxThreads) 364 | { Write-Verbose "Thread count hit max of $MaxThreads, waiting for threads to finish..." 365 | Start-Sleep -Milliseconds 5000 366 | } 367 | Start-Job -ArgumentList $InServer,$OutServer,$Group,$Folder.ReplicatedFolderName -ScriptBlock { 368 | Param ( 369 | [string]$InServer, 370 | [string]$OutServer, 371 | [object]$Group, 372 | [string]$ReplicationFolder 373 | ) 374 | 375 | Function Get-WQLQuery 376 | { Param ( 377 | [String]$WMIQuery, 378 | [String]$Computer 379 | ) 380 | 381 | $ErrorCount = 0 382 | While ($ErrorCount -le 2) 383 | { $WMIObject = Get-WQLQueryObject -Namespace "root\MicrosoftDFS" -Query $WMIQuery -ComputerNameName $Computer 384 | If ($WMIObject) 385 | { $Status = "Success" 386 | $ErrorDetail = "" 387 | Break 388 | } 389 | Else 390 | { $ErrorCount ++ 391 | $Status = "Error" 392 | $ErrorDetail = $Error[0] 393 | } 394 | } 395 | New-Object PSObject -Property @{ 396 | Status = $Status 397 | Object = $WMIObject 398 | Error = $ErrorDetail 399 | } 400 | } 401 | $ErrorCount = 0 402 | $BacklogConnCount = 0 403 | $ErrorDetail = "" 404 | 405 | $WMIQuery = "SELECT * FROM DfsrReplicatedFolderConfig WHERE ReplicationGroupGUID = '" + $Group.ReplicationGroupGUID + "' AND ReplicatedFolderName = '" + $ReplicationFolder + "'" 406 | $WMIObject = Get-WQLQuery -Query $WMIQuery -ComputerName $InServer 407 | If ($WMIObject.Status -eq "Error") 408 | { $Status = "Error" 409 | $BacklogFiles = "WMI Error" 410 | $ErrorReport = "WMI Error on $InServer" 411 | $ErrorDetail = $WMIObject.Error 412 | } 413 | ElseIf ($WMIObject.Object.Enabled) 414 | { $WMIQuery = "SELECT * FROM DfsrReplicatedFolderConfig WHERE ReplicationGroupGUID = '" + $Group.ReplicationGroupGUID + "' AND ReplicatedFolderName = '" + $ReplicationFolder + "'" 415 | $WMIObject = Get-WQLQuery -Query $WMIQuery -ComputerName $OutServer 416 | If ($WMIObject.Status -eq "Error") 417 | { $Status = "Error" 418 | $BacklogFiles = "WMI Error" 419 | $ErrorReport = "WMI Error on $OutServer" 420 | $ErrorDetail = $WMIObject.Error 421 | } 422 | ElseIf ($WMIObject.Object.Enabled) 423 | { #Get the version vector of the partner 424 | $WMIQuery = "SELECT * FROM DfsrReplicatedFolderInfo WHERE ReplicationGroupGUID = '" + $Group.ReplicationGroupGUID + "' AND ReplicatedFolderName = '" + $ReplicationFolder + "'" 425 | $WMIObject = Get-WQLQuery -Query $WMIQuery -ComputerName $OutServer 426 | If ($WMIObject.Status -eq "Error") 427 | { $Status = "Error" 428 | $BacklogFiles = "WMI Error" 429 | $ErrorReport = "WMI Error occurred on $OutServer" 430 | $ErrorDetail = $WMIObject.Error 431 | } 432 | Else 433 | { $Vv = $WMIObject.Object.GetVersionVector().VersionVector 434 | #Get the backlog count from the partner 435 | $WMIQuery = "SELECT * FROM DfsrReplicatedFolderInfo WHERE ReplicationGroupGUID = '" + $Group.ReplicationGroupGUID + "' AND ReplicatedFolderName = '" + $ReplicationFolder + "'" 436 | $WMIObject = Get-WQLQuery -Query $WMIQuery -ComputerNameName $InServer 437 | If ($WMIObject.Status -eq "Error") 438 | { $Status = "WMI Error" 439 | $BacklogFiles = "WMI Error" 440 | $ErrorReport = "WMI Error occurred on $OutServer" 441 | $ErrorDetail = $WMIObject.Error 442 | } 443 | Else 444 | { $BacklogConnCount = $WMIObject.Object.GetOutboundBacklogFileCount($Vv).BacklogFileCount 445 | $arrFiles = $WMIObject.Object.GetOutboundBacklogFileIDRecords($Vv).BacklogIdRecords 446 | If ($BacklogConnCount -eq 0) 447 | { $Files = " " 448 | } 449 | Else 450 | { $Files = "" 451 | ForEach ($FileLine in $arrFiles) 452 | { $Files += $FileLine.FileName + "
" 453 | } 454 | } 455 | $Status = "Success" 456 | $BacklogFiles = $Files 457 | $ErrorReport = "" 458 | } 459 | } 460 | } 461 | Else 462 | { $Status = "Disabled" 463 | $BacklogFiles = "Disabled" 464 | $ErrorReport = "Folder $($Group.ReplicationGroupName)/$ReplicationFolder disabled on $OutServer" 465 | } 466 | } 467 | Else 468 | { $Status = "Disabled" 469 | $BacklogFiles = "Disabled" 470 | $ErrorReport = "Folder $($Group.ReplicationGroupName)/$ReplicationFolder disabled on $InServer" 471 | } 472 | New-Object PSObject -Property @{ 473 | Status = $Status 474 | BacklogFiles = $BacklogFiles 475 | ErrorReport = $ErrorReport 476 | ErrorDetail = $ErrorDetail 477 | GroupObject = $Group.ReplicationGroupGUID 478 | Folder = $ReplicationFolder 479 | InServer = $InServer 480 | OutServer = $OutServer 481 | BacklogCount = $BacklogConnCount 482 | } 483 | } | Out-Null 484 | $InServer = $Connection.PartnerName 485 | $OutServer = $FileServer 486 | } 487 | } 488 | } 489 | } 490 | } 491 | 492 | #Wait for all the jobs to finish 493 | While (@(Get-Job -State "Running").count -gt 0) 494 | { Write-Verbose "All threads submitted, waiting for them to finish..." 495 | Start-Sleep -Milliseconds 5000 496 | } 497 | 498 | #Now read the job data into data 499 | Write-Verbose "All threads completed. Threads run: $(@(Get-Job).Count)" 500 | $Output = @() 501 | ForEach ($Job in Get-Job) 502 | { $ErrorCount = 0 503 | Do 504 | { Write-Verbose "Receiving job number: $($Job.Id)" 505 | $Result = Receive-Job $Job 506 | If ($Result -eq $null) 507 | { $ErrorCount ++ 508 | If ($ErrorCount -eq 4) 509 | { Write-Verbose "Unable to retrieve job: $($Job.id)" 510 | $Result = "Fail" 511 | } 512 | Else 513 | { Write-Verbose "Problem retrieving job: $($Job.id), Retry: $ErrorCount" 514 | Start-Sleep -Seconds 3 515 | } 516 | } 517 | Else 518 | { $ErrorCount = 4 519 | } 520 | } While ($ErrorCount -lt 4) 521 | If ($Result -eq "Fail") 522 | { Remove-Job $Job 523 | Continue 524 | } 525 | #$GroupName = ($AllGroupNames | Where {$_ -match $Result.Group}).Split(":") 526 | $NewData += ,@($AllGroupNames[$Result.GroupObject],$Result.Folder,$Result.InServer,$Result.OutServer,$Result.BacklogCount,$Result.BacklogFiles) 527 | $Output += New-Object PSCustomObject -Property @{ 528 | GroupName = $AllGroupNames[$Result.GroupObject] 529 | GroupGUID = $Result.GroupObject 530 | Folder = $Result.Folder 531 | InServer = $Result.InServer 532 | OutServer = $Result.OutServer 533 | Backlog = $Result.BacklogCount 534 | BackLogFiles = $Result.BacklogFiles 535 | } 536 | If ($Result.Status -ne "Success") 537 | { If ($AlertReport -notcontains $Result.ErrorReport) 538 | { $AlertReport += $Result.ErrorReport 539 | Write-Verbose $Result.ErrorDetail 540 | } 541 | } 542 | Remove-Job $Job 543 | } 544 | 545 | #Now add the new data 546 | ForEach ($GroupName in $AllGroupNames.Values) 547 | { #$GroupName = ($Group.Split(":"))[1] 548 | $UniqueReplFolders = $Output | Where {$_.GroupName -eq $GroupName} | Select Folder -Unique 549 | ForEach ($Folder in $UniqueReplFolders) 550 | { $BacklogCount = ($Output | Where {$_.GroupName -eq $GroupName -and $_.Folder -eq $Folder.Folder} | Measure-Object Backlog -sum).Sum 551 | $NewRGName = $Folder.Folder + ":" + $GroupName 552 | $Data += New-Object PSCustomObject -Property @{ 553 | RFName = $Folder.Folder 554 | RGGUID = $NewRGName 555 | BacklogCount = $BacklogCount 556 | RunDate = $ScriptRunDate 557 | } 558 | } 559 | } 560 | 561 | If ($Data -eq $Null) 562 | { #Something went horribly wrong! 563 | Write-Verbose "No data found!" 564 | Throw 565 | } 566 | Else 567 | { #Delete oldest detail and debug files 568 | Get-ChildItem $OutputLocation\dfsdetails*.html | Where {$_.CreationTime -lt $SaveDate} | Remove-Item 569 | Get-ChildItem $DataLocation\debug*.log | Where {$_.CreationTime -lt $SaveDate} | Remove-Item 570 | 571 | ## 572 | ## Now build the detailed DFS monitor page 573 | ## 574 | Write-Verbose "--Creating detailed monitoring page..." 575 | $html = @() 576 | $html = "`n" 577 | $html += "`n" 584 | $html += "
Report Date: $ScriptRunDate

" 585 | If ($AlertReport) 586 | { $html += "Alerts:
`n" 587 | ForEach ($Line in $AlertReport) 588 | { $html += "" + $Line + "
`n" 589 | } 590 | } 591 | $html += "`n" 592 | $html += "`n" 593 | $TRDomain = "d1" 594 | $NewData = $NewData | Sort 595 | ForEach ($Line in $NewData) 596 | { If ($TRDomain -eq "d1") 597 | {$TRDomain = "d0" 598 | } 599 | Else 600 | { $TRDomain = "d1" 601 | } 602 | $html += "" 603 | $html += "" 604 | $html += "" 605 | $html += "" 606 | $html += "" 607 | If ($Line[4] -eq 0) 608 | { $html += "" 609 | } 610 | Else 611 | { $html += "" 612 | } 613 | If ($Line[5] -like "*Disabled*" -or $Line[5] -like "*WMI Error*") 614 | { $html += "" 615 | } 616 | Else 617 | { $html += "" 618 | } 619 | $html += "`n" 620 | } 621 | $html += "
Replication GroupReplication FolderSending PartnerReceiving PartnerBacklogFiles
" + $Line[0] + "" + $Line[1] + "" + $Line[2] + "" + $Line[3] + "0" + $Line[4] + "" + $Line[5] + "" + $Line[5] + "
" 622 | $html | Out-File $OutputLocation\DFSDetails$SaveFormatDate.html 623 | 624 | # Now create the detail launch page 625 | $html = @() 626 | $html = "`n" 627 | $html += "DFS Replication Details`n" 628 | $html += "`n" 638 | $html += "`n" 639 | $html += "`n" 640 | $html += "
Show Report from:  `n" 648 | $html | Out-File $OutputLocation\DFSDetailIndex.html 649 | 650 | ## 651 | ## Now create the Google visualization 652 | ## 653 | Write-Verbose "--Now for the Annotated Timeline..." 654 | $html = "`n" 655 | $html += "`n" 656 | $html += "`n" 657 | $html += "`n" 703 | $html += "`n" 704 | $html += "`n" 705 | If ($AlertReport) 706 | { $html += "Alerts:
`n" 707 | ForEach ($Line in $AlertReport) 708 | { $html += "" + $Line + "
`n" 709 | } 710 | } 711 | $html += "
`n" 712 | $html += "Details from last run ($ScriptRunDate)`n" 713 | $html += "" 714 | $html | Out-File $OutputLocation\DFSMonitorGrid.html 715 | } 716 | 717 | #And Save the data 718 | Write-Verbose "--Saving the data..." 719 | $Data = $Data | Where {$_.RFName -ne ""} 720 | $Data | Export-Clixml $DataLocation\DFSData.xml 721 | 722 | #All done! 723 | Write-Verbose "Done!" 724 | Stop-Transcript -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Version 2.6 - Now saves historical data in XML format instead of CSV, which will better preserve the [datetime] variable type. If you are running an older version and want to keep your historical data you must run Convert-DFSDataToXML once: http://community.spiceworks.com/scripts/show/1723-convert-dfsdatatoxml 2 | 3 | DFS is a fantastic technology that Microsoft put together for sharing and replicating files. Unfortunately the tools for monitoring the replication leave a lot to be desired. What I wanted to do with this script is create a graph that would tell me how replication is going, something I could see and comprehend at a glance. This script not only gives you that, but it also keeps the detailed information from every time it's run, something you can review at the click of a button. 4 | 5 | Setup: 6 | 7 | 1. Create a directory 8 | 2. Download the script and call it DFSMonitorWithHistory.PS1 and save it into that directory. 9 | 3. Make sure you have a working web server. Figure out the path to the web directory. On IIS that would be: c:\inetpub\wwwroot (your path will vary depending on your installation). 10 | 4. Edit the script, scroll down to the PARAM section and edit: 11 | 4a. $DFSServers: This should be changed to your DFS servers. You should include every one in your DFS replication tree. They should be seperated by comma's and surrounded by quotes. IE: "server1,server2,server3" 12 | 4b. $DaysToSaveData: this is how many days you want to keep data in history. I recommend no more then 7 days as any more will cause the script to run slower. 13 | 4c. $DataLocation: Where you want to save your data. This includes the debug log and the raw data (in the form of a CSV). I recommend using the directory you created in step #1. 14 | 4d. $OutputLocation: This is the path to your web site that you figured out in step #3. 15 | 4e. $MaxThreads: This script uses multi-threading and limits the # of threads to 10. I don't recommend using any more as I found my systems actually got all of the data slower once the thread count got over 10 and you could also get more WMI errors with a higher thread count. So don't change it! 16 | 5. Create a scheduled task. Set it to run every hour. 17 | 5a. Command to run: Powershell.exe 18 | 5b. Argument: -ExecutionPolicy Bypass -file c:\pathfromstep1\DFSMonitorWithHistory.ps1 19 | 5c. Make sure to use a service account with sufficient rights 20 | 5d. To see additional information for scheduling Powershell tasks, see: http://community.spiceworks.com/how_to/show/17736-run-powershell-scripts-from-task-scheduler 21 | 6. Copy error_event.png from your Spiceworks install: C:\Program Files\pkg\gems\spiceworks_public-\images\icons\small to the same location you set in 4d above. 22 | 23 | To view the built in help type: 24 | 25 | Get-Help pathtoscript\DFSMonitorWithHistory.ps1 -Full 26 | 27 | See the examples for running the script with custom parameters--you can now run different scans without editing code! 28 | 29 | To see your results, navigate to your web server and use this file name: DFSMonitorGrid.html 30 | All past history is saved (link's are in the html page listed). 31 | 32 | Rate and comment! Enjoy 33 | --------------------------------------------------------------------------------