Modules/Public/Get-S2DCapacityWaterfall.ps1
|
function Get-S2DCapacityWaterfall { <# .SYNOPSIS Computes the 8-stage capacity waterfall from raw physical to final usable capacity. .DESCRIPTION Performs the complete S2D capacity accounting pipeline: Stage 1 Raw physical capacity — capacity-tier disks only (cache excluded) Stage 2 After vendor TB-label → TiB adjustment (1 TB = 0.909 TiB) Stage 3 After storage pool overhead (~1%) Stage 4 After reserve space (min(NodeCount,4) × largest capacity drive) Stage 5 After infrastructure volume (Azure Local infra CSV) Stage 6 Available for workload volumes Stage 7 After resiliency overhead (per-volume, supports mixed types) Stage 8 Final usable capacity Uses data already collected by the other Get-S2D* cmdlets when available, or runs the collectors itself. .EXAMPLE Get-S2DCapacityWaterfall .EXAMPLE Get-S2DCapacityWaterfall | Select-Object -ExpandProperty Stages | Format-Table .OUTPUTS S2DCapacityWaterfall #> [CmdletBinding()] [OutputType([S2DCapacityWaterfall])] param() # ── Gather prerequisite data from cache or live queries ─────────────────── $physDisks = @($Script:S2DSession.CollectedData['PhysicalDisks']) if (-not $physDisks) { Write-Verbose "Collecting physical disk data for waterfall." $physDisks = @(Get-S2DPhysicalDiskInventory) } $pool = $Script:S2DSession.CollectedData['StoragePool'] if (-not $pool) { Write-Verbose "Collecting storage pool data for waterfall." $pool = Get-S2DStoragePoolInfo } $volumes = @($Script:S2DSession.CollectedData['Volumes']) if (-not $volumes) { Write-Verbose "Collecting volume data for waterfall." $volumes = @(Get-S2DVolumeMap) } $nodeCount = if ($Script:S2DSession.Nodes.Count -gt 0) { $Script:S2DSession.Nodes.Count } else { # Estimate from disk symmetry $perNode = @($physDisks | Group-Object NodeName) if ($perNode.Count -gt 0) { $perNode.Count } else { 4 } } # ── Stage 1: Raw physical capacity (capacity-tier only, cache excluded) ─── $capacityDisks = @($physDisks | Where-Object { $_.Role -eq 'Capacity' }) if (-not $capacityDisks) { # Fall back: all non-Journal disks $capacityDisks = @($physDisks | Where-Object { $_.Usage -ne 'Journal' -and $_.Usage -ne 'Retired' }) } $stage1Bytes = [int64]($capacityDisks | Measure-Object -Property SizeBytes -Sum).Sum $largestDriveBytes = [int64]($capacityDisks | Measure-Object -Property SizeBytes -Maximum).Maximum # ── Stage 2: TB-label → TiB adjustment ─────────────────────────────────── # Vendor labels drives in decimal (1 TB = 1,000,000,000,000 bytes). # Windows reports in binary (1 TiB = 1,099,511,627,776 bytes). # The raw bytes ARE the binary value — the "adjustment" is conceptual: we show # what the vendor advertises vs what Windows sees. $vendorLabeledTB = [math]::Round($stage1Bytes / 1000000000000, 2) $stage2Bytes = $stage1Bytes # bytes don't change; this stage is for display clarity # ── Stage 3: Pool overhead (~1%) ────────────────────────────────────────── $poolTotalBytes = if ($pool -and $pool.TotalSize) { $pool.TotalSize.Bytes } else { $stage2Bytes } $stage3Bytes = $poolTotalBytes $poolOverheadBytes = $stage2Bytes - $stage3Bytes # ── Stage 4: Reserve space ──────────────────────────────────────────────── $reserveCalc = Get-S2DReserveCalculation -NodeCount $nodeCount ` -LargestCapacityDriveSizeBytes $largestDriveBytes ` -PoolFreeBytes ($pool ? $pool.RemainingSize.Bytes : 0) $reserveBytes = $reserveCalc.ReserveRecommendedBytes $stage4Bytes = $stage3Bytes - $reserveBytes # ── Stage 5: Infrastructure volume ─────────────────────────────────────── $infraVolumes = @($volumes | Where-Object IsInfrastructureVolume) $infraBytes = [int64]0 foreach ($iv in $infraVolumes) { if ($iv.FootprintOnPool) { $infraBytes += $iv.FootprintOnPool.Bytes } elseif ($iv.Size) { $infraBytes += $iv.Size.Bytes } } $stage5Bytes = $stage4Bytes - $infraBytes # ── Stage 6: Available for workload volumes ─────────────────────────────── $stage6Bytes = $stage5Bytes # ── Stage 7: After resiliency overhead (per workload volume) ───────────── $workloadVolumes = @($volumes | Where-Object { -not $_.IsInfrastructureVolume }) $totalFootprintBytes = [int64]0 $totalUsableBytes = [int64]0 $blendedEff = 0.0 foreach ($vol in $workloadVolumes) { $fp = if ($vol.FootprintOnPool) { $vol.FootprintOnPool.Bytes } else { [int64]0 } $size = if ($vol.Size) { $vol.Size.Bytes } else { [int64]0 } $totalFootprintBytes += $fp $totalUsableBytes += $size } $stage7Bytes = if ($totalFootprintBytes -gt 0) { $stage6Bytes - $totalFootprintBytes } else { $stage6Bytes } # Blended efficiency across all workload volumes $blendedEff = if ($workloadVolumes.Count -gt 0) { [math]::Round( ($workloadVolumes | Measure-Object -Property EfficiencyPercent -Average).Average, 1 ) } else { 0.0 } # ── Stage 8: Final usable capacity ──────────────────────────────────────── $stage8Bytes = $totalUsableBytes # ── Overcommit detection ────────────────────────────────────────────────── $isOvercommitted = $pool -and $pool.OvercommitRatio -gt 1.0 $overcommitRatio = if ($pool) { $pool.OvercommitRatio } else { 0.0 } # ── Build stage objects ─────────────────────────────────────────────────── function local:New-WaterfallStage { param([int]$Stage, [string]$Name, [int64]$Bytes, [int64]$PrevBytes, [string]$Description, [string]$Status = 'OK') $s = [S2DWaterfallStage]::new() $s.Stage = $Stage $s.Name = $Name $s.Size = if ($Bytes -gt 0) { [S2DCapacity]::new($Bytes) } else { [S2DCapacity]::new([int64]0) } $s.Delta = if ($PrevBytes -gt $Bytes -and $PrevBytes -gt 0) { [S2DCapacity]::new($PrevBytes - $Bytes) } else { $null } $s.Description = $Description $s.Status = $Status $s } $reserveStatus = $reserveCalc.Status $stage4Status = switch ($reserveStatus) { 'Adequate' { 'OK' } 'Warning' { 'Warning' } default { 'Critical' } } $stages = @( (New-WaterfallStage 1 'Raw Physical' $stage1Bytes $stage1Bytes "Sum of capacity-tier disk sizes ($($capacityDisks.Count) drives × $('{0:N2}' -f ($largestDriveBytes/1TB)) TB)"), (New-WaterfallStage 2 'Vendor Label (TB)' $stage2Bytes $stage1Bytes "Vendor labels drives in decimal TB; Windows reports binary TiB. Raw: $vendorLabeledTB TB labeled"), (New-WaterfallStage 3 'Pool (after overhead)' $stage3Bytes $stage2Bytes "Storage pool overhead. Pool total: $(if($pool){"$($pool.TotalSize.TiB) TiB"} else {"N/A"})"), (New-WaterfallStage 4 'After Reserve' $stage4Bytes $stage3Bytes "Reserve: min($nodeCount,4)×$('{0:N2}' -f ($largestDriveBytes/1TB)) TB = $('{0:N2}' -f ($reserveBytes/1TB)) TB" $stage4Status), (New-WaterfallStage 5 'After Infra Volume' $stage5Bytes $stage4Bytes "Infrastructure volume footprint: $(if($infraBytes -gt 0){"$([math]::Round($infraBytes/1073741824,1)) GiB"} else {"None detected"})"), (New-WaterfallStage 6 'Available' $stage6Bytes $stage5Bytes "Pool space available for workload volumes"), (New-WaterfallStage 7 'After Resiliency' $stage7Bytes $stage6Bytes "Resiliency overhead for $($workloadVolumes.Count) workload volume(s). Blended efficiency: $blendedEff%"), (New-WaterfallStage 8 'Final Usable' $stage8Bytes $stage7Bytes "Final usable capacity across all workload volumes") ) $waterfall = [S2DCapacityWaterfall]::new() $waterfall.Stages = $stages $waterfall.RawCapacity = [S2DCapacity]::new($stage1Bytes) $waterfall.UsableCapacity = if ($stage8Bytes -gt 0) { [S2DCapacity]::new($stage8Bytes) } else { [S2DCapacity]::new([int64]0) } $waterfall.ReserveRecommended = $reserveCalc.ReserveRecommended $waterfall.ReserveActual = $reserveCalc.ReserveActual $waterfall.ReserveStatus = $reserveStatus $waterfall.IsOvercommitted = $isOvercommitted $waterfall.OvercommitRatio = $overcommitRatio $waterfall.NodeCount = $nodeCount $waterfall.BlendedEfficiencyPercent = $blendedEff $Script:S2DSession.CollectedData['CapacityWaterfall'] = $waterfall $waterfall } |