Public/Steps/Invoke-HDTApplyUnattendStep.ps1
|
function Invoke-HDTApplyUnattendStep { <# .SYNOPSIS Stages the unattend where Windows Setup consumes it. .DESCRIPTION Staging an unattend, as a step: - name: Apply Unattend type: ApplyUnattend template: unattend.xml # relative to the sequence folder target: "%HDTOSVolume%" # optional expand: true # optional, default true THE DESTINATION IS <target>:\Windows\Panther\unattend.xml AND NOTHING ELSE IS VERIFIED. S7 deployed a real Windows 11 machine from a document staged exactly there: ComputerName applied in the specialize pass, OOBE skipped, the built-in Administrator enabled, FirstLogonCommands run, and autologon armed with the password held as an LSA secret rather than in the registry. THE PASSWORD HAS THREE SOURCES AND NO FOURTH OUTCOME: 1. an HDTAdminPassword variable the rules resolved - used; 2. otherwise $Context.State.deploymentPassword, when the state carries one (the oobeSystem AutoLogon block is what arms the first logon); 3. otherwise ONE IS MINTED, written back to $Context.State.deploymentPassword and used. STEP 3 IS NOT TIDINESS - IT IS THE REASON THIS STEP IS SAFE TO RUN. Invoke-HDTTaskSequence mints the deployment password only when a step returns RebootRequested, and a WinPE-half sequence such as DEMO-M3 has no Restart step. Without step 3 the token stays unresolved, Expand-HDTVariableToken leaves it literal, and HDT deploys a machine whose local Administrator password is the string '%HDTAdminPassword%' - identical on every machine it ever builds. That is worse than a failed step, and it would have shipped green. Writing it back to the state is what makes a later Restart arm autologon with the SAME secret: one machine, one secret per run. THE DOCUMENT IS NEVER LOGGED, AT ANY LEVEL. Only the path and the byte count. It carries the secret twice - Setup reads UserAccounts and AutoLogon separately - so a Debug-level dump of it would put the local Administrator password of every machine this toolkit builds into a log file that gets copied to a share. AN UNRESOLVED TOKEN IS LEFT LITERAL AND REPORTED (02-03's rule), by NAME rather than by quoting the line it appeared on. .PARAMETER Step A flattened step from Import-HDTSequenceDocument. .PARAMETER Context A New-HDTExecutionContext context. Its Service catalog must carry a FileSystem service. .OUTPUTS A New-HDTStepResult. Data carries path and byteCount - never the document. .EXAMPLE Invoke-HDTApplyUnattendStep -Step $step -Context $context #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Step, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Context ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $fail = { param([string] $Message, [string] $ErrorId) $data = [ordered] @{} if (-not [string]::IsNullOrWhiteSpace($ErrorId)) { $data['errorId'] = $ErrorId } Write-HDTLog -Context $Context.Log -Message $Message -Severity Error -Event step.fail ` -Component 'ApplyUnattend' -Data $data return (New-HDTStepResult -Status Failed -Message $Message -Data $data) } try { $template = Get-HDTStepProperty -Step $Step -Name 'template' -Context $Context -Expand -As String $target = Get-HDTStepProperty -Step $Step -Name 'target' -Context $Context -Expand -As String $expand = Get-HDTStepProperty -Step $Step -Name 'expand' -Default $true -Context $Context -Expand -As Bool } catch { return (& $fail ([string] $_.Exception.Message) 'HDTConfigurationError') } if ([string]::IsNullOrWhiteSpace($template)) { return (& $fail ("step '{0}' declares no template. An ApplyUnattend step names the unattend to stage." -f $Step.Name) 'HDTConfigurationError') } try { $fileSystem = $Context.Service.GetRequired('FileSystem', 'ApplyUnattend') } catch { return (& $fail ([string] $_.Exception.Message) '') } # -- where the template lives ----------------------------------------- $templatePath = $template if (-not [System.IO.Path]::IsPathRooted($template)) { # No literal 'TaskSequences' anywhere: the workspace layout has one # owner, and it is Get-HDTWorkspacePath. $sequenceId = '' if ($null -ne $Context.State -and $null -ne $Context.State.PSObject.Properties['sequenceId']) { $sequenceId = [string] $Context.State.sequenceId } if ([string]::IsNullOrWhiteSpace($sequenceId)) { $sequenceId = [string] $Context.Variable['HDTTaskSequenceID'] } if ([string]::IsNullOrWhiteSpace($sequenceId)) { return (& $fail ("step '{0}' names a template relative to the sequence folder, and this run does not know which sequence it is running. Set HDTTaskSequenceID, or give the template a rooted path." -f $Step.Name) 'HDTConfigurationError') } $templatePath = Get-HDTWorkspacePath -Root ([string] $Context.WorkspaceRoot) -Kind TaskSequences ` -ChildPath $sequenceId, $template } if (-not $fileSystem.TestPath($templatePath)) { return (& $fail ("the unattend template '{0}' is not in this workspace ({1})." -f $template, $templatePath) 'HDTConfigurationError') } # -- where it goes ---------------------------------------------------- $letter = $target if ([string]::IsNullOrWhiteSpace($letter)) { $letter = [string] $Context.Variable['HDTOSVolume'] } if ([string]::IsNullOrWhiteSpace($letter)) { return (& $fail ("step '{0}' stages the unattend on the primary volume and HDTOSVolume is not set. The partition step publishes it; set target: on this step if the sequence knows better." -f $Step.Name) 'HDTConfigurationError') } $letter = $letter.Trim().TrimEnd(':') $volume = $letter.Substring(0, 1).ToUpperInvariant() # SPIKES S7's VERIFIED LOCATION. Nothing else has been proven to work. $pantherPath = '{0}:\Windows\Panther' -f $volume $unattendPath = '{0}\unattend.xml' -f $pantherPath # -- the document ----------------------------------------------------- try { $text = $fileSystem.ReadAllText($templatePath) } catch { return (& $fail ("the unattend template '{0}' could not be read: {1}" -f $templatePath, [string] $_.Exception.Message) '') } # -- the one value Windows will silently discard ---------------------- # # FOUND BY DEPLOYING A REAL MACHINE (04-04). The sample rules.yaml sets # HDTComputerName to 'PC-%HDTSerialNumber%', and a Hyper-V VM's serial is 32 # characters - so HDT wrote a 35-character ComputerName into the unattend. # WINDOWS SETUP IGNORED IT WITHOUT COMPLAINT and named the machine # WIN-N91191NN153. # # That is the worst shape a defect can take. Every step reported Completed, # the deployment succeeded, no log said anything, and the machine came up # with a name nobody chose. So the limit is enforced HERE, where the value # is about to become a machine's identity, and the run stops instead. # # 15 characters is the NetBIOS limit. The illegal characters are the ones # Windows itself rejects; a name is otherwise left exactly as authored - # HDT does not truncate, because a silently shortened name is the same # failure with a different spelling. # # THE RULE ITSELF IS Test-HDTComputerName, AND IT IS NOT COPIED HERE. The # wizard asks a technician for this same value and has to refuse the same # names as they type, so the rule grew a second caller - and a rule with two # copies is a rule that drifts. The advice about WHERE a bad name came from # stays here, because it is true of a rules-built name and not of a typed # one. # # AN EMPTY VALUE IS STILL SKIPPED HERE. Test-HDTComputerName refuses one, # which is right in front of a technician looking at an empty box; in a # sequence, a template that carries the token and a run that never set the # variable is a different failure and not this step's to report. if ($text -match '%HDTComputerName%') { $computerName = [string] $Context.Variable['HDTComputerName'] if (-not [string]::IsNullOrWhiteSpace($computerName)) { $judgement = Test-HDTComputerName -Name $computerName if (-not $judgement.IsValid) { return (& $fail ("{0} Shorten HDTComputerName - a rule that builds it from a serial number is the usual cause." -f $judgement.Reason) 'HDTConfigurationError') } # A WARNING IS NOT A REFUSAL. A name DNS cannot carry is still a # legal computer name, and refusing one here would stop a deployment # over something Windows itself permits. It is recorded instead, so # the machine that later has trouble joining a domain has a log line # that said so on the day it was built. if ($judgement.Severity -eq 'Warning') { Write-HDTLog -Context $Context.Log -Severity Warning -Component 'ApplyUnattend' ` -Message ([string] $judgement.Reason) ` -Data ([ordered] @{ computerName = $computerName; isDnsSafe = [bool] $judgement.IsDnsSafe }) } } } $document = $text if ($expand) { $scope = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($key in @($Context.Variable.Keys)) { $scope[[string] $key] = $Context.Variable[$key] } # The three sources, in order, and there is no fourth outcome. if ($text -match '%HDTAdminPassword%') { $secret = [string] $scope['HDTAdminPassword'] if ([string]::IsNullOrEmpty($secret) -and $null -ne $Context.State -and $null -ne $Context.State.PSObject.Properties['deploymentPassword']) { $secret = [string] $Context.State.deploymentPassword } if ([string]::IsNullOrEmpty($secret)) { $secret = New-HDTDeploymentPassword Write-HDTLog -Context $Context.Log -Component 'ApplyUnattend' ` -Message 'this run had no deployment password, so one was generated for the unattend and recorded in the run state.' } if ($null -ne $Context.State -and $null -ne $Context.State.PSObject.Properties['deploymentPassword'] -and [string]::IsNullOrEmpty([string] $Context.State.deploymentPassword)) { # One machine, one secret per run: a later Restart arms autologon # with this one rather than making a second. $Context.State.deploymentPassword = $secret } $scope['HDTAdminPassword'] = $secret } $unresolved = New-Object -TypeName System.Collections.ArrayList $document = Expand-HDTVariableToken -Value $text -Scope $scope -Unresolved $unresolved -Path $templatePath if (@($unresolved).Count -gt 0) { Write-HDTLog -Context $Context.Log -Severity Warning -Component 'ApplyUnattend' ` -Message ("{0} variable token(s) in the unattend were never supplied and are left unexpanded: {1}." -f @($unresolved).Count, (@($unresolved) -join ', ')) ` -Data ([ordered] @{ path = $unattendPath; unresolved = [string[]] @($unresolved) }) } } # -- staging it ------------------------------------------------------- try { $fileSystem.CreateDirectory($pantherPath) $fileSystem.WriteAllText($unattendPath, $document) } catch { return (& $fail ("the unattend could not be staged at {0}: {1}" -f $unattendPath, [string] $_.Exception.Message) '') } $Context.Variable['HDTUnattendPath'] = $unattendPath # THE PATH AND THE BYTE COUNT, AND NOTHING ELSE. The document carries the # local Administrator password of the machine being built, twice. $byteCount = [long] [System.Text.Encoding]::UTF8.GetByteCount($document) $message = 'staged the unattend at {0} ({1} bytes).' -f $unattendPath, $byteCount Write-HDTLog -Context $Context.Log -Message $message -Component 'ApplyUnattend' ` -Data ([ordered] @{ path = $unattendPath; byteCount = $byteCount; template = $templatePath }) return (New-HDTStepResult -Status Completed -Message $message ` -Data ([ordered] @{ path = $unattendPath; byteCount = $byteCount; template = $templatePath })) } |