From 7ed30f9b85ce415ab900e8d09bd72113a8fa5891 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 6 Aug 2026 15:58:15 -0400 Subject: [PATCH] Display: install the ShopDB enforce client once AESFMA is reachable A PXE-imaged display ends up with no GE-Enforce client at all. Confirmed on 579C144, 2026-08-06: is the client installed? NOT FOUND scheduled tasks that would run it? NONE Not a broken configuration - nothing had ever tried. Install-ShopdbKiosk.ps1 downloads itself from {BaseUrl}/installers/kiosk over HTTPS, and ShopDB is only reachable after the bay joins the AESFMA wifi SSID, so it cannot run during imaging. Nothing was arranged to run it afterwards. 09-Setup-Display.ps1 now registers 'ShopDB Kiosk Bootstrap' as a SYSTEM task at boot and every 15 minutes. Install-ShopdbKiosk-WhenOnline.ps1 does nothing until ShopDB answers, then runs the vendor installer once, verifies BaseUrl and the enforce task exist, deletes the staged key and unregisters itself. A bay cabled up days later still self-configures. It deliberately does not reimplement the installer - it waits, invokes, verifies and cleans up, so the vendor script can be replaced wholesale without touching this. The key file is LABELLED (collector=, fetch=) rather than positional. The two tokens are not interchangeable and a mix-up is silent: a fetch token in the collector slot leaves asset reporting broken while everything looks configured. A bare unlabelled line is ignored rather than guessed at. Missing keys are not fatal - the fetch token is unnecessary on an IP-allowlisted subnet, and the installer skips the asset-report task rather than failing. Staged on the share: the bootstrap, the vendor installer under kiosk\, and the updated display setup. The key itself is NOT staged yet - see the commit discussion; it needs to be labelled with which scope it carries first. --- .../09-Setup-Display.ps1 | 34 ++++ .../Install-ShopdbKiosk-WhenOnline.ps1 | 180 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 playbook/shopfloor-setup/gea-shopfloor-display/Install-ShopdbKiosk-WhenOnline.ps1 diff --git a/playbook/shopfloor-setup/gea-shopfloor-display/09-Setup-Display.ps1 b/playbook/shopfloor-setup/gea-shopfloor-display/09-Setup-Display.ps1 index bfc0429..1f9738c 100644 --- a/playbook/shopfloor-setup/gea-shopfloor-display/09-Setup-Display.ps1 +++ b/playbook/shopfloor-setup/gea-shopfloor-display/09-Setup-Display.ps1 @@ -52,6 +52,40 @@ Write-Host " Edge RelaunchNotificationPeriod=1h" Write-Host " Edge RelaunchHeadsUpPeriod=1m" Write-Host " Edge RelaunchWindow=02:00-04:00" +# --- Arm the ShopDB kiosk bootstrap ------------------------------------- +# A PXE-imaged display ends up with no GE-Enforce client: the installer pulls +# itself from {BaseUrl}/installers/kiosk over HTTPS, and ShopDB is unreachable +# until the bay joins the AESFMA wifi SSID. So it cannot run now. +# +# Register a SYSTEM task that does nothing until ShopDB answers, then runs the +# vendor installer once and unregisters itself. At boot and every 15 minutes, +# so a bay cabled up days later still self-configures. +Write-Host '=== Arming ShopDB kiosk bootstrap ===' +$bootstrap = Join-Path $PSScriptRoot 'Install-ShopdbKiosk-WhenOnline.ps1' +$taskName = 'ShopDB Kiosk Bootstrap' + +if (-not (Test-Path $bootstrap)) { + Write-Warning " $bootstrap not found - kiosk will NOT self-configure. Tell the PXE admin." +} else { + try { + $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` + -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}" -TaskName "{1}"' -f $bootstrap, $taskName) + $trigBoot = New-ScheduledTaskTrigger -AtStartup + $trigRep = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) ` + -RepetitionInterval (New-TimeSpan -Minutes 15) + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` + -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 30) + Register-ScheduledTask -TaskName $taskName -Action $action ` + -Trigger @($trigBoot, $trigRep) -Principal $principal -Settings $settings ` + -Force -ErrorAction Stop | Out-Null + Write-Host " Registered '$taskName' (at boot + every 15 min, SYSTEM)" + Write-Host " It waits for ShopDB, installs the enforce client, then removes itself." + } catch { + Write-Warning " Failed to register '$taskName': $_" + } +} + if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) { Send-PxeStatus -Stage '09-Setup-Display: complete' -StageIndex 4 -StageTotal 8 } diff --git a/playbook/shopfloor-setup/gea-shopfloor-display/Install-ShopdbKiosk-WhenOnline.ps1 b/playbook/shopfloor-setup/gea-shopfloor-display/Install-ShopdbKiosk-WhenOnline.ps1 new file mode 100644 index 0000000..7ad86ca --- /dev/null +++ b/playbook/shopfloor-setup/gea-shopfloor-display/Install-ShopdbKiosk-WhenOnline.ps1 @@ -0,0 +1,180 @@ +<# +.SYNOPSIS + Run the ShopDB kiosk installer as soon as ShopDB becomes reachable. + +.DESCRIPTION + A PXE-imaged display currently ends up with NO GE-Enforce client at all. + Confirmed on 579C144, 2026-08-06: + + is the client installed? NOT FOUND + scheduled tasks that would run it? NONE + + The client is delivered by Install-ShopdbKiosk.ps1, which downloads itself + from {BaseUrl}/installers/kiosk over HTTPS - and ShopDB is only reachable + once the bay has joined the AESFMA wifi SSID. So the installer cannot run + during imaging, and nothing was arranged to run it afterwards. + + This closes that gap. Imaging stages the installer and the key; this waiter + runs as SYSTEM on a schedule, does nothing until ShopDB answers, then runs + the installer once and removes itself. + + Deliberately does NOT reimplement the installer. It waits, invokes, verifies + and cleans up - the vendor script keeps doing the actual work, so it can be + replaced wholesale without touching this. + +.PARAMETER EnrollmentRoot + Where imaging staged things. Expects: + \display-type.txt Dashboard | Lobby | 3DPrintRoom + \kiosk\Install-ShopdbKiosk.ps1 + \kiosk\shopdb-key.txt (optional) line1 collector.ingest PAT, + line2 geenforce.fetch PAT + +.PARAMETER TaskName + The scheduled task to remove once installation succeeds. + +.NOTES + The key file is deleted after a successful install. The installer writes the + secrets into HKLM:\SOFTWARE\GE\ShopDB, which is ACLed to SYSTEM and + Administrators - a better home than a file readable by any local user until + lockdown. Bounding that exposure to the imaging window is the point. +#> + +[CmdletBinding()] +param( + [string]$EnrollmentRoot = 'C:\Enrollment', + [string]$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb', + [string]$TaskName = 'ShopDB Kiosk Bootstrap', + [int]$TimeoutSeconds = 30 +) + +$ErrorActionPreference = 'Continue' +$logDir = 'C:\Logs\ShopDB' +$log = Join-Path $logDir 'kiosk-bootstrap.log' +New-Item -ItemType Directory -Path $logDir -Force -EA SilentlyContinue | Out-Null + +function Log { param([string]$m) + $line = "{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $m + Write-Host $line + Add-Content -Path $log -Value $line -EA SilentlyContinue +} + +function Remove-Self { + try { + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -EA Stop + Log "Unregistered '$TaskName' - nothing further to do." + } catch { Log "Could not unregister '$TaskName': $_" } +} + +Log "=== kiosk bootstrap ===" + +# --- already done? ------------------------------------------------------ +# Both conditions, not just the registry key: BaseUrl alone can be present from +# a partial run, and the enforce task is what actually proves an install. +$reg = 'HKLM:\SOFTWARE\GE\ShopDB' +$haveCfg = (Test-Path $reg) -and ((Get-ItemProperty $reg -EA SilentlyContinue).BaseUrl) +$haveTask = [bool](Get-ScheduledTask -EA SilentlyContinue | + Where-Object { $_.TaskName -match '(?i)shopdb.*enforce' }) +if ($haveCfg -and $haveTask) { + Log "Already installed (BaseUrl set, enforce task present)." + Remove-Self + return +} + +# --- what kind of display is this? -------------------------------------- +$dtFile = Join-Path $EnrollmentRoot 'display-type.txt' +if (-not (Test-Path $dtFile)) { + Log "No $dtFile - cannot choose a DisplayType. Leaving the task armed." + return +} +$displayType = (Get-Content $dtFile -First 1).Trim() +if ($displayType -notin @('Dashboard','Lobby','3DPrintRoom')) { + Log "display-type.txt says '$displayType', which the installer will reject. Leaving armed." + return +} +Log "DisplayType: $displayType" + +$installer = Join-Path $EnrollmentRoot 'kiosk\Install-ShopdbKiosk.ps1' +if (-not (Test-Path $installer)) { + Log "Installer not staged at $installer. Leaving armed." + return +} + +# --- is ShopDB reachable yet? ------------------------------------------- +# Expected to fail until the bay joins AESFMA. That is the whole reason this +# script exists, so a failure here is logged quietly and retried, not raised. +$probe = "$($BaseUrl.TrimEnd('/'))/api/docs" +try { + $r = Invoke-WebRequest -Uri $probe -UseBasicParsing -TimeoutSec $TimeoutSeconds -EA Stop + Log "ShopDB reachable (HTTP $($r.StatusCode))." +} catch { + $code = try { $_.Exception.Response.StatusCode.value__ } catch { $null } + if ($code) { + # Answered at all = reachable. 401/403 just means no token on this probe. + Log "ShopDB reachable (HTTP $code)." + } else { + Log "Not reachable yet - waiting for AESFMA. ($($_.Exception.Message))" + return + } +} + +# --- keys ---------------------------------------------------------------- +$keyFile = Join-Path $EnrollmentRoot 'kiosk\shopdb-key.txt' +$collectorKey = '' +$fetchToken = '' +if (Test-Path $keyFile) { + # LABELLED format, because the two tokens are not interchangeable and a + # positional mix-up is silent: a fetch token in the collector slot leaves + # asset reporting broken while everything looks configured. + # + # collector= always needed for asset reporting + # fetch= optional when the subnet is allowlisted + # + # A bare single line is REJECTED rather than guessed at. + foreach ($line in (Get-Content $keyFile -EA SilentlyContinue)) { + $t = $line.Trim() + if (-not $t -or $t.StartsWith('#')) { continue } + if ($t -match '^(?i)collector\s*=\s*(.+)$') { $collectorKey = $Matches[1].Trim() } + elseif ($t -match '^(?i)fetch\s*=\s*(.+)$') { $fetchToken = $Matches[1].Trim() } + else { + Log "Key file line is not labelled 'collector=' or 'fetch=' - ignoring it rather than guessing which token it is." + } + } + Log ("Key file present (collector={0}, fetch={1})" -f ` + $(if ($collectorKey) { 'yes' } else { 'no' }), $(if ($fetchToken) { 'yes' } else { 'no' })) +} else { + # Not fatal. The fetch token is unnecessary when the kiosk subnet is + # IP-allowlisted; only the asset-report task needs the collector key, and + # the installer skips that task rather than failing. + Log "No key file - installing without tokens (allowlisted subnets still work; no asset reporting)." +} + +# --- run the vendor installer ------------------------------------------- +$args = @{ DisplayType = $displayType; BaseUrl = $BaseUrl } +if ($collectorKey) { $args.CollectorKey = $collectorKey } +if ($fetchToken) { $args.ShopdbToken = $fetchToken } + +Log "Running $installer ..." +try { + & $installer @args 2>&1 | ForEach-Object { Log " $_" } +} catch { + Log "Installer threw: $_" + return +} + +# --- verify, then clean up ---------------------------------------------- +$haveCfg = (Test-Path $reg) -and ((Get-ItemProperty $reg -EA SilentlyContinue).BaseUrl) +$haveTask = [bool](Get-ScheduledTask -EA SilentlyContinue | + Where-Object { $_.TaskName -match '(?i)shopdb.*enforce' }) +if ($haveCfg -and $haveTask) { + Log "Install verified: BaseUrl set and enforce task registered." + if (Test-Path $keyFile) { + # The installer has moved the secrets into HKLM (SYSTEM/Admins ACL). + # The staged copy is readable by any local user until lockdown, so it + # goes now rather than lingering. + Remove-Item $keyFile -Force -EA SilentlyContinue + Log "Removed staged key file." + } + Remove-Self +} else { + Log "Installer ran but verification FAILED (BaseUrl=$haveCfg, enforceTask=$haveTask). Leaving armed to retry." +}