Public/New-HDTDiskLayoutPlan.ps1
|
function New-HDTDiskLayoutPlan { <# .SYNOPSIS Turns a named disk layout and a disk size into the exact partitions to create. .DESCRIPTION The arithmetic, which is the whole point of the function. A layout says "Windows takes the remainder"; only a plan says how many bytes that is on this disk, and it has to be right before anything destructive runs against it. uefi-standard windows = DiskSizeByte - 260MB(ESP) - 16MB(MSR) - 1GB(recovery) - 1MB(alignment) the recovery row carries UseMaximumSize, so the slack lands there bios-standard windows = UseMaximumSize after a 500MB system partition THE 16 MB IS SUBTRACTED AND NEVER PLANNED. Initialize-Disk -PartitionStyle GPT creates the Microsoft Reserved partition itself. A plan that also created one produces a duplicate; a plan that forgot to subtract it sizes Windows 16 MB too large and the last partition does not fit. Both failures are silent until a real disk is in front of you, which is why the allowance is data on the layout rather than a number in this function. A DISK TOO SMALL IS A POINTED REFUSAL, NOT A PLAN WITH A NEGATIVE PARTITION IN IT. The message names the disk size and the shortfall, because "the disk is too small" without a number is a message that sends a technician to measure the disk by hand. It performs no I/O, takes no service and returns new rows: the layout it was handed is not modified, so the same layout object plans twice. .PARAMETER Layout A layout as Get-HDTDiskLayout returns it. .PARAMETER DiskSizeByte The size of the disk being planned, from IDiskService's disk row. .PARAMETER MinimumWindowsSizeByte The smallest Windows partition worth creating. Defaults to 20 GB. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject[] - ordered rows carrying Order, Role, SizeByte, UseMaximumSize, FileSystem, Label, DriveLetter, GptType, CreateGptType and IsActive. .EXAMPLE New-HDTDiskLayoutPlan -Layout (Get-HDTDiskLayout -Name uefi-standard) -DiskSizeByte 68719476736 The plan a 64 GiB lab VM gets: ESP 260 MB, Windows, recovery last. .EXAMPLE New-HDTDiskLayoutPlan -Layout $layout -DiskSizeByte $disk.SizeBytes -MinimumWindowsSizeByte 40GB A tighter floor, for a build that will not fit in 20 GB. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory plan; it creates no partition and changes no state.')] [CmdletBinding()] [OutputType([pscustomobject[]])] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNull()] [object] $Layout, [Parameter(Mandatory = $true, Position = 1)] [long] $DiskSizeByte, [Parameter()] [long] $MinimumWindowsSizeByte = 21474836480 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $row = @($Layout.Partition) # WHICH ROW GETS WHAT NOTHING ELSE CLAIMED. A named layout means Windows by # Role and says nothing else; an authored one names the row outright with # TakesRemainder, because it may have no partition called Windows at all. # # READ THROUGH PSObject.Properties, because a named layout's rows do not # carry these members and StrictMode throws on a missing one. $has = { param([object] $Row, [string] $Name) if ($null -eq $Row.PSObject.Properties[$Name]) { return $false } return $true } $remainderRole = 'Windows' foreach ($current in $row) { if ((& $has $current 'TakesRemainder') -and [bool] $current.TakesRemainder) { $remainderRole = [string] $current.Role break } } # Everything that is neither the remainder row nor a percentage takes its # declared size, whatever it is eventually created with. The recovery row # declares 1 GB AND carries UseMaximumSize: the declared size is what # Windows must leave behind, and the flag is what makes the slack land in # recovery rather than unallocated. $claimed = [long] 0 $percentTotal = 0 foreach ($current in $row) { if ($current.Role -eq $remainderRole) { continue } $percent = 0 if (& $has $current 'PercentOfRemainder') { $percent = [int] $current.PercentOfRemainder } if ($percent -gt 0) { $percentTotal += $percent continue } $claimed += [long] $current.SizeByte } $overhead = $claimed + [long] $Layout.ReservedSizeByte + [long] $Layout.AlignmentSizeByte # WHAT THE PERCENTAGES ARE A PERCENTAGE OF: what is left after the fixed # rows and the allowances, which is MDT's own wording - "a percentage of # remaining free space". A percentage of the WHOLE disk differs by more # than a gigabyte on a 128 GB disk, and differs silently: the disk still # partitions, just not as the author meant. $available = [long] $DiskSizeByte - $overhead $percentByte = @{} $percentClaimed = [long] 0 foreach ($current in $row) { if ($current.Role -eq $remainderRole) { continue } if (-not (& $has $current 'PercentOfRemainder')) { continue } $percent = [int] $current.PercentOfRemainder if ($percent -le 0) { continue } # FLOOR, NEVER ROUND. Rounding up hands out a byte the disk does not # have, and the failure lands on whichever partition is created last. $size = [long] [math]::Floor($available * ($percent / 100)) $percentByte[[string] $current.Role] = $size $percentClaimed += $size } $windowsSizeByte = $available - $percentClaimed if ($windowsSizeByte -lt $MinimumWindowsSizeByte) { $shortfall = $MinimumWindowsSizeByte - $windowsSizeByte # THE ROW THAT WOULD GET NOTHING IS NAMED. On a named layout that is # Windows and always was; on an authored one it is whichever row said # 'remainder', and "Windows would get" would name a partition the # document does not contain. $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Layout.Name ` -Message ("disk layout '{0}' does not fit on a disk of {1} bytes: after {2} bytes of fixed partitions and allowances and {3} bytes claimed by percentages, '{4}' would get {5} bytes, which is {6} bytes short of the {7} byte minimum." -f $Layout.Name, $DiskSizeByte, $overhead, $percentClaimed, $remainderRole, $windowsSizeByte, $shortfall, $MinimumWindowsSizeByte))) } $plan = New-Object -TypeName System.Collections.ArrayList $order = 0 foreach ($current in $row) { $order++ $sizeByte = [long] $current.SizeByte if ($current.Role -eq $remainderRole) { $sizeByte = $windowsSizeByte } if ($percentByte.ContainsKey([string] $current.Role)) { $sizeByte = $percentByte[[string] $current.Role] } [void] $plan.Add([pscustomobject] @{ Order = $order Role = [string] $current.Role SizeByte = $sizeByte UseMaximumSize = [bool] $current.UseMaximumSize FileSystem = [string] $current.FileSystem Label = [string] $current.Label DriveLetter = [string] $current.DriveLetter GptType = [string] $current.GptType CreateGptType = [string] $current.CreateGptType IsActive = [bool] $current.IsActive }) } return [pscustomobject[]] @($plan) } |