Modules/Private/Invoke-S2DWaterfallCalculation.ps1
|
# Invoke-S2DWaterfallCalculation — pure waterfall math, no session dependency. # Called by Get-S2DCapacityWaterfall (live cluster) and Invoke-S2DCapacityWhatIf (what-if modeling). function Invoke-S2DWaterfallCalculation { <# .SYNOPSIS Computes the 7-stage S2D capacity waterfall from explicit numeric inputs. .DESCRIPTION Pure function — no PowerShell session, no module state, no live CIM queries. All inputs are passed explicitly. Returns an S2DCapacityWaterfall object. Stage 1 Raw physical capacity (pool-member capacity disks) Stage 2 Vendor TB label note (informational — no bytes deducted) Stage 3 After storage pool overhead Stage 4 After reserve space (min(NodeCount,4) × largest drive) Stage 5 After infrastructure volume Stage 6 Available for workload volumes Stage 7 Usable capacity after resiliency overhead (pipeline terminus) .PARAMETER RawDiskBytes Sum of all pool-member capacity-tier disk sizes in bytes (Stage 1). .PARAMETER NodeCount Number of nodes in the cluster. Used for reserve calculation. .PARAMETER LargestDiskSizeBytes Size in bytes of the largest capacity-tier disk. Used for reserve calculation. .PARAMETER PoolTotalBytes Storage pool total size in bytes (Stage 3). If 0, estimated as RawDiskBytes × (1 - PoolOverheadFraction). .PARAMETER PoolFreeBytes Current unallocated pool bytes. Used for reserve status only (Adequate/Warning/Critical). Does not affect stage values. .PARAMETER PoolOverheadFraction Pool overhead as a fraction (default 0.01 = 1%). Used only when PoolTotalBytes is 0. .PARAMETER InfraVolumeBytes Infrastructure volume pool footprint in bytes (Stage 5 deduction). .PARAMETER ResiliencyFactor Number of data copies for resiliency (default 3.0 for 3-way mirror). Stage 7 = Stage 6 / ResiliencyFactor. .PARAMETER ResiliencyName Human-readable label for the resiliency type (default '3-way mirror'). #> [CmdletBinding()] [OutputType([S2DCapacityWaterfall])] param( [Parameter(Mandatory)] [int64] $RawDiskBytes, [Parameter(Mandatory)] [int] $NodeCount, [Parameter(Mandatory)] [int64] $LargestDiskSizeBytes, [int64] $PoolTotalBytes = 0, [int64] $PoolFreeBytes = 0, [double] $PoolOverheadFraction = 0.01, [int64] $InfraVolumeBytes = 0, [double] $ResiliencyFactor = 3.0, [string] $ResiliencyName = '3-way mirror' ) # ── Stage 1: Raw physical ───────────────────────────────────────────────── $stage1Bytes = $RawDiskBytes # ── Stage 2: Vendor TB label note (no deduction) ───────────────────────── $vendorLabeledTB = [math]::Round($stage1Bytes / 1000000000000, 2) $stage2Bytes = $stage1Bytes # ── Stage 3: Pool overhead ──────────────────────────────────────────────── $stage3Bytes = if ($PoolTotalBytes -gt 0) { $PoolTotalBytes } else { [int64]($stage2Bytes * (1.0 - $PoolOverheadFraction)) } # ── Stage 4: Reserve space ──────────────────────────────────────────────── $reserveCalc = Get-S2DReserveCalculation ` -NodeCount $NodeCount ` -LargestCapacityDriveSizeBytes $LargestDiskSizeBytes ` -PoolFreeBytes $PoolFreeBytes $reserveBytes = $reserveCalc.ReserveRecommendedBytes $stage4Bytes = $stage3Bytes - $reserveBytes # ── Stage 5: Infrastructure volume ─────────────────────────────────────── $stage5Bytes = $stage4Bytes - $InfraVolumeBytes # ── Stage 6: Available ──────────────────────────────────────────────────── $stage6Bytes = $stage5Bytes # ── Stage 7: Theoretical resiliency ────────────────────────────────────── $stage7Bytes = [int64]($stage6Bytes / $ResiliencyFactor) $theoreticalEffPct = [math]::Round(100.0 / $ResiliencyFactor, 1) # Stage 7 is the pipeline terminus — no Stage 8. # ── Build stage objects ─────────────────────────────────────────────────── function local:New-Stage { param([int]$N, [string]$Name, [int64]$Bytes, [int64]$Prev, [string]$Desc, [string]$Status = 'OK') $s = [S2DWaterfallStage]::new() $s.Stage = $N $s.Name = $Name $s.Size = if ($Bytes -gt 0) { [S2DCapacity]::new($Bytes) } else { [S2DCapacity]::new([int64]0) } $s.Delta = if ($Prev -gt $Bytes -and $Prev -gt 0) { [S2DCapacity]::new($Prev - $Bytes) } else { $null } $s.Description = $Desc $s.Status = $Status $s } # All stages are theoretical — no stage carries a health status. # Reserve adequacy is reported via ReserveStatus on the waterfall object and # evaluated in Health Checks (Check 1). It does not belong on a pipeline stage. $driveCount = if ($LargestDiskSizeBytes -gt 0) { [math]::Round($RawDiskBytes / $LargestDiskSizeBytes) } else { 0 } $infraDisplay = if ($InfraVolumeBytes -gt 0) { "$([math]::Round($InfraVolumeBytes/1073741824,1)) GiB" } else { 'None detected' } $stages = @( (New-Stage 1 'Raw Capacity' $stage1Bytes $stage1Bytes "All pool-member capacity drives. $driveCount × $('{0:N2}' -f ($LargestDiskSizeBytes/1TB)) TB = $('{0:N2}' -f ($stage1Bytes/1TB)) TB"), (New-Stage 2 'Vendor (TB)' $stage2Bytes $stage1Bytes "Informational. Vendor labels use decimal TB; Windows reports binary TiB. Vendor label: $vendorLabeledTB TB. No deduction."), (New-Stage 3 'Pool Overhead' $stage3Bytes $stage2Bytes "~$([math]::Round($PoolOverheadFraction*100,0))% held by the storage pool for internal metadata. Deduction: $('{0:N2}' -f (($stage2Bytes-$stage3Bytes)/1TB)) TB"), (New-Stage 4 'Reserve' $stage4Bytes $stage3Bytes "Per Microsoft: one drive per server, up to 4 servers. $([math]::Min($NodeCount,4)) × $('{0:N2}' -f ($LargestDiskSizeBytes/1TB)) TB = $('{0:N2}' -f ($reserveBytes/1TB)) TB held for repair."), (New-Stage 5 'Infrastructure Volume' $stage5Bytes $stage4Bytes "Azure Local system volume pool footprint deducted. $infraDisplay"), (New-Stage 6 'Available for Volumes' $stage6Bytes $stage5Bytes "Pool space remaining for workload volume footprint after all deductions."), (New-Stage 7 'Usable Capacity' $stage7Bytes $stage6Bytes "$ResiliencyName writes $([int]$ResiliencyFactor) copies of every byte. $('{0:N2}' -f ($stage6Bytes/1TB)) TB pool ÷ $([int]$ResiliencyFactor) copies = $('{0:N2}' -f ($stage7Bytes/1TB)) TB you can actually store.") ) $wf = [S2DCapacityWaterfall]::new() $wf.Stages = $stages $wf.RawCapacity = [S2DCapacity]::new($stage1Bytes) $wf.UsableCapacity = if ($stage7Bytes -gt 0) { [S2DCapacity]::new($stage7Bytes) } else { [S2DCapacity]::new([int64]0) } $wf.ReserveRecommended = $reserveCalc.ReserveRecommended $wf.ReserveActual = $reserveCalc.ReserveActual $wf.ReserveStatus = $reserveCalc.Status $wf.IsOvercommitted = $false $wf.OvercommitRatio = 0.0 $wf.NodeCount = $NodeCount $wf.BlendedEfficiencyPercent = $theoreticalEffPct $wf } |