Public/Get-HDTConsoleWorkspace.ps1
|
function Get-HDTConsoleWorkspace { <# .SYNOPSIS Reads a deployment share and returns everything the admin console shows about it. .DESCRIPTION The backend half of the console, and the whole of what the window knows. The rule is that the console may not do anything the cmdlets can't, so this reads the share through the SAME commands an administrator would type: Import-HDTWorkspaceDocument the share identity and its deployRoot Get-HDTWorkspacePath where each catalog folder is Import-HDTSequenceDocument one task sequence Get-HDTOperatingSystem one operating system Nothing here re-parses YAML the engine already parses, and nothing here knows a folder name the engine does not know. A console that grew its own reader would be a second opinion about what is on the share, and the deployment's opinion is the one that matters. THE PATH IT WAS OPENED THROUGH AND THE deployRoot IT DECLARES ARE BOTH REPORTED, AND THEY ARE NOT THE SAME FACT. The lab share is C:\HDTLab\Share to the administrator sitting at the host and \\192.168.2.108\HDTShare to a machine that booted the image; the boot image carries the second. Showing only one of them is how an admin edits a share that no client can reach and cannot see why. ONE UNREADABLE DOCUMENT DOES NOT EMPTY THE CONSOLE. A sequence whose YAML does not parse, or an os.yaml that fails validation, becomes a row with Status 'Error' carrying the engine's own message - the file and the line included, because that is what the engine's error says. Deployment Workbench shows the broken item and complains about it; throwing instead would show an administrator nothing at all on exactly the day something on their share is broken. THE ROOT workspace.yaml IS THE ONE EXCEPTION: without it there is no share to show, so that failure is terminating and names the file. THE BOOT IMAGE COMES FROM THE MANIFEST, NOT FROM THE ARTIFACTS. Update-HDTBootImage writes Boot\<name>.manifest.json beside the .wim and the .iso, and it records the build date, the machine, the engine version and the SHA-256 of both artifacts. Reading it is how the console can state the ISO claim - that the WIM inside the ISO hashes equal to the standalone WIM - instead of hashing 500 MB twice to re-derive it. A share whose image has never been built says so and names Update-HDTBootImage, rather than showing an image with empty hashes. .PARAMETER Path The deployment share to open - a local path or a UNC share. The console only ever reads it. .PARAMETER FileSystem An IFileSystem - the real adapter by default, New-HDTFakeFileSystem in a test. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject: Root, WorkspacePath, SchemaVersion, Id, Name, DeployRoot, LogLevel, CredentialUser, Status, Error TaskSequence [pscustomobject[]] Id, Name, Description, StepCount, GroupCount, Step, Group, Variable, Path, Status, Error. Step is the engine's flat ordered step list, each carrying its GroupPath; Group is the group list. Both are empty when Status is 'Error'. OperatingSystem [pscustomobject[]] Id, Name, Description, Type, Architecture, DefaultIndex, ImageCount, Image, SourcePath, ImagePath, Path, Status, Error Driver Folder, Present - the folder only; the engine has no driver catalog to read BootImage Name, Architecture, Language, ManifestPath, Status ('Ok', 'Missing' or 'Error'), Error, BuildId, BuiltUtc, BuiltOn, EngineVersion, WimPath, WimSha256, WimSizeBytes, IsoPath, IsoSha256, IsoSizeBytes, IsoBootWimSha256, HashMatch .EXAMPLE Get-HDTConsoleWorkspace -Path 'C:\HDTLab\Share' What the console calls when it opens a share. .EXAMPLE (Get-HDTConsoleWorkspace -Path '\\192.168.2.108\HDTShare').TaskSequence | Format-Table Id, Name, StepCount, Status The same answer without a window - the console shows nothing the command line cannot. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [AllowNull()] [object] $FileSystem, # THE MONITOR NEEDS ONE, AND IT COMES IN HERE so a share can be opened # at a known instant. "How long since this deployment said anything" is # the only thing on this screen that changes without anything being # written, and a share read against the real wall clock could only be # tested by sleeping. [Parameter()] [AllowNull()] [object] $Clock ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem } if ($null -eq $Clock) { $Clock = New-HDTClock } # A trailing separator would put a doubled one in every path built below and # in every path shown on screen. 'C:\' is three characters and its separator # is part of the root. $root = $Path if ($root.Length -gt 3) { $root = $root.TrimEnd('\', '/') } $workspacePath = [System.IO.Path]::Combine($root, 'workspace.yaml') if (-not $FileSystem.TestPath($workspacePath)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $workspacePath ` -Category ObjectNotFound ` -Message ("there is no workspace document here, so '{0}' is not a deployment share. A share declares its identity and its deployRoot in workspace.yaml at its root." -f $root))) } # Deliberately NOT wrapped: a workspace.yaml that does not parse is a share # that cannot be shown at all, and the engine's error already names the file # and the line. $workspace = Import-HDTWorkspaceDocument -Path $workspacePath -FileSystem $FileSystem $credentialUser = '' if ($null -ne $workspace.Credential) { $credentialUser = [string] $workspace.Credential.Username } # -- task sequences ---------------------------------------------------- $sequenceRow = New-Object -TypeName System.Collections.ArrayList foreach ($entry in @(Get-HDTConsoleCatalogEntry -Root $root -Kind TaskSequences ` -DocumentName 'sequence.yaml' -FileSystem $FileSystem)) { # Step and Group carry the ENGINE'S OWN resolution of the document - # a flat, ordered step list where each step knows its GroupPath. The # console renders that; it does not re-parse the YAML and does not # decide an order of its own, so what the tree shows is what # Invoke-HDTTaskSequence would run. $row = [pscustomobject] @{ Id = $entry.Id Name = $entry.Id Description = '' StepCount = 0 GroupCount = 0 Step = @() Group = @() # WHAT THE NEW SEQUENCE WINDOW WROTE, and what its editor's # Variables tab shows: the block an author is expected to change. # Empty rather than absent, so a window may read it without asking # whether the document declared one. Variable = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase) # WHICH FOLDER THE TREE DRAWS IT UNDER. '' rather than absent: the # key is optional and most documents do not carry it, so every # reader would otherwise need a guard. Folder = '' Path = $entry.DocumentPath Status = 'Ok' Error = '' # WHAT THE LINT SAID, carried here so nothing downstream runs it # again. Test-HDTTaskSequence answers the question a schema cannot - # "would this sequence actually work on the machine you are about to # deploy" - and its own header names the console as the place those # findings are meant to surface (DESIGN 12: validation, inline). Finding = [pscustomobject[]] @() ErrorCount = 0 WarningCount = 0 } try { $sequence = Import-HDTSequenceDocument -Path $entry.DocumentPath -FileSystem $FileSystem $row.Name = [string] $sequence.Name $row.Description = [string] $sequence.Description $row.Step = @($sequence.Step) $row.Group = @($sequence.Group) $row.StepCount = @($sequence.Step).Count $row.GroupCount = @($sequence.Group).Count $row.Folder = [string] $sequence.Folder if ($null -ne $sequence.PSObject.Properties['Variable'] -and $null -ne $sequence.Variable) { foreach ($name in @($sequence.Variable.Keys)) { $row.Variable[[string] $name] = $sequence.Variable[$name] } } # THE LINT IS NOT ALLOWED TO TAKE THE SEQUENCE OFF THE SCREEN. It is # a lint: it returns findings rather than throwing, but a step type # registry that could not be read, or a rule that trips over an # unusual document, must not turn a readable sequence into an # unreadable one. A share full of task sequences is the console's # whole subject. try { $row.Finding = [pscustomobject[]] @(Test-HDTTaskSequence -Sequence $sequence) } catch { $row.Finding = [pscustomobject[]] @() } $row.ErrorCount = @($row.Finding | Where-Object { $_.Severity -eq 'Error' }).Count $row.WarningCount = @($row.Finding | Where-Object { $_.Severity -eq 'Warning' }).Count } catch { $row.Status = 'Error' $row.Error = [string] $_.Exception.Message } [void] $sequenceRow.Add($row) } # -- operating systems ------------------------------------------------- $osRow = New-Object -TypeName System.Collections.ArrayList foreach ($entry in @(Get-HDTConsoleCatalogEntry -Root $root -Kind OperatingSystems ` -DocumentName 'os.yaml' -FileSystem $FileSystem)) { $row = [pscustomobject] @{ Id = $entry.Id Name = $entry.Id Description = '' # WHICH FOLDER THE TREE DRAWS IT UNDER, as on a task sequence row. Folder = '' Type = '' Architecture = '' DefaultIndex = 0 ImageCount = 0 Image = [pscustomobject[]] @() SourcePath = '' ImagePath = '' Path = $entry.DocumentPath Status = 'Ok' Error = '' } try { $operatingSystem = Get-HDTOperatingSystem -WorkspaceRoot $root -Id $entry.Id -FileSystem $FileSystem $row.Name = [string] $operatingSystem.Name $row.Description = [string] $operatingSystem.Description $row.Folder = [string] $operatingSystem.Folder $row.Type = [string] $operatingSystem.Type $row.Architecture = [string] $operatingSystem.Architecture $row.DefaultIndex = [int] $operatingSystem.DefaultIndex $row.Image = [pscustomobject[]] @($operatingSystem.Images) $row.ImageCount = @($operatingSystem.Images).Count $row.SourcePath = [string] $operatingSystem.SourcePath $row.ImagePath = [string] $operatingSystem.ImagePath } catch { $row.Status = 'Error' $row.Error = [string] $_.Exception.Message } [void] $osRow.Add($row) } # -- applications ------------------------------------------------------ # # THE CATALOG IS THE DIRECTORY (DESIGN 2.1): one folder per application # holding its app.yaml, so the rows are an enumeration and a read, exactly # as the operating systems above are. Get-HDTApplication is the command an # administrator would type, and it is the one used here. $appRow = New-Object -TypeName System.Collections.ArrayList foreach ($entry in @(Get-HDTConsoleCatalogEntry -Root $root -Kind Applications ` -DocumentName 'app.yaml' -FileSystem $FileSystem)) { $row = [pscustomobject] @{ Id = $entry.Id Name = $entry.Id Description = '' # WHO MAKES IT AND WHICH VERSION - Workbench's other two questions, # and what tells two entries called Reader apart. Publisher = '' Version = '' # WHICH FOLDER THE TREE DRAWS IT UNDER, as on the other two. Folder = '' Install = '' Uninstall = '' RunIn = '' Detection = '' # THE RULE AS THE DOCUMENT WRITES IT, beside the sentence that # describes it. The pane's box writes app.yaml, so it has to hold # what app.yaml holds; the sentence is for reading and would become # the document's detection rule if it were ever typed back. DetectText = '' # WHAT WILL HAPPEN, INHERITED DEFAULTS INCLUDED. DESIGN 8 gives an # application that declares neither key 0 and 3010 for success and # 3010 for reboot, and a blank box where those are in force would # read as "no code succeeds" - the opposite of what is true. SuccessCode = [int[]] @() RebootCode = [int[]] @() Dependency = [string[]] @() SourcePath = '' Path = $entry.DocumentPath Status = 'Ok' Error = '' } try { $application = Get-HDTApplication -WorkspaceRoot $root -Id $entry.Id -FileSystem $FileSystem $row.Name = [string] $application.Name $row.Description = [string] $application.Description $row.Publisher = [string] $application.Publisher $row.Version = [string] $application.Version $row.Folder = [string] $application.Folder $row.Install = [string] $application.Install $row.Uninstall = [string] $application.Uninstall $row.RunIn = [string] $application.RunIn $row.Detection = Get-HDTConsoleDetectionText -Detect $application.Detect $row.DetectText = Get-HDTConsoleDetectRuleText -Detect $application.Detect $row.SuccessCode = [int[]] @($application.SuccessCodes) $row.RebootCode = [int[]] @($application.RebootCodes) $row.Dependency = [string[]] @($application.Dependencies) $row.SourcePath = [string] $application.SourcePath } catch { $row.Status = 'Error' $row.Error = [string] $_.Exception.Message } [void] $appRow.Add($row) } # -- drivers ----------------------------------------------------------- # # THE FOLDER, AND NOTHING ABOUT ITS CONTENTS. DESIGN 7 describes a driver # store; the engine has no command that reads one - no Get-HDTDriver, no # driver schema, nothing (M5 is deferred). So the console reports where the # folder is and whether it exists, which is true, and says the rest is not # built yet. Enumerating the tree here would put a driver inventory on # screen that no deployment could act on, and inventing a reader is exactly # what DESIGN 12's "the console may not do anything the cmdlets can't" # forbids. $driverFolder = Get-HDTWorkspacePath -Root $root -Kind Drivers $driver = [pscustomobject] @{ Folder = $driverFolder Present = [bool] $FileSystem.TestPath($driverFolder) } # -- the boot image ---------------------------------------------------- $bootImage = Get-HDTConsoleBootImage -Root $root -BootImage $workspace.BootImage -FileSystem $FileSystem return [pscustomobject] @{ Root = $root WorkspacePath = $workspacePath SchemaVersion = [int] $workspace.SchemaVersion Id = [string] $workspace.Id Name = [string] $workspace.Name DeployRoot = [string] $workspace.DeployRoot LogLevel = [string] $workspace.LogLevel CredentialUser = $credentialUser # A share this command returned is by definition one it could open. The # member is here so a share that could NOT be opened # (New-HDTConsoleShareFailure) is the same shape, and nothing downstream # needs to know which kind it is holding. Status = 'Ok' Error = '' # THE FOLDERS THE SHARE DECLARES, which are the ones nothing is in # yet - see Add-HDTWorkspaceFolder. A folder a document names needs # no declaration, and the tree draws the union of the two. Folder = $workspace.Folder TaskSequence = [pscustomobject[]] @($sequenceRow) OperatingSystem = [pscustomobject[]] @($osRow) Application = [pscustomobject[]] @($appRow) Driver = $driver BootImage = $bootImage # WHAT IS RUNNING ON IT, right now. DESIGN 12 lists Monitoring among the # tree's categories, so it is part of what a share IS rather than a # separate command an administrator has to know exists. Monitor = (Get-HDTConsoleMonitor -Path $root -FileSystem $FileSystem -Clock $Clock) } } |