{"slug": "invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit", "title": "Invoke-OSDDiskLayout.ps1 | ConfigMgr OSD v1.0.0 snapshot (MIT)", "summary": "A developer released Invoke-OSDDiskLayout.ps1 v1.0.0 under the MIT license, a PowerShell script that selects a single approved target disk and creates a firmware-appropriate GPT or MBR layout in one Configuration Manager Run PowerShell Script step. Running with no parameters in a WinPE task sequence irreversibly erases the only eligible internal disk, while zero or multiple eligible disks halt execution before any modification, and outside a task sequence the script refuses to run without -Preview. The script defaults to EFI 1024 MiB, MSR 16 MiB and Recovery 2048 MiB for UEFI, and a 512 MiB active System partition for legacy BIOS.", "body_md": "|  | #requires -Version 5.1 | \n|  | <# | \n|  | .SYNOPSIS | \n|  | Selects one approved target and creates a firmware-appropriate GPT or MBR | \n|  | layout in one Configuration Manager Run PowerShell Script step. | \n|  | .DESCRIPTION | \n|  | In an active WinPE task sequence, NO PARAMETERS irreversibly erase the | \n|  | only eligible internal disk. UEFI boot creates GPT/EFI/MSR/Windows/Recovery; | \n|  | legacy BIOS boot creates MBR/active System/Windows/Recovery. | \n|  | Zero or multiple eligible disks stop before any disk modification. | \n|  | Outside a task sequence, no-parameter execution refuses to run; use | \n|  | -Preview to inventory disks without changing them. | \n|  | Default UEFI: EFI 1024 MiB \\| MSR 16 MiB \\| Windows \\| Recovery 2048 MiB. | \n|  | Default BIOS: System 512 MiB \\| Windows \\| Recovery 2048 MiB. | \n|  | Microsoft recommends the partition order/types, not these fixed EFI and | \n|  | Recovery sizes; they are conservative, configurable choices. | \n|  | Up to 32 MiB is reserved for alignment and disk metadata; the Recovery | \n|  | partition is last but a small tail may remain unallocated. | \n|  | This script does not apply Windows or configure WinRE after setup. | \n|  | On BIOS hardware, use a compatible Windows 10 (or other supported legacy) | \n|  | image. A Windows 11 image cannot boot from the BIOS/MBR layout. | \n|  |  | \n|  | A disk hidden by a missing WinPE storage driver cannot be discovered; | \n|  | single-disk mode is inappropriate on known multi-drive hardware. | \n|  | .PARAMETER Preview | \n|  | Read-only inventory and layout plan. In a task sequence this deliberately | \n|  | fails the step after preview, so Apply OS cannot continue accidentally. | \n|  | .PARAMETER PreviewFirmware | \n|  | UEFI or BIOS layout to simulate from a full Windows preview. In WinPE the | \n|  | detected actual boot mode is authoritative and must match this option. | \n|  | .PARAMETER RequireUEFI | \n|  | Stop before clean if the detected boot mode is BIOS. Set this on Windows | \n|  | 11-only task sequences to avoid creating an incompatible MBR layout. | \n|  | .PARAMETER RequireBIOS | \n|  | Stop before clean if WinPE booted in UEFI mode. Use only when the OS | \n|  | image/task sequence is explicitly limited to legacy BIOS. | \n|  | .PARAMETER AllowVirtualDisk | \n|  | Allows a virtual-machine target only with -TargetBusType Virtual. | \n|  | .PARAMETER TargetSerial | \n|  | Exact Get-Disk serial number. Preferred for model-specific exceptions. | \n|  | .PARAMETER TargetModelRegex | \n|  | Regex for Get-Disk.FriendlyName (DISK model, not computer model). | \n|  | .PARAMETER TargetComputerModelRegex | \n|  | Regex for Win32_ComputerSystem.Model. Required for Storage Spaces mode. | \n|  | .PARAMETER TargetBusType | \n|  | Optional bus filter. StorageSpaces maps to the Storage Spaces virtual bus. | \n|  | .PARAMETER AllowSurfaceStorageSpaces | \n|  | Enables an explicitly identified Surface Storage Spaces virtual target with | \n|  | -TargetBusType StorageSpaces and -TargetComputerModelRegex. The virtual | \n|  | disk must map uniquely to Get-VirtualDisk; pool member disks are not used. | \n|  | .PARAMETER MinimumDiskSizeGB | \n|  | Minimum candidate size in decimal GB. Default: 64. | \n|  | .PARAMETER MinimumWindowsSizeGiB | \n|  | Minimum planned Windows partition in GiB. Default: 40. Reducing it | \n|  | does not prove the image will have enough post-setup free space. | \n|  | .PARAMETER MaximumDiskSizeGB | \n|  | Maximum candidate size in decimal GB. Default: 8192; can be raised. | \n|  | .PARAMETER EfiSizeMiB | \n|  | EFI partition size in MiB. Default: 1024; minimum: 200 for 512-byte | \n|  | logical sectors or 300 for 4Kn storage. | \n|  | .PARAMETER BiosSystemSizeMiB | \n|  | Active NTFS system partition size in BIOS/MBR mode. Default: 512 MiB. | \n|  | .PARAMETER RecoverySizeMiB | \n|  | Recovery partition size in MiB. Default: 2048; minimum: 990. | \n|  | .PARAMETER WindowsSizeGiB | \n|  | Optional fixed Windows size (GiB); only valid with -CreateDataPartition. | \n|  | .PARAMETER CreateDataPartition | \n|  | Adds an NTFS Data partition after Recovery using remaining space. | \n|  | Requires -WindowsSizeGiB; the partition is not assigned a drive letter. | \n|  | .PARAMETER OSPartitionVariable | \n|  | Task-sequence variable receiving the temporary Windows drive, default OSPART. | \n|  | .PARAMETER DiskNumberVariable | \n|  | Task-sequence variable receiving the selected disk number, default OSDDiskIndex. | \n|  | .PARAMETER MinimumTargetSizeGB | \n|  | Optional additional inclusive lower bound for an approved target rule. | \n|  | .PARAMETER MaximumTargetSizeGB | \n|  | Optional additional inclusive upper bound for an approved target rule. | \n|  | .EXAMPLE | \n|  | .\\Invoke-OSDDiskLayout.ps1 | \n|  | In a ConfigMgr WinPE task sequence, choose GPT or MBR by actual boot mode. | \n|  | .EXAMPLE | \n|  | .\\Invoke-OSDDiskLayout.ps1 -Preview -PreviewFirmware BIOS | \n|  | Read-only BIOS/MBR plan from full Windows without disk writes. | \n|  | .EXAMPLE | \n|  | .\\Invoke-OSDDiskLayout.ps1 -RequireUEFI -TargetBusType NVMe -TargetModelRegex '^Approved SSD model' | \n|  | An approved, unique disk rule for a specific model group. | \n|  | .NOTES | \n|  | Version: 1.0.0. Not validated on live x86 or x64 WinPE hardware. | \n|  | Never follow this step with another Format and Partition Disk action. | \n|  | Apply Operating System must use OSPART, or the custom OSPartitionVariable. | \n|  | #> | \n|  | [CmdletBinding()] | \n|  | param( | \n|  | [switch] $Preview, | \n|  | [ValidateSet('UEFI', 'BIOS')] | \n|  | [string] $PreviewFirmware, | \n|  | [switch] $RequireUEFI, | \n|  | [switch] $RequireBIOS, | \n|  | [switch] $AllowVirtualDisk, | \n|  | [switch] $AllowSurfaceStorageSpaces, | \n|  | [string] $TargetSerial = '', | \n|  | [string] $TargetModelRegex = '', | \n|  | [string] $TargetComputerModelRegex = '', | \n|  | [ValidateSet('', 'NVMe', 'SATA', 'SAS', 'RAID', 'ATA', 'SCSI', 'Virtual', 'StorageSpaces')] | \n|  | [string] $TargetBusType = '', | \n|  | [ValidateRange(32, 65536)] | \n|  | [int] $MinimumDiskSizeGB = 64, | \n|  | [ValidateRange(32, 65536)] | \n|  | [int] $MaximumDiskSizeGB = 8192, | \n|  | [ValidateRange(20, 1024)] | \n|  | [int] $MinimumWindowsSizeGiB = 40, | \n|  | [ValidateRange(0, 65536)] | \n|  | [int] $MinimumTargetSizeGB = 0, | \n|  | [ValidateRange(0, 65536)] | \n|  | [int] $MaximumTargetSizeGB = 0, | \n|  | [ValidateRange(200, 8192)] | \n|  | [int] $EfiSizeMiB = 1024, | \n|  | [ValidateRange(100, 8192)] | \n|  | [int] $BiosSystemSizeMiB = 512, | \n|  | [ValidateRange(990, 32768)] | \n|  | [int] $RecoverySizeMiB = 2048, | \n|  | [ValidateRange(0, 65536)] | \n|  | [int] $WindowsSizeGiB = 0, | \n|  | [switch] $CreateDataPartition, | \n|  | [ValidatePattern('^[A-Za-z][A-Za-z0-9_-]{0,255}$')] | \n|  | [string] $OSPartitionVariable = 'OSPART', | \n|  | [ValidatePattern('^[A-Za-z][A-Za-z0-9_-]{0,255}$')] | \n|  | [string] $DiskNumberVariable = 'OSDDiskIndex' | \n|  | ) | \n|  |  | \n|  | $ErrorActionPreference = 'Stop' | \n|  | Set-StrictMode -Version Latest | \n|  | $script:TSEnv = $null | \n|  | $PlanPath = $null | \n|  | $EfiMiB = $EfiSizeMiB | \n|  | $MsrMiB = 16 | \n|  | $RecoveryMiB = $RecoverySizeMiB | \n|  | $AlignmentReserveMiB = 32 | \n|  | $RecoveryGuid = 'de94bba4-06d1-4d40-a16a-bfd50179d6ac' | \n|  |  | \n|  | function Set-TSValue { | \n|  | param( | \n|  | [Parameter(Mandatory)][string] $Name, | \n|  | [AllowEmptyString()][string] $Value | \n|  | ) | \n|  | if ($script:TSEnv) { | \n|  | $script:TSEnv.Value($Name) = $Value | \n|  | } | \n|  | } | \n|  |  | \n|  | function ConvertTo-OSDBusType { | \n|  | param([string] $BusType) | \n|  | if ($BusType -in @('Storage Spaces', 'Spaces', 'StorageSpaces')) { | \n|  | return 'StorageSpaces' | \n|  | } | \n|  | return $BusType | \n|  | } | \n|  |  | \n|  | function Test-OSDRegexMatch { | \n|  | param( | \n|  | [AllowEmptyString()][string] $Value, | \n|  | [Parameter(Mandatory)][string] $Pattern | \n|  | ) | \n|  | try { | \n|  | return [regex]::IsMatch($Value, $Pattern, | \n|  | [System.Text.RegularExpressions.RegexOptions]::IgnoreCase, | \n|  | [timespan]::FromSeconds(1)) | \n|  | } | \n|  | catch [System.Text.RegularExpressions.RegexMatchTimeoutException] { | \n|  | throw 'Approved disk or computer model pattern took too long to match.' | \n|  | } | \n|  | catch [System.ArgumentException] { | \n|  | throw 'Approved disk or computer model pattern is not a valid regex.' | \n|  | } | \n|  | } | \n|  |  | \n|  | function ConvertFrom-PEFirmwareType { | \n|  | param([Parameter(Mandatory)][int] $Value) | \n|  | switch ($Value) { | \n|  | 1 { return 'BIOS' } | \n|  | 2 { return 'UEFI' } | \n|  | default { throw \"Unknown WinPE firmware mode: $Value\" } | \n|  | } | \n|  | } | \n|  |  | \n|  | function Resolve-OSDFirmwareMode { | \n|  | param( | \n|  | [Parameter(Mandatory)][int] $PEFirmwareType, | \n|  | [string] $TaskSequenceUEFIFlag = '', | \n|  | [string] $SimulatedFirmware = '' | \n|  | ) | \n|  |  | \n|  | $Mode = ConvertFrom-PEFirmwareType -Value $PEFirmwareType | \n|  | if ($TaskSequenceUEFIFlag -and | \n|  | $TaskSequenceUEFIFlag -notin @('true', 'false')) { | \n|  | throw 'ConfigMgr reported an unrecognized firmware flag.' | \n|  | } | \n|  | if (($TaskSequenceUEFIFlag -ieq 'true' -and $Mode -ne 'UEFI') -or | \n|  | ($TaskSequenceUEFIFlag -ieq 'false' -and $Mode -ne 'BIOS')) { | \n|  | throw 'WinPE firmware detection conflicts with the ConfigMgr firmware flag.' | \n|  | } | \n|  | if ($SimulatedFirmware -and $Mode -ne $SimulatedFirmware) { | \n|  | throw 'Requested preview firmware differs from the actual WinPE boot mode.' | \n|  | } | \n|  | return $Mode | \n|  | } | \n|  |  | \n|  | function Assert-OSDFirmwarePolicy { | \n|  | param( | \n|  | [Parameter(Mandatory)][ValidateSet('UEFI', 'BIOS')] | \n|  | [string] $FirmwareMode, | \n|  | [bool] $RequireUEFI, | \n|  | [bool] $RequireBIOS | \n|  | ) | \n|  | if ($RequireUEFI -and $RequireBIOS) { | \n|  | throw 'Cannot require both UEFI and BIOS boot.' | \n|  | } | \n|  | if ($RequireUEFI -and $FirmwareMode -ne 'UEFI') { | \n|  | throw 'This task sequence requires UEFI, but WinPE booted in legacy BIOS mode.' | \n|  | } | \n|  | if ($RequireBIOS -and $FirmwareMode -ne 'BIOS') { | \n|  | throw 'This task sequence requires legacy BIOS, but WinPE booted in UEFI mode.' | \n|  | } | \n|  | } | \n|  |  | \n|  | function Test-OSDDiskEligible { | \n|  | param( | \n|  | [Parameter(Mandatory)] $Item, | \n|  | [Parameter(Mandatory)][string[]] $AllowedBuses, | \n|  | [Parameter(Mandatory)][int] $MinimumDiskSizeGB, | \n|  | [int] $MaximumDiskSizeGB = 8192, | \n|  | [bool] $AllowStorageSpacesDisk = $false | \n|  | ) | \n|  |  | \n|  | $Reasons = @() | \n|  | if ($Item.BusType -notin $AllowedBuses) { | \n|  | $Reasons += \"unapproved bus $($Item.BusType)\" | \n|  | } | \n|  | if ($Item.BusType -eq 'StorageSpaces' -and | \n|  | (-not $AllowStorageSpacesDisk -or -not $Item.VirtualVerified)) { | \n|  | $Reasons += 'Storage Spaces virtual disk not verified' | \n|  | } | \n|  | if (-not $Item.WmiFound -and | \n|  | -not ($Item.BusType -eq 'StorageSpaces' -and | \n|  | $AllowStorageSpacesDisk -and $Item.VirtualVerified)) { | \n|  | $Reasons += 'no device inventory' | \n|  | } | \n|  | if ($Item.IsOffline -or $Item.IsReadOnly -or $Item.IsClustered) { | \n|  | $Reasons += 'offline, read-only, or clustered' | \n|  | } | \n|  | if ($Item.HealthStatus -notin @('Healthy', 'Unknown') -or | \n|  | ($Item.BusType -eq 'StorageSpaces' -and $Item.HealthStatus -ne 'Healthy')) { | \n|  | $Reasons += \"health $($Item.HealthStatus)\" | \n|  | } | \n|  | if ($Item.SizeBytes -lt ([uint64]$MinimumDiskSizeGB * 1000000000)) { | \n|  | $Reasons += 'below minimum size' | \n|  | } | \n|  | if ($Item.SizeBytes -gt ([uint64]$MaximumDiskSizeGB * 1000000000)) { | \n|  | $Reasons += 'above maximum size' | \n|  | } | \n|  | if ($Item.WmiFound -and | \n|  | (($Item.WmiInterface -eq 'USB') -or | \n|  | ($Item.WmiMedia -match 'Removable\\|External') -or | \n|  | ($Item.PnpId -match '^(USBSTOR\\|UASPSTOR\\|USB)\\\\') -or | \n|  | ($Item.WmiStatus -notin @('', 'OK', 'Unknown')))) { | \n|  | $Reasons += 'removable, external, or unhealthy device' | \n|  | } | \n|  |  | \n|  | return [pscustomobject]@{ | \n|  | Eligible = ($Reasons.Count -eq 0) | \n|  | Reasons = ($Reasons -join '; ') | \n|  | } | \n|  | } | \n|  |  | \n|  | function Select-OSDTargetDisk { | \n|  | param( | \n|  | [Parameter(Mandatory)][object[]] $Inventory, | \n|  | [string] $TargetSerial = '', | \n|  | [string] $TargetModelRegex = '', | \n|  | [string] $TargetBusType = '', | \n|  | [int] $MinimumDiskSizeGB = 64, | \n|  | [int] $MinimumTargetSizeGB = 0, | \n|  | [int] $MaximumTargetSizeGB = 0 | \n|  | ) | \n|  |  | \n|  | $HasRule = [bool]($TargetSerial -or $TargetModelRegex -or $TargetBusType -or | \n|  | $MinimumTargetSizeGB -or $MaximumTargetSizeGB) | \n|  | $Eligible = @($Inventory \\| Where-Object { $_.Eligible }) | \n|  | if ($Eligible.Count -gt 1 -and -not $HasRule) { | \n|  | throw 'Multiple eligible disks; supply a unique target rule before formatting.' | \n|  | } | \n|  | if ($Eligible.Count -gt 1 -and | \n|  | -not ($TargetSerial -or $TargetModelRegex -or $TargetBusType)) { | \n|  | throw 'Size-only rules cannot authorize a multi-disk deployment.' | \n|  | } | \n|  |  | \n|  | $Matched = @($Eligible) | \n|  | if ($TargetSerial) { | \n|  | $Matched = @($Matched \\| Where-Object { $_.Serial -ieq $TargetSerial }) | \n|  | } | \n|  | if ($TargetModelRegex) { | \n|  | $Matched = @($Matched \\| Where-Object { | \n|  | Test-OSDRegexMatch -Value $_.Model -Pattern $TargetModelRegex | \n|  | }) | \n|  | } | \n|  | if ($TargetBusType) { | \n|  | $Matched = @($Matched \\| Where-Object { $_.BusType -eq $TargetBusType }) | \n|  | } | \n|  | if ($MinimumTargetSizeGB -gt 0) { | \n|  | $Matched = @($Matched \\| Where-Object { | \n|  | $_.SizeBytes -ge ([uint64]$MinimumTargetSizeGB * 1000000000) | \n|  | }) | \n|  | } | \n|  | if ($MaximumTargetSizeGB -gt 0) { | \n|  | $Matched = @($Matched \\| Where-Object { | \n|  | $_.SizeBytes -le ([uint64]$MaximumTargetSizeGB * 1000000000) | \n|  | }) | \n|  | } | \n|  |  | \n|  | if (-not $HasRule) { | \n|  | $KnownExternalBuses = @( | \n|  | 'USB', 'SD', 'MMC', 'iSCSI', 'Fibre Channel', | \n|  | 'File Backed Virtual', '1394' | \n|  | ) | \n|  | $Uncertain = @($Inventory \\| Where-Object { | \n|  | (-not $_.Eligible) -and | \n|  | ($_.SizeBytes -ge ([uint64]$MinimumDiskSizeGB * 1000000000)) -and | \n|  | ($_.BusType -notin $KnownExternalBuses) | \n|  | }) | \n|  | if ($Uncertain.Count -gt 0) { | \n|  | throw 'Another potentially internal disk is present but ineligible; use an explicit target rule.' | \n|  | } | \n|  | } | \n|  | if ($Matched.Count -ne 1) { | \n|  | throw \"Expected exactly one approved disk; matched $($Matched.Count). No disk was changed.\" | \n|  | } | \n|  |  | \n|  | $Selected = $Matched[0] | \n|  | if (-not $Selected.Serial -and -not $Selected.PnpId -and | \n|  | -not ($Selected.VirtualVerified -and $Selected.VirtualId)) { | \n|  | throw 'Selected disk lacks a serial, PnP device ID, or verified virtual ID that survives cleanup.' | \n|  | } | \n|  | if ($Selected.Serial -and | \n|  | @($Inventory \\| Where-Object { $_.Serial -ieq $Selected.Serial }).Count -ne 1) { | \n|  | throw 'Selected disk serial is not unique among visible disks.' | \n|  | } | \n|  | if ($Selected.VirtualId -and | \n|  | @($Inventory \\| Where-Object { $_.VirtualId -eq $Selected.VirtualId }).Count -ne 1) { | \n|  | throw 'Selected virtual disk ID is not unique among visible disks.' | \n|  | } | \n|  | if (-not $Selected.Serial -and -not $Selected.VirtualVerified -and $Selected.PnpId -and | \n|  | @($Inventory \\| Where-Object { $_.PnpId -eq $Selected.PnpId }).Count -ne 1) { | \n|  | throw 'Selected PnP device ID is not unique among visible disks.' | \n|  | } | \n|  | return $Selected | \n|  | } | \n|  |  | \n|  | function Assert-OSDFirmwareDiskGeometry { | \n|  | param( | \n|  | [Parameter(Mandatory)] $Disk, | \n|  | [Parameter(Mandatory)][ValidateSet('UEFI', 'BIOS')] | \n|  | [string] $FirmwareMode, | \n|  | [int] $EfiMiB = 1024 | \n|  | ) | \n|  | if ($Disk.LogicalSectorSize -notin @(512, 4096)) { | \n|  | throw 'Only 512-byte or 4096-byte logical sectors are supported for Windows boot.' | \n|  | } | \n|  | if ($FirmwareMode -eq 'BIOS') { | \n|  | if ($Disk.LogicalSectorSize -ne 512) { | \n|  | throw 'BIOS/MBR mode requires 512-byte logical sectors; 4Kn is unsupported.' | \n|  | } | \n|  | if ($Disk.SizeBytes -gt [uint64]2000000000000) { | \n|  | throw 'BIOS/MBR mode refuses disks larger than 2 TB decimal.' | \n|  | } | \n|  | } | \n|  | elseif (($Disk.LogicalSectorSize -eq 4096 -and $EfiMiB -lt 300) -or | \n|  | ($Disk.LogicalSectorSize -eq 512 -and $EfiMiB -lt 200)) { | \n|  | throw 'EFI partition is below the minimum for this logical sector size.' | \n|  | } | \n|  | } | \n|  |  | \n|  | function New-OSDPartitionPlan { | \n|  | param( | \n|  | [Parameter(Mandatory)][uint64] $DiskSizeBytes, | \n|  | [ValidateSet('UEFI', 'BIOS')][string] $FirmwareMode = 'UEFI', | \n|  | [int] $EfiMiB = 1024, | \n|  | [int] $BiosSystemMiB = 512, | \n|  | [int] $MsrMiB = 16, | \n|  | [int] $RecoveryMiB = 2048, | \n|  | [int] $AlignmentReserveMiB = 32, | \n|  | [int] $WindowsSizeGiB = 0, | \n|  | [int] $MinimumWindowsSizeGiB = 40, | \n|  | [bool] $CreateDataPartition = $false | \n|  | ) | \n|  |  | \n|  | if ($FirmwareMode -eq 'BIOS' -and $DiskSizeBytes -gt [uint64]2000000000000) { | \n|  | throw 'BIOS/MBR profile refuses disks larger than 2 TB decimal.' | \n|  | } | \n|  | $BootMiB = $EfiMiB | \n|  | $ReservedMiB = $MsrMiB | \n|  | $ActualEfiMiB = $EfiMiB | \n|  | $ActualBiosSystemMiB = 0 | \n|  | if ($FirmwareMode -eq 'BIOS') { | \n|  | $BootMiB = $BiosSystemMiB | \n|  | $ReservedMiB = 0 | \n|  | $ActualEfiMiB = 0 | \n|  | $ActualBiosSystemMiB = $BiosSystemMiB | \n|  | } | \n|  | $TotalMiB = [int64][math]::Floor($DiskSizeBytes / 1MB) | \n|  | $AvailableMiB = $TotalMiB - $BootMiB - $ReservedMiB - $RecoveryMiB - $AlignmentReserveMiB | \n|  | if ($CreateDataPartition -ne ($WindowsSizeGiB -gt 0)) { | \n|  | throw 'Data layout requires both -CreateDataPartition and -WindowsSizeGiB.' | \n|  | } | \n|  | $DataMiB = [int64]0 | \n|  | $WindowsMiB = $AvailableMiB | \n|  | if ($CreateDataPartition) { | \n|  | $WindowsMiB = [int64]$WindowsSizeGiB * 1024 | \n|  | $DataMiB = $AvailableMiB - $WindowsMiB | \n|  | if ($DataMiB -lt 1024) { | \n|  | throw 'Data partition would be smaller than 1 GiB.' | \n|  | } | \n|  | } | \n|  | if ($WindowsMiB -lt ([int64]$MinimumWindowsSizeGiB * 1024)) { | \n|  | throw \"Planned Windows partition is under $MinimumWindowsSizeGiB GiB.\" | \n|  | } | \n|  | return [pscustomobject]@{ | \n|  | FirmwareMode = $FirmwareMode | \n|  | EfiMiB = $ActualEfiMiB | \n|  | BiosSystemMiB = $ActualBiosSystemMiB | \n|  | MsrMiB = $ReservedMiB | \n|  | WindowsMiB = $WindowsMiB | \n|  | RecoveryMiB = $RecoveryMiB | \n|  | DataMiB = $DataMiB | \n|  | AlignmentReserveMiB = $AlignmentReserveMiB | \n|  | } | \n|  | } | \n|  |  | \n|  | function New-OSDDiskPartCommands { | \n|  | param( | \n|  | [Parameter(Mandatory)][int] $DiskNumber, | \n|  | [Parameter(Mandatory)] $Plan, | \n|  | [Parameter(Mandatory)][string] $RecoveryGuid | \n|  | ) | \n|  |  | \n|  | $Commands = @(\"select disk $DiskNumber\", 'clean') | \n|  | if ($Plan.FirmwareMode -eq 'UEFI') { | \n|  | $Commands += 'convert gpt' | \n|  | $Commands += \"create partition efi size=$($Plan.EfiMiB)\" | \n|  | $Commands += 'format quick fs=fat32 label=System' | \n|  | $Commands += \"create partition msr size=$($Plan.MsrMiB)\" | \n|  | } | \n|  | elseif ($Plan.FirmwareMode -eq 'BIOS') { | \n|  | $Commands += 'convert mbr' | \n|  | $Commands += \"create partition primary size=$($Plan.BiosSystemMiB)\" | \n|  | $Commands += 'format quick fs=ntfs label=\"System Reserved\"' | \n|  | $Commands += 'assign letter=S' | \n|  | $Commands += 'active' | \n|  | } | \n|  | else { | \n|  | throw 'Unrecognized partition plan firmware mode.' | \n|  | } | \n|  | $Commands += \"create partition primary size=$($Plan.WindowsMiB)\" | \n|  | $Commands += 'format quick fs=ntfs label=Windows' | \n|  | $Commands += 'assign letter=W' | \n|  | $Commands += \"create partition primary size=$($Plan.RecoveryMiB)\" | \n|  | $Commands += 'format quick fs=ntfs label=Recovery' | \n|  | if ($Plan.FirmwareMode -eq 'UEFI') { | \n|  | $Commands += \"set id=$RecoveryGuid\" | \n|  | $Commands += 'gpt attributes=0x8000000000000001' | \n|  | } | \n|  | else { | \n|  | $Commands += 'set id=27' | \n|  | } | \n|  | $Commands += 'detail partition' | \n|  | if ($Plan.DataMiB -gt 0) { | \n|  | $Commands += \"create partition primary size=$($Plan.DataMiB)\" | \n|  | $Commands += 'format quick fs=ntfs label=Data' | \n|  | } | \n|  | $Commands += 'exit' | \n|  | return $Commands | \n|  | } | \n|  |  | \n|  | function Assert-OSDPartitionStructure { | \n|  | param( | \n|  | [Parameter(Mandatory)][object[]] $Parts, | \n|  | [Parameter(Mandatory)] $Plan, | \n|  | [Parameter(Mandatory)][string] $RecoveryGuid | \n|  | ) | \n|  |  | \n|  | $FirmwareMode = $Plan.FirmwareMode | \n|  | $WindowsIndex = if ($FirmwareMode -eq 'UEFI') { 2 } else { 1 } | \n|  | $RecoveryIndex = $WindowsIndex + 1 | \n|  | $DataIndex = $RecoveryIndex + 1 | \n|  | $ExpectedCount = $RecoveryIndex + 1 | \n|  | if ($Plan.DataMiB -gt 0) { $ExpectedCount++ } | \n|  | if ($Parts.Count -ne $ExpectedCount) { | \n|  | throw \"Expected $ExpectedCount new partitions.\" | \n|  | } | \n|  |  | \n|  | if ($FirmwareMode -eq 'UEFI') { | \n|  | $ExpectedTypes = @( | \n|  | 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b' | \n|  | 'e3c9e316-0b5c-4db8-817d-f92df00215ae' | \n|  | 'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7' | \n|  | $RecoveryGuid | \n|  | ) | \n|  | if ($Plan.DataMiB -gt 0) { | \n|  | $ExpectedTypes += 'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7' | \n|  | } | \n|  | for ($i = 0; $i -lt $ExpectedTypes.Count; $i++) { | \n|  | if (([string]$Parts[$i].GptType).Trim('{}') -ine $ExpectedTypes[$i]) { | \n|  | throw \"Unexpected GPT type or order at partition $($i + 1).\" | \n|  | } | \n|  | } | \n|  | if ($Parts[1].Size -ne ([uint64]$Plan.MsrMiB * 1MB) -or | \n|  | -not $Parts[$RecoveryIndex].NoDefaultDriveLetter) { | \n|  | throw 'GPT MSR size or Recovery attribute verification failed.' | \n|  | } | \n|  | } | \n|  | elseif ($FirmwareMode -eq 'BIOS') { | \n|  | $ExpectedTypes = @(7, 7, 39) | \n|  | if ($Plan.DataMiB -gt 0) { $ExpectedTypes += 7 } | \n|  | for ($i = 0; $i -lt $ExpectedTypes.Count; $i++) { | \n|  | if ([int]$Parts[$i].MbrType -ne $ExpectedTypes[$i]) { | \n|  | throw \"Unexpected MBR type or order at partition $($i + 1).\" | \n|  | } | \n|  | } | \n|  | $ActiveParts = @($Parts \\| Where-Object { $_.IsActive }) | \n|  | if ($ActiveParts.Count -ne 1 -or | \n|  | $ActiveParts[0].PartitionNumber -ne $Parts[0].PartitionNumber -or | \n|  | $Parts[0].DriveLetter -ne 'S') { | \n|  | throw 'Only the BIOS system partition may be active; it must be S: in WinPE.' | \n|  | } | \n|  | } | \n|  | else { | \n|  | throw 'Unrecognized partition plan firmware mode.' | \n|  | } | \n|  |  | \n|  | $BootMiB = if ($FirmwareMode -eq 'UEFI') { | \n|  | $Plan.EfiMiB | \n|  | } else { | \n|  | $Plan.BiosSystemMiB | \n|  | } | \n|  | if ($Parts[0].Size -ne ([uint64]$BootMiB * 1MB) -or | \n|  | $Parts[$WindowsIndex].Size -ne ([uint64]$Plan.WindowsMiB * 1MB) -or | \n|  | $Parts[$RecoveryIndex].Size -ne ([uint64]$Plan.RecoveryMiB * 1MB)) { | \n|  | throw 'A partition has an unexpected size.' | \n|  | } | \n|  | $RecoveryGap = [int64]$Parts[$RecoveryIndex].Offset - | \n|  | ([int64]$Parts[$WindowsIndex].Offset + [int64]$Parts[$WindowsIndex].Size) | \n|  | if ($RecoveryGap -lt 0 -or $RecoveryGap -gt 1MB) { | \n|  | throw 'Recovery is not immediately after Windows.' | \n|  | } | \n|  | if ($Parts[$WindowsIndex].DriveLetter -ne 'W' -or | \n|  | ($FirmwareMode -eq 'UEFI' -and $Parts[$RecoveryIndex].DriveLetter)) { | \n|  | throw 'Windows or GPT Recovery drive-letter verification failed.' | \n|  | } | \n|  | if ($Plan.DataMiB -gt 0) { | \n|  | if ($Parts[$DataIndex].Size -ne ([uint64]$Plan.DataMiB * 1MB) -or | \n|  | $Parts[$DataIndex].Offset -lt | \n|  | ($Parts[$RecoveryIndex].Offset + $Parts[$RecoveryIndex].Size)) { | \n|  | throw 'Data partition size or order verification failed.' | \n|  | } | \n|  | if ($FirmwareMode -eq 'UEFI' -and | \n|  | [string]::IsNullOrWhiteSpace([string]$Parts[$DataIndex].Guid)) { | \n|  | throw 'Data partition GUID verification failed.' | \n|  | } | \n|  | } | \n|  | return [pscustomobject]@{ | \n|  | WindowsIndex = $WindowsIndex | \n|  | RecoveryIndex = $RecoveryIndex | \n|  | DataIndex = $DataIndex | \n|  | RecoveryHasLetter = [bool]$Parts[$RecoveryIndex].DriveLetter | \n|  | } | \n|  | } | \n|  |  | \n|  | function Assert-OSDDiskState { | \n|  | param( | \n|  | [Parameter(Mandatory)] $Current, | \n|  | [Parameter(Mandatory)] $Selection | \n|  | ) | \n|  | if (($Current.Size -ne $Selection.SizeBytes) -or | \n|  | ([uint32]$Current.LogicalSectorSize -ne $Selection.LogicalSectorSize) -or | \n|  | ((ConvertTo-OSDBusType ([string]$Current.BusType)) -ne $Selection.BusType) -or | \n|  | ([string]$Current.FriendlyName -ne $Selection.Model)) { | \n|  | throw 'Disk identity changed since selection; refusing to erase it.' | \n|  | } | \n|  | if ($Current.IsOffline -or $Current.IsReadOnly -or $Current.IsClustered -or | \n|  | ([string]$Current.HealthStatus -notin @('Healthy', 'Unknown'))) { | \n|  | throw 'Selected disk is no longer healthy, writable, or available.' | \n|  | } | \n|  | if ($Selection.Serial -and | \n|  | (([string]$Current.SerialNumber).Trim() -ne $Selection.Serial)) { | \n|  | throw 'Disk serial changed since selection; refusing to erase it.' | \n|  | } | \n|  | if ($Selection.UniqueId -and | \n|  | ([string]$Current.UniqueId -ne $Selection.UniqueId)) { | \n|  | throw 'Disk unique ID changed since selection; refusing to erase it.' | \n|  | } | \n|  | } | \n|  |  | \n|  | function Test-SameDisk { | \n|  | param([Parameter(Mandatory)] $Selection) | \n|  |  | \n|  | $Current = Get-Disk -Number $Selection.Number -ErrorAction Stop | \n|  | Assert-OSDDiskState -Current $Current -Selection $Selection | \n|  |  | \n|  | if ($Selection.VirtualVerified) { | \n|  | $Virtual = @(Get-VirtualDisk -Disk $Current -ErrorAction Stop) | \n|  | if ($Virtual.Count -ne 1 -or | \n|  | ([string]$Virtual[0].UniqueId -ne $Selection.VirtualId) -or | \n|  | ([string]$Virtual[0].HealthStatus -ne 'Healthy')) { | \n|  | throw 'Storage Spaces virtual disk identity or health changed.' | \n|  | } | \n|  | $Mapping = @(Get-Disk -VirtualDisk $Virtual[0] -ErrorAction Stop) | \n|  | if ($Mapping.Count -ne 1 -or $Mapping[0].Number -ne $Selection.Number) { | \n|  | throw 'Storage Spaces virtual disk mapping changed.' | \n|  | } | \n|  | } | \n|  |  | \n|  | $Drive = @(Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction Stop \\| | \n|  | Where-Object { [int]$_.Index -eq $Selection.Number }) | \n|  | if ($Selection.WmiFound) { | \n|  | if (($Drive.Count -ne 1) -or | \n|  | ([string]$Drive[0].PNPDeviceID -ne $Selection.PnpId)) { | \n|  | throw 'Disk device identity changed since selection; refusing to erase it.' | \n|  | } | \n|  | if ([string]$Drive[0].InterfaceType -ne $Selection.WmiInterface -or | \n|  | [string]$Drive[0].MediaType -ne $Selection.WmiMedia -or | \n|  | [string]$Drive[0].Status -notin @('', 'OK', 'Unknown')) { | \n|  | throw 'Disk media, interface, or device health changed since selection.' | \n|  | } | \n|  | } | \n|  | elseif (-not $Selection.VirtualVerified -or $Drive.Count -gt 0) { | \n|  | throw 'Disk inventory changed since selection; refusing to erase it.' | \n|  | } | \n|  | } | \n|  |  | \n|  | function Assert-OSDPathOffTargetDisk { | \n|  | param( | \n|  | [AllowEmptyString()][string] $Path, | \n|  | [Parameter(Mandatory)][int] $TargetDiskNumber, | \n|  | [Parameter(Mandatory)][string] $Description | \n|  | ) | \n|  | if ([string]::IsNullOrWhiteSpace($Path)) { return } | \n|  | $Expanded = [Environment]::ExpandEnvironmentVariables($Path.Trim()) | \n|  | if ($Expanded.StartsWith('\\\\')) { | \n|  | if ($Expanded.StartsWith('\\\\?\\') -or | \n|  | $Expanded.StartsWith('\\\\.\\')) { | \n|  | throw \"$Description uses a device path that cannot be mapped safely.\" | \n|  | } | \n|  | $Server = ($Expanded.Substring(2) -split '[\\\\/]', 2)[0] | \n|  | if ($Server -in @('', '.', 'localhost', '127.0.0.1', | \n|  | '[::1]', [string]$env:COMPUTERNAME)) { | \n|  | throw \"$Description uses a local network alias that may point to the selected disk.\" | \n|  | } | \n|  | return | \n|  | } | \n|  | if ($Expanded -notmatch '^([A-Za-z]):[\\\\/]') { | \n|  | throw \"$Description is not an absolute drive or network path; target safety cannot be checked.\" | \n|  | } | \n|  | $Letter = [char]$Matches[1].ToUpperInvariant() | \n|  | if ($Letter -eq 'X') { return } # WinPE's RAM drive. | \n|  |  | \n|  | $Partitions = @(Get-Partition -DriveLetter $Letter -ErrorAction SilentlyContinue) | \n|  | if ($Partitions.Count -gt 1) { | \n|  | throw \"$Description maps to multiple partitions; refusing to erase a disk.\" | \n|  | } | \n|  | if ($Partitions.Count -eq 1) { | \n|  | if ($Partitions[0].DiskNumber -eq $TargetDiskNumber) { | \n|  | throw \"$Description resides on the selected disk; refusing to erase task-sequence content.\" | \n|  | } | \n|  | return | \n|  | } | \n|  | $Drive = Get-PSDrive -Name $Letter -PSProvider FileSystem -ErrorAction SilentlyContinue | \n|  | if ($Drive) { | \n|  | $RemoteRoot = [string]$Drive.DisplayRoot | \n|  | if (-not $RemoteRoot.StartsWith('\\\\')) { | \n|  | $RemoteRoot = [string]$Drive.Root | \n|  | } | \n|  | if ($RemoteRoot.StartsWith('\\\\')) { | \n|  | Assert-OSDPathOffTargetDisk -Path $RemoteRoot ` | \n|  | -TargetDiskNumber $TargetDiskNumber -Description $Description | \n|  | return | \n|  | } | \n|  | } | \n|  | throw \"$Description has no verifiable off-target disk or network mapping.\" | \n|  | } | \n|  |  | \n|  | function Assert-OSDTaskSequenceSource { | \n|  | param( | \n|  | [Parameter(Mandatory)] $TaskSequenceEnvironment, | \n|  | [AllowEmptyString()][string] $RunningScriptPath, | \n|  | [Parameter(Mandatory)][int] $TargetDiskNumber | \n|  | ) | \n|  | Assert-OSDPathOffTargetDisk ` | \n|  | -Path ([string]$TaskSequenceEnvironment.Value('_SMSTSMDataPath')) ` | \n|  | -TargetDiskNumber $TargetDiskNumber -Description 'Task-sequence working data' | \n|  | Assert-OSDPathOffTargetDisk ` | \n|  | -Path ([string]$TaskSequenceEnvironment.Value('_SMSTSLogPath')) ` | \n|  | -TargetDiskNumber $TargetDiskNumber -Description 'Task-sequence logs' | \n|  | Assert-OSDPathOffTargetDisk -Path $RunningScriptPath ` | \n|  | -TargetDiskNumber $TargetDiskNumber -Description 'Running PowerShell script' | \n|  | } | \n|  |  | \n|  | function Assert-OSDWinPEDriveSafety { | \n|  | param( | \n|  | [Parameter(Mandatory)][int] $TargetDiskNumber, | \n|  | [Parameter(Mandatory)][ValidateSet('UEFI', 'BIOS')] | \n|  | [string] $FirmwareMode, | \n|  | [string] $SystemRootPath = $env:SystemRoot | \n|  | ) | \n|  | $SystemLetter = [char]$SystemRootPath.Substring(0, 1) | \n|  | $SystemPartition = @(Get-Partition -DriveLetter $SystemLetter -ErrorAction SilentlyContinue) | \n|  | if (@($SystemPartition \\| Where-Object { $_.DiskNumber -eq $TargetDiskNumber }).Count -gt 0) { | \n|  | throw 'The WinPE system root is on the selected disk; refusing to erase it.' | \n|  | } | \n|  | $Letters = @('W') | \n|  | if ($FirmwareMode -eq 'BIOS') { $Letters += 'S' } | \n|  | foreach ($Letter in $Letters) { | \n|  | $Existing = @(Get-Partition -DriveLetter $Letter -ErrorAction SilentlyContinue) | \n|  | if (@($Existing \\| Where-Object { $_.DiskNumber -ne $TargetDiskNumber }).Count -gt 0) { | \n|  | throw \"$($Letter): belongs to another disk and is unavailable.\" | \n|  | } | \n|  | if ((Get-PSDrive -Name $Letter -PSProvider FileSystem -ErrorAction SilentlyContinue) -and | \n|  | $Existing.Count -eq 0) { | \n|  | throw \"$($Letter): is already in use outside the selected disk.\" | \n|  | } | \n|  | } | \n|  | } | \n|  |  | \n|  | function Get-OSDFirmwareMode { | \n|  | param( | \n|  | $TaskSequenceEnvironment, | \n|  | [bool] $IsPreview, | \n|  | [string] $SimulatedFirmware | \n|  | ) | \n|  |  | \n|  | $InWinPE = Test-Path -LiteralPath 'HKLM:\\System\\CurrentControlSet\\Control\\MiniNT' | \n|  | if ($TaskSequenceEnvironment -and | \n|  | $TaskSequenceEnvironment.Value('_SMSTSInWinPE') -ieq 'true') { | \n|  | $InWinPE = $true | \n|  | } | \n|  | if (-not $InWinPE) { | \n|  | if (-not $IsPreview) { | \n|  | throw 'Destructive layout requires an actual WinPE boot.' | \n|  | } | \n|  | if ($SimulatedFirmware) { | \n|  | Write-Host \"Full-Windows preview: SIMULATING $SimulatedFirmware; not proof of WinPE boot mode.\" | \n|  | return $SimulatedFirmware | \n|  | } | \n|  | Write-Host 'Full-Windows preview: assuming UEFI for the plan only; specify -PreviewFirmware BIOS to simulate BIOS.' | \n|  | return 'UEFI' | \n|  | } | \n|  |  | \n|  | $WpeUtil = Join-Path $env:SystemRoot 'System32\\wpeutil.exe' | \n|  | if (-not (Test-Path -LiteralPath $WpeUtil -PathType Leaf)) { | \n|  | throw 'WinPE firmware detection requires wpeutil.exe.' | \n|  | } | \n|  | & $WpeUtil UpdateBootInfo \\| Out-Null | \n|  | if ($LASTEXITCODE -ne 0) { | \n|  | throw 'WinPE could not update boot-mode information.' | \n|  | } | \n|  | $BootInfo = Get-ItemProperty -LiteralPath ` | \n|  | 'HKLM:\\System\\CurrentControlSet\\Control' -Name PEFirmwareType -ErrorAction Stop | \n|  | $TSUEFI = '' | \n|  | if ($TaskSequenceEnvironment) { | \n|  | $TSUEFI = [string]$TaskSequenceEnvironment.Value('_SMSTSBootUEFI') | \n|  | if (-not $TSUEFI) { | \n|  | Write-Host 'ConfigMgr firmware flag absent; using the detected WinPE firmware type.' | \n|  | } | \n|  | } | \n|  | $Mode = Resolve-OSDFirmwareMode -PEFirmwareType ([int]$BootInfo.PEFirmwareType) ` | \n|  | -TaskSequenceUEFIFlag $TSUEFI -SimulatedFirmware $SimulatedFirmware | \n|  | Write-Host \"WinPE firmware mode: $Mode\" | \n|  | return $Mode | \n|  | } | \n|  |  | \n|  | try { | \n|  | try { | \n|  | $script:TSEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction Stop | \n|  | } | \n|  | catch { | \n|  | if (-not $Preview) { | \n|  | throw 'Destructive mode requires an active ConfigMgr task sequence; use -Preview for standalone inventory.' | \n|  | } | \n|  | } | \n|  |  | \n|  | # Clear any values from a prior attempt before making a new selection. | \n|  | Set-TSValue -Name 'DiskSelectionStatus' -Value 'Failed' | \n|  | Set-TSValue -Name 'DiskLayoutStatus' -Value 'Failed' | \n|  | Set-TSValue -Name 'DiskLayoutMode' -Value '' | \n|  | Set-TSValue -Name 'DiskPartitionStyle' -Value '' | \n|  | Set-TSValue -Name 'DiskDataPartitionGuid' -Value '' | \n|  | $ReservedOutputs = @( | \n|  | 'DiskSelectionStatus', 'DiskLayoutStatus', | \n|  | 'DiskLayoutMode', 'DiskPartitionStyle', | \n|  | 'DiskDataPartitionGuid', 'OSDTargetSystemDrive' | \n|  | ) | \n|  | if ($OSPartitionVariable -eq $DiskNumberVariable -or | \n|  | $OSPartitionVariable -in $ReservedOutputs -or | \n|  | $DiskNumberVariable -in $ReservedOutputs) { | \n|  | throw 'Output variable names must be distinct and cannot replace status or ConfigMgr-owned outputs.' | \n|  | } | \n|  | Set-TSValue -Name $DiskNumberVariable -Value '' | \n|  | Set-TSValue -Name $OSPartitionVariable -Value '' | \n|  |  | \n|  | if ($PreviewFirmware -and -not $Preview) { | \n|  | throw '-PreviewFirmware is only valid with -Preview.' | \n|  | } | \n|  | if (-not $Preview) { | \n|  | if ($script:TSEnv.Value('_SMSTSInWinPE') -ine 'true') { | \n|  | throw 'Destructive mode requires an active WinPE task sequence.' | \n|  | } | \n|  | if ($script:TSEnv.Value('OSDMigrateUseHardlinks') -eq 'true') { | \n|  | throw 'Hard-link migration is active; this fresh-install script must not erase the disk.' | \n|  | } | \n|  | if (-not [string]::IsNullOrWhiteSpace( | \n|  | [string]$script:TSEnv.Value('_SMSTSClientCache'))) { | \n|  | throw 'A task-sequence client cache is present; refusing to erase its possible host disk.' | \n|  | } | \n|  | if ($script:TSEnv.Value('_SMSTSMediaType') -eq 'OEMMedia') { | \n|  | throw 'OEM media mode is not approved for destructive disk layout.' | \n|  | } | \n|  | if ($script:TSEnv.Value('_SMSTSLaunchMode') -eq 'HD') { | \n|  | throw 'Prestaged hard-disk boot media may reside on the target; refusing to erase it.' | \n|  | } | \n|  | if (-not [string]::IsNullOrWhiteSpace( | \n|  | [string]$script:TSEnv.Value('OSDTargetSystemDrive'))) { | \n|  | throw 'The OS target is already set; disk layout must run before Apply Operating System.' | \n|  | } | \n|  | } | \n|  | $FirmwareMode = Get-OSDFirmwareMode -TaskSequenceEnvironment $script:TSEnv ` | \n|  | -IsPreview ([bool]$Preview) -SimulatedFirmware $PreviewFirmware | \n|  | Assert-OSDFirmwarePolicy -FirmwareMode $FirmwareMode ` | \n|  | -RequireUEFI ([bool]$RequireUEFI) -RequireBIOS ([bool]$RequireBIOS) | \n|  | if ($FirmwareMode -eq 'BIOS' -and $PSBoundParameters.ContainsKey('EfiSizeMiB')) { | \n|  | throw '-EfiSizeMiB only applies to UEFI/GPT.' | \n|  | } | \n|  | if ($FirmwareMode -eq 'UEFI' -and $PSBoundParameters.ContainsKey('BiosSystemSizeMiB')) { | \n|  | throw '-BiosSystemSizeMiB only applies to BIOS/MBR.' | \n|  | } | \n|  | Import-Module Storage -ErrorAction Stop | \n|  |  | \n|  | if ($MaximumTargetSizeGB -gt 0 -and | \n|  | $MinimumTargetSizeGB -gt $MaximumTargetSizeGB) { | \n|  | throw 'Minimum target size exceeds maximum target size.' | \n|  | } | \n|  | if ($MinimumDiskSizeGB -gt $MaximumDiskSizeGB) { | \n|  | throw 'Minimum disk size exceeds maximum disk size.' | \n|  | } | \n|  |  | \n|  | $TargetSerial = $TargetSerial.Trim() | \n|  | $TargetModelRegex = $TargetModelRegex.Trim() | \n|  | $TargetComputerModelRegex = $TargetComputerModelRegex.Trim() | \n|  | if ($AllowVirtualDisk -and $TargetBusType -ne 'Virtual') { | \n|  | throw '-AllowVirtualDisk also requires -TargetBusType Virtual.' | \n|  | } | \n|  | if ($AllowSurfaceStorageSpaces -and | \n|  | ($TargetBusType -ne 'StorageSpaces' -or -not $TargetComputerModelRegex)) { | \n|  | throw 'Surface Storage Spaces mode requires -TargetBusType StorageSpaces and a computer-model rule.' | \n|  | } | \n|  | if ($TargetBusType -eq 'StorageSpaces' -and -not $AllowSurfaceStorageSpaces) { | \n|  | throw 'A Storage Spaces target requires -AllowSurfaceStorageSpaces.' | \n|  | } | \n|  |  | \n|  | $Computer = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop | \n|  | $ComputerModel = [string]$Computer.Model | \n|  | Write-Host \"Computer: $($Computer.Manufacturer) $ComputerModel\" | \n|  | if ($TargetComputerModelRegex -and | \n|  | -not (Test-OSDRegexMatch -Value $ComputerModel ` | \n|  | -Pattern $TargetComputerModelRegex)) { | \n|  | throw 'Computer model did not match the approved model rule.' | \n|  | } | \n|  | if ($AllowSurfaceStorageSpaces -and | \n|  | (([string]$Computer.Manufacturer -notmatch '^Microsoft') -or | \n|  | ($ComputerModel -notmatch 'Surface'))) { | \n|  | throw 'Storage Spaces exception is restricted to identified Microsoft Surface devices.' | \n|  | } | \n|  |  | \n|  | $AllowedBuses = @('NVMe', 'SATA', 'RAID', 'ATA') | \n|  | # SAS/SCSI may also describe attached storage. Require a specific rule. | \n|  | if ($TargetBusType -in @('SAS', 'SCSI')) { $AllowedBuses += $TargetBusType } | \n|  | if ($AllowVirtualDisk) { $AllowedBuses += 'Virtual' } | \n|  | if ($AllowSurfaceStorageSpaces) { $AllowedBuses += 'StorageSpaces' } | \n|  | $DeviceDrives = @(Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction Stop) | \n|  |  | \n|  | $Disks = @(Get-Disk -ErrorAction Stop) | \n|  | if ($Disks.Count -eq 0) { throw 'No disks are visible in WinPE.' } | \n|  | $Inventory = foreach ($Disk in $Disks) { | \n|  | $Number = [int]$Disk.Number | \n|  | $Bus = ConvertTo-OSDBusType ([string]$Disk.BusType) | \n|  | $MatchingDrives = @($DeviceDrives \\| Where-Object { $_.Index -eq $Number }) | \n|  | $Drive = $null | \n|  | if ($MatchingDrives.Count -eq 1) { $Drive = $MatchingDrives[0] } | \n|  | $VirtualVerified = $false | \n|  | $VirtualId = '' | \n|  | if ($Bus -eq 'StorageSpaces' -and $AllowSurfaceStorageSpaces) { | \n|  | try { | \n|  | $Virtual = @(Get-VirtualDisk -Disk $Disk -ErrorAction Stop) | \n|  | if ($Virtual.Count -eq 1 -and | \n|  | [string]$Virtual[0].HealthStatus -eq 'Healthy' -and | \n|  | -not [string]::IsNullOrWhiteSpace([string]$Virtual[0].UniqueId)) { | \n|  | $Mapped = @(Get-Disk -VirtualDisk $Virtual[0] -ErrorAction Stop) | \n|  | if ($Mapped.Count -eq 1 -and $Mapped[0].Number -eq $Number) { | \n|  | $VirtualVerified = $true | \n|  | $VirtualId = [string]$Virtual[0].UniqueId | \n|  | } | \n|  | } | \n|  | } | \n|  | catch { | \n|  | Write-Host \"Disk $Number : Storage Spaces association unavailable; disk remains ineligible.\" | \n|  | } | \n|  | } | \n|  | $PnpId = '' | \n|  | $WmiInterface = '' | \n|  | $WmiMedia = '' | \n|  | $WmiStatus = '' | \n|  | if ($null -ne $Drive) { | \n|  | $PnpId = [string]$Drive.PNPDeviceID | \n|  | $WmiInterface = [string]$Drive.InterfaceType | \n|  | $WmiMedia = [string]$Drive.MediaType | \n|  | $WmiStatus = [string]$Drive.Status | \n|  | } | \n|  |  | \n|  | $Entry = [pscustomobject]@{ | \n|  | Number = $Number | \n|  | Model = [string]$Disk.FriendlyName | \n|  | Serial = ([string]$Disk.SerialNumber).Trim() | \n|  | UniqueId = [string]$Disk.UniqueId | \n|  | PnpId = $PnpId | \n|  | WmiFound = ($null -ne $Drive) | \n|  | WmiInterface = $WmiInterface | \n|  | WmiMedia = $WmiMedia | \n|  | WmiStatus = $WmiStatus | \n|  | BusType = $Bus | \n|  | SizeBytes = [uint64]$Disk.Size | \n|  | LogicalSectorSize = [uint32]$Disk.LogicalSectorSize | \n|  | HealthStatus = [string]$Disk.HealthStatus | \n|  | IsOffline = [bool]$Disk.IsOffline | \n|  | IsReadOnly = [bool]$Disk.IsReadOnly | \n|  | IsClustered = [bool]$Disk.IsClustered | \n|  | VirtualVerified = $VirtualVerified | \n|  | VirtualId = $VirtualId | \n|  | } | \n|  | $Eligibility = Test-OSDDiskEligible -Item $Entry -AllowedBuses $AllowedBuses ` | \n|  | -MinimumDiskSizeGB $MinimumDiskSizeGB ` | \n|  | -MaximumDiskSizeGB $MaximumDiskSizeGB ` | \n|  | -AllowStorageSpacesDisk ([bool]$AllowSurfaceStorageSpaces) | \n|  | $Entry \\| Add-Member -NotePropertyName Eligible -NotePropertyValue $Eligibility.Eligible | \n|  | $Entry \\| Add-Member -NotePropertyName Reasons -NotePropertyValue $Eligibility.Reasons | \n|  | $Entry | \n|  | } | \n|  |  | \n|  | foreach ($Item in $Inventory) { | \n|  | Write-Host (\"Disk {0}: {1}; bus={2}; sizeGiB={3:N1}; sector={4} B; serial='{5}'; eligible={6}; {7}\" -f | \n|  | $Item.Number, $Item.Model, $Item.BusType, | \n|  | ($Item.SizeBytes / 1GB), $Item.LogicalSectorSize, | \n|  | $Item.Serial, $Item.Eligible, $Item.Reasons) | \n|  | } | \n|  | $SpacesDisks = @($Inventory \\| Where-Object { $_.BusType -eq 'StorageSpaces' }) | \n|  | if ($SpacesDisks.Count -gt 0 -and -not $AllowSurfaceStorageSpaces) { | \n|  | throw 'Storage Spaces is visible; an ordinary disk rule must not target its pool members.' | \n|  | } | \n|  | if ([string]$Computer.Manufacturer -match '^Microsoft' -and | \n|  | $ComputerModel -match 'Surface' -and -not $AllowSurfaceStorageSpaces) { | \n|  | $PossiblePoolMembers = @($Disks \\| Where-Object { | \n|  | $_.Size -ge 400GB -and $_.Size -le 600GB -and | \n|  | ([string]$_.BusType -in @('NVMe', 'SATA', 'RAID')) | \n|  | }) | \n|  | if ($PossiblePoolMembers.Count -ge 2) { | \n|  | throw 'Surface has multiple 512-GB-class disks; verify its Storage Spaces configuration first.' | \n|  | } | \n|  | } | \n|  |  | \n|  | $SelectionRules = @{ | \n|  | Inventory = @($Inventory) | \n|  | TargetSerial = $TargetSerial | \n|  | TargetModelRegex = $TargetModelRegex | \n|  | TargetBusType = $TargetBusType | \n|  | MinimumDiskSizeGB = $MinimumDiskSizeGB | \n|  | MinimumTargetSizeGB = $MinimumTargetSizeGB | \n|  | MaximumTargetSizeGB = $MaximumTargetSizeGB | \n|  | } | \n|  | $Selected = Select-OSDTargetDisk @SelectionRules | \n|  | Assert-OSDFirmwareDiskGeometry -Disk $Selected -FirmwareMode $FirmwareMode ` | \n|  | -EfiMiB $EfiMiB | \n|  | $Plan = New-OSDPartitionPlan -DiskSizeBytes $Selected.SizeBytes ` | \n|  | -FirmwareMode $FirmwareMode -EfiMiB $EfiMiB ` | \n|  | -BiosSystemMiB $BiosSystemSizeMiB -MsrMiB $MsrMiB ` | \n|  | -RecoveryMiB $RecoveryMiB ` | \n|  | -AlignmentReserveMiB $AlignmentReserveMiB -WindowsSizeGiB $WindowsSizeGiB ` | \n|  | -MinimumWindowsSizeGiB $MinimumWindowsSizeGiB ` | \n|  | -CreateDataPartition ([bool]$CreateDataPartition) | \n|  | if ($FirmwareMode -eq 'UEFI') { | \n|  | Write-Host (\"Target Disk {0}; GPT: EFI {1} MiB, MSR {2} MiB, Windows {3} MiB, Recovery {4} MiB, Data {5} MiB.\" -f | \n|  | $Selected.Number, $Plan.EfiMiB, $Plan.MsrMiB, $Plan.WindowsMiB, | \n|  | $Plan.RecoveryMiB, $Plan.DataMiB) | \n|  | } | \n|  | else { | \n|  | Write-Host (\"Target Disk {0}; MBR: System {1} MiB, Windows {2} MiB, Recovery {3} MiB, Data {4} MiB.\" -f | \n|  | $Selected.Number, $Plan.BiosSystemMiB, $Plan.WindowsMiB, | \n|  | $Plan.RecoveryMiB, $Plan.DataMiB) | \n|  | } | \n|  |  | \n|  | if ($Preview) { | \n|  | Test-SameDisk -Selection $Selected | \n|  | $PreviewCommands = @(New-OSDDiskPartCommands -DiskNumber $Selected.Number ` | \n|  | -Plan $Plan -RecoveryGuid $RecoveryGuid) | \n|  | Write-Host 'PREVIEW ONLY: the following commands would be generated, NOT executed:' | \n|  | foreach ($Command in $PreviewCommands) { Write-Host \"  $Command\" } | \n|  | $EnvironmentName = 'Standalone Windows or WinPE; not an active task sequence' | \n|  | if ($script:TSEnv) { | \n|  | if ($script:TSEnv.Value('_SMSTSInWinPE') -eq 'true') { | \n|  | $EnvironmentName = 'Active task sequence in WinPE' | \n|  | } | \n|  | else { | \n|  | $EnvironmentName = 'Active task sequence in full Windows' | \n|  | } | \n|  | } | \n|  | Write-Host \"Preview environment: $EnvironmentName\" | \n|  | Write-Host 'Full-Windows disk inventory can differ from the ConfigMgr WinPE boot image.' | \n|  | if ($script:TSEnv) { | \n|  | if ($script:TSEnv.Value('_SMSTSInWinPE') -ieq 'true') { | \n|  | Assert-OSDTaskSequenceSource -TaskSequenceEnvironment $script:TSEnv ` | \n|  | -RunningScriptPath ([string]$PSCommandPath) ` | \n|  | -TargetDiskNumber $Selected.Number | \n|  | Assert-OSDWinPEDriveSafety -TargetDiskNumber $Selected.Number ` | \n|  | -FirmwareMode $FirmwareMode | \n|  | } | \n|  | throw 'Preview only: task sequence stopped before formatting.' | \n|  | } | \n|  | Write-Host 'No disk was modified.' | \n|  | exit 0 | \n|  | } | \n|  |  | \n|  | Test-SameDisk -Selection $Selected | \n|  | Assert-OSDTaskSequenceSource -TaskSequenceEnvironment $script:TSEnv ` | \n|  | -RunningScriptPath ([string]$PSCommandPath) ` | \n|  | -TargetDiskNumber $Selected.Number | \n|  | Assert-OSDWinPEDriveSafety -TargetDiskNumber $Selected.Number ` | \n|  | -FirmwareMode $FirmwareMode | \n|  |  | \n|  | $WorkRoot = Join-Path $env:SystemRoot 'Temp' | \n|  | if (-not (Test-Path -LiteralPath $WorkRoot -PathType Container)) { | \n|  | throw 'The WinPE temporary folder is unavailable.' | \n|  | } | \n|  | $DiskPartPath = Join-Path $env:SystemRoot 'System32\\diskpart.exe' | \n|  | if (-not (Test-Path -LiteralPath $DiskPartPath -PathType Leaf)) { | \n|  | throw 'DiskPart is unavailable in the WinPE boot image.' | \n|  | } | \n|  |  | \n|  | # One DiskPart process: no hard-coded disk number and no second formatting | \n|  | # step. Extra alignment space is intentionally left after Recovery. | \n|  | [string[]]$Commands = @(New-OSDDiskPartCommands -DiskNumber $Selected.Number ` | \n|  | -Plan $Plan -RecoveryGuid $RecoveryGuid) | \n|  | $PlanPath = Join-Path $WorkRoot ('OSD-Partition-' + [guid]::NewGuid().ToString('N') + '.txt') | \n|  | [System.IO.File]::WriteAllLines($PlanPath, $Commands, | \n|  | (New-Object System.Text.ASCIIEncoding)) | \n|  |  | \n|  | # Recheck immediately before the first destructive command. | \n|  | Test-SameDisk -Selection $Selected | \n|  | Write-Host \"ERASING Disk $($Selected.Number): $($Selected.Model); serial=$($Selected.Serial)\" | \n|  | $Output = & $DiskPartPath /s $PlanPath 2>&1 | \n|  | $DiskPartExitCode = $LASTEXITCODE | \n|  | foreach ($Line in $Output) { Write-Host ([string]$Line) } | \n|  | if ($DiskPartExitCode -ne 0) { | \n|  | throw \"Partitioning failed; DiskPart returned $DiskPartExitCode. Do not continue the task sequence.\" | \n|  | } | \n|  |  | \n|  | $ExpectedStyle = if ($FirmwareMode -eq 'UEFI') { 'GPT' } else { 'MBR' } | \n|  | $WindowsIndex = if ($FirmwareMode -eq 'UEFI') { 2 } else { 1 } | \n|  | $RecoveryIndex = $WindowsIndex + 1 | \n|  | $DataIndex = $RecoveryIndex + 1 | \n|  | $ExpectedCount = $RecoveryIndex + 1 | \n|  | if ($Plan.DataMiB -gt 0) { $ExpectedCount++ } | \n|  |  | \n|  | # Storage WMI may lag DiskPart briefly. Retry only read-only inspection, | \n|  | # never the destructive command sequence. | \n|  | $ResultDisk = $null | \n|  | $Parts = @() | \n|  | for ($Attempt = 1; $Attempt -le 3; $Attempt++) { | \n|  | try { | \n|  | $ResultDisk = Get-Disk -Number $Selected.Number -ErrorAction Stop | \n|  | $Parts = @(Get-Partition -DiskNumber $Selected.Number -ErrorAction Stop \\| | \n|  | Sort-Object Offset) | \n|  | if ($ResultDisk.PartitionStyle -eq $ExpectedStyle -and | \n|  | $Parts.Count -eq $ExpectedCount) { | \n|  | break | \n|  | } | \n|  | } | \n|  | catch { | \n|  | if ($Attempt -eq 3) { throw } | \n|  | } | \n|  | if ($Attempt -lt 3) { | \n|  | Start-Sleep -Seconds 2 | \n|  | } | \n|  | } | \n|  | if (($ResultDisk.PartitionStyle -ne $ExpectedStyle) -or | \n|  | ($ResultDisk.Size -ne $Selected.SizeBytes) -or | \n|  | ($Selected.Serial -and | \n|  | (([string]$ResultDisk.SerialNumber).Trim() -ne $Selected.Serial))) { | \n|  | throw 'Post-format disk identity or partition-style verification failed.' | \n|  | } | \n|  | $PostDevice = @(Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction Stop \\| | \n|  | Where-Object { $_.Index -eq $Selected.Number }) | \n|  | if ($Selected.WmiFound -and | \n|  | ($PostDevice.Count -ne 1 -or | \n|  | [string]$PostDevice[0].PNPDeviceID -ne $Selected.PnpId)) { | \n|  | throw 'Post-format device identity verification failed.' | \n|  | } | \n|  | if ($Selected.VirtualVerified) { | \n|  | $PostVirtual = @(Get-VirtualDisk -Disk $ResultDisk -ErrorAction Stop) | \n|  | if ($PostVirtual.Count -ne 1 -or | \n|  | [string]$PostVirtual[0].UniqueId -ne $Selected.VirtualId) { | \n|  | throw 'Post-format Storage Spaces association changed.' | \n|  | } | \n|  | } | \n|  | $Layout = Assert-OSDPartitionStructure -Parts $Parts -Plan $Plan ` | \n|  | -RecoveryGuid $RecoveryGuid | \n|  | $WindowsIndex = $Layout.WindowsIndex | \n|  | $RecoveryIndex = $Layout.RecoveryIndex | \n|  | $DataIndex = $Layout.DataIndex | \n|  | if ($FirmwareMode -eq 'BIOS' -and $Layout.RecoveryHasLetter) { | \n|  | Write-Host 'MBR Recovery has a temporary WinPE letter; verify it is hidden after first boot.' | \n|  | } | \n|  | $WindowsVolume = Get-Volume -DriveLetter W -ErrorAction Stop | \n|  | $SystemVolume = Get-Volume -Partition $Parts[0] -ErrorAction Stop | \n|  | $ExpectedSystemFS = if ($FirmwareMode -eq 'UEFI') { 'FAT32' } else { 'NTFS' } | \n|  | if ($WindowsVolume.FileSystem -ne 'NTFS' -or | \n|  | $SystemVolume.FileSystem -ne $ExpectedSystemFS) { | \n|  | throw 'Windows or system-partition filesystem verification failed.' | \n|  | } | \n|  | $RecoveryVolume = @(Get-Volume -Partition $Parts[$RecoveryIndex] -ErrorAction SilentlyContinue) | \n|  | if ($RecoveryVolume.Count -gt 1 -or | \n|  | ($RecoveryVolume.Count -eq 1 -and $RecoveryVolume[0].FileSystem -ne 'NTFS')) { | \n|  | throw 'Recovery NTFS verification failed.' | \n|  | } | \n|  | if ($RecoveryVolume.Count -eq 0) { | \n|  | Write-Host 'Recovery filesystem is not exposed in WinPE; validate it after Windows setup.' | \n|  | } | \n|  | if ($Plan.DataMiB -gt 0) { | \n|  | $DataVolume = Get-Volume -Partition $Parts[$DataIndex] -ErrorAction Stop | \n|  | if ($DataVolume.FileSystem -ne 'NTFS') { | \n|  | throw 'Data partition filesystem verification failed.' | \n|  | } | \n|  | } | \n|  |  | \n|  | Set-TSValue -Name $DiskNumberVariable -Value ([string]$Selected.Number) | \n|  | Set-TSValue -Name $OSPartitionVariable -Value 'W:' | \n|  | if ($Plan.DataMiB -gt 0 -and $FirmwareMode -eq 'UEFI') { | \n|  | Set-TSValue -Name 'DiskDataPartitionGuid' -Value ([string]$Parts[$DataIndex].Guid) | \n|  | } | \n|  | Set-TSValue -Name 'DiskLayoutMode' -Value 'FreshInstall' | \n|  | Set-TSValue -Name 'DiskPartitionStyle' -Value $ExpectedStyle | \n|  | Set-TSValue -Name 'DiskSelectionStatus' -Value 'Success' | \n|  | Set-TSValue -Name 'DiskLayoutStatus' -Value 'Success' | \n|  | Write-Host \"SUCCESS: $ExpectedStyle layout verified; $OSPartitionVariable=W:. Check boot and WinRE after setup.\" | \n|  | exit 0 | \n|  | } | \n|  | catch { | \n|  | $Failure = $_.Exception.Message | \n|  | if ($script:TSEnv) { | \n|  | try { | \n|  | Set-TSValue -Name $DiskNumberVariable -Value '' | \n|  | Set-TSValue -Name $OSPartitionVariable -Value '' | \n|  | Set-TSValue -Name 'DiskDataPartitionGuid' -Value '' | \n|  | Set-TSValue -Name 'DiskLayoutMode' -Value '' | \n|  | Set-TSValue -Name 'DiskPartitionStyle' -Value '' | \n|  | Set-TSValue -Name 'DiskSelectionStatus' -Value 'Failed' | \n|  | Set-TSValue -Name 'DiskLayoutStatus' -Value 'Failed' | \n|  | } | \n|  | catch { | \n|  | Write-Verbose 'Unable to update failure status variables during cleanup.' | \n|  | } | \n|  | } | \n|  | Write-Host \"STOPPED: $Failure\" | \n|  | exit 1 | \n|  | } | \n|  | finally { | \n|  | if ($PlanPath -and (Test-Path -LiteralPath $PlanPath)) { | \n|  | Remove-Item -LiteralPath $PlanPath -Force -ErrorAction SilentlyContinue | \n|  | } | \n|  | } |", "url": "https://wpnews.pro/news/invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit", "canonical_source": "https://gist.github.com/vartaxe/eb00eb325f357e5ce75c8613a9f37b12", "published_at": "2026-10-07 14:49:22+00:00", "updated_at": "2026-10-07 15:16:34.457405+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Microsoft Configuration Manager", "WinPE", "PowerShell", "Microsoft"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit", "markdown": "https://wpnews.pro/news/invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit.md", "text": "https://wpnews.pro/news/invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit.txt", "jsonld": "https://wpnews.pro/news/invoke-osddisklayout-ps1-configmgr-osd-v1-0-0-snapshot-mit.jsonld"}}