All checks were successful
GitOps Automatic Deployment / GitOps Automatic Deployment (push) Successful in 8s
389 lines
17 KiB
Markdown
389 lines
17 KiB
Markdown
## Purpose
|
|
If you want data available from a single, consistent UNC path while hosting it on multiple file servers, use **DFS Namespaces (DFSN)**. A namespace presents a *virtual* folder tree (for example, `\\bunny-lab.io\Projects`) whose folders point to one or more **folder targets** (actual SMB shares on your servers).
|
|
**DFS Replication (DFSR)** is a *separate* feature you configure to keep the contents of those targets in sync.
|
|
|
|
This document walks through creating a domain-based DFS namespace and enabling DFS Replication for two servers.
|
|
|
|
!!! info "Assumptions"
|
|
You have two Windows Server machines (e.g., `LAB-FPS-01` and `LAB-FPS-02`) running an edition that supports DFS (Standard or Datacenter), both activated, domain-joined, and using static IPs.
|
|
|
|
### Installing Server Roles
|
|
Install the roles on **both servers**:
|
|
|
|
* **Server Manager → Manage → Add Roles and Features**
|
|
* Click **Next** to **Server Roles**
|
|
* Expand **File and Storage Services**
|
|
* Expand **File and iSCSI Services**
|
|
* Check **File Server**
|
|
* Check **DFS Namespaces**
|
|
* Check **DFS Replication**
|
|
* **Next → Next → Install**, then finish.
|
|
|
|
### Create & Configure Network Shares
|
|
Create (or identify) the folders you want to publish in the namespace, and share them on **each** server. Be sure to enable **Access-based Enumeration** on all of the folder shares for additional security. You only need to ensure that the folders exist on one of the servers, it will be created on-the-fly and replicated automatically.
|
|
|
|
Additionally, it is recommended (if possible) to set the share names to be hidden. For example `\\LAB-FPS-01\Projects$`, that way it ensures that users access the share via DFS at `\\bunny-lab.io\Projects` and users don't accidentally access the network shares directly, bypassing DFS. For example, the local path would be `Z:\Projects` but the network share would be `\\LAB-FPS-01\Projects$`. *This wouldn't break things like replication, but it would muck things up a little bit organizationally.*
|
|
|
|
!!! warning "What must match vs. what can differ"
|
|
- **Must exist on each server:** a shared folder to act as the *folder target* (path can differ per server).
|
|
- **Share permissions:** are **not replicated**; set them on each server.
|
|
- **NTFS permissions inside the replicated folder:** **are replicated** by DFSR and should be consistent.
|
|
- Targets do **not** have to use identical share names/paths, but keeping them consistent simplifies things.
|
|
|
|
| **Permission Type** | **User / Group** | **Access** | Level** |
|
|
| :---- | :---- | :---- | :---- |
|
|
| Share | `Everyone` (or `Authenticated Users`) | Full Control | Best practice is to grant broad Full Control on the **share** and enforce access with NTFS. |
|
|
| NTFS | `SYSTEM` | Full Control | Required for DFSR service. |
|
|
| NTFS | `Share_Admins` | Full Control | Optional admin group for data management. |
|
|
| NTFS | *Business groups needing access* | Modify | Grant least privilege to required users/groups. |
|
|
|
|
!!! info "Note On Inheritance"
|
|
Disabling inheritance is **not required** for DFS/DFSR. Keep it enabled unless you have a clear reason to flatten ACLs; inheritance often reduces long-term admin overhead.
|
|
|
|
### DFS Breakdown
|
|
A **namespace** is a logical view like `\\bunny-lab.io\Projects`. Inside it, you create DFS **folders** (e.g., `Scripting`) that point to one or more **folder targets**, such as:
|
|
|
|
* `\\LAB-FPS-01\Projects$\Scripting`
|
|
* `\\LAB-FPS-02\Projects$\Scripting`
|
|
|
|
The namespace root itself isn't where you store data; it's a directory of links. Place data in the folder targets the DFS folder points to.
|
|
|
|
### DFS Configuration
|
|
You can run these steps from either server (or any admin workstation with the RSAT tools). DFSN configuration is stored in AD and on namespace servers and applies across members automatically.
|
|
|
|
#### Create Namespace
|
|
|
|
* **Server Manager → Tools → DFS Management**
|
|
* Right-click **Namespaces** → **New Namespace...**
|
|
* Choose a server to host the namespace (e.g., `LAB-FPS-01`) → **Next**
|
|
* Name the namespace (e.g., `Projects`) → **Next**
|
|
* You can leave **Edit Settings** at defaults; those control the local folder that backs the namespace root, not your data.
|
|
* Choose **Domain-based namespace** and check **Enable Windows Server 2008 mode** (required for larger scale and Access-based enumeration).
|
|
* Resulting path: `\\bunny-lab.io\Projects`
|
|
* **Next → Create**
|
|
|
|
#### Make Namespace Highly-Available
|
|
We have to perform an extra step to ensure that every file server can act as within a multi-master context, allowing for high availability. To do this in this example, we will add `LAB-FPS-02` as a secondary namespace server for every namespace that we create.
|
|
|
|
- Right-Click **DFS Management** > **Namespaces** > `\\bunny-lab.io\Projects`
|
|
- Click **Add Namespace Server...**
|
|
- Under "Namespace Server" enter `LAB-FPS-02` then click **OK**.
|
|
|
|
#### Enable Access-Based Enumeration on Namespace
|
|
|
|
- Right-Click **DFS Management** > **Namespaces** > `\\bunny-lab.io\Projects`
|
|
- Click **Properties**
|
|
- Click **Advanced**
|
|
- Check **Enable access-based enumeration for this namespace**
|
|
- Click **OK**
|
|
|
|
#### Link Folders to Namespace
|
|
Create the DFS folders and add folder targets:
|
|
|
|
* Right-click the new namespace (e.g., `\\bunny-lab.io\Projects`) → **New Folder...**
|
|
* **Name:** `Scripting`
|
|
* **Add** folder targets (one per server), e.g.:
|
|
* `\\LAB-FPS-01\Projects$\Scripting`
|
|
* `\\LAB-FPS-02\Projects$\Scripting`
|
|
* You can simply copy-paste the previous server location and substitute the hostname (e.g. switching `01` to `02`) instead of browsing for the folder.
|
|
* You *may* be prompted to create the folder because it does not exist on `LAB-FPS-02`, in this circumstance, you can tell it to create the folder automatically with read-only permissions. *Don't worry, when replication from `LAB-FPS-01` occurs, NTFS permissions will be overwritten to the correct users and groups.*
|
|
* When prompted *"Create a replication group to synchronize the folder targets?"*, click **Yes** to launch the DFS Replication wizard.
|
|
|
|
!!! info "**Be patient**"
|
|
The Replication wizard can take ~1 minute to appear.
|
|
|
|
#### Configure Replication Group
|
|
In the Replication wizard that appears after about a minute, you can configure the replication group for the folder:
|
|
|
|
!!! bug "If Wizard did Not Appear (or Crashed)"
|
|
In my homelab testing, I had two times when the wizard crashed or simply never opened. If this happens to you, you can manually re-trigger the wizard for the target folder by right-clicking the folder (e.g. `\\bunny-lab.io\Projects\Scripting`) and selecting **Replicate Folder**.
|
|
|
|
* **Replication Group Name**: *(leave as suggested)*
|
|
* **Replicated Folder Name**: *(leave as suggested)*
|
|
* **Next → Next**
|
|
* **Primary member**: pick the server with the **most up-to-date** copy of the data (e.g., `LAB-FPS-01`).
|
|
|
|
!!! warning "Important"
|
|
In DFSR, "Primary member" is used **only for initial sync conflict resolution**. It does **not** permanently dominate, and DFSR will not blindly wipe unique files on other members; conflicts are handled via versioning (e.g., "ConflictAndDeleted"). For large datasets, consider *pre-seeding* to reduce initial replication time via robocopy.
|
|
|
|
* **Topology**: `Full mesh` (good for two servers; for many sites, consider hub-and-spoke).
|
|
* **Replication schedule**: leave **Full** (24x7) unless you need bandwidth windows.
|
|
* **Create**
|
|
|
|
!!! success "Replication group created"
|
|
You should see green ticks for the following. Give everything some time to replicate as it depends on active directory replication speeds to push out the configuration across the DFS member servers and begin the replication.
|
|
|
|
- Create replication group
|
|
- Create members
|
|
- Update folder security
|
|
- Create replicated folder
|
|
- Create membership objects
|
|
- Update folder properties
|
|
- Create connections
|
|
|
|
### Checking DFS Status
|
|
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
|
|
``` |