Public/Import-HDTBootImageToWds.ps1
|
function Import-HDTBootImageToWds { <# .SYNOPSIS Imports the HDT boot image into Windows Deployment Services, replacing an image of the same name rather than adding a second one. .DESCRIPTION HDT does not ship a PXE server; WDS serves the WIM. This is the command that puts it there, and REPLACE-IN-PLACE IS THE WHOLE POINT OF IT ("WDS import replacing rather than duplicating an existing image"). An MDT operator who has run Update-MDTDeploymentShare a dozen times recognises the failure this prevents: a PXE boot menu with a column of identically named images and no way to tell which one the fleet is actually booting. So this command asks the server what it already has, and when a row matches by name it REMOVES THAT ROW BEFORE IT IMPORTS - in that order, which is what the ordered journal in the unit suite asserts. An import before the remove would leave two images for a moment and then delete the new one. FIVE STEPS, AND THE ORDER IS THE CONTRACT: 1. refuse a -Path that is not an existing .wim, NAMING Update-HDTBootImage - and call nothing. A refusal that had already asked the server for its image list is a refusal that touched production; 2. -ImageName defaults to the WIM's base name (HDTPE_x64); 3. GetBootImage(<architecture>); 4. a row matching -ImageName CASE-INSENSITIVELY, on the same architecture: RemoveBootImage then ImportBootImage. Both are logged at Info, naming the image being replaced and its previous version, because an administrator needs to know what was thrown away. No match: ImportBootImage alone; 5. return ImageName, Architecture, Path, Replaced, PreviousVersion. SupportsShouldProcess, and not as decoration: step 4 deletes a boot image a fleet PXE boots from. Under -WhatIf NOTHING is called, including the read. IT HAS NEVER RUN AGAINST A REAL WDS SERVER. There is none on this host - it is Windows 11 Pro, and WDS is a Windows Server role - and PROJECT.md's lab safety rules forbid standing one up beside CM01's PXE responder. Everything above is asserted against New-HDTFakeWdsService; the one real assertion this machine can make is that New-HDTWdsService refuses with a named dependency error, and it is made against the real adapter. .PARAMETER Path The boot WIM to import, normally <workspace>\Boot\<name>.wim as Update-HDTBootImage wrote it. .PARAMETER ImageName What to call it on the server. Defaults to the WIM's base name. .PARAMETER Architecture The WDS image architecture: x64 (default), x86 or arm64. Note that this is WDS's vocabulary, not the ADK's - the ADK calls the same thing amd64. .PARAMETER WdsService An IWdsService. Defaults to the real adapter, which throws HDTDependencyError on a machine with no WDS module. .PARAMETER FileSystem An IFileSystem. Defaults to the real adapter. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject with ImageName, Architecture, Path, Replaced and PreviousVersion. .EXAMPLE Import-HDTBootImageToWds -Path 'C:\HDTLab\Share\Boot\HDTPE_x64.wim' Imports as HDTPE_x64, replacing an existing HDTPE_x64 if the server has one. .EXAMPLE Import-HDTBootImageToWds -Path $build.WimPath -ImageName 'HDT (test ring)' -WhatIf What it would remove and what it would import, without touching the server. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The noun is BootImageToWds, and Wds is the acronym for Windows Deployment Services - not a plural. The analyzer sees the trailing s. DESIGN 6.1 names this command.')] [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [ValidateNotNullOrEmpty()] [string] $ImageName, [Parameter()] [ValidateSet('x64', 'x86', 'arm64')] [string] $Architecture = 'x64', [Parameter()] [AllowNull()] [object] $WdsService, [Parameter()] [AllowNull()] [object] $FileSystem ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem } # ===================================================================== # 1. THE PATH, JUDGED BEFORE THE SERVICE IS EVEN BUILT # ===================================================================== # # BEFORE, not after: the default -WdsService is New-HDTWdsService, which # throws on a machine with no WDS module, and a bad path should say so rather # than being masked by a dependency error. It is also what makes "it called # nothing" true of the refusal. if ([System.IO.Path]::GetExtension($Path) -ne '.wim') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("that is not a .wim, and Windows Deployment Services serves boot images as WIMs. Import the file Update-HDTBootImage wrote to <workspace>\Boot\<name>.wim; the .iso beside it is the debugging vehicle, not something WDS can serve."))) } if (-not $FileSystem.TestPath($Path)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path -Category ObjectNotFound ` -Message ("there is no boot image there to import. Run Update-HDTBootImage against the workspace first - it writes <workspace>\Boot\<name>.wim and the manifest beside it."))) } $name = $ImageName if (-not $PSBoundParameters.ContainsKey('ImageName')) { $name = [System.IO.Path]::GetFileNameWithoutExtension($Path) } # ===================================================================== # 2. WHAT THE SERVER ALREADY HAS # ===================================================================== $description = 'Import boot image ''{0}'' ({1})' -f $name, $Architecture if (-not $PSCmdlet.ShouldProcess($Path, $description)) { return [pscustomobject] @{ ImageName = $name Architecture = $Architecture Path = $Path Replaced = $false PreviousVersion = '' } } if ($null -eq $WdsService) { $WdsService = New-HDTWdsService } $existing = @($WdsService.GetBootImage($Architecture)) # CASE-INSENSITIVELY. WDS image names are not case sensitive, and neither is # the decision about whether one is already there. $match = @($existing | Where-Object { [string] $_.ImageName -eq $name -or ([string] $_.ImageName).Equals($name, [System.StringComparison]::OrdinalIgnoreCase) }) $replaced = $false $previousVersion = '' # ===================================================================== # 3. REMOVE, THEN IMPORT - IN THAT ORDER # ===================================================================== if ($match.Count -gt 0) { $previousVersion = [string] $match[0].Version $replaced = $true # WHAT WAS THROWN AWAY, said out loud. A fleet boots from this image; # a run that replaced one silently would leave nobody able to say what # the previous one was. Write-Information ("Replacing the existing WDS boot image '{0}' ({1}), version '{2}', file '{3}'. It is removed before the new image is imported, so the server ends with one image of this name rather than two." -f [string] $match[0].ImageName, $Architecture, $previousVersion, [string] $match[0].FileName) $WdsService.RemoveBootImage([string] $match[0].ImageName, $Architecture) } Write-Information ("Importing '{0}' into WDS as boot image '{1}' ({2})." -f $Path, $name, $Architecture) $WdsService.ImportBootImage($Path, $name, $Architecture) return [pscustomobject] @{ ImageName = $name Architecture = $Architecture Path = $Path Replaced = $replaced PreviousVersion = $previousVersion } } |