Public/Get-HDTWizardSummary.ps1
|
function Get-HDTWizardSummary { <# .SYNOPSIS Builds the summary page: what was chosen, which variable holds it, and what to write in rules.yaml so nobody sees the wizard again. .DESCRIPTION MDT'S SUMMARY CONFIRMS. THIS ONE ALSO TEACHES, and that is deliberate: DESIGN 11.2 says every page is individually skippable and that a page whose values are all supplied never appears, but nothing anywhere TOLD a technician the variable names. The skip model was documented and undiscoverable - the only way to learn it was to read the design document, which is not where somebody standing at a bench is looking. So every row carries three facts rather than one: Setting what the page asked 'Computer name' Value what the technician chose 'HDT-01' Variable what HOLDS it 'HDTComputerName' Skip what hides that page 'HDTSkipComputerName' And the snippet is those rows AS rules.yaml, ready to paste. An administrator drives the wizard by hand once and leaves with the file that means they never have to again. HDTSkipWizard IS IN THE SNIPPET, not the individual skips. Hiding every screen is what was asked for, and one line that does it is easier to get right than eight that must agree with each other. The per-page skip variable is on the ROW, for the site that wants to hide one page and keep the rest. A VALUE NOBODY CHOSE IS OMITTED FROM THE SNIPPET RATHER THAN WRITTEN EMPTY. A rule that sets a variable to nothing is worse than no rule at all: it RESOLVES that variable, so every later rule that would have supplied a real one is skipped - first match wins, DESIGN 3.1 - and the deployment proceeds with an empty name it will not explain. IT REPORTS AND DOES NOT DECIDE. Nothing here resolves, validates or applies anything; Resolve-HDTVariable owns that, and a second opinion in a summary screen is how two answers start disagreeing. .PARAMETER Page The pages the wizard asked. A page that declares Collect - a Control and the Variable it fills - becomes a row; one that collects nothing, like the summary page itself, does not. .PARAMETER Value What has been collected so far, variable name to value. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject with Row (Setting, Value, Variable, Skip, IsSet) and Snippet. .EXAMPLE Get-HDTWizardSummary -Page $page -Value @{ HDTComputerName = 'HDT-01' } .EXAMPLE (Get-HDTWizardSummary -Page $page -Value $chosen).Snippet The rules.yaml that makes this deployment unattended. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowNull()] [AllowEmptyCollection()] [object[]] $Page, [Parameter()] [AllowNull()] [hashtable] $Value ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if (@($Page).Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Category InvalidArgument ` -Message 'there are no wizard pages to summarise.')) } $chosen = $Value if ($null -eq $chosen) { $chosen = @{} } # WHAT YAML WOULD READ AS SOMETHING ELSE. A name of 'true' unquoted is a # boolean, '0123' is the number 123, and a value carrying a %Token% or a # leading character YAML reserves stops the document parsing at all. A # snippet that does not paste cleanly is a snippet that cost somebody an # afternoon - and quoting EVERYTHING instead would make the common case # noisier to read, which is how a snippet stops being read. $needsQuote = { param([string] $Text) if ([string]::IsNullOrEmpty($Text)) { return $true } if ($Text -match '^[A-Za-z][A-Za-z0-9\-\.]*$' -and $Text -notmatch '^(true|false|yes|no|on|off|null|y|n)$') { return $false } return $true } $row = @() $setLine = @() $skipLine = @() $secretLine = @() foreach ($current in @($Page)) { # EVERY PAGE IS SKIPPABLE, INCLUDING ONE THAT COLLECTS NOTHING. The # summary page has no value to set and still has to be hidden, so the # skip line is gathered before the row is decided. if ($null -ne $current.PSObject.Properties['Skip'] -and -not [string]::IsNullOrWhiteSpace([string] $current.Skip)) { $skipLine += (' {0}: true' -f [string] $current.Skip) } $collect = $null if ($null -ne $current.PSObject.Properties['Collect']) { $collect = $current.Collect } # THE SUMMARY PAGE IS NOT A ROW ABOUT ITSELF. if ($null -eq $collect) { continue } # ONE PAGE, SEVERAL VARIABLES. MDT's Computer Details pane collects the # name, the domain or the workgroup, the OU and the account that joins - # so a page that could name only one variable could not describe itself. # @() around a single declaration is that declaration, so a page with one # needs no special case. $skip = '' if ($null -ne $current.PSObject.Properties['Skip']) { $skip = [string] $current.Skip } # ONE DECLARATION CAN FILL TWO VARIABLES. The join account is typed once # as CORP\svc-hdt-join and lands in HDTDomainAdmin and # HDTDomainAdminDomain; a snippet naming only the first would leave the # join to guess which domain authenticates it. $named = @() foreach ($declaration in @($collect)) { if ($null -eq $declaration) { continue } $isSecret = $false if ($null -ne $declaration.PSObject.Properties['IsSecret']) { $isSecret = [bool] $declaration.IsSecret } $named += [pscustomobject] @{ Variable = [string] $declaration.Variable; IsSecret = $isSecret } if ($null -ne $declaration.PSObject.Properties['SplitVariable'] -and -not [string]::IsNullOrWhiteSpace([string] $declaration.SplitVariable)) { # THE SPLIT HALF INHERITS THE SECRECY. Nothing splits a password # today, and a half of a secret printed because it arrived by a # different route is exactly the shape a leak takes. $named += [pscustomobject] @{ Variable = [string] $declaration.SplitVariable; IsSecret = $isSecret } } } foreach ($declaration in $named) { $variable = [string] $declaration.Variable $isSecret = [bool] $declaration.IsSecret $isSet = $false $shown = '(not set)' if ($chosen.ContainsKey($variable) -and -not [string]::IsNullOrWhiteSpace([string] $chosen[$variable])) { $isSet = $true $shown = [string] $chosen[$variable] # A SECRET IS CONFIRMED, NOT DISPLAYED. Get-HDTVariableMap # already says of HDTDomainAdminPassword: "never written to a # log" - and a screen a technician photographs, or a YAML block # they paste into a file on a share, is worse than a log. if ($isSecret) { $shown = '(set, not shown)' } } $row += [pscustomobject] @{ Setting = [string] $current.Title Value = $shown Variable = $variable Skip = $skip IsSet = $isSet } if ($isSecret) { # NOT EVEN EMPTY, AND NOT REDACTED. A rules.yaml carrying # HDTDomainAdminPassword: "" RESOLVES the variable to nothing, # so every later rule that would have supplied it is skipped and # the join fails with a password nobody set rather than one # nobody typed. A comment names it and says where it does not go. $secretLine += ('# {0} is not written here - it is collected at deployment time.' -f $variable) continue } if ($isSet) { $text = [string] $chosen[$variable] if (& $needsQuote $text) { $text = '"{0}"' -f $text } $setLine += (' {0}: {1}' -f $variable, $text) } } } # ONE RULE, WITH NO when:, WHICH IS A FALLBACK FOR EVERY MACHINE. That is # what an administrator who has just deployed one machine by hand actually # wants; narrowing it to a model or a subnet is the next thing they will do # and is not something this screen can guess. # # BOTH KINDS OF SKIP ARE WRITTEN OUT. HDTSkipWizard is the blunt one and is # a single line to get right; the per-page keys are what a site edits when # it wants to hide four pages and keep two, and a snippet that named only # the blunt one would leave an administrator to guess the fine-grained ones # exist - the same undiscoverability this whole screen exists to end. # # THE WELCOME SCREEN IS NOT IN THIS FILE, AND SAYING SO IS PART OF THE # ANSWER. rules.yaml lives ON THE SHARE, and the Welcome screen is what # makes the share reachable: HDTSkipWelcome here is a rule the machine # cannot read until after the screen it was meant to skip has been shown. # It belongs in bootstrap.json inside the boot image. MDT has the same split # for the same reason - SkipBDDWelcome is in Bootstrap.ini and every other # Skip* is in CustomSettings.ini. # $secretHeader = @() if (@($secretLine).Count -gt 0) { $secretHeader = @('#') + $secretLine } $snippet = @( '# Paste into rules.yaml on the deployment share.' '#' '# This hides every wizard page. The WELCOME screen is not in this file:' '# it runs before the share is reachable, so it is set in bootstrap.json' '# inside the boot image (HDTSkipWelcome, or build with a credential and' '# without -PromptForCredential).' ) + $secretHeader + @( 'schemaVersion: 1' 'rules:' ' - name: Unattended' ' set:' ) + $setLine + @(' HDTSkipWizard: true') + $skipLine return [pscustomobject] @{ Row = $row Snippet = ($snippet -join [System.Environment]::NewLine) } } |