geenforce: ship shadow mode through the share, and correct the cutover doc
The cheap cutover the doc described - manifest over API, payloads still on the
share - does not work. Measured on the win11 VM against lib 2.6 with the same
manifest and engine, varying only InstallerRoot: the payload cache gives
0 installed / 2 failed ("CMD/BAT not found" under the cache), the scope
directory gives 2 installed / 0 failed.
Two causes. Invoke-ShopdbEnforce.ps1 takes no -InstallerRoot and hardcodes the
payload cache outside shadow mode, which is right for displays where every
entry is inline and wrong for every smb entry. And -IncludeCommon cannot work
for smb payloads at all: they live under their own scope directory, so a merged
manifest needs two roots where the engine takes one, and Join-InstallerPath
rejects rooted values by design so absolute paths are not a way out. The real
dispatcher never merges - it calls the engine once per scope with that scope's
root - and a fix should follow that shape.
Both claims are corrected in the doc rather than deleted, because reading it
was what made this look like a twenty-minute job; it would have failed 29 of 31
entries on the pilot bay.
Shadow mode is unaffected - it points InstallerRoot at the share scope dir,
which is the working path - so the pilot proceeds. Register-ShopdbShadow.ps1
is the reference copy of the script that puts a share-attached bay into shadow:
it writes BaseUrl, then registers the task as SYSTEM. It derives the share
manifest from $PSScriptRoot rather than hardcoding a drive, because GE-Enforce
mounts the share to a letter it picks at run time and SYSTEM has no standing
UNC access. Delivered as a manifest entry gated to one hostname, so the pilot
needs no hands on the bay and heals if it drifts.
This commit is contained in:
@@ -195,9 +195,16 @@ is the bridge that lets the UNCHANGED engine install share-less:
|
|||||||
|
|
||||||
The runner (`Invoke-ShopdbEnforce.ps1`) then sets the engine's
|
The runner (`Invoke-ShopdbEnforce.ps1`) then sets the engine's
|
||||||
`-InstallerRoot` to that same payloads directory, so the engine's
|
`-InstallerRoot` to that same payloads directory, so the engine's
|
||||||
`Join-Path $InstallerRoot <leaf>` resolves to the staged file. `smb` entries in
|
`Join-Path $InstallerRoot <leaf>` resolves to the staged file.
|
||||||
a mixed manifest are left untouched and still resolve against the share (a PC
|
|
||||||
that has it).
|
**`smb` entries in a mixed manifest DO NOT survive this.** An earlier version of
|
||||||
|
this doc claimed they were "left untouched and still resolve against the share";
|
||||||
|
that is false outside shadow mode and was measured wrong on the VM (see the
|
||||||
|
blockers in section 11 step 1). `Resolve-ShopdbPayloads` does leave them
|
||||||
|
untouched, but the runner has already pointed `-InstallerRoot` at the payload
|
||||||
|
cache, so a share-relative path resolves under the cache and is reported
|
||||||
|
`not found`. A manifest that mixes `smb` with http/inline cannot be served by a
|
||||||
|
single `InstallerRoot` at all.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -672,12 +679,39 @@ Checklist for cutting any of the remaining scopes (`gea-shopfloor-cmm`,
|
|||||||
`-nocollections`, `common`) over to the API.
|
`-nocollections`, `common`) over to the API.
|
||||||
|
|
||||||
1. **Decide the delivery model.** Does this pc-type keep SMB access? If yes,
|
1. **Decide the delivery model.** Does this pc-type keep SMB access? If yes,
|
||||||
the cheap cutover is manifest-over-API + payloads-still-smb (entries stay
|
the intended cheap cutover is manifest-over-API + payloads-still-smb
|
||||||
`smb`, nothing to upload, the engine resolves share paths as today). Only
|
(entries stay `smb`, nothing to upload). Only a genuinely share-less PC
|
||||||
a genuinely share-less PC needs http/inline payload conversion. Note the
|
needs http/inline payload conversion. Note the payload endpoint's 512 MB
|
||||||
payload endpoint's 512 MB default ceiling
|
default ceiling (`GEENFORCE_PAYLOAD_MAX_BYTES`) before promising huge
|
||||||
(`GEENFORCE_PAYLOAD_MAX_BYTES`) before promising huge installers over
|
installers over HTTPS.
|
||||||
HTTPS.
|
|
||||||
|
**THE CHEAP CUTOVER DOES NOT WORK WITH THE CURRENT RUNNER. Verified on the
|
||||||
|
win11 VM against lib 2.6, 2026-08-13.** Two blockers, both in delivery, not
|
||||||
|
in the manifests:
|
||||||
|
|
||||||
|
- `Invoke-ShopdbEnforce.ps1` has no `-InstallerRoot` parameter. Outside
|
||||||
|
shadow mode it hardcodes the payloads cache
|
||||||
|
(`Join-Path (Split-Path -Parent $manifestToRun) 'payloads'`). That is
|
||||||
|
right for displays, where every entry is inline, and wrong for every
|
||||||
|
`smb` entry: the engine resolves `Join-InstallerPath $InstallerRoot
|
||||||
|
<field>`, so `scripts/Install-AcroReader.cmd` is looked for under the
|
||||||
|
payload cache and logs `CMD/BAT not found`. Measured on the VM with the
|
||||||
|
same manifest and engine: InstallerRoot=payloads gave 0 installed /
|
||||||
|
2 failed, InstallerRoot=<scope dir> gave 2 installed / 0 failed.
|
||||||
|
- `-IncludeCommon` cannot work for `smb` payloads at all. Payloads live
|
||||||
|
under their own scope directory (`common/scripts/...` vs
|
||||||
|
`gea-shopfloor-nocollections/scripts/...`), so a merged manifest needs
|
||||||
|
two roots and the engine takes one. `Join-InstallerPath` also rejects
|
||||||
|
rooted values outright, so absolute paths are not a workaround - that
|
||||||
|
rejection is deliberate, to stop an entry escaping its share root.
|
||||||
|
|
||||||
|
This is why the real dispatcher does NOT merge: `common/GE-Enforce.ps1`
|
||||||
|
builds a target list and calls the engine ONCE PER SCOPE with
|
||||||
|
`-InstallerRoot $t.Root`. Any fix should follow that shape.
|
||||||
|
|
||||||
|
Until it is fixed, a share-attached cohort can still run SHADOW mode
|
||||||
|
(below), which is unaffected: shadow sets InstallerRoot to the share scope
|
||||||
|
dir, which is the working path.
|
||||||
2. **Get the scope into shopdb.** Existing share manifest:
|
2. **Get the scope into shopdb.** Existing share manifest:
|
||||||
`flask geenforce parity` then `flask geenforce import-share --scope
|
`flask geenforce parity` then `flask geenforce import-share --scope
|
||||||
<name>`. New/reworked scope: author in code following
|
<name>`. New/reworked scope: author in code following
|
||||||
|
|||||||
79
plugins/geenforce/client/Register-ShopdbShadow.ps1
Normal file
79
plugins/geenforce/client/Register-ShopdbShadow.ps1
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Register-ShopdbShadow.ps1 -- put this bay into shopdb SHADOW mode.
|
||||||
|
#
|
||||||
|
# Shadow mode = fetch the shopdb manifest, diff it against the share manifest,
|
||||||
|
# report the cycle to shopdb, and install FROM THE SHARE exactly as today. Zero
|
||||||
|
# behaviour change. It is the observable step before any cutover.
|
||||||
|
#
|
||||||
|
# Runs as SYSTEM under GE-Enforce, from a manifest entry gated to one hostname.
|
||||||
|
# Idempotent: re-registers the task each cycle so drift self-heals, and writes
|
||||||
|
# BaseUrl only when it differs.
|
||||||
|
#
|
||||||
|
# WHY the share manifest is read through the mounted drive: GE-Enforce.ps1
|
||||||
|
# mounts the SFLD share with SFLD credentials before invoking the engine, and
|
||||||
|
# this script runs inside that window. SYSTEM has no standing access to the UNC
|
||||||
|
# path, so the mounted drive is the only path that resolves. The drive letter is
|
||||||
|
# not fixed, so it is derived from where this script is running rather than
|
||||||
|
# hardcoded.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
|
||||||
|
$TaskName = 'ShopDB GE-Enforce (shadow)'
|
||||||
|
$InstallDir = 'C:\Program Files\GE\Shopfloor'
|
||||||
|
$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb'
|
||||||
|
$Scope = 'gea-shopfloor-nocollections'
|
||||||
|
|
||||||
|
function Write-ShadowLog {
|
||||||
|
param([string]$Message)
|
||||||
|
$line = "[{0}] [shadow-setup] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
# --- BaseUrl (the client reads this; no token needed on an allowlisted subnet)
|
||||||
|
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||||
|
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
|
||||||
|
$current = (Get-ItemProperty -Path $regPath -Name BaseUrl -ErrorAction SilentlyContinue).BaseUrl
|
||||||
|
if ($current -ne $BaseUrl) {
|
||||||
|
Set-ItemProperty -Path $regPath -Name BaseUrl -Value $BaseUrl
|
||||||
|
Write-ShadowLog "BaseUrl set to $BaseUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
$runner = Join-Path $InstallDir 'Invoke-ShopdbEnforce.ps1'
|
||||||
|
$engine = Join-Path $InstallDir 'lib\Install-FromManifest.ps1'
|
||||||
|
foreach ($p in @($runner, $engine)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $p)) {
|
||||||
|
Write-ShadowLog "MISSING $p - the File entries have not landed yet; will retry next cycle."
|
||||||
|
exit 0 # fail-safe: never break the bay, the entry re-runs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the share manifest this scope is enforced from, via the mounted drive.
|
||||||
|
# $PSScriptRoot is <drive>:\<scope>\shopdb-client, so its grandparent is the
|
||||||
|
# scope dir. Deriving it keeps this correct whatever letter GE-Enforce mounted.
|
||||||
|
$scopeDir = Split-Path -Parent $PSScriptRoot
|
||||||
|
$shareManifest = Join-Path $scopeDir 'manifest.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $shareManifest)) {
|
||||||
|
Write-ShadowLog "share manifest not found at $shareManifest - not registering."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -Scope "{1}" -EnginePath "{2}" -ShadowMode -ShareManifestPath "{3}"' `
|
||||||
|
-f $runner, $Scope, $engine, $shareManifest
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments
|
||||||
|
# RepetitionInterval ALONE - passing RepetitionDuration serializes to a
|
||||||
|
# Duration the Task Scheduler schema rejects.
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15)
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -RunLevel Highest
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||||
|
|
||||||
|
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||||
|
-Principal $principal -Settings $settings -Force | Out-Null
|
||||||
|
Write-ShadowLog "registered '$TaskName' (every 15 min), shadowing $shareManifest"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fail-safe: a broken setup script must never stop the bay enforcing.
|
||||||
|
Write-ShadowLog "FAILED: $_"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user