Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ powershell -ExecutionPolicy Bypass -File Tests\pssa-check.ps1

## Pull Request Checklist

- [ ] All 5,045 tests pass (`Run-Tests.ps1` exits with code 0)
- [ ] All 5,048 tests pass (`Run-Tests.ps1` exits with code 0)
- [ ] PSScriptAnalyzer reports 0 errors (`pssa-check.ps1`)
- [ ] Monolithic synced (`sync-to-monolithic.ps1` shows 0 parse errors)
- [ ] New functions follow PowerShell verb-noun naming (`Get-`, `Set-`, `Test-`, `Show-`)
Expand Down
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v1.112.0

Richer VM inventory — the existing `VMInventoryExport` action now captures more per-VM detail for fleet auditing:

- **Cluster Shared Volume ownership** — for each virtual disk on a CSV, which node currently owns that CSV (so you can see where a VM's storage actually lives in a cluster).
- **Checkpoint chain** — the ordered list of checkpoint names, not just the count.
- **Replication health** — the replica health and last successful replication time alongside the replication state.

Read-only enrichment of an existing action — no new CLI action, no menu change. The added fields appear in both the console summary's underlying data and the `-OutputFormat JSON` export.

## v1.111.0

Failover Cluster validation report — new `ClusterValidationReport` CLI action. Runs a **non-disruptive** failover-cluster validation (Inventory + Network + System Configuration categories) against the cluster's nodes (or this node if it isn't yet clustered), archives the native HTML report to an admin-only path, and reports a best-effort overall result (pass / warning / failed counts).
Expand Down
2 changes: 1 addition & 1 deletion Header.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
7h3 4b1d3r

.VERSION
1.111.0
1.112.0

