# DeployCenter - Load and validate partitioning profiles # PSD1 profiles are data files; they contain no executable code. function Get-DeployCenterPartitionProfileNames { [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$ProfilesRoot) if (-not (Test-Path -LiteralPath $ProfilesRoot -PathType Container)) { return @() } $names = @() foreach ($file in (Get-ChildItem -LiteralPath $ProfilesRoot -Filter '*.psd1' -File -ErrorAction SilentlyContinue)) { if ($file.BaseName -match '^[A-Za-z0-9_-]+$') { $names += $file.BaseName } } return @($names | Sort-Object -Unique) } function Import-DeployCenterPartitionProfile { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ProfilesRoot, [Parameter(Mandatory=$true)][string]$ProfileName ) if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid profile name: $ProfileName" } $path = Join-Path $ProfilesRoot ($ProfileName + '.psd1') if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profile not found: $ProfileName" } $profile = Import-PowerShellDataFile -LiteralPath $path foreach ($key in @('Id','Name','Description','Firmware','PartitionStyle','Partitions')) { if (-not $profile.ContainsKey($key)) { throw "The profile '$ProfileName' does not contain key '$key'." } } if ([string]$profile.Id -ne $ProfileName) { throw "Profile ID '$ProfileName' does not match the file name." } if (@($profile.Partitions).Count -lt 2) { throw 'The profile must define at least two partitions.' } if (@($profile.Partitions | Where-Object { [string]$_.Role -eq 'Windows' }).Count -ne 1) { throw 'The profile must define exactly one Windows partition.' } $userInputPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'UserInputGB' }) $remainderPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'RemainderAfterInputs' }) $remainingPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'Remaining' }) if ($userInputPartitions.Count -gt 0) { if ($remainderPartitions.Count -ne 1 -or $remainingPartitions.Count -gt 0) { throw "Profile '$ProfileName' must associate user inputs with exactly one remaining-space partition." } } elseif ($remainingPartitions.Count -ne 1 -or [string]$profile.Partitions[-1].Size.Policy -ne 'Remaining') { throw "Profile '$ProfileName' must place exactly one remaining-space partition last." } foreach ($partition in $profile.Partitions) { foreach ($key in @('Name','Role','FileSystem','Size')) { if (-not $partition.ContainsKey($key)) { throw "A profile partition '$ProfileName' does not contain key '$key'." } } if (-not $partition.Size.ContainsKey('Policy')) { throw "The size of a profile partition '$ProfileName' does not define Policy." } switch ([string]$partition.Size.Policy) { 'FixedMB' { if ([int]$partition.Size.Value -le 0) { throw "A fixed size in profile '$ProfileName' must be greater than zero." } } 'ImageSizePlusFreeSpace' { if ([int]$partition.Size.FreeSpaceMB -lt 250) { throw 'The WinRE free-space allowance must be at least 250 MB.' } if ([int]$partition.Size.PreviewMB -le 0) { throw "Profile '$ProfileName' must define a PreviewMB reserve to estimate available capacity." } } 'ConfiguredMB' { if ([string]::IsNullOrWhiteSpace([string]$partition.Size.ConfigKey) -or [int]$partition.Size.Value -le 0 -or [int]$partition.Size.PreviewMB -le 0) { throw "The configured size of partition '$($partition.Name)' in profile '$ProfileName' must define ConfigKey and strictly positive sizes." } } 'UserInputGB' { if ([int]$partition.Size.MinimumGB -le 0 -or [int]$partition.Size.DefaultPercent -le 0 -or [int]$partition.Size.DefaultPercent -ge 100) { throw 'The user-size input must define MinimumGB and a DefaultPercent from 1 to 99.' } if ([string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { throw "The profile user-size input '$ProfileName' must define Label." } } 'RemainderAfterInputs' { if ([double]$partition.Size.MinimumGB -lt 0 -or [string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { throw "The remaining-space partition in profile '$ProfileName' must define a non-negative MinimumGB and a Label." } } 'Remaining' { } default { throw "Unknown size policy in profile '$ProfileName': $($partition.Size.Policy)" } } } return $profile } function Get-DeployCenterPartitionSizingCapacity { [CmdletBinding()] param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)][long]$DiskSizeBytes ) $reservedMB = 0.0 foreach ($partition in $Profile.Partitions) { switch ([string]$partition.Size.Policy) { { $_ -in @('FixedMB','ConfiguredMB') } { $reservedMB += [double]$partition.Size.Value } default { if ($partition.Size.ContainsKey('PreviewMB')) { $reservedMB += [double]$partition.Size.PreviewMB } } } } $capacityBytes = $DiskSizeBytes - [long][math]::Ceiling($reservedMB * 1MB) if ($capacityBytes -le 0) { throw "The disk is too small for the profile reserves '$($Profile.Name)'." } return [pscustomobject]@{ UsableBytes = [long]$capacityBytes UsableGB = [math]::Round(($capacityBytes / 1GB), 1) ReservedMB = [math]::Ceiling($reservedMB) } } function New-DeployCenterPartitionSizingState { [CmdletBinding()] param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)]$DiskInfo, [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Selection ) return [pscustomobject]@{ ProfileId = [string]$Profile.Id DiskIndex = [int]$DiskInfo.Index Inputs = $Selection.Inputs RemainderRole = [string]$Selection.RemainderRole RemainderGB = [double]$Selection.RemainderGB AvailableGB = [double]$Selection.AvailableGB } } Export-ModuleMember -Function Get-DeployCenterPartitionProfileNames, Import-DeployCenterPartitionProfile, Get-DeployCenterPartitionSizingCapacity, New-DeployCenterPartitionSizingState