Restructured Documentation
Automatic Documentation Deployment / Sync Docs to https://kb.bunny-lab.io (push) Successful in 8s

This commit is contained in:
2026-09-05 14:08:43 -06:00
parent c4bd235eba
commit 289769a601
281 changed files with 5403 additions and 3563 deletions
@@ -5,22 +5,28 @@ tags:
- Scripting
---
**Purpose**: Sometimes you will need to connect to Office365 via powershell in order to perform troubleshooting / automation that either is too complex to do via the website, or is not exposed / possible to do via the website.
## Purpose
Sometimes you will need to connect to Office365 via powershell in order to perform troubleshooting / automation that either is too complex to do via the website, or is not exposed / possible to do via the website.
## Update Nuget Package Manager
``` powershell
```powershell
Install-PackageProvider -Name NuGet -Force -ForceBootstrap
```
## Install ExchangeOnlineManagement Powershell Modules
You will need to install and import the modules for Exchange Online before you can run the commands necessary for interacting with it.
``` powershell
```powershell
Install-Module -Name ExchangeOnlineManagement -Force
Import-Module ExchangeOnlineManagement
```
## Connect to Exchange Online
When you run the following command, it will open a dialog box to take the username, password, and MFA code (if applicable) for an administrative account in the Exchange Online environment.
``` powershell
```powershell
Connect-ExchangeOnline -UserPrincipalName admin@domain.com
```
```
## Related Documentation
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,11 +4,12 @@ tags:
- Email
---
**Purpose**: Sometimes you need to set an autoreply on a mailbox on behalf of someone else. In these cases, you can leverage the "Exchange Admin Shell" to configure an auto-reply to anyone who sends an email to the mailbox.
## Purpose
Sometimes you need to set an autoreply on a mailbox on behalf of someone else. In these cases, you can leverage the "Exchange Admin Shell" to configure an auto-reply to anyone who sends an email to the mailbox.
In the example below, replace `<username>` with the shortened username of the target user. (e.g. `nicole.rappe` not `nicole.rappe@bunny-lab.io`)
``` powershell
```powershell
Set-MailboxAutoReplyConfiguration -Identity <username> -AutoReplyState Scheduled -StartTime "1/1/2025 00:00:00" -EndTime "1/15/2025 00:00:00" -InternalMessage "Example,<br><br>Message here.<br><br>Thank you." -ExternalMessage "Example,<br><br>Message here.<br><br>Thank you."
```
@@ -17,10 +18,14 @@ Set-MailboxAutoReplyConfiguration -Identity <username> -AutoReplyState Scheduled
!!! example "Example Email Reply"
The email auto reply will look something like this based on the command above.
```
```text
Example,
Message Here.
Thank you.
```
```
## Related Documentation
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,27 @@
---
tags:
- Microsoft Exchange
- Email
---
## Purpose
Start the Microsoft Exchange and supporting Windows services selected by this PowerShell script. The script sets their startup type to Automatic, so review the service requirements for the target server before running it.
!!! warning "Review Service Startup Changes"
The preserved script sets every selected service to Automatic and starts it, including services that may intentionally be disabled. It does not perform a service restart. Do not use it as the return-to-service step for Exchange SE; follow the baseline-specific service validation in the rolling-update workflow.
```powershell
$servicelist = Get-Service | Where-Object {$_.DisplayName -like "Microsoft Exchange *"}
$servicelist += Get-Service | Where-Object {$_.DisplayName -eq "IIS Admin Service"}
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "Windows Management Instrumentation" }
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "World Wide Web Publishing Service" }
foreach($service in $servicelist){
Set-Service $service -StartupType Automatic
Start-Service $service
}
```
## Related Documentation
- [Exchange SE Maintenance](<../../../../workflows/Applications/Email/Microsoft Exchange/Perform Exchange SE DAG Rolling Updates.md>) — Use its service-baseline validation when maintaining an SE DAG.
- [Related Email Documentation](<../../../../reference/Applications/Email/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -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.
@@ -4,7 +4,7 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
Sometimes you just need a basic script that outputs a pretty directory and file tree. This script offers files and folders to ignore, and outputs a fancy directory tree.
```powershell
@@ -26,8 +26,8 @@ function Export-Tree {
[string]$Prefix
)
$items = Get-ChildItem -Path $Folder -Force | Where-Object {
$_.Name -ne "." -and $_.Name -ne ".." -and
$items = Get-ChildItem -Path $Folder -Force | Where-Object {
$_.Name -ne "." -and $_.Name -ne ".." -and
($global:IgnoreList -notcontains $_.Name)
} | Sort-Object PSIsContainer, Name
@@ -51,4 +51,7 @@ function Export-Tree {
# Run it
Export-Tree -Path "." -OutFile "directory_tree.txt"
```
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -5,10 +5,10 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
Locate specific files, and copy them with a renamed datestamp appended to a specific directory.
``` powershell
```powershell
# Define an array of objects, each having a prefix and a suffix
$files = @(
@{Prefix="name"; Suffix="Extension"},
@@ -41,4 +41,7 @@ foreach ($file in $files) {
Copy-Item -Path $match.FullName -Destination (Join-Path -Path $destination -ChildPath $newName)
}
}
```
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -6,10 +6,10 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
This script will iterate over all network shares hosted by the computer it is running on, and will give *recursive* permissions to all folders, subfolders, and files, including hidden ones. It is very I/O intensive given it iterates recursively on every file/folder being shared.
``` powershell
```powershell
$AllShares = Get-SMBShare | Where-Object {$_.Description -NotMatch "Default share|Remote Admin|Remote IPC|Printer Drivers"} | Select-Object -ExpandProperty Path
$Output = @()
ForEach ($SMBDirectory in $AllShares)
@@ -17,13 +17,16 @@ ForEach ($SMBDirectory in $AllShares)
$FolderPath = Get-ChildItem -Directory -Path $SMBDirectory -Recurse -Force
ForEach ($Folder in $FolderPath) {
$Acl = Get-Acl -Path $Folder.FullName
ForEach ($Access in $Acl.Access)
ForEach ($Access in $Acl.Access)
{
$Properties = [ordered]@{'Folder Name'=$Folder.FullName;'Group/User'=$Access.IdentityReference;'Permissions'=$Access.FileSystemRights;'Inherited'=$Access.IsInherited}
$Output += New-Object -TypeName PSObject -Property $Properties
$Output += New-Object -TypeName PSObject -Property $Properties
}
}
}
$Output | Export-CSV -Path C:\SMB_REPORT.csv -NoTypeInformation -Append
```
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -6,13 +6,16 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
This script will iterate over all network shares hosted by the computer it is running on, and will give *top-level* permissions to all the shared folders. It will not navigate deeper than the top-level in its report. Very I/O friendly.
``` powershell
```powershell
$AllShares = Get-SMBShare | Where-Object {$_.Description -NotMatch "Default share|Remote Admin|Remote IPC|Printer Drivers"} | Select-Object -ExpandProperty Name
ForEach ($SMBDirectory in $AllShares)
{
Get-SMBShareAccess -Name $SMBDirectory | Export-CSV -Path C:\SMB_REPORT.csv -NoTypeInformation -Append
}
```
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -5,12 +5,13 @@ tags:
- Scripting
---
**Purpose**: In some unique cases, you want to be able to either perform backups of data or exfiltrate data to Nextcloud from a local device via the use of a script. Doing such a thing with Nextcloud as the destination is not very documented, but you can achieve that result by running a script like what is seen below:
## Purpose
In some unique cases, you want to be able to either perform backups of data or exfiltrate data to Nextcloud from a local device via the use of a script. Doing such a thing with Nextcloud as the destination is not very documented, but you can achieve that result by running a script like what is seen below:
## Windows
!!! abstract "Environment Variables"
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
| **Variable** | **Default Value** | **Description** |
| :--- | :--- | :--- |
| `NEXTCLOUD_SERVER_URL` | `https://cloud.bunny-lab.io` | This is the base URL of the Nextcloud server that data will be copied to. |
@@ -22,7 +23,7 @@ tags:
| `LOGFILE` | `C:\Windows\Temp\nc_pull.log` | This file is how the script has "persistence". In case the computer is shut down, rebooted, etc, when it comes back online and the script is re-ran against it, it reads this file to pick up where it last was, and attempts to resume from that point. If this transfer is meant to be hidden, put this file somewhere someone is not likely to find it easily. |
### Powershell Script
``` powershell
```powershell
# --------------------------
# Function for File Upload Logic
# --------------------------
@@ -59,7 +60,7 @@ Function Upload-Files ($targetDir) {
# Record this file in the log since it was successfully uploaded
Add-Content -Path $LOGFILE -Value $_.FullName
} else {
Write-Host "Skipping previously uploaded file $($_.FullName)"
}
@@ -99,8 +100,8 @@ Function Upload-Files ($targetDir) {
## MacOS/Linux
!!! abstract "Environment Variables"
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
You will need to assign the following variables either within the script or externally via environment variables at the time the script is executed.
| **Variable** | **Default Value** | **Description** |
| :--- | :--- | :--- |
| `NEXTCLOUD_SERVER_URL` | `https://cloud.bunny-lab.io` | This is the base URL of the Nextcloud server that data will be copied to. |
@@ -109,8 +110,8 @@ Function Upload-Files ($targetDir) {
| `DATA_TO_COPY` | `/home/bunny/example` | This directory target is the primary focus of the upload / backup / exfiltration. The script will iterate through this target first before it moves onto the secondary target. The target can be a directory or a single file. This will act as the main priority of the transfer. |
| `LOGFILE` | `/tmp/uploaded_files.log` | This file is how the script has "persistence". In case the computer is shut down, rebooted, etc, when it comes back online and the script is re-ran against it, it reads this file to pick up where it last was, and attempts to resume from that point. If this transfer is meant to be hidden, put this file somewhere someone is not likely to find it easily. |
### Bash Script
``` sh
### Bash Script
```sh
#!/bin/bash
# Directory to search
@@ -128,7 +129,7 @@ fi
find "$DIR" -type f -print0 | while IFS= read -r -d '' file; do
# Extract just the filename
filename=$(basename "$file")
# Check if this file has been uploaded before
if ! grep -q "$file" "$LOGFILE"; then
echo "Uploading $file ..."
@@ -153,4 +154,7 @@ find "$DIR" -type f -print0 | while IFS= read -r -d '' file; do
echo "Skipping previously uploaded file $file"
fi
done
```
```
## Related Documentation
- [Related Files and Collaboration Documentation](<../../../reference/Applications/Files and Collaboration/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -5,7 +5,8 @@ tags:
- Scripting
---
**Purpose**: This script was purpose-built for the homelab Minecraft servers in my homelab. It may need to be ported based on your own needs.
## Purpose
This script was purpose-built for the homelab Minecraft servers in my homelab. It may need to be ported based on your own needs.
```powershell
clear
@@ -34,7 +35,7 @@ function Get-ZipFileName {
Write-Host "File not found! Please check the file name and try again." -ForegroundColor Red
exit
}
Write-Host "ZIP file found: $zipFilePath" -ForegroundColor Green
return $zipFilePath
}
@@ -103,7 +104,7 @@ function Rename-OldServer {
$currentDate = Get-Date -Format "MM-dd-yyyy"
$backupFolderPath = "$oldFolderPath.backup.$currentDate"
Write-Host "Step 4: Renaming old server folder to: $backupFolderPath" -ForegroundColor Yellow
Rename-Item -Path $oldFolderPath -NewName $backupFolderPath
Write-Host "Old server folder renamed to: $backupFolderPath" -ForegroundColor Green
@@ -158,4 +159,7 @@ Write-Host "Press any key to exit the script"
[System.Console]::ReadKey($true) # Waits for a key press and doesn't display the pressed key
clear
```
```
## Related Documentation
- [Related Applications Documentation](<../../../reference/Applications/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -7,7 +7,10 @@ tags:
- Windows
---
``` batch
## Purpose
This document records the procedure for blue iris server watchdog. Follow the environment assumptions and commands below.
```batch
@echo off
REM Change to the Blue Iris 5 directory
@@ -28,4 +31,7 @@ timeout /t 10 /nobreak >nul
REM Go back to the beginning of the loop
GOTO :LOOP
```
```
## Related Documentation
- [Related Applications Documentation](<../../../reference/Applications/index.md>) — Find the connected deployments, procedures, and references for this subject.
+19
View File
@@ -0,0 +1,19 @@
---
tags:
- Applications
- Scripts
- Documentation
---
# Applications
## Purpose
Find scripts for applications. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Email
- Files and Collaboration
- Gaming and Media
- Home Automation
## Follow the Subject
[Applications](<../../reference/Applications/index.md>) explains the relationships and offers starting points for the documented tasks.
@@ -5,11 +5,10 @@ tags:
- Linux
---
# Git Repo Updater (Script)
## Purpose
Standalone `repo_watcher.sh` script used by the Git Repo Updater container. This script clones or pulls one or more repositories and rsyncs them into destination paths.
Standalone `repo_watcher.sh` script used by the Git Repo Updater container. This script clones or pulls one or more repositories and rsyncs them into destination paths.
For the containerized version and deployment details, see the [Git Repo Updater container doc](../../deployments/platforms/containerization/docker/custom-containers/git-repo-updater.md).
For the containerized version and deployment details, see the [Git Repo Updater container doc](<../../../deployments/Containers/Docker/Git Repo Updater.md>).
## Script
```sh
@@ -67,3 +66,5 @@ while true; do
done
```
## Related Documentation
- [Related Gitea Workflows](<../../../reference/Automation/Gitea Configuration Delivery.md>) — Find the connected deployments, procedures, and references for this subject.
+16
View File
@@ -0,0 +1,16 @@
---
tags:
- Automation
- Scripts
- Documentation
---
# Automation
## Purpose
Find scripts for automation. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Gitea
## Follow the Subject
[Automation](<../../reference/Automation/index.md>) explains the relationships and offers starting points for the documented tasks.
@@ -1,26 +0,0 @@
---
tags:
- SSH
- Bash
- Authentication
- Scripting
- Linux
---
*Purpose*: Sometimes you need two linux computers to be able to talk to eachother without requiring a password. Passwordless SSH can be achieved by running the following commands:
!!! note "Non-Root Key Storage Considerations"
When you generate SSH keys, they will be stored in a specific user's profile, the one currently executing the commands. If you want to have passwordless SSH, you would run the commands from a non-root user (e.g. `nicole`).
``` sh
ssh-keygen # (1)
ssh-copy-id -i /home/nicole/.ssh/id_rsa.pub nicole@192.168.3.18 # (2)
ssh -i /home/nicole/.ssh/id_rsa nicole@192.168.3.18 # (3)
```
1. Just leave all of the default options and do not put a password on the SSH key. )
2. Change the directories to account for your given username, and change the destination to the user@IP corresponding to the remote server. You will be prompted to enter the password once to store the SSH public key on the remote computer.
3. This command is to validate that everything worked. If the remote user is the same as the local user (e.g. `nicole`) then you dont need to add the `-i /home/nicole/.ssh/id_rsa` section to the SSH command.
!!! warning "Run before configuring Global SSH Infrastructure Key"
There is a global automation that leverages a [Global Infrastructure Public SSH Key](https://git.bunny-lab.io/Infrastructure/LinuxServer_SSH_PublicKey). If this runs before you run the commands above, you will be unable to configure SSH key relationships and it will need to be done manually.
@@ -1,12 +0,0 @@
---
tags:
- Linux
- Bash
- Scripting
---
``` sh
xrandr --auto
xrandr --setprovideroutputsource 4 0
xrandr --output HDMI-1 --primary --mode 1920x1080 --rate 75.00 --output DVI-I-1-1 --mode 1920x1080 --rate 60.00 --right-of HDMI-1 --output -eDP-1 --off
```
-27
View File
@@ -1,27 +0,0 @@
---
tags:
- Bash
- QEMU
- Scripting
- Linux
---
**Purpose**:
You may need to install the QEMU guest agent on linux VMs manually, while Windows-based devices work out-of-the-box after installing the VirtIO guest tools installer.
=== "Ubuntu Server"
```sh
sudo su
apt update
apt install -y qemu-guest-agent
systemctl enable --now qemu-guest-agent
```
=== "Rocky Linux"
```sh
sudo su
dnf install -y qemu-guest-agent
systemctl enable --now qemu-guest-agent
```
-26
View File
@@ -1,26 +0,0 @@
---
tags:
- XRDP
- Bash
- Scripting
- Linux
---
**Purpose**:
If you need to set up RDP access to a Linux environment, you will want to install XRDP. Once it is installed, you can leverage other tools such as Apache Guacamole to remotely connect to it.
```
# Install and Start XRDP Service
sudo dnf install epel-release -y
sudo dnf install xrdp -y
sudo systemctl enable --now xrdp
# Open Firewall Rules for RDP Traffic
sudo firewall-cmd --permanent --add-port=3389/tcp
sudo firewall-cmd --reload
# Configure Desktop Environment to Launch when you Login via RDP (Run as Non-Root User)
# XFCE4 Desktop Environment
echo "startxfce4" > ~/.Xclients
chmod +x ~/.Xclients
```
-13
View File
@@ -1,13 +0,0 @@
---
tags:
- RAID
- Bash
- Scripting
- Linux
---
https://www.digitalocean.com/community/tutorials/how-to-create-raid-arrays-with-mdadm-on-ubuntu-16-04
``` sh
sudo mdadm --grow /dev/md0 -l 5
cat /proc/mdstat
```
-15
View File
@@ -1,15 +0,0 @@
---
tags:
- Bash
- Ports
- Scripting
- Linux
---
**Purpose**:
If you want to check if a certain TCP port is open on a server.
## Netcat Command
``` sh
netcat -z -n -v <IP ADDRESS> <PORT>
```
-19
View File
@@ -1,19 +0,0 @@
---
tags:
- Bash
- Time Sync
- Scripting
- Linux
---
The commands outlined in this short document are meant to be a quick-reference for setting the timezone and date/time of a Linux-based server.
### Set Timezone:
```sh
sudo timedatectl set-timezone America/Denver
```
### Set Time & Date
```sh
date -s "1 JAN 2025 03:30:00"
```
@@ -1,58 +0,0 @@
---
tags:
- Containers
- Docker
- Bash
- Scripting
- Linux
---
**Purpose**:
If you find that you need to migrate a container, along with any supporting files, permissions, etc from an old server to a new server, rsync helps make this as painless as possible.
Be sure to perform the following steps to make sure that you can copy the container's files.
!!! warning
You need to stop the running containers on the old server before copying their data over, otherwise the state of the data may be unstable. Once you have migrated the data, you can spin up the containers on the new server and confirm they work before deleting the data on the old server.
On the destination (new) server, the directory needs to exist and be writable via the person copying the data over SSH:
## Copying Data Between the Old and New Servers
=== "Safe Method"
``` sh
sudo mkdir -p /srv/containers/example
sudo chmod 740 /srv/containers/example
sudo chown nicole:nicole /srv/containers/example
```
=== "Quick & Dirty Method"
``` sh
sudo mkdir -p /srv/containers
sudo chmod 777 /srv/containers
```
On the source (old) server, perform an rsync over to the new server, authenticating yourself as you will be prompted to do so:
=== "Safe Method"
``` sh
rsync -avz -e ssh --progress /srv/containers/example/* nicole@192.168.3.30:/srv/containers/example
```
=== "Quick & Dirty Method"
``` sh
rsync -avz -e ssh --progress /srv/containers/example nicole@192.168.3.30:/srv/containers
```
=== "Quick & Dirty w/ Provided SSH Key Method"
This method assumes that you have the private key for your SSH-based authentication locally on the server somewhere safe with permissions `chmod 600` applied to it. In this example, I placed the private key at `/tmp/id_rsa_OpenSSH`.
``` sh
rsync -avz -e "ssh -i /tmp/id_rsa_OpenSSH" --progress /srv/containers/pihole nicole@192.168.3.62:/srv/containers
```
## Spinning Up Docker / Portainer Stack
Once everything has been moved over, copy the `docker-compose` and `.env` (environment variables) from the old server to the new one, pointing to the same location since we maintained the same folder structure, and the container should spin up like nothing ever happened.
@@ -1,28 +0,0 @@
---
tags:
- Bash
- Netcat
- File Transfer
- Scripting
- Linux
---
**Purpose**: You may find that you need to transfer a file, such as a public SSH key, or some other kind of file between two devices. In this scenario, we assume both devices have the `netcat` command available to them. By putting a network listener on the device recieving the file, then sending the file to that device's IP and port, you can successfully transfer data between computers without needing to set up SSH, FTP, or anything else to establish initial trust between the devices. [Original Reference Material](https://www.youtube.com/shorts/1j17UBGqSog).
!!! warning
The data being transferred will not be encrypted. If you are transferring relatively-safe files such as public SSH keys, etc, this should be fine.
### Destination Computer
Run the following command on the computer that will be recieving the file.
``` sh
netcat -l <random-port> > /tmp/OUTPUT-AS-FILE.txt
```
### Source Computer
Run the following command on the computer that will be sending the file to the destination computer.
``` sh
cat INPUT-DATA.txt | netcat <IP-of-Destination-Computer> <Port-of-Destination-Computer> -q 0
```
!!! info
The `-q 0` command argument causes the netcat connection to close itself automatically when the transfer is complete.
@@ -0,0 +1,17 @@
---
tags:
- Group Policy
- PowerShell
- Scripting
---
## Purpose
This document records the procedure for force group policy updates across the domain. Follow the environment assumptions and commands below.
```powershell
$computers = Get-ADComputer -Filter * -SearchBase "OU=Computers,DC=bunny-lab,DC=io"
$computers | ForEach-Object -Process {Invoke-GPUpdate -Computer $_.name -RandomDelayInMinutes 0 -Force}
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -5,9 +5,12 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
Sometimes you need a report of every user in a domain, and if/when their passwords will expire. This one-liner command will help automate that reporting.
``` powershell
```powershell
Get-Aduser -filter "enabled -eq 'true'" -properties passwordlastset, passwordneverexpires | ft Name, passwordlastset, Passwordneverexpires > C:\PWReport.txt
```
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,18 @@
---
tags:
- PowerShell
- Reporting
- Scripting
---
## Purpose
This document records the procedure for inactive computers. Follow the environment assumptions and commands below.
```powershell
$DaysInactive = 30
$time = (Get-Date).Adddays(-($DaysInactive))
Get-ADComputer -Filter {LastLogonTimeStamp -lt $time} -ResultPageSize 2000 -resultSetSize $null -Properties Name | Select Name
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,19 @@
---
tags:
- PowerShell
- Reporting
- Scripting
---
## Purpose
This document records the procedure for inactive users. Follow the environment assumptions and commands below.
```powershell
InactiveDays = 30
$Days = (Get-Date).Adddays(-($InactiveDays))
Get-ADUser -Filter {LastLogonTimeStamp -lt $Days -and enabled -eq $true} -Properties LastLogonTimeStamp |
select-object Name,@{Name="Date"; Expression={[DateTime]::FromFileTime($_.lastLogonTimestamp).ToString('MM-dd-yyyy')}}
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -5,17 +5,24 @@ tags:
- Scripting
---
!!! info "Prerequesite: [Connect to Azure AD](./connect-to-azure-ad.md)"
## Purpose
This document records the procedure for check email aliases. Follow the environment assumptions and commands below.
!!! info "Prerequesite: [Connect to Azure AD](<Connect to Azure AD.md>)"
The uppercase `SMTP` address is the primary address, while lowercase `smtp` are aliases. You can find the value in active directory in **"User > Attribute Editor > proxyAddresses"**.
``` powershell
```powershell
Get-AzureADUser -ObjectId "user@domain.com" | Select -Property ProxyAddresses
```
!!! example "Example Output"
``` powershell
```powershell
smtp:alias@domain.com
smtp:alias@domain.onmicrosoft.com
SMTP:primaryaddress@domain.com
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,21 +4,27 @@ tags:
- Scripting
---
**Purpose**: Sometimes you will need to connect to Azure AD via powershell in order to perform troubleshooting / automation.
## Purpose
Sometimes you will need to connect to Azure AD via powershell in order to perform troubleshooting / automation.
## Update Nuget Package Manager
``` powershell
```powershell
Install-PackageProvider -Name NuGet -Force -ForceBootstrap
```
## Install AzureAD Powershell Modules
You will need to install the modules for AzureAD before you can run the commands necessary for querying Azure.
``` powershell
```powershell
Install-Module -Name AzureAD
```
## Connect to AzureAD
When you run the following command, it will open a dialog box to take the username, password, and MFA code (if applicable) for an administrative account in the Azure Active Directory.
``` powershell
```powershell
Connect-AzureAD
```
```
## Related Documentation
- [Related Identity and Certificates Documentation](<../../../reference/Identity and Certificates/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,17 @@
---
tags:
- Identity and Certificates
- Scripts
- Documentation
---
# Identity and Certificates
## Purpose
Find scripts for identity and certificates. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Active Directory
- Microsoft 365
## Follow the Subject
[Identity and Certificates](<../../reference/Identity and Certificates/index.md>) explains the relationships and offers starting points for the documented tasks.
@@ -1,5 +1,11 @@
---
tags:
- Networking and Access
- Change DNS Client Server Settings Remotely
---
## Purpose
You may find that for one reason or another, you need to change DNS records of remote windows devices and cannot login via RDP or via console, yet somehow WinRM continues to work. In these scenarios, you can use the following commands to identify the network adapter, change its DNS servers, and verify the settings afterwards.
Change the DNS server addresses used by the selected Windows network adapters through PowerShell remoting. This changes client resolver settings rather than records hosted in a DNS zone.
!!! info "Run as Domain Admin"
You need to run the following commands within the context of a powershell session running as a domain admin, otherwise the `Invoke-Command` commands will fail to execute.
@@ -16,4 +22,7 @@ Invoke-Command -ComputerName $DEVICE -ScriptBlock { Get-DnsClientServerAddress -
# Replace Current DNS Servers for the selected interface
Invoke-Command -ComputerName $DEVICE -ScriptBlock { Set-DnsClientServerAddress -InterfaceAlias "Ethernet" -ServerAddresses ("192.168.3.25","192.168.3.26") }
```
```
## Related Documentation
- [Related Networking and Access Documentation](<../../reference/Networking and Access/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,18 @@
---
tags:
- Bash
- Ports
- Scripting
- Linux
---
## Purpose
If you want to check if a certain TCP port is open on a server.
## Netcat Command
```sh
netcat -z -n -v <IP ADDRESS> <PORT>
```
## Related Documentation
- [Related Networking and Access Documentation](<../../reference/Networking and Access/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -98,4 +98,7 @@ foreach ($adapter in $adapters) {
}
Write-Host "DNS check and correction completed for adapters with a default gateway."
```
```
## Related Documentation
- [Related Networking and Access Documentation](<../../reference/Networking and Access/index.md>) — Find the connected deployments, procedures, and references for this subject.
+18
View File
@@ -0,0 +1,18 @@
---
tags:
- Networking and Access
- Scripts
- Documentation
---
# Networking and Access
## Purpose
Find scripts for networking and access. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Change DNS Client Server Settings Remotely
- Check Open Ports
- Correct DNS Server Priority
## Follow the Subject
[Networking and Access](<../../reference/Networking and Access/index.md>) explains the relationships and offers starting points for the documented tasks.
@@ -1,11 +0,0 @@
---
tags:
- Group Policy
- PowerShell
- Scripting
---
``` powershell
$computers = Get-ADComputer -Filter * -SearchBase "OU=Computers,DC=bunny-lab,DC=io"
$computers | ForEach-Object -Process {Invoke-GPUpdate -Computer $_.name -RandomDelayInMinutes 0 -Force}
```
@@ -1,518 +0,0 @@
---
tags:
- Rclone
- PowerShell
- Synchronization
- Google Drive
---
## Purpose
This document explains the practical differences between the rclone `copy`, `sync`, `check`, and `bisync` commands and provides a safety-first workflow for configuring and recovering a bidirectional synchronization pair. The examples use PowerShell and assume that one side of the synchronization is a Google Drive remote.
Rclone is a command-line file-management program that supports cloud storage providers, object-storage platforms, local filesystems, and standard transfer protocols.
[Download rClone](https://rclone.org/downloads)
[Official rClone Documentation](https://rclone.org/docs/)
!!! info "Version Context"
This document was reviewed against rclone `v1.75.0`. Older releases may not support every bisync flag documented here, and future versions may change some recovery behavior.
!!! danger "Rclone Can Delete or Overwrite Data"
The `sync` and `bisync` commands can delete, replace, rename, or propagate the deletion of files. Always confirm the source and destination paths, maintain a separate backup or snapshot, and preview unfamiliar operations with `--dry-run` before allowing them to modify data.
A dry run is a preview and is not a replacement for a backup.
## Command Behavior
The correct command depends on the intended relationship between the source and destination.
| **Command** | **Primary Behavior** | **Deletes Destination-Only Files** | **Direction** |
| :--- | :--- | :--- | :--- |
| `copy` | Adds or updates source files at the destination while retaining unrelated destination files | No | One-way |
| `sync` | Makes the destination match the source | Yes | One-way |
| `check` | Compares files without modifying either side | No | Read-only |
| `bisync` | Detects and propagates changes made on either side by comparing the current state against prior listings | Yes | Two-way |
### Use `copy` for Additive Transfers
Use `copy` when you need to add or update files without deleting files that already exist only at the destination.
```powershell
& $Rclone copy "Source" "Destination" --update --dry-run --verbose
```
The `--update` flag skips a source file when the corresponding destination file has a newer modification time. It does not create version history, and it does not prevent an older destination file from being replaced by a newer source file.
After reviewing the dry-run output, repeat the operation without `--dry-run`:
```powershell
& $Rclone copy "Source" "Destination" --update --verbose
```
### Use `sync` Only for Intentional Mirroring
Use `sync` when the destination must become a one-way mirror of the source.
!!! danger "`sync` Deletes Destination-Only Files"
The `sync` command removes files from the destination when they do not exist in the source. This is true even when `--update` is present.
The `--update` flag only prevents a newer destination file from being replaced by an older source file. It does not convert `sync` into an additive operation and does not protect destination-only files from deletion.
Preview the operation first:
```powershell
& $Rclone sync "Source" "Destination" --dry-run --verbose
```
Only remove `--dry-run` after confirming that every proposed copy, replacement, and deletion is intentional:
```powershell
& $Rclone sync "Source" "Destination" --verbose
```
### Use `check` for Read-Only Comparison
The `check` command compares files on both sides without copying, replacing, or deleting them.
```powershell
& $Rclone check "Source" "Destination" --combined "rclone-comparison.txt" --log-level INFO --log-file "rclone-check.log"
```
The combined report uses the following symbols:
| **Symbol** | **Meaning** |
| :--- | :--- |
| `=` | The file exists on both sides and matches |
| `+` | The file exists only in the source |
| `-` | The file exists only in the destination |
| `*` | The same path exists on both sides, but the files differ |
| `!` | The file could not be read or compared |
The `check` command compares files but does not report missing empty directories.
## Understand Bisync State
Bisync is a stateful two-way synchronization command. It retains listings of Path1 and Path2 from the prior successful run and compares those listings against the current state during the next run.
Bisync does not maintain a file-version archive or historical copies of every changed file. The listings record synchronization state and metadata needed to determine whether a file is new, changed, or deleted relative to the previous run.
!!! warning "Protect the Bisync Work Directory"
Use a stable, explicit `--workdir` for every run of a given bisync pair. Do not change the work directory, reverse the order of Path1 and Path2, delete the listing files, or run the same pair with different filters without understanding that the existing state may no longer be valid.
On Windows, bisync otherwise stores its state beneath the profile of the account running rclone. This can cause scheduled and interactive executions to use different state directories when they run as different accounts.
## Define the Example Environment
Replace the example values before running the commands in this document.
```powershell
$Rclone = "C:\Path\To\rclone.exe"
$LocalPath = "C:\Path\To\Local"
$RemotePath = "GoogleDrive:Path/To/Remote"
$WorkDir = "C:\Path\To\BisyncState"
$LogDir = "C:\Path\To\Logs"
$FilterFile = "C:\Path\To\bisync-filters.txt"
```
Create the state and log directories:
```powershell
New-Item -Path $WorkDir,$LogDir -ItemType Directory -Force | Out-Null
```
Create a consistent filters file:
```text title="C:\Path\To\bisync-filters.txt"
- *.lnk
```
The `*.lnk` rule excludes Windows shortcut files at any depth beneath the synchronization root.
!!! warning "Filter Changes Require a Reviewed Rebaseline"
Bisync compares current listings against prior listings. Adding, removing, or changing a filter can make previously tracked files disappear from the new listings and appear to have been deleted.
Treat filter changes as a state-changing event. Stop scheduled runs, review the new scope, and perform a dry-run resync before committing the change.
## Validate the Paths
Confirm the installed rclone version:
```powershell
& $Rclone version
```
Confirm that the local root already exists:
```powershell
Test-Path -LiteralPath $LocalPath
```
Expected result:
```text
True
```
Confirm that the configured remote path is reachable:
```powershell
& $Rclone lsd $RemotePath
```
!!! warning "Do Not Automatically Create an Unexpected Root"
Stop if either path is missing or points to an unexpected location. Automatically creating an empty root can conceal a path, authentication, drive-mount, or configuration failure and can cause bisync to interpret an entire data set as deleted.
## Configure the Access Check
The `--check-access` flag verifies that matching files named `RCLONE_TEST` are visible in the same relative locations on both sides. This provides additional protection against an unavailable mount, incorrect remote root, or incomplete listing being interpreted as mass deletion.
Create the access-check file at the root of the local path and copy it to the root of the remote path:
```powershell
$AccessFile = Join-Path $LocalPath "RCLONE_TEST"
New-Item -Path $AccessFile -ItemType File -Force | Out-Null
& $Rclone copyto $AccessFile "$RemotePath/RCLONE_TEST"
```
Confirm that the file appears on both sides:
```powershell
& $Rclone lsf $LocalPath --include "RCLONE_TEST"
& $Rclone lsf $RemotePath --include "RCLONE_TEST"
```
Expected result from both commands:
```text
RCLONE_TEST
```
!!! warning "Do Not Delete the Access-Check File"
The `RCLONE_TEST` file must remain visible on both sides while `--check-access` is enabled. Bisync will abort when the file cannot be found.
## Audit the Existing Data
Before establishing or rebuilding the bisync state, compare the current file sets without changing either side:
```powershell
& $Rclone check $LocalPath $RemotePath --filter-from $FilterFile --drive-skip-gdocs --combined (Join-Path $LogDir "pre-bisync-check.txt") --log-level INFO --log-file (Join-Path $LogDir "pre-bisync-check.log")
```
Review all source-only, destination-only, differing, and unreadable files before proceeding. Determine whether the expected recovery is to merge both sides, prefer one authoritative side, or preserve selected files manually.
## Initialize a Bisync Pair
A new bisync pair requires an initial resync to establish its Path1 and Path2 listings. A preliminary `sync --update` operation is not required and can unnecessarily delete destination-only files.
!!! danger "Resync Is Not a Database-Only Repair"
A resync reconciles the actual contents of both paths while rebuilding the listings.
Files that exist on only one side are copied to the other side. When the same relative path contains different files on both sides, `--resync-mode` selects one version as the winner and overwrites the other version.
The conflict flags used during normal bisync runs do not apply during a resync. A resync does not rename the losing version into a conflict copy.
### Select the Resync Policy
Choose the resync policy according to which data is authoritative:
| **Mode** | **Use When** |
| :--- | :--- |
| `path1` | Path1 is authoritative and must win every same-path difference |
| `path2` | Path2 is authoritative and must win every same-path difference |
| `newer` | Modification times are trustworthy and the newest same-path version should win |
| `larger` | File size is a more reliable winner than modification time |
A bare `--resync` is equivalent to `--resync-mode path1`. Do not use a bare `--resync` unless Path1 is intentionally authoritative.
The examples below use `--resync-mode newer`. This is appropriate only when both sides provide trustworthy modification times and the intended policy is to preserve the newer same-path file.
### Preview the Initial Resync
Run the initial resync as a dry run:
```powershell
& $Rclone bisync $LocalPath $RemotePath --workdir $WorkDir --resync-mode newer --create-empty-src-dirs --compare size,modtime,checksum --check-access --max-delete 0 --filters-file $FilterFile --drive-skip-gdocs --fix-case --dry-run --verbose --log-file (Join-Path $LogDir "bisync-resync-dry-run.log")
```
Review the complete log for:
- Files copied from Path1 to Path2
- Files copied from Path2 to Path1
- Same-path files that would be replaced
- Unexpected paths
- Access or checksum errors
- Proposed deletion behavior
!!! note "Dry-Run Deletion Messages"
A bisync dry run may display confusing deletion messages because the simulated copy that would normally precede the deletion did not actually occur. Review the complete sequence rather than evaluating an isolated deletion line.
Do not dismiss an unexpected deletion unless the log clearly shows that it is a dry-run artifact associated with a preceding simulated copy.
The `--max-delete 0` setting blocks ordinary file deletions during this recovery run. It does not prevent a same-path losing version from being overwritten during resync, so a separate backup or snapshot remains required.
### Perform the Initial Resync
After reviewing and approving the dry-run output, repeat the command without `--dry-run`:
```powershell
& $Rclone bisync $LocalPath $RemotePath --workdir $WorkDir --resync-mode newer --create-empty-src-dirs --compare size,modtime,checksum --check-access --max-delete 0 --filters-file $FilterFile --drive-skip-gdocs --fix-case --verbose --log-file (Join-Path $LogDir "bisync-resync-live.log")
```
A successful run should end with:
```text
Bisync successful
```
### Validate the Reconciled Data
Compare both sides after the live resync:
```powershell
& $Rclone check $LocalPath $RemotePath --filter-from $FilterFile --drive-skip-gdocs --combined (Join-Path $LogDir "post-resync-check.txt") --log-level INFO --log-file (Join-Path $LogDir "post-resync-check.log")
```
The expected result is:
```text
0 differences found
```
## Run Normal Bisync Operations
After the baseline has been established, all normal runs must omit `--resync` and `--resync-mode`.
Preview the first normal run:
```powershell
& $Rclone bisync $LocalPath $RemotePath --workdir $WorkDir --create-empty-src-dirs --conflict-resolve newer --conflict-loser num --compare size,modtime,checksum --resilient --recover --max-lock 2m --check-access --max-delete 10 --filters-file $FilterFile --drive-skip-gdocs --fix-case --dry-run --log-level INFO --log-file (Join-Path $LogDir "bisync-normal-dry-run.log")
```
A healthy unchanged pair should report:
```text
No changes found
Updating listings
Bisync successful
```
After reviewing the dry run, perform one controlled live run:
```powershell
& $Rclone bisync $LocalPath $RemotePath --workdir $WorkDir --create-empty-src-dirs --conflict-resolve newer --conflict-loser num --compare size,modtime,checksum --resilient --recover --max-lock 2m --check-access --max-delete 10 --filters-file $FilterFile --drive-skip-gdocs --fix-case --log-level INFO --log-file (Join-Path $LogDir "bisync.log")
```
This normal command can be placed into the scheduled automation after it has completed successfully under the same Windows account and execution context that will run the scheduled task.
## Understand the Normal Bisync Flags
| **Flag** | **Behavior** |
| :--- | :--- |
| `--workdir` | Stores the prior Path1 and Path2 listings in an explicit, stable directory |
| `--create-empty-src-dirs` | Synchronizes the creation and deletion of empty directories |
| `--compare size,modtime,checksum` | Uses size, modification time, and checksums when determining file state |
| `--conflict-resolve newer` | Prefers the newer file when the same file changed independently on both sides since the prior run |
| `--conflict-loser num` | Preserves the losing conflict version under a numbered `.conflictN` name |
| `--resilient` | Allows certain less-serious errors to be retried during future runs rather than immediately requiring resync |
| `--recover` | Uses a backup listing to recover from some interrupted or ungracefully terminated runs |
| `--max-lock 2m` | Allows a stale bisync lock to expire after two minutes |
| `--check-access` | Aborts when matching `RCLONE_TEST` files cannot be found on both sides |
| `--max-delete 10` | Aborts when more than 10 percent of tracked files appear deleted on either side |
| `--filters-file` | Applies one consistent set of exclusions to both bisync listings |
| `--drive-skip-gdocs` | Makes native Google Docs, Sheets, Slides, and other Google-native documents invisible to rclone |
| `--fix-case` | Allows supported case-only filename corrections between filesystems |
| `--log-level INFO` | Records normal operations, changes, warnings, and errors rather than errors alone |
### Understand Conflict Resolution
A bisync conflict occurs when the same relative file is new or changed on both sides compared with the prior successful run and the current file contents are not identical.
The `--conflict-resolve newer` option does not mean that rclone blindly compares every file and always keeps whichever modification time is newest. It applies specifically to a detected two-sided conflict.
With `--conflict-loser num`, the winner retains the original filename and the losing version is preserved with a numbered conflict suffix, such as:
```text
Document.docx
Document.docx.conflict1
```
This is safer than `--conflict-loser delete`, which permanently removes the losing conflict version.
### Understand the Deletion Limit
The `--max-delete 10` value is a conservative example rather than a universal requirement. Select a threshold appropriate for the size of the data set and the organization's normal deletion patterns.
A large folder rename may appear as many deletions and many new files and can exceed the configured threshold.
!!! danger "Do Not Use `--force` as a Recurring Option"
The `--force` flag bypasses the `--max-delete` safety check. Do not include it in a normal scheduled command.
When a legitimate change exceeds the threshold, stop the scheduled operation, review the proposed changes with `--dry-run`, and use a one-time explicitly approved threshold instead of permanently disabling the protection.
### Understand Google-Native Documents
The `--drive-skip-gdocs` flag removes native Google documents from rclone listings. These objects are not downloaded, exported, compared, or synchronized while the flag is enabled.
This flag does not merely ignore local files with extensions such as `.gdoc` or `.gsheet`. It makes the corresponding native Google Drive objects effectively invisible to rclone.
Remove this flag only when Google-native documents must be exported and synchronized and the desired export formats have been deliberately configured. Changing this behavior on an established bisync pair requires a reviewed rebaseline because it changes which objects appear in the listings.
## Repair a Broken Bisync Pair
A request for `--resync` does not automatically mean that the data is damaged. Bisync may be unable to locate or trust its prior state because the work directory changed, the command used different paths or filters, the execution account changed, or a prior run ended with a critical error.
### Stop Automated Runs
Stop all scheduled or looping bisync processes before investigating or repairing the state. Confirm that another rclone process is not modifying either path or the bisync listings.
### Review the Logs
Search the most recent log for the first critical error rather than relying only on the final resync message.
```powershell
Get-Content (Join-Path $LogDir "bisync.log") -Tail 300
```
Look for:
- Authentication or remote-access errors
- Missing-path errors
- Missing or invalid listing files
- Access-check failures
- Excessive deletion warnings
- File-copy or file-move failures
- Lock-file errors
- Filter changes
- `Bisync aborted`
- `Bisync critical error`
### Verify the Existing Configuration
Confirm that the repair command uses:
- The original Path1 and Path2 in the original order
- The original `--workdir`
- The original filter rules
- The intended rclone configuration file
- The correct remote account and storage location
- The same Google Docs behavior
- The same comparison settings
List the current bisync state directory:
```powershell
Get-ChildItem -LiteralPath $WorkDir
```
The directory should contain Path1 and Path2 `.lst` files for the configured pair.
### Test Remote and Local Access
Confirm that both roots are present and readable:
```powershell
Test-Path -LiteralPath $LocalPath
& $Rclone lsd $RemotePath
```
Confirm that the access-check file remains visible:
```powershell
& $Rclone lsf $LocalPath --include "RCLONE_TEST"
& $Rclone lsf $RemotePath --include "RCLONE_TEST"
```
### Compare the Current Data
Run a read-only comparison before deciding to resync:
```powershell
& $Rclone check $LocalPath $RemotePath --filter-from $FilterFile --drive-skip-gdocs --combined (Join-Path $LogDir "repair-comparison.txt") --log-level INFO --log-file (Join-Path $LogDir "repair-check.log")
```
Review whether one-sided files are expected, whether one side is authoritative, and whether same-path differences should be resolved manually.
### Attempt Normal Recovery When Appropriate
When the prior run was only interrupted and the normal command already uses `--recover`, a normal controlled run may recover without requiring a resync.
Run it first as a dry run:
```powershell
& $Rclone bisync $LocalPath $RemotePath --workdir $WorkDir --create-empty-src-dirs --conflict-resolve newer --conflict-loser num --compare size,modtime,checksum --resilient --recover --max-lock 2m --check-access --max-delete 10 --filters-file $FilterFile --drive-skip-gdocs --fix-case --dry-run --log-level INFO --log-file (Join-Path $LogDir "bisync-recovery-dry-run.log")
```
Proceed with a live normal run only when the output is understood and expected.
### Rebuild the State Only When Required
Use a resync only when:
- The pair has never been initialized
- The listings are missing or cannot be trusted
- A critical bisync error explicitly requires a resync
- The path scope or filter rules intentionally changed
- You are deliberately establishing a new authoritative baseline
Select the correct `--resync-mode`, perform a dry run, review every proposed change, and then follow the initialization and validation sequence documented above.
!!! danger "Do Not Describe Resync as Non-Destructive"
Resync can overwrite a same-path file and can restore files that were intentionally deleted by copying one-sided files back to the other side.
The `--conflict-resolve` and `--conflict-loser` flags are ignored during resync. Maintain an independent backup and do not proceed until the chosen resync policy is understood.
## Validate Bidirectional Synchronization
After the initial setup or a repair, validate both directions with disposable files.
- Create a test text file beneath the local root.
- Run a normal bisync and confirm that the file appears remotely.
- Create a second test text file beneath the remote root.
- Run another normal bisync and confirm that the file appears locally.
- Delete both test files.
- Run another normal bisync and confirm that the deletions propagate as intended.
- Confirm that the final run ends with `Bisync successful`.
- Run `rclone check` and confirm that no file differences remain.
Remove all test artifacts when validation is complete.
## Troubleshooting
### Google Drive Reports Shared Drive Not Found
An error such as `404: Shared Drive not found` normally means that the authenticated Google account cannot access the configured Shared Drive ID.
Verify:
- The correct Google account completed OAuth
- The account still has access to the Shared Drive
- The configured Shared Drive ID is correct
- The Shared Drive was not deleted and recreated under a new ID
Do not replace the drive ID with another visible Shared Drive merely because authentication succeeded.
### Bisync Cannot Find Its Listings
Confirm that the command uses the original `--workdir` and that the scheduled task runs under the expected account.
Do not perform an immediate resync when the actual problem is that rclone is looking in the wrong state directory.
### The Deletion Limit Was Exceeded
Stop the operation and determine why so many files appear deleted. Common causes include:
- An unavailable local mount
- An inaccessible remote path
- An incorrect synchronization root
- A large folder rename
- Changed filter rules
- An actual mass deletion
Do not use `--force` until the reported deletions have been independently reviewed and approved.
### Conflict Files Are Appearing
Files ending in `.conflict1`, `.conflict2`, or another numbered suffix indicate that both sides changed independently and bisync preserved the losing version.
Review the contents, retain the correct version, and remove the obsolete conflict copy after confirming that it is no longer needed.
### Google Docs Are Missing
Native Google Docs are intentionally absent when `--drive-skip-gdocs` is enabled. Remove or change this behavior only as a deliberate configuration change with a reviewed resync.
### Dry Run Appears to Delete a Newly Copied File
Review the complete dry-run sequence. Bisync can display an apparent deletion because the preceding simulated copy did not actually create the file on the other side.
Do not ignore unrelated or unexplained deletion messages.
### Google Drive Reports Duplicate Objects
Google Drive can contain multiple objects with the same name in one folder. List duplicate names with:
```powershell
& $Rclone dedupe list $RemotePath
```
Use the interactive resolver only after reviewing the file sizes, modification times, and hashes:
```powershell
& $Rclone dedupe interactive $RemotePath
```
When the correct version cannot be determined confidently, rename and preserve both objects rather than deleting one automatically.
## Reference Documentation
- [Rclone Command Overview](https://rclone.org/commands/)
- [Rclone Bisync](https://rclone.org/bisync/)
- [Rclone Copy](https://rclone.org/commands/rclone_copy/)
- [Rclone Sync](https://rclone.org/commands/rclone_sync/)
- [Rclone Check](https://rclone.org/commands/rclone_check/)
- [Rclone Filtering](https://rclone.org/filtering/)
- [Rclone Google Drive Backend](https://rclone.org/drive/)
- [Rclone Changelog](https://rclone.org/changelog/)
@@ -1,12 +0,0 @@
---
tags:
- PowerShell
- Reporting
- Scripting
---
``` powershell
$DaysInactive = 30
$time = (Get-Date).Adddays(-($DaysInactive))
Get-ADComputer -Filter {LastLogonTimeStamp -lt $time} -ResultPageSize 2000 -resultSetSize $null -Properties Name | Select Name
```
@@ -1,13 +0,0 @@
---
tags:
- PowerShell
- Reporting
- Scripting
---
``` powershell
InactiveDays = 30
$Days = (Get-Date).Adddays(-($InactiveDays))
Get-ADUser -Filter {LastLogonTimeStamp -lt $Days -and enabled -eq $true} -Properties LastLogonTimeStamp |
select-object Name,@{Name="Date"; Expression={[DateTime]::FromFileTime($_.lastLogonTimestamp).ToString('MM-dd-yyyy')}}
```
@@ -1,78 +0,0 @@
---
tags:
- Microsoft Exchange
- Email
---
## Purpose
If you operate an Exchange Database Availability Group (DAG) with 2 or more servers, you may need to do maintenance to one of the members, and during that maintenance, it's possible that one of the databases of the server that was rebooted etc will be out-of-date. In case this happens, it may suspend the database replication to one of the DAG's member servers.
## Checking DAG Database Replication Status
You will want to first log into one of the DAG servers and open the *"Exchange Management Shell"*. From there, run the following command to get the status of database replication. An example of the kind of output you would see is below the command.
```powershell
Get-MailboxDatabaseCopyStatus * | Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ContentIndexState
```
| **Name** | **Status** | **CopyQueueLength** | **ReplayQueueLength** | **ContentIndexState** |
| :--- | ---: | ---: | ---: | ---: |
| DB01\MX-DAG-01 | Mounted | 0 | 0 | Healthy |
| DB01\MX-DAG-02 | Healthy | 0 | 0 | Healthy |
!!! info "Example Output Breakdown"
In the above example output, you can see that there are two member servers in the DAG, `MX-DAG-01` and `MX-DAG-02`. Then you will see that there is a status of `Mounted`, this means that `MX-DAG-01` is the active production server; this means that it is handling all mailflow and web requests / webmail.
**CopyQueueLength**: This is a number of database "*transaction logs*" that have taken place since a replica database stopped getting updates. This is the queue of all database transactions that are being copied from the production (mounted) database to replica databases. This data is not immediately written to the replica database(s).
**CopyReplayLength**: This represents the queue of all data that was successfully copied from the production database to the replica database on the given DAG member that still needs to process on the replica database. The "**CopyQueueLength**" will need to reach zero before the "**CopyReplayLength**" will start making meaningful progress to reaching zero.
When both the "**CopyQueueLength**" and "**CopyReplayLength**" queues have reached zero, the replica database(s) will have reached 100% parity with the production (active/mounted) database.
## Changing Active/Mounted DAG Member
You may find that you need to perform work on one of the DAG members, and that requires you to failover the responsibility of hosting the Exchange environment to one of the other members of the DAG. You can generally do this with one command, seen below:
```powershell
Move-ActiveMailboxDatabase -Identity "DB01" -ActivateOnServer "MX-DAG-02" -MountDialOverride BestAvailability
```
!!! info "Argument Breakdown"
`-MountDialOverride`
Specifies how tolerant Exchange should be to database copy health when mounting a database on the target server. This setting controls the level of availability Exchange requires before mounting the mailbox database after the move.
`-MountDialOverride`
Instructs Exchange to mount the database as long as at least one healthy copy is available. This option maximizes uptime by allowing a database to mount even if some copies are unhealthy, prioritizing availability over strict health checks.
## Troubleshooting
You may run into issues where either the `Status` or `ContentIndexState` are either Unhealthy, Suspended, or Failed. If this happens, you need to resume replication of the database from the production active/mounted server to the server that is having issues. In the worst-case, you would re-seed the replica database from-scratch.
### If `Status` is Unhealthy or Suspended
If one of the DAG members has a status of "**Unhealthy**", you can run the following command to attempt to resume replication.
```powershell
Resume-MailboxDatabaseCopy -Identity "DB01\MX-DAG-02"
```
If this fails to cause replication to resume, you can try telling the database to just focus on replication, which tells it to copy the queues and replay them on the replica database, while avoiding interacting with the "**ContentIndexState**" which can be individually fixed in the commands below:
```powershell
Resume-MailboxDatabaseCopy -Identity "DB01\MX-DAG-02" -ReplicationOnly
```
### If `Status` is `ServiceDown`
If you see this, it generally means that the Exchange Services for some reason or another are not running. You can remediate this with a powershell script. You will then have to double-check your work to ensure that all "Microsoft Exchange" services that have a startup mode of "Automatic" are running, if not, manually start them, then check on the status of the DAG again to see if the status changes from `ServiceDown` to `Healthy`. Depending on the speed of the Exchange server, it may take a few minutes, 5-10 minutes, for the services to fully initialize and be ready to handle requests. Go get a coffee and come back and check on the status of the DAG at that time.
[:material-powershell: Restart Exchange Services Script](../restart-exchange-services.md){ .md-button }
### If `ContentIndexState` is Unhealthy or Suspended
If you see that the "ContentIndexState" is unhappy, you can run the following command to force it to re-seed / rebuild itself. (This is non-destructive this this is happening on a replica database).
```powershell
Update-MailboxDatabaseCopy "DB01\MX05" -CatalogOnly -BeginSeed
```
### If Replica Database is FUBAR
If the replica database just is not playing nice, you can take the *nuclear option* of completely rebuilding the replica database.
!!! warning
This will destroy the replica database, so be careful to ensure you have a backup (if possible) before you do this. The following command will completely replace the replica database and replicate the data from the production active/mounted database to the newly-created replica database.
```powershell
Update-MailboxDatabaseCopy -Identity "DB01\MX-DAG-02" -SourceServer "MX-DAG-01"
```
@@ -1,930 +0,0 @@
---
tags:
- Exchange Server
- Database Availability Group
- Maintenance
- Windows Server
- PowerShell
---
## Purpose
This workflow applies Exchange Server Subscription Edition (SE), Windows Server, and approved prerequisite updates to a three-member database availability group (DAG) by updating one DAG member at a time. The procedure drains client and transport activity, moves active mailbox databases, places the target member into maintenance mode, installs updates, validates the updated member, and restores the intended database placement before the next cycle begins.
All organization names, hostnames, FQDNs, DAG names, database names, and example values in this page describe the fictional Bunny Lab environment. The examples use the `bunny-lab.io` DNS namespace and do not identify another organization.
!!! warning "Update One DAG Member at a Time"
Only one DAG member may be in maintenance mode at a time. Do not begin the next cycle until the previous member has returned to service, all database copies are healthy, all copy and replay queues have drained, transport queues are clear, and replication health passes across the entire DAG.
## Assumptions and Risk Boundaries
- The Exchange organization is running Exchange Server SE on three Mailbox servers in one DAG.
- Every mailbox database has at least two healthy passive copies before maintenance begins.
- A current backup and a tested Exchange recovery path exist. DAG replication provides availability, but it is not a substitute for backup.
- The operator has the Exchange and local administrative permissions required by the selected update. A CU that extends the schema or prepares Active Directory may require additional directory permissions before the first server is upgraded.
- The administrative shell host remains online and is not the current maintenance target.
- The Exchange DAG maintenance scripts are available through `$ExScripts`, and the administrative shell host has the Failover Clustering management tools installed.
- The current Exchange release notes, prerequisites, known issues, and update-specific manual actions have been reviewed.
- The required Exchange update media and Windows updates are approved and staged before the maintenance window begins.
- Any load balancer, monitoring platform, backup platform, mail gateway, or third-party application that targets an individual Exchange server has an established drain and restore procedure.
- All DAG members are returned to the same Exchange CU, SU, and HU level during the rolling update window.
!!! danger "Do Not Use a Snapshot as the Only Recovery Plan"
Do not begin the rolling update without an Exchange-aware backup and documented recovery method. If an update fails, keep the affected server isolated in maintenance mode and repair that server before continuing to another DAG member.
## Example Bunny Lab Environment
### Exchange Topology
| **Object** | **Example Value** |
| :--- | :--- |
| Organization | `Bunny Lab` |
| Active Directory DNS domain | `bunny-lab.io` |
| DAG | `BL-DAG-01` |
| Exchange version | Exchange Server Subscription Edition |
| Update staging directory | `C:\ExchangeUpdates` |
| Support scripts directory | `C:\Scripts` |
### DAG Members
| **Short Name** | **FQDN** | **Normal Role** |
| :--- | :--- | :--- |
| `EXCH-SE-01` | `EXCH-SE-01.bunny-lab.io` | DAG member and primary administrative shell host |
| `EXCH-SE-02` | `EXCH-SE-02.bunny-lab.io` | DAG member and alternate administrative shell host |
| `EXCH-SE-03` | `EXCH-SE-03.bunny-lab.io` | DAG member |
Exchange cmdlets in this page normally use the Exchange server object name, such as `EXCH-SE-01`. Commands that require an FQDN, including `Redirect-Message -Target`, use the corresponding `bunny-lab.io` FQDN.
### Intended Database Placement
| **Active Database** | **Intended Active Server** | **Passive Copy Servers** |
| :--- | :--- | :--- |
| `BL-MBX-01` | `EXCH-SE-01` | `EXCH-SE-02`, `EXCH-SE-03` |
| `BL-ARC-01` | `EXCH-SE-01` | `EXCH-SE-02`, `EXCH-SE-03` |
| `BL-MBX-02` | `EXCH-SE-02` | `EXCH-SE-01`, `EXCH-SE-03` |
| `BL-ARC-02` | `EXCH-SE-02` | `EXCH-SE-01`, `EXCH-SE-03` |
| `BL-MBX-03` | `EXCH-SE-03` | `EXCH-SE-01`, `EXCH-SE-02` |
### Rolling Upgrade Plan
| **Cycle** | **Administrative Shell Host** | **Maintenance Target** | **Transport Redirect Target** | **Temporary Database Placement** |
| ---: | :--- | :--- | :--- | :--- |
| `1` | `EXCH-SE-01` | `EXCH-SE-03` | `EXCH-SE-01.bunny-lab.io` | `BL-MBX-03` to `EXCH-SE-01` |
| `2` | `EXCH-SE-01` | `EXCH-SE-02` | `EXCH-SE-03.bunny-lab.io` | `BL-MBX-02` to `EXCH-SE-01`; `BL-ARC-02` to `EXCH-SE-03` |
| `3` | `EXCH-SE-02` | `EXCH-SE-01` | `EXCH-SE-03.bunny-lab.io` | `BL-MBX-01` to `EXCH-SE-02`; `BL-ARC-01` to `EXCH-SE-03` |
This order is specific to the fictional topology above. In another environment, choose an order that preserves quorum, keeps an administrative shell host available, and distributes active databases across healthy remaining members.
## Prepare the Exchange Updates
### Determine the Required Exchange Build
From Exchange Management Shell on a healthy DAG member, record the base Exchange build stored in Active Directory:
```powershell
$DagMembers = @("EXCH-SE-01", "EXCH-SE-02", "EXCH-SE-03")
$DagMembers | ForEach-Object {
Get-ExchangeServer -Identity $_
} | Format-Table Name, Edition, AdminDisplayVersion -Auto
```
`AdminDisplayVersion` identifies the base release or CU, but it does not reliably identify the installed SU or HU. Query the local `ExSetup.exe` file on every DAG member to record the full installed file version:
```powershell
Invoke-Command -ComputerName $DagMembers -ScriptBlock {
$VersionInfo = (Get-Item (Join-Path $env:ExchangeInstallPath "bin\ExSetup.exe")).VersionInfo
[PSCustomObject]@{
Server = $env:COMPUTERNAME
ProductVersion = $VersionInfo.ProductVersion
FileVersion = $VersionInfo.FileVersion
}
} | Format-Table -Auto
```
Compare the results with the current Microsoft build table and the release article for the intended update. Install the latest supported CU required for the target release, then install the latest applicable SU or HU according to that release article. Do not install an SU or HU that was built for a different CU.
!!! warning "Release-Specific Instructions Take Precedence"
Review the selected update's prerequisites, Active Directory preparation requirements, known issues, and manual post-installation actions before changing the first DAG member. This generic workflow does not replace release-specific Microsoft instructions.
### Run the Exchange Health Checker
Download and stage the current Microsoft Exchange Health Checker script before the maintenance window. From an elevated PowerShell session on the administrative shell host, run it against every DAG member:
```powershell
$DagMembers | ForEach-Object {
& "C:\Scripts\HealthChecker.ps1" -Server $_
}
```
Resolve update-blocking findings before continuing. Preserve the generated reports with the maintenance record so the pre-update and post-update states can be compared.
### Stage Update Media
Create the local staging directory on every DAG member:
```powershell
Invoke-Command -ComputerName $DagMembers -ScriptBlock {
New-Item -Path "C:\ExchangeUpdates" -ItemType Directory -Force | Out-Null
}
```
Copy the approved Exchange CU media, SU or HU package, prerequisite installers, and any required scripts to `C:\ExchangeUpdates` on every DAG member. Use the exact package linked by the applicable Microsoft release article and verify that the file is complete before the maintenance window begins.
## Select the Current Upgrade Cycle
Set these variables in Exchange Management Shell on the administrative shell host before running the common workflow. Run only the block for the current cycle.
### Cycle 1: Update `EXCH-SE-03`
Run from Exchange Management Shell on `EXCH-SE-01`:
```powershell
$DagName = "BL-DAG-01"
$DagMembers = @("EXCH-SE-01", "EXCH-SE-02", "EXCH-SE-03")
$AdminHost = "EXCH-SE-01"
$TargetServer = "EXCH-SE-03"
$RedirectTargetFqdn = "EXCH-SE-01.bunny-lab.io"
```
### Cycle 2: Update `EXCH-SE-02`
Run from Exchange Management Shell on `EXCH-SE-01` after Cycle 1 is fully validated:
```powershell
$DagName = "BL-DAG-01"
$DagMembers = @("EXCH-SE-01", "EXCH-SE-02", "EXCH-SE-03")
$AdminHost = "EXCH-SE-01"
$TargetServer = "EXCH-SE-02"
$RedirectTargetFqdn = "EXCH-SE-03.bunny-lab.io"
```
### Cycle 3: Update `EXCH-SE-01`
Run from Exchange Management Shell on `EXCH-SE-02` after Cycle 2 is fully validated:
```powershell
$DagName = "BL-DAG-01"
$DagMembers = @("EXCH-SE-01", "EXCH-SE-02", "EXCH-SE-03")
$AdminHost = "EXCH-SE-02"
$TargetServer = "EXCH-SE-01"
$RedirectTargetFqdn = "EXCH-SE-03.bunny-lab.io"
```
Confirm that the current Exchange Management Shell session is running on `$AdminHost` and that `$AdminHost` is not equal to `$TargetServer`:
```powershell
[PSCustomObject]@{
CurrentComputer = $env:COMPUTERNAME
AdminHost = $AdminHost
TargetServer = $TargetServer
}
```
Do not continue if `CurrentComputer` does not match `AdminHost`, or if the administrative shell host is not fully healthy.
### Capture a Pre-Update Service Baseline
Capture the Exchange-related service state on each server before the first maintenance action. Run this block once for the current target server from an elevated PowerShell session:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
Get-CimInstance Win32_Service |
Where-Object { $_.Name -like "MSExchange*" -or $_.Name -eq "FMS" } |
Select-Object Name, DisplayName, State, StartMode, ExitCode |
Export-Csv -Path "C:\ExchangeUpdates\PreUpdate-Services.csv" -NoTypeInformation
}
```
Do not replace this baseline with a service list copied from another organization. Exchange service startup modes can differ because of installed roles, enabled protocols, product version, and local design decisions.
## Validate DAG Health Before Each Cycle
### Check Database Copy Health
From the administrative shell host, check every database copy:
```powershell
Get-MailboxDatabaseCopyStatus * |
Sort-Object Name |
Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ContentIndexState -Auto
```
The preflight passes only when:
- Every active copy reports `Mounted`
- Every passive copy reports `Healthy`
- No copy reports `Failed`, `Suspended`, `Disconnected`, `ServiceDown`, or another unresolved failure state
- Every copy queue is `0`
- Every replay queue is `0`, or is low and demonstrably draining before any state-changing action
- `ContentIndexState` matches the expected Exchange SE state; `NotApplicable` is normal for the BigFunnel search architecture and is not, by itself, a failure
### Check Replication Health
Run replication health against every DAG member:
```powershell
$DagMembers | ForEach-Object {
Test-ReplicationHealth -Identity $_
}
```
Every applicable check must return `Passed`, and the `Error` field must be blank. Investigate any failure before moving a database or entering maintenance mode.
### Check Cluster State and Primary Active Manager
Confirm every cluster node is up and identify the current Primary Active Manager (PAM):
```powershell
Get-ClusterNode | Format-Table Name, State, NodeWeight, DynamicWeight -Auto
Get-DatabaseAvailabilityGroup -Identity $DagName -Status |
Format-List Name, PrimaryActiveManager, OperationalServers
```
All DAG members must be operational before the cycle begins. The maintenance script will move critical DAG functionality away from the target and pause its cluster node.
### Check Exchange Service Health
Check Exchange service health on the target and the administrative shell host:
```powershell
Test-ServiceHealth -Server $TargetServer
Test-ServiceHealth -Server $AdminHost
```
Resolve any required service failure before continuing.
### Check Active Database Placement
List the currently mounted copies:
```powershell
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" } |
Sort-Object ActiveDatabaseCopy, Name |
Format-Table Name, Status, ActiveDatabaseCopy, CopyQueueLength, ReplayQueueLength -Auto
```
Compare the result with the intended placement table. A database may be temporarily active on another healthy member because of an earlier event, but its current state and all candidate copies must be understood before the maintenance move begins.
### Check Transport Queues
Inspect transport queues on the target before draining it:
```powershell
Get-Queue -Server $TargetServer |
Sort-Object MessageCount -Descending |
Format-Table Identity, Status, MessageCount, NextHopDomain -Auto
```
Investigate growing, retrying, or unreachable queues before maintenance. Redirecting a queue does not correct an underlying transport or name-resolution failure.
!!! warning "Preflight Stop Conditions"
Stop the cycle if any database copy is failed or suspended, replication health does not pass, a required Exchange service is unavailable, a cluster node is down, quorum is at risk, transport queues are persistently growing, the update prerequisites are unresolved, or the recovery path is unavailable.
## Move Active Databases Away From the Target
### Confirm Candidate Copies
Before each move, confirm that the chosen destination server holds a healthy passive copy with drained queues:
```powershell
Get-MailboxDatabaseCopyStatus * |
Sort-Object Name |
Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ActiveDatabaseCopy -Auto
```
Confirm the selected destination copy reports `Healthy` with `CopyQueueLength` and `ReplayQueueLength` equal to `0` before moving the active database.
!!! warning "Run Only the Current Cycle's Move Block"
The following move blocks are cycle-specific. Do not run move commands for a different maintenance target.
### Cycle 1 Database Move
Move `BL-MBX-03` from `EXCH-SE-03` to `EXCH-SE-01`:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-03" -ActivateOnServer "EXCH-SE-01" -Confirm:$false
```
### Cycle 2 Database Moves
Distribute the two active databases from `EXCH-SE-02` across the remaining healthy members:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-02" -ActivateOnServer "EXCH-SE-01" -Confirm:$false
Move-ActiveMailboxDatabase -Identity "BL-ARC-02" -ActivateOnServer "EXCH-SE-03" -Confirm:$false
```
### Cycle 3 Database Moves
Distribute the two active databases from `EXCH-SE-01` across the remaining healthy members:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-01" -ActivateOnServer "EXCH-SE-02" -Confirm:$false
Move-ActiveMailboxDatabase -Identity "BL-ARC-01" -ActivateOnServer "EXCH-SE-03" -Confirm:$false
```
### Validate the Database Moves
Confirm that no active database remains on the target:
```powershell
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" -and $_.Name -like "*\$TargetServer" } |
Format-Table Name, Status, ActiveDatabaseCopy, CopyQueueLength, ReplayQueueLength -Auto
```
Expected output:
```text
<no output>
```
Review each move result and confirm that `Status` is `Succeeded`, `NumberOfLogsLost` is `0`, and `MountStatusAtMoveEnd` is `Mounted`. If any move fails or reports log loss, stop the cycle and investigate before entering maintenance mode.
## Place the Target DAG Member Into Maintenance Mode
### Drain External Client Traffic
If the environment uses a load balancer, reverse proxy, monitoring probe, backup job, or third-party connector that targets individual Exchange servers, drain or disable the target member according to that platform's documented procedure. Confirm that healthy remaining members are serving the traffic before continuing.
### Drain Hub Transport
From Exchange Management Shell on the administrative shell host, set the target Hub Transport component to draining:
```powershell
Set-ServerComponentState -Identity $TargetServer -Component HubTransport -State Draining -Requester Maintenance
```
Restart the Microsoft Exchange Transport service on the target to initiate queue draining:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
Restart-Service MSExchangeTransport
}
```
### Run the DAG Maintenance Script
From Exchange Management Shell on the administrative shell host, run the Exchange-provided DAG maintenance script:
```powershell
Set-Location $ExScripts
.\StartDagServerMaintenance.ps1 -ServerName $TargetServer -MoveComment "Rolling Exchange update" -PauseClusterNode
```
The script blocks database activation, pauses the target cluster node, moves any remaining active databases, and moves critical DAG functionality away from the target. The command can take time without producing continuous console output.
### Redirect Pending Transport Messages
Redirect messages still pending on the target to the healthy server selected for the current cycle:
```powershell
Redirect-Message -Server $TargetServer -Target $RedirectTargetFqdn -Confirm:$false
```
### Set the Server-Wide Maintenance State
Place the target server into Exchange server-wide maintenance mode:
```powershell
Set-ServerComponentState -Identity $TargetServer -Component ServerWideOffline -State Inactive -Requester Maintenance
```
### Validate Maintenance Mode
Check effective Exchange component states:
```powershell
Get-ServerComponentState -Identity $TargetServer |
Format-Table Component, State -Auto
```
`ServerWideOffline` must report `Inactive`. In the standard maintenance state, only `Monitoring` and `RecoveryActionsEnabled` should remain `Active`; investigate any other component that remains active before rebooting the server.
Confirm the database activation policy is blocked:
```powershell
Get-MailboxServer -Identity $TargetServer |
Format-List Name, DatabaseCopyAutoActivationPolicy
```
Confirm that the target cluster node is paused:
```powershell
Get-ClusterNode -Name $TargetServer |
Format-List Name, State
```
Confirm that no active databases remain on the target:
```powershell
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" -and $_.Name -like "*\$TargetServer" } |
Format-Table Name, Status, ActiveDatabaseCopy -Auto
```
Expected output:
```text
<no output>
```
Confirm that the target transport queues have drained:
```powershell
Get-Queue -Server $TargetServer |
Sort-Object MessageCount -Descending |
Format-Table Identity, Status, MessageCount, NextHopDomain -Auto
```
Confirm that the PAM is hosted by another DAG member:
```powershell
Get-DatabaseAvailabilityGroup -Identity $DagName -Status |
Format-List PrimaryActiveManager, OperationalServers
```
!!! warning "Do Not Reboot Until Every Maintenance Gate Passes"
Do not install updates or reboot the target until it has no mounted databases, its database activation policy is `Blocked`, its cluster node is `Paused`, `ServerWideOffline` is `Inactive`, its transport queues are drained, and the PAM is hosted by another member.
## Install Exchange and Windows Updates
### Reboot Before Installing the Exchange Update
A clean reboot before Exchange Setup or an Exchange SU or HU reduces failures caused by pending file handles and services that do not stop cleanly. From the administrative shell host, reboot the target:
```powershell
Restart-Computer -ComputerName $TargetServer -Force
```
Wait until the server is reachable, then revalidate that maintenance state persisted:
```powershell
Get-ServerComponentState -Identity $TargetServer |
Where-Object { $_.Component -eq "ServerWideOffline" } |
Format-Table Server, Component, State -Auto
Get-ClusterNode -Name $TargetServer |
Format-List Name, State
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" -and $_.Name -like "*\$TargetServer" }
```
Do not launch the update if `ServerWideOffline` is not `Inactive`, the cluster node is not `Paused`, or a database mounted on the target after reboot.
### Install an Exchange CU or Build Upgrade
Mount the correct Exchange installation media on the target server. From an elevated Command Prompt on the target, run Setup by using the full media path so Windows does not invoke the installed `Setup.exe` from the Exchange binary directory.
=== "Diagnostic Data Off"
```cmd
D:\Setup.exe /Mode:Upgrade /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF
```
=== "Diagnostic Data On"
```cmd
D:\Setup.exe /Mode:Upgrade /IAcceptExchangeServerLicenseTerms_DiagnosticDataON
```
Replace `D:` with the actual mounted-media drive. Wait for Exchange Setup to complete successfully and review `C:\ExchangeSetupLogs\ExchangeSetup.log` before continuing.
!!! danger "A CU Upgrade Is Not Reversible by Uninstalling It"
Do not treat a CU as an ordinary removable patch. If the CU fails, keep the server in maintenance mode and repair or recover that server by following Microsoft Exchange recovery guidance.
### Install an Exchange SU or HU
Run the exact update package specified by the applicable release article from an elevated Command Prompt on the target server:
```cmd
C:\ExchangeUpdates\<EXCHANGE_UPDATE_FILENAME>.exe
```
Replace `<EXCHANGE_UPDATE_FILENAME>` with the staged package name. Do not launch the installer from a non-elevated shell or by double-clicking it in File Explorer.
Do not stop IIS, WMI, Windows Event Log, or other Windows services preemptively unless the release article or a matching Microsoft troubleshooting procedure explicitly instructs you to do so. Wait for the installer to report successful completion before continuing.
### Install Approved Windows Updates
Install the approved Windows Server updates while the target remains in maintenance mode. Apply prerequisites in the order required by the Exchange release notes, and continue rebooting as required until the server has no remaining approved updates or pending restart.
### Perform the Final Maintenance Reboot
After Exchange and Windows updates have completed, reboot the target one final time:
```powershell
Restart-Computer -ComputerName $TargetServer -Force
```
Wait for Windows, Active Directory connectivity, the Cluster service, and Exchange services to initialize before beginning return-to-service validation.
## Return the Updated DAG Member to Service
### Verify the Installed Exchange Build
From Exchange Management Shell on the administrative shell host, confirm the base build:
```powershell
Get-ExchangeServer -Identity $TargetServer |
Format-Table Name, Edition, AdminDisplayVersion -Auto
```
Query the local `ExSetup.exe` file on the target to identify the installed SU or HU file version:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
$VersionInfo = (Get-Item (Join-Path $env:ExchangeInstallPath "bin\ExSetup.exe")).VersionInfo
[PSCustomObject]@{
Server = $env:COMPUTERNAME
ProductVersion = $VersionInfo.ProductVersion
FileVersion = $VersionInfo.FileVersion
}
}
```
Compare both values with the intended Microsoft build. An unchanged `AdminDisplayVersion` does not, by itself, prove that an SU or HU failed to install.
### Validate Exchange Services
From the administrative shell host, run:
```powershell
Test-ServiceHealth -Server $TargetServer
```
Inspect Exchange-related services on the target:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
Get-CimInstance Win32_Service |
Where-Object { $_.Name -like "MSExchange*" -or $_.Name -eq "FMS" } |
Select-Object Name, DisplayName, State, StartMode, ExitCode
} | Format-Table -Auto
```
Compare the result with `C:\ExchangeUpdates\PreUpdate-Services.csv` from that same server. Do not enable a service merely because it is stopped; protocol services and role-specific services may intentionally be manual or stopped.
!!! warning "Keep the Server in Maintenance Mode During Repair"
If required Exchange services are disabled, fail to start, or report dependency errors, keep the target in maintenance mode. Repair the service state and complete the failed update before running the return-to-service commands.
### Remove the Server-Wide Maintenance State
From Exchange Management Shell on the administrative shell host, restore the server-wide component state:
```powershell
Set-ServerComponentState -Identity $TargetServer -Component ServerWideOffline -State Active -Requester Maintenance
```
### Run the DAG Return-to-Service Script
Run the Exchange-provided DAG maintenance exit script:
```powershell
Set-Location $ExScripts
.\StopDagServerMaintenance.ps1 -ServerName $TargetServer
```
The script resumes the cluster node, restores database activation policy, and resumes database copies hosted by the member.
### Resume Hub Transport
Return Hub Transport to active state:
```powershell
Set-ServerComponentState -Identity $TargetServer -Component HubTransport -State Active -Requester Maintenance
```
Restart transport on the target:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
Restart-Service MSExchangeTransport
}
```
### Validate the Restored Member
Confirm Exchange component states:
```powershell
Get-ServerComponentState -Identity $TargetServer |
Format-Table Component, State -Auto
```
Confirm database activation is unrestricted:
```powershell
Get-MailboxServer -Identity $TargetServer |
Format-List Name, DatabaseCopyAutoActivationPolicy
```
Confirm the cluster node is up:
```powershell
Get-ClusterNode -Name $TargetServer |
Format-List Name, State
```
Run service and replication health:
```powershell
Test-ServiceHealth -Server $TargetServer
Test-ReplicationHealth -Identity $TargetServer
```
Inspect transport queues:
```powershell
Get-Queue -Server $TargetServer |
Sort-Object MessageCount -Descending |
Format-Table Identity, Status, MessageCount, NextHopDomain -Auto
```
Do not restore the target to a load balancer or other external traffic source until required Exchange components are active, required services are running, the cluster node is up, replication health passes, and transport queues are processing normally.
### Restore External Client Traffic
Return the target to its load balancer pool, monitoring platform, backup schedule, mail gateway, and any other system that was intentionally drained. Confirm the external health checks recognize the server as healthy before restoring active mailbox databases to it.
## Restore the Intended Database Placement
### Wait for Database Copies to Catch Up
Before moving an active database back to the updated member, confirm every copy is healthy and all queues have drained:
```powershell
Get-MailboxDatabaseCopyStatus * |
Sort-Object Name |
Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ContentIndexState -Auto
```
Do not activate a copy on the updated member while it is `Failed`, `Suspended`, `Disconnected`, `ServiceDown`, or building a persistent queue.
!!! warning "Run Only the Current Cycle's Restore Block"
The following restore blocks are cycle-specific. Do not move databases for a different cycle.
### Cycle 1 Database Restore
Move `BL-MBX-03` back to `EXCH-SE-03`:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-03" -ActivateOnServer "EXCH-SE-03" -Confirm:$false
```
### Cycle 2 Database Restore
Move both databases back to `EXCH-SE-02`:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-02" -ActivateOnServer "EXCH-SE-02" -Confirm:$false
Move-ActiveMailboxDatabase -Identity "BL-ARC-02" -ActivateOnServer "EXCH-SE-02" -Confirm:$false
```
### Cycle 3 Database Restore
Move both databases back to `EXCH-SE-01`:
```powershell
Move-ActiveMailboxDatabase -Identity "BL-MBX-01" -ActivateOnServer "EXCH-SE-01" -Confirm:$false
Move-ActiveMailboxDatabase -Identity "BL-ARC-01" -ActivateOnServer "EXCH-SE-01" -Confirm:$false
```
### Validate the Restored Placement
Confirm active database placement:
```powershell
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" } |
Sort-Object ActiveDatabaseCopy, Name |
Format-Table Name, Status, ActiveDatabaseCopy, CopyQueueLength, ReplayQueueLength -Auto
```
Expected Bunny Lab placement:
```text
Name Status ActiveDatabaseCopy CopyQueueLength ReplayQueueLength
---- ------ ------------------ --------------- -----------------
BL-ARC-01\EXCH-SE-01 Mounted EXCH-SE-01 0 0
BL-MBX-01\EXCH-SE-01 Mounted EXCH-SE-01 0 0
BL-ARC-02\EXCH-SE-02 Mounted EXCH-SE-02 0 0
BL-MBX-02\EXCH-SE-02 Mounted EXCH-SE-02 0 0
BL-MBX-03\EXCH-SE-03 Mounted EXCH-SE-03 0 0
```
Temporary queues may appear immediately after activation. Wait until all copy and replay queues return to `0` before declaring the cycle complete.
## Validate the Completed Cycle
Run replication health against every DAG member:
```powershell
$DagMembers | ForEach-Object {
Test-ReplicationHealth -Identity $_
}
```
Check all database copies:
```powershell
Get-MailboxDatabaseCopyStatus * |
Sort-Object Name |
Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ContentIndexState -Auto
```
Run the Exchange Health Checker against the updated member:
```powershell
& "C:\Scripts\HealthChecker.ps1" -Server $TargetServer
```
The cycle is complete only when:
- The target reports the intended Exchange build
- Required Exchange services pass `Test-ServiceHealth`
- The target cluster node is `Up`
- Database activation policy is `Unrestricted`
- Required Exchange components are active
- Every applicable replication-health check returns `Passed`
- Every active database copy reports `Mounted`
- Every passive database copy reports `Healthy`
- Every copy and replay queue is `0`
- Transport queues are processing normally
- The intended active database placement is restored
- External health checks and monitoring report the target as healthy
- The post-update Health Checker report contains no unresolved update-blocking finding
Do not begin the next cycle until all conditions are satisfied. Repeat the common workflow with the next cycle's variables and database move block.
## Final DAG Validation
### Confirm a Consistent Exchange Build
After all three cycles are complete, confirm the base build recorded for every DAG member:
```powershell
$DagMembers | ForEach-Object {
Get-ExchangeServer -Identity $_
} | Format-Table Name, Edition, AdminDisplayVersion -Auto
```
Confirm the local `ExSetup.exe` file version on every member:
```powershell
Invoke-Command -ComputerName $DagMembers -ScriptBlock {
$VersionInfo = (Get-Item (Join-Path $env:ExchangeInstallPath "bin\ExSetup.exe")).VersionInfo
[PSCustomObject]@{
Server = $env:COMPUTERNAME
ProductVersion = $VersionInfo.ProductVersion
FileVersion = $VersionInfo.FileVersion
}
} | Sort-Object Server | Format-Table -Auto
```
All three servers must report the same intended Exchange build unless Microsoft explicitly documents a temporary mixed-build state during the active maintenance window.
### Validate Database Health and Placement
Run:
```powershell
Get-MailboxDatabaseCopyStatus * |
Sort-Object Name |
Format-Table Name, Status, CopyQueueLength, ReplayQueueLength, ContentIndexState -Auto
Get-MailboxDatabaseCopyStatus * |
Where-Object { $_.Status -eq "Mounted" } |
Sort-Object ActiveDatabaseCopy, Name |
Format-Table Name, Status, ActiveDatabaseCopy, CopyQueueLength, ReplayQueueLength -Auto
```
Confirm every active copy is `Mounted`, every passive copy is `Healthy`, all queues are `0`, and placement matches the Bunny Lab topology table.
### Validate Replication, Services, Components, and Cluster State
Run:
```powershell
$DagMembers | ForEach-Object {
Test-ServiceHealth -Server $_
Test-ReplicationHealth -Identity $_
}
Get-ClusterNode |
Format-Table Name, State, NodeWeight, DynamicWeight -Auto
$DagMembers | ForEach-Object {
Get-ServerComponentState -Identity $_ |
Where-Object { $_.State -ne "Active" } |
Select-Object Server, Component, State
}
```
Investigate every unexpected non-active component state. Protocol components that are deliberately disabled must match the documented Bunny Lab design rather than being assumed healthy merely because they were disabled before the update.
### Validate Mail Flow
From Exchange Management Shell on each source server, test mail flow to another DAG member. The following examples cover all three members:
```powershell
Test-Mailflow -Identity "EXCH-SE-01" -TargetMailboxServer "EXCH-SE-02"
Test-Mailflow -Identity "EXCH-SE-02" -TargetMailboxServer "EXCH-SE-03"
Test-Mailflow -Identity "EXCH-SE-03" -TargetMailboxServer "EXCH-SE-01"
```
Each test must return `Success`. Also validate inbound and outbound mail flow through the organization's real mail gateways and confirm the client-access namespaces used by the environment are healthy.
### Run the Final Health Checker Audit
Run the current Exchange Health Checker against every DAG member:
```powershell
$DagMembers | ForEach-Object {
& "C:\Scripts\HealthChecker.ps1" -Server $_
}
```
Archive the final reports with the change record.
## Troubleshooting
### The Installer Reports Files in Use or Cannot Stop Services
Do not select an installer option that ignores locked files, and do not terminate the Windows Event Log service. Exit the installer, confirm the server remains in maintenance mode, reboot it, and rerun the update from an elevated Command Prompt.
If the problem persists:
- Confirm the update package matches the installed CU
- Confirm the server was rebooted immediately before the installation attempt
- Review the selected update's known issues and antivirus exclusion guidance
- Preserve `C:\ExchangeSetupLogs`
- Use the Microsoft SetupAssist or Setup Log Reviewer tooling appropriate to the failure
- Follow the matching procedure in [Fix Failed Exchange Server Updates](https://learn.microsoft.com/en-us/troubleshoot/exchange/client-connectivity/exchange-security-update-issues)
Do not reuse process IDs from an earlier attempt or another server. Process IDs are transient, and terminating an unidentified Windows or Exchange process can leave the installation in a worse state.
### Exchange Services Are Disabled After the Update
Compare the current service configuration with `C:\ExchangeUpdates\PreUpdate-Services.csv` from the same server:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
$Baseline = Import-Csv "C:\ExchangeUpdates\PreUpdate-Services.csv"
$Current = Get-CimInstance Win32_Service |
Where-Object { $_.Name -like "MSExchange*" -or $_.Name -eq "FMS" }
foreach ($Before in $Baseline) {
$After = $Current | Where-Object { $_.Name -eq $Before.Name }
if ($After -and ($After.State -ne $Before.State -or $After.StartMode -ne $Before.StartMode)) {
[PSCustomObject]@{
Name = $Before.Name
BeforeState = $Before.State
AfterState = $After.State
BeforeStartMode = $Before.StartMode
AfterStartMode = $After.StartMode
}
}
}
} | Format-Table -Auto
```
Restore only a service whose required startup mode is confirmed by the server's baseline, current Exchange role, and Microsoft guidance:
```powershell
Set-Service -Name <SERVICE_NAME> -StartupType Automatic
Start-Service -Name <SERVICE_NAME>
```
If the service fails with dependency error `1068`, inspect its required services:
```powershell
Get-Service -Name <SERVICE_NAME> -RequiredServices |
Format-Table Name, DisplayName, Status, StartType -Auto
```
Correct the failed dependency before retrying the dependent service. Do not automatically enable IMAP, POP, EdgeSync, or another optional service that was intentionally disabled before the update.
### An Exchange Component Remains Inactive
Inspect the effective and requester-specific component states:
```powershell
Get-ServerComponentState -Identity $TargetServer |
Format-Table Component, State -Auto
Get-ServerComponentState -Identity $TargetServer -Component <COMPONENT_NAME> |
Format-List Component, State, LocalStates, RemoteStates
```
Correct the requester that actually holds the component inactive. Do not repeatedly issue `Requester Maintenance` commands when the inactive state belongs to `Functional`, `HealthAPI`, or another requester.
If a failed Exchange update left `ServerWideOffline`, `Monitoring`, or `RecoveryActionsEnabled` inactive under the `Functional` requester, and the failed installation has already been repaired, restore only the affected states:
```powershell
Set-ServerComponentState -Identity $TargetServer -Component ServerWideOffline -State Active -Requester Functional
Set-ServerComponentState -Identity $TargetServer -Component Monitoring -State Active -Requester Functional
Set-ServerComponentState -Identity $TargetServer -Component RecoveryActionsEnabled -State Active -Requester Functional
```
Rerun `Test-ServiceHealth` and `Test-ReplicationHealth` after the correction.
### High Availability Remains Offline After Maintenance Ends
If a database move fails with an error stating that the `HighAvailability` component is offline, inspect its requester-specific state:
```powershell
Get-ServerComponentState -Identity $TargetServer -Component HighAvailability |
Format-List Component, State, LocalStates, RemoteStates
```
Confirm `StopDagServerMaintenance.ps1` completed successfully, the cluster node is `Up`, database activation is `Unrestricted`, and `MSExchangeRepl` is running. If every requester reports `Active` but Active Manager state remains stale, restart the replication service on the target:
```powershell
Invoke-Command -ComputerName $TargetServer -ScriptBlock {
Restart-Service MSExchangeRepl
Get-Service MSExchangeRepl
}
```
Rerun replication health and retry the database move only after every applicable check passes.
### Exchange Setup Reports Success but the Build Does Not Change
Confirm Setup was launched from the mounted media by using an absolute path such as `D:\Setup.exe` or, from PowerShell in the media root, `.\Setup.exe`. Running only `Setup.exe` can invoke the installed copy under the Exchange binary path instead of the intended installation media.
Review `C:\ExchangeSetupLogs\ExchangeSetup.log`, correct the launch path, and rerun Setup while the server remains in maintenance mode.
### Outlook on the Web or the Exchange Admin Center Fails After the Update
First confirm the Exchange update completed successfully and required services are running. Review the matching symptom in Microsoft's failed-update guidance rather than running a generic post-update command sequence.
For an IIS state that specifically requires a service restart, run from an elevated PowerShell session on the affected server:
```powershell
Restart-Service -Name WAS, W3SVC
```
Do not make `UpdateCas.ps1`, `UpdateConfigFiles.ps1`, `IISADMIN`, ADSI changes, or forced process termination part of the normal update workflow. Use a repair command only when an authoritative troubleshooting procedure identifies the same failure condition and explains the required validation.
## Recovery and Stop Conditions
If a target server cannot be returned to service:
- Keep `ServerWideOffline` inactive and keep the cluster node paused
- Keep database activation blocked on the failed member
- Leave active databases on healthy DAG members
- Do not begin maintenance on another member
- Preserve Exchange Setup logs, Windows event logs, Health Checker reports, and the pre-update service baseline
- Repair the update or perform the documented Exchange server recovery procedure
- Revalidate quorum, database redundancy, transport, client access, and backup status before resuming the rolling upgrade
The rolling update is complete only after all three members report the intended Exchange build, every required health check passes, the documented database placement is restored, and Bunny Lab mail flow and client access are validated end to end.
## Reference Documentation
- [Manage Database Availability Groups in Exchange Server](https://learn.microsoft.com/en-us/exchange/high-availability/manage-ha/manage-dags)
- [Upgrade Exchange to the Latest Cumulative Update](https://learn.microsoft.com/en-us/exchange/plan-and-deploy/install-cumulative-updates)
- [Use Unattended Mode in Exchange Setup](https://learn.microsoft.com/en-us/exchange/plan-and-deploy/deploy-new-installations/unattended-installs)
- [Exchange Server Build Numbers and Release Dates](https://learn.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates)
- [Exchange Server Update FAQ](https://learn.microsoft.com/en-us/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-server-update-faq)
- [Fix Failed Exchange Server Updates](https://learn.microsoft.com/en-us/troubleshoot/exchange/client-connectivity/exchange-security-update-issues)
- [Exchange Server Health Checker](https://microsoft.github.io/CSS-Exchange/Diagnostics/HealthChecker/)
@@ -1,20 +0,0 @@
---
tags:
- Microsoft Exchange
- Email
---
### Purpose:
Sometimes Microsoft Exchange Server will misbehave and the services will need to be *bumped* to fix them. This script iterates over all of the Exchange-related services and restarts them automatically for you.
``` powershell
$servicelist = Get-Service | Where-Object {$_.DisplayName -like "Microsoft Exchange *"}
$servicelist += Get-Service | Where-Object {$_.DisplayName -eq "IIS Admin Service"}
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "Windows Management Instrumentation" }
$servicelist += Get-Service | Where-Object { $_.DisplayName –eq "World Wide Web Publishing Service" }
foreach($service in $servicelist){
Set-Service $service -StartupType Automatic
Start-Service $service
}
```
@@ -10,8 +10,9 @@ Sometimes things go awry with backup servers and Hyper-V and a bunch of extra `.
This script automatically iterates through the entire differencing disk chain all the way back to the base disk / parent, and automatically collapses the chain downward from the newest checkpoint (provided as an argument to the script) to the original (non-differencing) base disk. This can automate a huge amount of work when this issue happens due to backup servers or other unexplainable anomalies.
## Powershell Script
You need to copy the contents of the following somewhere on your computer and save it as `Get-HyperVParentDisks.ps1`.
``` powershell
You need to copy the contents of the following somewhere on your computer and save it as `Get-HyperVParentDisks.ps1`.
```powershell
param (
[Parameter(Mandatory=$true, HelpMessage="Specify the path to the AVHDX file.")]
[string]$AVHDXPath,
@@ -36,11 +37,11 @@ function Get-AllParentDisksChain {
$parentDiskChain = @()
while ($CurrentDisk) {
$parentDisk = Get-ParentDisk -ChildDisk $CurrentDisk
if ($parentDisk) {
if ($parentDisk) {
$parentDiskChain += $CurrentDisk # Add the current disk to the chain before moving to the parent
$CurrentDisk = $parentDisk
} else {
break
$CurrentDisk = $parentDisk
} else {
break
}
}
$parentDiskChain += $CurrentDisk # Add the base disk at the end of the chain
@@ -49,7 +50,7 @@ function Get-AllParentDisksChain {
function Merge-DiskIntoParent {
param ([string]$ChildDisk, [string]$ParentDisk, [int]$DiskNumber, [int]$TotalDisks)
if ($DryRun) {
Write-Output "[Differential Disk $DiskNumber of $TotalDisks]"
Write-Output "Child: $ChildDisk"
@@ -100,7 +101,8 @@ Merge Disks:
`.\Get-HyperVParentDisks.ps1 -MergeIntoParents -AVHDXPath "Z:\Example\Virtual Hard Disks\Example.avhdx"`
!!! info "Example Output"
```
```text
Starting parent disk search for: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_E5F78673-3DAD-4211-AC0A-A3BDEB763B63.avhdx
Total parent disks found: 6
@@ -136,7 +138,8 @@ Dry Run (Non-Destructive):
`.\Get-HyperVParentDisks.ps1 -MergeIntoParents -DryRun -AVHDXPath "Z:\Example\Virtual Hard Disks\Example.avhdx"`
!!! info "Example Output"
```
```text
Starting parent disk search for: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_E5F78673-3DAD-4211-AC0A-A3BDEB763B63.avhdx
Total parent disks found: 6
@@ -145,7 +148,7 @@ Dry Run (Non-Destructive):
Child: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_E5F78673-3DAD-4211-AC0A-A3BDEB763B63.avhdx
Parent: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_8B9EDF27-6B7D-4766-AE60-ED67BF3055AE.avhdx
[Dry Run] Would merge child into parent
[Differential Disk 2 of 6]
Child: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_8B9EDF27-6B7D-4766-AE60-ED67BF3055AE.avhdx
Parent: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER_6607B03C-E3F8-49CC-A69B-68BA3DACE81F.avhdx
@@ -166,4 +169,7 @@ Dry Run (Non-Destructive):
Parent: Z:\DISK-MERGE-TESTER\Virtual Hard Disks\DISK-MERGE-TESTER.vhdx
[Dry Run] Would merge child into parent
Merge process completed.
```
```
## Related Documentation
- [Related Virtualization and Storage Documentation](<../../../reference/Virtualization and Storage/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,7 +4,7 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
You may find that you cannot delete a VHDX file for a virtual machine you removed from Hyper-V and/or Hyper-V Failover Cluster, and either cannot afford to, or do not want to reboot your virtualization host(s) to unlock the file locked by `SYSTEM`.
Run the following commands to unlock the file and delete it:
@@ -12,4 +12,7 @@ Run the following commands to unlock the file and delete it:
```powershell
Dismount-VHD -Path "C:\Path\To\Disk.vhdx" -ErrorAction SilentlyContinue
Remove-Item -Path "C:\Path\To\Disk.vhdx" -Force
```
```
## Related Documentation
- [Related Virtualization and Storage Documentation](<../../../reference/Virtualization and Storage/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,11 +4,12 @@ tags:
- Scripting
---
**Purpose**: Sometimes a Hyper-V Failover Cluster node does not want to shut down, or is having issues preventing you from migrating VMs to another node in the cluster, etc. In these situations, you can run this script to force a cluster node to reboot itself.
## Purpose
Sometimes a Hyper-V Failover Cluster node does not want to shut down, or is having issues preventing you from migrating VMs to another node in the cluster, etc. In these situations, you can run this script to force a cluster node to reboot itself.
!!! warning "Run from a Different Server"
You absolutely do not want to run the script locally on the node that is having the issues. There are commands that can only take place if the script is ran on another node in the cluster (or another domain-joined device) logged-in with a domain administrator account.
```powershell
# PowerShell Script to Reboot a Hyper-V Failover Cluster Node and Kill clussvc
@@ -62,4 +63,7 @@ Invoke-Command -ComputerName $hostName -ScriptBlock {
# Output the completion
Write-Host "Reboot for $hostName should now be underway."
```
```
## Related Documentation
- [Related Virtualization and Storage Documentation](<../../../../reference/Virtualization and Storage/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,10 +4,10 @@ tags:
- Scripting
---
**Purpose**:
This script *bumps* any replication that has entered a paused state due to a replication error. The script will record failed attempts at restarting the replication. The logs will rotate out every 5-days.
## Purpose
This script *bumps* any replication that has entered a paused state due to a replication error. The script will record failed attempts at restarting the replication. The logs will rotate out every 5-days.
``` powershell
```powershell
# Define the directory to store the log files
$logDir = "C:\ClusterStorage\Volume1\Scripts\Logs"
if (-not (Test-Path $logDir)) {
@@ -30,7 +30,7 @@ if (-not (Test-Path $logFile)) {
}
# Delete log files older than 5 days
Get-ChildItem -Path $logDir -Filter "ReplicationLog_*.txt" | Where-Object {
Get-ChildItem -Path $logDir -Filter "ReplicationLog_*.txt" | Where-Object {
$_.CreationTime -lt (Get-Date).AddDays(-5)
} | Remove-Item
@@ -72,4 +72,7 @@ foreach ($node in $clusterNodes) {
}
}
}
```
```
## Related Documentation
- [Related Virtualization and Storage Documentation](<../../../../reference/Virtualization and Storage/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,20 @@
---
tags:
- RAID
- Bash
- Scripting
- Linux
---
## Purpose
Keep the recorded mdadm grow command and array-progress check together. Confirm the current array layout and intended RAID conversion before adapting the `/dev/md0` example.
https://www.digitalocean.com/community/tutorials/how-to-create-raid-arrays-with-mdadm-on-ubuntu-16-04
```sh
sudo mdadm --grow /dev/md0 -l 5
cat /proc/mdstat
```
## Related Documentation
- [Related Virtualization and Storage Documentation](<../../../reference/Virtualization and Storage/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -10,7 +10,6 @@ tags:
This script is ran via cronjob on `cluster-node-02` at midnight to rollback the deeplab environment automatically to a previous snapshot nightly.
### Bash Script
```sh title="/root/deeplab-rollback.sh"
#!/usr/bin/env bash
# ProxmoxVE Nightly DeepLab Rollback Script
@@ -58,12 +57,15 @@ Type `crontab -e` to add an entry to run the job at midnight every day.
=== "With Logging"
``` sh
```sh
0 0 * * * /root/deeplab-rollback.sh >> /var/log/deeplab-rollback.log 2>&1
```
=== "Without Logging"
``` sh
```sh
0 0 * * * /root/deeplab-rollback.sh 2>&1
```
```
## Related Documentation
- [Related Proxmox Documentation](<../../../reference/Virtualization and Storage/Proxmox/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -0,0 +1,18 @@
---
tags:
- Virtualization and Storage
- Scripts
- Documentation
---
# Virtualization and Storage
## Purpose
Find scripts for virtualization and storage. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Hyper-V
- Linux
- Proxmox
## Follow the Subject
[Virtualization and Storage](<../../reference/Virtualization and Storage/index.md>) explains the relationships and offers starting points for the documented tasks.
@@ -19,8 +19,9 @@ This script is designed to iterate over every computer device within an Active D
### Script
You can find the full script below, save it as `UserProfileDataPruner.ps1`:
```powershell
<#
<#
UserProfileDataPruner.ps1
Prune stale local user profile data on Windows workstations.
@@ -328,4 +329,7 @@ process {
}
end { }
```
```
## Related Documentation
- [Related Windows and Linux Documentation](<../../../reference/Windows and Linux/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -8,7 +8,7 @@ tags:
## Purpose
Sometimes when you try to run Windows Updates, you may run into issues where updates just fail to install for seemingly nebulous reasons. You can run the following commands (in order) to try to resolve the issue.
!!! info "Run Commands from (CMD) Commandline, not powershell.
!!! info "Run Commands from (CMD) Commandline, not powershell."
```powershell
# Imaging integrity Rrepair tools
@@ -36,4 +36,7 @@ net start usosvc
```
!!! info "Attempt Windows Updates"
At this point, you can try re-running Windows Updates and seeing if the device makes it past the errors and installs the updates successfully or not. If not, **panic**.
At this point, you can try re-running Windows Updates and seeing if the device makes it past the errors and installs the updates successfully or not. If not, **panic**.
## Related Documentation
- [Related Windows and Linux Documentation](<../../../reference/Windows and Linux/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -4,8 +4,8 @@ tags:
- Scripting
---
**Purpose**:
Sometimes you need to restart a service across every computer in an Active Directory Domain. This powershell script will restart a specific service by name domain-wide. Each device will be processed in a serialized nature, one-by-one.
## Purpose
Start the `cagservice` RMM agent service on the domain servers selected by the script. The current implementation starts that service; it does not reboot computers or restart every service.
!!! warning "Under Connstruction"
This document is under construction and not generalized for general purpose use yet. Manual work needs to be done to repurpose this script for general usage.
@@ -37,4 +37,7 @@ foreach ($server in $servers) {
}
Write-Host "Script execution completed." -ForegroundColor Green
```
```
## Related Documentation
- [Related Windows and Linux Documentation](<../../../reference/Windows and Linux/index.md>) — Find the connected deployments, procedures, and references for this subject.
@@ -6,7 +6,7 @@ tags:
- Scripting
---
**Purpose**:
## Purpose
You may need to upgrade a device to Windows 11 using an ISO stored on a UNC Network Share, the script below handles that.
!!! note "Environment Variables"
@@ -23,7 +23,7 @@ You may need to upgrade a device to Windows 11 using an ISO stored on a UNC Netw
```powershell
function generateSHA256 ($executable, $storedHash) {
$fileBytes = [io.File]::ReadAllBytes("$executable")
$bytes = [Security.Cryptography.HashAlgorithm]::Create("SHA256").ComputeHash($fileBytes)
$bytes = [Security.Cryptography.HashAlgorithm]::Create("SHA256").ComputeHash($fileBytes)
$varCalculatedHash=-Join ($bytes | ForEach {"{0:x2}" -f $_})
if ($storedHash -match $varCalculatedHash) {
write-host "+ Filehash verified for file $executable`: $storedHash"
@@ -239,7 +239,7 @@ if (($env:usrImagePath -as [string]).Length -lt 2 -or $env:usrImagePath -eq 'Sup
write-host `r
write-host " Generate a Windows 11 ISO download link good for 24 hours at:"
write-host " https://www.microsoft.com/software-download/windows11"
exit 1
exit 1
} elseif ($env:usrImagePath -match 'software-download.microsoft.com') {
#microsoft
write-host ": ISO Download location: Microsoft servers."
@@ -372,7 +372,7 @@ switch -Regex ($varTPM -as [string]) {
} '1$' {
write-host "+ TPM installed and active."
} $null {
write-host "! ERROR: A fault has occurred during the TPM checking subroutine. Please report this."
write-host "! ERROR: A fault has occurred during the TPM checking subroutine. Please report this."
quitOr
}
@@ -516,4 +516,7 @@ if ($env:usrReboot -match 'true') {
write-host " Please allow ~4 hours for the setup preparation step to conclude and then reboot the"
write-host " device to begin the upgrade process."
}
```
```
## Related Documentation
- [Related Windows and Linux Documentation](<../../../reference/Windows and Linux/index.md>) — Find the connected deployments, procedures, and references for this subject.
+16
View File
@@ -0,0 +1,16 @@
---
tags:
- Windows and Linux
- Scripts
- Documentation
---
# Windows and Linux
## Purpose
Find scripts for windows and linux. Follow the subject guide to choose the relevant environment and connect this material to the other document types.
## Includes
- Windows
## Follow the Subject
[Windows and Linux](<../../reference/Windows and Linux/index.md>) explains the relationships and offers starting points for the documented tasks.
-36
View File
@@ -1,36 +0,0 @@
---
tags:
- Robocopy
- Batch
- Scripting
- Windows
---
Robocopy is a useful tool that can be leveraged to copy files and folders from one location to another (e.g. Over the network to another server) without losing file and folder ACLs (permissions / ownership data).
!!! warning "Run as Domain Admin"
When you run Robocopy, especially when transferring data across the network to another remote server, you need to be sure to run the command prompt under the session of a domain admin. Secondly, it needs to be ran as an administrator to ensure the command is successful. This can be done by going to the start menu and typing "**Command Prompt**" > **Right Clicking** > "**Run as Administrator**" while logged in as a domain administrator.
An example of using Robocopy is below, with a full breakdown:
```powershell
robocopy "E:\Source" "Z:\Destination" /Z /B /R:5 /W:5 /MT:4 /COPYALL /E
```
- `robocopy "Source" "Destination"` : Initiates the Robocopy command to copy files from the specified source directory to the designated destination directory.
- `/Z` : Enables Robocopy's restartable mode, which allows it to resume file transfer from the point of interruption once the network connection is re-established.
- `/B` : Activates Backup Mode, enabling Robocopy to override Access Control Lists (ACLs) and copy files regardless of the existing file or folder permissions.
- `R:5` : Sets the maximum retry count to 5, meaning Robocopy will attempt to copy a file up to five times if the initial attempt fails.
- `W:5` : Configures a wait time of 5 seconds between retry attempts, providing a brief pause before trying to copy a file again.
- `/MT:4` : Employs multi-threading with 4 threads, allowing Robocopy to process multiple files simultaneously, each in its own thread.
- `/COPYALL` : Instructs Robocopy to preserve all file and folder attributes, including security permissions, timestamps, and ownership information during the copy process.
- `/E` : Directs Robocopy to include all subdirectories in the copy operation, ensuring even empty directories are replicated in the destination.
!!! tip "Usage of Administrative Shares"
Whenever dealing with copying data from one server to another, try to leverage "Administrative Shares", also referred to as "Default Shares". These exist in such a way that, if the server exists in a Windows-based domain, you can type something like `\\SERVER\C$` or `\\SERVER\E$` to access files and bypass most file access restrictions (ACLs). This generally only applies to read-access, write-access may be denied in some circumstances.
An adjusted example can be seen below to account for this usage.
**This example assumes you are running robocopy from the destination computer**.
**Remember**: You are always **PULLING** data with administrative shares, not pushing it, the source should be the administrative share, and the destination should be local (in this example). There are scenarios where you can move data between two network shares, but its best (and cleaner) to always have a remote/local relationship in the transfer.
```powershell
robocopy "\\SERVER\E$\SOURCE" "E:\DESTINATION" /Z /B /R:5 /W:5 /MT:4 /COPYALL /E
```
+16 -22
View File
@@ -1,34 +1,28 @@
---
tags:
- Scripts
- Index
- Documentation
---
# Scripts
## Purpose
Quick-use scripts and snippets for day-to-day operations.
Find reusable utilities by the system or task they manage. Each page retains its language-specific code and operational context.
## Includes
- Bash, PowerShell, and Batch snippets
- One-off utilities and helpers
- Applications
- Automation
- Identity and Certificates
- Networking and Access
- Virtualization and Storage
- Windows and Linux
## New Document Template
````markdown
# <Script Title>
## Purpose
<why this script exists>
## Start with a Subject
- [Applications](<../reference/Applications/index.md>) — Find applications by the service they provide, then continue to their deployment, authentication, data, and maintenance documentation.
- [Automation](<../reference/Automation/index.md>) — Connect source control, automation controllers, managed hosts, and configuration delivery. Use the documented execution environment and authentication method for each workflow.
- [Identity and Certificates](<../reference/Identity and Certificates/index.md>) — Connect directory services, certificate trust, single sign-on, and application authentication. Start with the identity system involved, then follow the integration or maintenance procedure.
- [Networking and Access](<../reference/Networking and Access/index.md>) — Find the DNS, proxy, VPN, and remote-access instructions that connect users and services. Use the address plans to identify the intended network before changing connectivity.
- [Virtualization and Storage](<../reference/Virtualization and Storage/index.md>) — Follow the relationship between hypervisors, shared storage, guest disks, and recovery procedures. Select the documented storage design before choosing a maintenance command.
- [Windows and Linux](<../reference/Windows and Linux/index.md>) — Find workstation and server operating-system setup, updates, and repairs. Storage, networking, and identity tasks are linked to their subject guides when they cross operating-system boundaries.
## Script
```sh
# Script content
```
## Usage
```sh
# Example usage
```
## Notes
- <edge cases or caveats>
````
## Find Related Knowledge
[The subject guides](<../reference/index.md>) connect these scripts to the other document roles.