.LAST UPDATED
05/23/2026
Expand Down
2 changes: 1 addition & 1 deletion Modules/00-Initialization.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) {
if (-not $script:ModuleRoot -and $script:ScriptPath) {
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
}
$script:ScriptVersion = "1.111.0"
$script:ScriptVersion = "1.112.0"
$script:ScriptStartTime = Get-Date

# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
Expand Down
36 changes: 32 additions & 4 deletions Modules/50-EntryPoint.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10973,6 +10973,17 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px}
else {
Write-OutputColor " VMs: $($vms.Count)" -color "Info"
Write-OutputColor "" -color "Info"
# Build a Cluster Shared Volume owner map once (friendly path -> owning node).
$csvMap = @{}
try {
if (Get-Command -Name Get-ClusterSharedVolume -ErrorAction SilentlyContinue) {
foreach ($csv in (Get-ClusterSharedVolume -ErrorAction SilentlyContinue)) {
$fv = "$($csv.SharedVolumeInfo.FriendlyVolumeName)"
if ($fv) { $csvMap[$fv] = "$($csv.OwnerNode.Name)" }
}
}
}
catch { }
$vmData = @()
foreach ($vm in $vms) {
$vmName = $vm.Name
Expand All @@ -10987,11 +10998,18 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px}
$vhds = @(Get-VMHardDiskDrive -VM $vm -ErrorAction SilentlyContinue)
foreach ($vhd in $vhds) {
$vhdInfo = try { Get-VHD -Path $vhd.Path -ErrorAction SilentlyContinue } catch { $null }
# CSV ownership: if the disk path lives under a Cluster Shared
# Volume, record which node currently owns that CSV.
$csvOwner = ""
foreach ($csvPath in $csvMap.Keys) {
if ("$($vhd.Path)".StartsWith($csvPath, [System.StringComparison]::OrdinalIgnoreCase)) { $csvOwner = $csvMap[$csvPath]; break }
}
$disks += @{
Path = "$($vhd.Path)"
SizeGB = if ($vhdInfo) { [math]::Round($vhdInfo.Size / 1GB, 1) } else { 0 }
UsedGB = if ($vhdInfo -and $vhdInfo.FileSize) { [math]::Round($vhdInfo.FileSize / 1GB, 1) } else { 0 }
Type = if ($vhdInfo) { "$($vhdInfo.VhdType)" } else { "Unknown" }
CSVOwner = $csvOwner
}
}
} catch { }
Expand All @@ -11009,10 +11027,17 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px}
}
}
} catch { }
# Checkpoints
$snapCount = @(Get-VMCheckpoint -VM $vm -ErrorAction SilentlyContinue).Count
# Replication
$replState = try { $r = Get-VMReplication -VM $vm -ErrorAction SilentlyContinue; if ($r) { "$($r.State)" } else { "None" } } catch { "None" }
# Checkpoints — count + the chain (ordered by creation time).
$chkpts = @(Get-VMCheckpoint -VM $vm -ErrorAction SilentlyContinue | Sort-Object CreationTime)
$snapCount = $chkpts.Count
$chkChain = @($chkpts | ForEach-Object { "$($_.Name)" })
# Replication — state + health + last successful replication time.
$replState = "None"; $replHealth = "None"; $replLast = ""
try {
$r = Get-VMReplication -VM $vm -ErrorAction SilentlyContinue
if ($r) { $replState = "$($r.State)"; $replHealth = "$($r.Health)"; if ($r.LastReplicationTime) { $replLast = "$($r.LastReplicationTime)" } }
}
catch { }
$vmData += @{
Name = $vm.Name
State = "$($vm.State)"
Expand All @@ -11026,7 +11051,10 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px}
Disks = $disks
NICs = $nics
Checkpoints = $snapCount
CheckpointChain = $chkChain
Replication = $replState
ReplicationHealth = $replHealth
LastReplication = $replLast
Path = "$($vm.Path)"
Notes = "$($vm.Notes)" -replace "`r`n|`n", " "
}
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<a href="https://www.bestpractices.dev/projects/12921"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/12921/badge"></a>
<a href="https://codecov.io/gh/TheAbider/RackStack"><img alt="codecov" src="https://codecov.io/gh/TheAbider/RackStack/branch/master/graph/badge.svg"></a>
<img alt="PSScriptAnalyzer 0 errors" src="https://img.shields.io/badge/PSScriptAnalyzer-0%20errors-brightgreen">
<img alt="5045 structural tests" src="https://img.shields.io/badge/structural%20tests-5045-brightgreen">
<img alt="5048 structural tests" src="https://img.shields.io/badge/structural%20tests-5048-brightgreen">
<img alt="Pester 312 tests" src="https://img.shields.io/badge/Pester-312%20tests-brightgreen">
<img alt="SLSA Level 3" src="https://slsa.dev/images/gh-badge-level3.svg">
</p>
Expand Down Expand Up @@ -487,7 +487,7 @@ RackStack/
│ ├── ... # 75 more modules
│ └── 77-WindowsAdminCenter.ps1
├── Tests/
│ ├── Run-Tests.ps1 # 5,045 automated tests
│ ├── Run-Tests.ps1 # 5,048 automated tests
│ ├── Validate-Release.ps1 # Pre-release validation suite
│ └── ...
└── docs/
Expand Down Expand Up @@ -525,7 +525,7 @@ RackStack/
## Testing

```powershell
# Full test suite (5,045 tests, ~4 minutes)
# Full test suite (5,048 tests, ~4 minutes)
powershell -ExecutionPolicy Bypass -File Tests\Run-Tests.ps1

# PSScriptAnalyzer (0 errors on all 78 modules + monolithic)
Expand Down
2 changes: 1 addition & 1 deletion RackStack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Environment-specific settings are configured via defaults.json.

.VERSION
1.111.0
1.112.0

.NOTES
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)
Expand Down
2 changes: 1 addition & 1 deletion RackStack.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'RackStack.psm1'
ModuleVersion = '1.111.0'
ModuleVersion = '1.112.0'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
Expand Down
15 changes: 15 additions & 0 deletions Tests/Run-Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9089,6 +9089,21 @@ catch {
Write-TestResult "Cluster Validation Report Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 179: VM INVENTORY ENRICHMENT (v1.112.0, VMInventoryExport)
# ============================================================================
Write-SectionHeader "SECTION 179: VM INVENTORY ENRICHMENT (VMInventoryExport)"

try {
$epVmi = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw
Write-TestResult "VMInventoryExport: maps Cluster Shared Volume ownership" ($epVmi -match 'Get-ClusterSharedVolume' -and $epVmi -match 'CSVOwner')
Write-TestResult "VMInventoryExport: includes the checkpoint chain" ($epVmi -match 'CheckpointChain')
Write-TestResult "VMInventoryExport: includes replication health + last time" ($epVmi -match 'ReplicationHealth' -and $epVmi -match 'LastReplication')
}
catch {
Write-TestResult "VM Inventory Enrichment Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase)
# ============================================================================
Expand Down
Loading