From 372a693e7ebab7a36f446f03944bcd4e5578dafe Mon Sep 17 00:00:00 2001 From: TheAbider <51920546+TheAbider@users.noreply.github.com> Date: Fri, 29 May 2026 16:11:38 -0700 Subject: [PATCH] Enrich VM inventory export (CSV ownership, checkpoint chain, replica health) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing VMInventoryExport action now captures more per-VM detail: * Cluster Shared Volume ownership — for each virtual disk on a CSV, which node currently owns that CSV (where a VM's storage actually lives). * Checkpoint chain — the ordered checkpoint names, not just the count. * Replication health + last successful replication time alongside the state. Read-only enrichment of an existing action — no new CLI action, no menu change, no count change. The added fields flow into both the console summary data and the -OutputFormat JSON export. 5048 tests, 0 failures; PSScriptAnalyzer clean. --- CONTRIBUTING.md | 2 +- Changelog.md | 10 ++++++++++ Header.ps1 | 2 +- Modules/00-Initialization.ps1 | 2 +- Modules/50-EntryPoint.ps1 | 36 +++++++++++++++++++++++++++++++---- README.md | 6 +++--- RackStack.ps1 | 2 +- RackStack.psd1 | 2 +- Tests/Run-Tests.ps1 | 15 +++++++++++++++ 9 files changed, 65 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56274d1..7425ab6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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-`) diff --git a/Changelog.md b/Changelog.md index f1e212b..0108d80 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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). diff --git a/Header.ps1 b/Header.ps1 index e6ca5f2..92a0a30 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,7 +30,7 @@ 7h3 4b1d3r .VERSION - 1.111.0 + 1.112.0 .LAST UPDATED 05/23/2026 diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index 839f9aa..5246ba4 100644 --- a/Modules/00-Initialization.ps1 +++ b/Modules/00-Initialization.ps1 @@ -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. diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1 index 1690054..50649ef 100644 --- a/Modules/50-EntryPoint.ps1 +++ b/Modules/50-EntryPoint.ps1 @@ -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 @@ -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 { } @@ -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)" @@ -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", " " } diff --git a/README.md b/README.md index 1beabbf..1ac4440 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5045 structural tests + 5048 structural tests Pester 312 tests SLSA Level 3

@@ -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/ @@ -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) diff --git a/RackStack.ps1 b/RackStack.ps1 index 3f194a7..a756126 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -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) diff --git a/RackStack.psd1 b/RackStack.psd1 index 2b2eb87..406ec54 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -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' diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index 683b203..8aa8737 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -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) # ============================================================================