Restructured Documentation
Automatic Documentation Deployment / Sync Docs to https://kb.bunny-lab.io (push) Successful in 8s
Automatic Documentation Deployment / Sync Docs to https://kb.bunny-lab.io (push) Successful in 8s
This commit is contained in:
+281
@@ -0,0 +1,281 @@
|
||||
---
|
||||
tags:
|
||||
- DFS
|
||||
- Active Directory
|
||||
- PowerShell
|
||||
---
|
||||
|
||||
## Purpose
|
||||
Report the DFS namespaces, folder targets, and replication configuration in the current Active Directory domain. Run the script in PowerShell with the DFSN and DFSR modules and permission to query the domain.
|
||||
|
||||
## Script
|
||||
You may want to put together a simple table report of the DFS namespaces, replication info, and target folders. You can run the following powershell script to generate a nice table-based report of the current structure of the DFS namespaces in your domain.
|
||||
|
||||
??? example "Powershell Reporting Script"
|
||||
|
||||
```powershell
|
||||
# Automatically detect current AD domain and use it as DFS prefix
|
||||
try {
|
||||
$Domain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name
|
||||
$DomainPrefix = "\\$Domain"
|
||||
} catch {
|
||||
Write-Warning "Unable to detect domain automatically. Falling back to manual value."
|
||||
$DomainPrefix = "\\bunny-lab.io"
|
||||
}
|
||||
|
||||
Import-Module DFSN -ErrorAction Stop
|
||||
Import-Module DFSR -ErrorAction Stop
|
||||
|
||||
function Get-ServerNameFromPath {
|
||||
param([string]$Path)
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
||||
if ($Path -like "\\*") { return ($Path -split '\\')[2] }
|
||||
return $null
|
||||
}
|
||||
function Get-Max3 {
|
||||
param([int[]]$Values)
|
||||
if (-not $Values) { return 0 }
|
||||
return (($Values | Measure-Object -Maximum).Maximum)
|
||||
}
|
||||
|
||||
# Build: GroupName (lower) -> memberships[]
|
||||
$allGroups = Get-DfsReplicationGroup -ErrorAction SilentlyContinue
|
||||
$groupMembershipMap = @{}
|
||||
foreach ($g in $allGroups) {
|
||||
$ms = Get-DfsrMembership -GroupName $g.GroupName -ErrorAction SilentlyContinue
|
||||
$groupMembershipMap[$g.GroupName.ToLower()] = $ms
|
||||
}
|
||||
|
||||
# Flatten all memberships for regex fallback
|
||||
$allMemberships = @()
|
||||
foreach ($arr in $groupMembershipMap.Values) { if ($arr) { $allMemberships += $arr } }
|
||||
|
||||
$rows = New-Object System.Collections.Generic.List[psobject]
|
||||
|
||||
# Enumerate namespace roots
|
||||
$roots = Get-DfsnRoot -ErrorAction Stop | Where-Object { $_.Path -like "$DomainPrefix\*" }
|
||||
|
||||
Write-Host "DFS Namespace and Replication Overview" -ForegroundColor Cyan
|
||||
Write-Host "------------------------------------------------------`n"
|
||||
|
||||
foreach ($root in $roots) {
|
||||
|
||||
$rootPath = $root.Path
|
||||
$rootLeaf = ($rootPath -split '\\')[-1]
|
||||
|
||||
$nsServers = @()
|
||||
$rootTargets = Get-DfsnRootTarget -Path $rootPath -ErrorAction SilentlyContinue
|
||||
foreach ($rt in $rootTargets) {
|
||||
$srv = Get-ServerNameFromPath $rt.TargetPath
|
||||
if ($srv) { $nsServers += $srv }
|
||||
}
|
||||
|
||||
# Folders under this root
|
||||
$folders = Get-DfsnFolder -Path "$rootPath\*" -ErrorAction SilentlyContinue | Sort-Object Path
|
||||
|
||||
foreach ($f in $folders) {
|
||||
$namespaceFull = $f.Path
|
||||
$leaf = ($f.Path -split '\\')[-1]
|
||||
|
||||
# DFSN folder targets
|
||||
$targets = Get-DfsnFolderTarget -Path $f.Path -ErrorAction SilentlyContinue
|
||||
$targets = @($targets | Sort-Object { Get-ServerNameFromPath $_.TargetPath }) # ensure array
|
||||
|
||||
# Map to DFSR group by naming; fallback to regex on ContentPath
|
||||
$candidateGroup = ((($rootPath -replace '^\\\\','') + '\' + $leaf).ToLower())
|
||||
if ($groupMembershipMap.ContainsKey($candidateGroup)) {
|
||||
$msForFolder = $groupMembershipMap[$candidateGroup]
|
||||
} else {
|
||||
$escapedRootLeaf = [regex]::Escape($rootLeaf)
|
||||
$escapedLeaf = [regex]::Escape($leaf)
|
||||
$regex = "\\$escapedRootLeaf\\$escapedLeaf($|\\)"
|
||||
$msForFolder = $allMemberships | Where-Object { $_.ContentPath -imatch $regex }
|
||||
}
|
||||
$msForFolder = @($msForFolder) # normalize to array
|
||||
|
||||
# Build aligned rows: one per target
|
||||
$targetLines = @()
|
||||
$replLines = @()
|
||||
|
||||
foreach ($t in $targets) {
|
||||
$tServer = Get-ServerNameFromPath $t.TargetPath
|
||||
$targetLines += $t.TargetPath
|
||||
|
||||
$msForServer = $null
|
||||
if ($msForFolder.Count -gt 0) {
|
||||
$msForServer = $msForFolder | Where-Object { $_.ComputerName -ieq $tServer } | Select-Object -First 1
|
||||
}
|
||||
if ($msForServer -and $msForServer.ContentPath) { $replLines += $msForServer.ContentPath } else { $replLines += '' }
|
||||
}
|
||||
|
||||
# Max line count for row expansion (PS 5.1 safe)
|
||||
$maxLines = Get-Max3 @($targetLines.Count, $replLines.Count, $nsServers.Count)
|
||||
|
||||
for ($i = 0; $i -lt $maxLines; $i++) {
|
||||
|
||||
# Precompute values (PS 5.1: no inline-if in hashtables)
|
||||
$nsVal = ''
|
||||
if ($i -eq 0) { $nsVal = $namespaceFull }
|
||||
|
||||
$targetVal = ''
|
||||
if ($i -lt $targetLines.Count) { $targetVal = $targetLines[$i] }
|
||||
|
||||
$replVal = ''
|
||||
if ($i -lt $replLines.Count) { $replVal = $replLines[$i] }
|
||||
|
||||
$nsServerVal = ''
|
||||
if ($i -lt $nsServers.Count) { $nsServerVal = $nsServers[$i] }
|
||||
|
||||
$row = [PSCustomObject]@{
|
||||
'Namespace' = $nsVal
|
||||
'Member Folder Target(s)' = $targetVal
|
||||
'Replication Locations' = $replVal
|
||||
'Namespace Servers' = $nsServerVal
|
||||
}
|
||||
$rows.Add($row) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Render as a PowerShell bordered grid with one-space left/right padding in every cell
|
||||
function Write-DfsGrid {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[System.Collections.IEnumerable]$Data,
|
||||
|
||||
[string[]]$Columns = @('Namespace','Member Folder Target(s)','Replication Locations','Namespace Servers'),
|
||||
|
||||
# Reasonable max widths; tune to your console (these are content+padding widths)
|
||||
[int[]]$MaxWidths = @(70, 70, 52, 30),
|
||||
|
||||
[switch]$Ascii # use +-| instead of box-drawing if your console garbles Unicode
|
||||
)
|
||||
|
||||
# Ensure arrays align
|
||||
if ($MaxWidths.Count -lt $Columns.Count) {
|
||||
$pad = New-Object System.Collections.Generic.List[int]
|
||||
$pad.AddRange($MaxWidths)
|
||||
for ($i=$MaxWidths.Count; $i -lt $Columns.Count; $i++) { $pad.Add(40) }
|
||||
$MaxWidths = $pad.ToArray()
|
||||
}
|
||||
|
||||
# Characters
|
||||
if ($Ascii) {
|
||||
$H = @{ tl='+'; tr='+'; bl='+'; br='+'; hz='-'; vt='|'; tj='+'; mj='+'; bj='+' }
|
||||
} else {
|
||||
# Box-drawing
|
||||
$H = @{ tl='┌'; tr='┐'; bl='└'; br='┘'; hz='─'; vt='│'; tj='┬'; mj='┼'; bj='┴' }
|
||||
try { [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 } catch {}
|
||||
}
|
||||
|
||||
function TruncPad([string]$s, [int]$w) {
|
||||
if ($null -eq $s) { $s = '' }
|
||||
$s = $s -replace '\r','' -replace '\t',' '
|
||||
if ($s.Length -le $w) { return $s.PadRight($w, ' ') }
|
||||
if ($w -le 1) { return $s.Substring(0, $w) }
|
||||
return ($s.Substring(0, $w-1) + '…')
|
||||
}
|
||||
|
||||
# Materialize and compute widths (include one-space left/right padding for header and data)
|
||||
$rows = @($Data | ForEach-Object {
|
||||
$o = @{}
|
||||
foreach ($c in $Columns) { $o[$c] = [string]($_.$c) }
|
||||
[pscustomobject]$o
|
||||
})
|
||||
|
||||
$widths = @()
|
||||
for ($i=0; $i -lt $Columns.Count; $i++) {
|
||||
$col = $Columns[$i]
|
||||
# Start with header length including padding
|
||||
$max = (" " + $col + " ").Length
|
||||
foreach ($r in $rows) {
|
||||
$len = (" " + [string]$r.$col + " ").Length
|
||||
if ($len -gt $max) { $max = $len }
|
||||
}
|
||||
$widths += [Math]::Min($max, $MaxWidths[$i])
|
||||
}
|
||||
|
||||
# Line builders
|
||||
function DrawTop() {
|
||||
$line = $H.tl
|
||||
for ($i = 0; $i -lt $widths.Count; $i++) {
|
||||
$line += ($H.hz * $widths[$i])
|
||||
if ($i -lt ($widths.Count - 1)) {
|
||||
$line += $H.tj
|
||||
} else {
|
||||
$line += $H.tr
|
||||
}
|
||||
}
|
||||
$line
|
||||
}
|
||||
function DrawMid([string[]]$Columns, [int[]]$widths, $H) {
|
||||
$line = $H.vt
|
||||
for ($i=0; $i -lt $widths.Count; $i++) {
|
||||
$line += TruncPad (" " + $Columns[$i] + " ") $widths[$i]
|
||||
$line += $H.vt
|
||||
}
|
||||
$line
|
||||
}
|
||||
function DrawSep() {
|
||||
$line = $H.vt
|
||||
for ($i=0; $i -lt $widths.Count; $i++) {
|
||||
$line += ($H.hz * $widths[$i])
|
||||
$line += $H.vt
|
||||
}
|
||||
$line
|
||||
}
|
||||
function DrawHeaderSep() {
|
||||
$line = $H.vt
|
||||
for ($i=0; $i -lt $widths.Count; $i++) {
|
||||
$line += ($H.hz * $widths[$i])
|
||||
$line += $H.vt
|
||||
}
|
||||
$line
|
||||
}
|
||||
function DrawBottom() {
|
||||
$line = $H.bl
|
||||
for ($i = 0; $i -lt $widths.Count; $i++) {
|
||||
$line += ($H.hz * $widths[$i])
|
||||
if ($i -lt ($widths.Count - 1)) {
|
||||
$line += $H.bj
|
||||
} else {
|
||||
$line += $H.br
|
||||
}
|
||||
}
|
||||
$line
|
||||
}
|
||||
function DrawRow($r, [string[]]$Columns, [int[]]$widths, $H) {
|
||||
$line = $H.vt
|
||||
for ($i=0; $i -lt $widths.Count; $i++) {
|
||||
$val = [string]$r.($Columns[$i])
|
||||
$line += TruncPad (" " + $val + " ") $widths[$i]
|
||||
$line += $H.vt
|
||||
}
|
||||
$line
|
||||
}
|
||||
|
||||
# Render with group separators between namespaces (when the Namespace cell is non-empty)
|
||||
Write-Host (DrawTop)
|
||||
Write-Host (DrawMid -Columns $Columns -widths $widths -H $H)
|
||||
Write-Host (DrawHeaderSep)
|
||||
|
||||
$first = $true
|
||||
foreach ($r in $rows) {
|
||||
if (-not $first -and ([string]$r.$($Columns[0])) ) {
|
||||
# Namespace changed → draw a separator
|
||||
Write-Host (DrawSep)
|
||||
}
|
||||
$first = $false
|
||||
Write-Host (DrawRow -r $r -Columns $Columns -widths $widths -H $H)
|
||||
}
|
||||
|
||||
Write-Host (DrawBottom)
|
||||
}
|
||||
|
||||
Write-DfsGrid -Data $rows
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
- [DFS Deployment](<../../../../deployments/Applications/Files and Collaboration/Windows Server/DFS Namespaces with Replication.md>) — Review the namespace and replication structure that this report inspects.
|
||||
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
tags:
|
||||
- DFS
|
||||
- Replication
|
||||
- PowerShell
|
||||
---
|
||||
|
||||
## Purpose
|
||||
Report the directional replication backlog between the DFS member servers in the script. Set the member names to the intended deployment and run from a PowerShell session with access to its DFS replication configuration.
|
||||
|
||||
## Script
|
||||
You may want to check that replication is occurring bi-directionally between every member server in your DFS deployment. I wrote a script below that effectively shows you every replication group and each directional backlog status.
|
||||
|
||||
```powershell
|
||||
# --- CONFIG ---
|
||||
$Members = @("LAB-FPS-01","LAB-FPS-02")
|
||||
$SummarizeAcrossFolders = $true # $true = one line per direction per RG; $false = per-folder lines
|
||||
|
||||
function Invoke-DfsrBacklogStatus {
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $RG,
|
||||
[Parameter(Mandatory)] [string] $RF,
|
||||
[Parameter(Mandatory)] [string] $Send,
|
||||
[Parameter(Mandatory)] [string] $Recv
|
||||
)
|
||||
|
||||
$out = & dfsrdiag backlog /rgname:"$RG" /rfname:"$RF" /sendingmember:"$Send" /receivingmember:"$Recv" 2>&1 | Out-String
|
||||
$outTrim = ($out -split "`r?`n" | ForEach-Object { $_.Trim() }) | Where-Object { $_ -ne "" }
|
||||
|
||||
if ($out -match 'No Backlog') {
|
||||
return [pscustomobject]@{ Status="No Backlog"; Count=0; Detail=$null }
|
||||
}
|
||||
|
||||
$count = $null
|
||||
$countLine = $outTrim | Where-Object { $_ -match '(?i)backlog' } | Select-Object -First 1
|
||||
if ($countLine -and ($countLine -match '(\d+)')) { $count = [int]$matches[1] }
|
||||
|
||||
$detail = ($outTrim | Select-Object -First 8) -join " | "
|
||||
|
||||
return [pscustomobject]@{
|
||||
Status = if ($count -ne $null) { "Backlog: $count" } else { "Backlog/Check Output" }
|
||||
Count = $count
|
||||
Detail = $detail
|
||||
}
|
||||
}
|
||||
|
||||
$groups = Get-DfsReplicationGroup | Sort-Object GroupName
|
||||
|
||||
foreach ($g in $groups) {
|
||||
$rg = $g.GroupName
|
||||
$rfs = Get-DfsReplicatedFolder -GroupName $rg | Sort-Object FolderName
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ("== Replication Group: {0} ==" -f $rg)
|
||||
|
||||
foreach ($send in $Members) {
|
||||
foreach ($recv in $Members) {
|
||||
if ($send -eq $recv) { continue }
|
||||
|
||||
if ($SummarizeAcrossFolders) {
|
||||
$worstCount = 0
|
||||
$nonZero = @()
|
||||
$errorsOrDetails = @()
|
||||
|
||||
foreach ($rfObj in $rfs) {
|
||||
$rf = $rfObj.FolderName
|
||||
$res = Invoke-DfsrBacklogStatus -RG $rg -RF $rf -Send $send -Recv $recv
|
||||
|
||||
if ($res.Status -ne "No Backlog") {
|
||||
$nonZero += [pscustomobject]@{ RF=$rf; Status=$res.Status; Count=$res.Count; Detail=$res.Detail }
|
||||
if ($res.Count -ne $null -and $res.Count -gt $worstCount) { $worstCount = $res.Count }
|
||||
|
||||
# ✅ FIX: ${rf} avoids the ':' parsing issue
|
||||
if ($res.Detail) { $errorsOrDetails += "RF=${rf}: $($res.Detail)" }
|
||||
}
|
||||
}
|
||||
|
||||
if ($nonZero.Count -eq 0) {
|
||||
Write-Host ("{0} -> {1}: No Backlog" -f $send, $recv)
|
||||
} else {
|
||||
if ($worstCount -gt 0) {
|
||||
Write-Host ("{0} -> {1}: Backlog (max {2} across RFs)" -f $send, $recv, $worstCount)
|
||||
} else {
|
||||
Write-Host ("{0} -> {1}: Backlog/Errors (see details)" -f $send, $recv)
|
||||
}
|
||||
|
||||
$errorsOrDetails | Select-Object -First 5 | ForEach-Object { Write-Host (" - {0}" -f $_) }
|
||||
if ($errorsOrDetails.Count -gt 5) { Write-Host " - ... (more omitted)" }
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($rfObj in $rfs) {
|
||||
$rf = $rfObj.FolderName
|
||||
$res = Invoke-DfsrBacklogStatus -RG $rg -RF $rf -Send $send -Recv $recv
|
||||
|
||||
if ($res.Status -eq "No Backlog") {
|
||||
Write-Host ("{0} -> {1} [{2}]: No Backlog" -f $send, $recv, $rf)
|
||||
} else {
|
||||
Write-Host ("{0} -> {1} [{2}]: {3}" -f $send, $recv, $rf, $res.Status)
|
||||
if ($res.Detail) { Write-Host (" - {0}" -f $res.Detail) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! example "Example Output"
|
||||
You will see output like the following when you run the script.
|
||||
|
||||
```powershell
|
||||
== Replication Group: bunny-lab.io\music\fl studio plugins ==
|
||||
LAB-FPS-01 -> LAB-FPS-02: No Backlog
|
||||
LAB-FPS-02 -> LAB-FPS-01: No Backlog
|
||||
|
||||
== Replication Group: bunny-lab.io\music\personal music ==
|
||||
LAB-FPS-01 -> LAB-FPS-02: No Backlog
|
||||
LAB-FPS-02 -> LAB-FPS-01: No Backlog
|
||||
|
||||
== Replication Group: bunny-lab.io\music\shared music ==
|
||||
LAB-FPS-01 -> LAB-FPS-02: No Backlog
|
||||
LAB-FPS-02 -> LAB-FPS-01: No Backlog
|
||||
|
||||
== Replication Group: bunny-lab.io\projects\coding ==
|
||||
LAB-FPS-01 -> LAB-FPS-02: No Backlog
|
||||
LAB-FPS-02 -> LAB-FPS-01: No Backlog
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
- [DFS Deployment](<../../../../deployments/Applications/Files and Collaboration/Windows Server/DFS Namespaces with Replication.md>) — Identify the replication members and folders.
|
||||
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
|
||||
Reference in New Issue
Block a user