Public/Get-HDTWizardSequence.ps1
|
function Get-HDTWizardSequence { <# .SYNOPSIS The task sequences this share actually carries, as rows the wizard's picker can show. .DESCRIPTION W3 OF .planning/WPF-FIRST.md, AND THE END OF A HAND-WRITTEN LIST. Scripts\UI\TaskSequence.xaml carried one <ListBoxItem> per sequence, typed by whoever added the sequence, and admitted as much in its own comment. A lab share holding eight sequences offered one - not as a failure anybody could see, but as a list that was quietly wrong, and the technician's only clue was that their sequence "was not there". THE LIST IS THE FOLDER. TaskSequences\<Id>\sequence.yaml is what the engine runs, so it is what the picker offers - the same rule the console's catalog uses, for the same reason: a folder counts when the document is in it, never because it is a directory. A lab share keeps a readme and a stray unattend.xml beside the sequence folders, and a picker that offered 'unattend.xml' would fail a deployment after the technician had answered every question. SORTED BY ID, because the order a file system hands folders back is not an order anybody can scan, and a list that rearranges itself between boots is one a technician stops trusting. A DOCUMENT THAT WILL NOT PARSE IS LEFT OUT AND SAID OUT LOUD. Both halves matter. Offering it means a deployment that dies at its first step; dropping it in silence means "why is mine not in the list?" has no answer anywhere. So it is excluded and named in Problem, which the payload logs. THE RESOLVED ID IS PRESELECTED, AND AN ID THE SHARE DOES NOT CARRY IS A PROBLEM RATHER THAN A SILENT FIRST ROW. A rule naming a sequence that is not on this share is a mistake somebody has to be told about, and selecting something else quietly would deploy the wrong build to a machine that is already open. IT RETURNS A FIELD, because that is how the wizard fills a control: Show-HDTWizardShell applies fields by name after each page loads, and New-HDTWizardHost writes Item to ItemsSource and Text to the property the field names. wizard.yaml's TaskSequence page collects HDTTaskSequenceList's SelectedValue into HDTTaskSequenceID, so those two names are what this fills in. .PARAMETER WorkspaceRoot The connected share's root. .PARAMETER FileSystem An IFileSystem. The share is already connected by the time the wizard runs, so this reads it as ordinary paths. .PARAMETER Variable The resolved variables, read for HDTTaskSequenceID and nothing else. .PARAMETER Control The control the field names. Defaults to HDTTaskSequenceList, which is what the shipped page calls it; a site that renamed it in its own page does not have to fork this command. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject with Choice (Id, Name, Description, Text), Selected, Problem and Field. .EXAMPLE Get-HDTWizardSequence -WorkspaceRoot 'Z:\Deploy' -FileSystem (New-HDTFileSystem) .EXAMPLE $sequence = Get-HDTWizardSequence -WorkspaceRoot $root -FileSystem $fs -Variable $resolved.Variable Show-HDTWizardShell -Page $ask.Page -Field (@($field) + @($sequence.Field)) -Title $title What the payload does: the picker is one more field, applied by name like every other. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string] $WorkspaceRoot, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $FileSystem, [Parameter()] [AllowNull()] [System.Collections.IDictionary] $Variable, [Parameter()] [ValidateNotNullOrEmpty()] [string] $Control = 'HDTTaskSequenceList' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $choice = New-Object -TypeName System.Collections.ArrayList $problem = New-Object -TypeName System.Collections.ArrayList $sequenceRoot = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind TaskSequences if (-not $FileSystem.TestPath($sequenceRoot)) { # NOT AN EXCEPTION. A share being set up has no TaskSequences\ yet, and # a wizard that refused to open would leave the technician with nothing # to read the reason on. [void] $problem.Add(("there is no TaskSequences folder at '{0}', so this share offers nothing to deploy." -f $sequenceRoot)) } else { foreach ($folder in @($FileSystem.GetChildItem($sequenceRoot))) { $documentPath = [System.IO.Path]::Combine([string] $folder, 'sequence.yaml') if (-not $FileSystem.TestPath($documentPath)) { continue } $folderName = [System.IO.Path]::GetFileName([string] $folder) # PARSED, NOT VALIDATED. A picker needs three fields off the top of # the document; whether every step in it is legal is the engine's # question, answered when the sequence runs and answered with a # message about the step. Refusing to LIST a sequence over a step # type would hide it from the one person who could fix it. $document = $null try { $document = ConvertFrom-HDTYaml -Yaml ([string] $FileSystem.ReadAllText($documentPath)) -Path $documentPath } catch { [void] $problem.Add(("'{0}' is not offered: its sequence.yaml could not be read - {1}" -f $folderName, [string] $_.Exception.Message)) continue } if ($null -eq $document -or $document -isnot [System.Collections.IDictionary]) { [void] $problem.Add(("'{0}' is not offered: its sequence.yaml is empty or is not a document." -f $folderName)) continue } # THE FOLDER IS THE ID, AND THE DOCUMENT'S id FIELD IS NOT CONSULTED # FOR IT. HDTTaskSequenceID names a FOLDER - the engine builds # TaskSequences\<Id>\sequence.yaml from it - so anything else on this # row would be a picker offering an id nothing can be looked up by. # # A REAL SHARE PROVED IT COSTS NOTHING TO GET WRONG. The lab's '001' # sequence declares `id: 001`, and YAML reads that as the NUMBER 1: # a picker trusting the document would offer '1' and the deployment # would look for TaskSequences\1\sequence.yaml, which is not there. # The same is true of any id a parser can read as a number, a date # or a boolean - and quoting it is a fix nobody will remember. $id = $folderName $name = $id if ($document.Contains('name') -and -not [string]::IsNullOrWhiteSpace([string] $document['name'])) { $name = [string] $document['name'] } $description = '' if ($document.Contains('description')) { $description = [string] $document['description'] } [void] $choice.Add([pscustomobject] @{ Id = $id Name = $name Description = $description # THE ID AND THE NAME ON ONE ROW. The id is what the # deployment records and what a rule names; the name is what # a technician recognises. A row carrying one of them makes # somebody guess at the other. Text = ('{0} - {1}' -f $id, $name) }) } } $ordered = [object[]] @($choice | Sort-Object -Property Id) # -- what is already chosen --------------------------------------------- $wanted = '' if ($null -ne $Variable -and $Variable.Contains('HDTTaskSequenceID')) { $wanted = [string] $Variable['HDTTaskSequenceID'] } $selected = '' if (@($ordered).Count -gt 0) { $selected = [string] $ordered[0].Id } if (-not [string]::IsNullOrWhiteSpace($wanted)) { $match = @($ordered | Where-Object { $_.Id -eq $wanted }) if (@($match).Count -gt 0) { $selected = [string] $match[0].Id } else { [void] $problem.Add(("HDTTaskSequenceID resolved to '{0}', which is not on this share. The picker opens on '{1}' instead." -f $wanted, $selected)) } } return [pscustomobject] @{ Choice = $ordered Selected = $selected Problem = [string[]] @($problem) Field = [pscustomobject] @{ Name = $Control Property = 'SelectedValue' Text = $selected Item = $ordered } } } |