Metrology PCs (CMM, Keyence, Genspect, wax-and-trace imaging pc-types) drive an attached measuring instrument. The PC itself stays a shopfloor PC, but the collector now models the instrument: - New METROLOGY_TOOL_MAP (pctypemap.py) maps those pc-types to a MeasuringToolType (CMM, Vision System, Genspect, Form Tracer). - ComputersPlugin._sync_measuringtool_link creates the MeasuringTool asset once and a directional PC->tool "controls" relationship, tagged collector:measuringtool. Idempotent (re-push reuses, no duplicate asset) and self-archiving (a PC re-imaged to a non-metrology type deactivates the link but keeps the asset and any calibration history). Mirrors the printer-link pattern. The MeasuringToolType is created on demand if not seeded. - 4 tests: create+link, idempotent re-push, non-metrology skip, repurpose archives. Non-metrology PCs never warn about a missing controls type. Settings rail cleanup: - Collapsible groups so the 13-group rail fits without scrolling (1511px -> 488px). The group containing the current page expands; the rest collapse. CSS-drawn caret (ASCII source, no Unicode). Empty groups never render, in both the rail and the landing page. - Measuring Tools group placed with the other asset groups (right after Machines) instead of appended last; empty placeholder positions the plugin-contributed cards. - Operating Systems moved from PCs to General Reference: OS is cross-asset (PCs, machines, measuring tools, network devices all run one). Plus docs/proposals/ge-enforce-plugin.md: a planning doc for refactoring GE-Enforce/DSC into a shopdb plugin (manifest as shopdb data, payloads on SMB/HTTP/inline), grounded in the real manifest schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
332 lines
17 KiB
Markdown
332 lines
17 KiB
Markdown
# Proposal: GE-Enforce as a shopdb plugin
|
|
|
|
Status: DRAFT / planning only. Not accepted, not built.
|
|
Author: planning session 2026-07-12.
|
|
|
|
## 1. What this is
|
|
|
|
Today GE-Enforce is a PowerShell manifest engine that reads per-PC-type
|
|
`manifest.json` files off an SMB share (`\\tsgwp00525.wjs.geaerospace.net\
|
|
shared\dt\shopfloor\`). Each logon, a scheduled task running as SYSTEM mounts
|
|
the share, reads the manifest for the machine's PC type, and installs or
|
|
self-heals apps, files, drivers, registry values, and scripts. A parallel
|
|
`preinstall.json` runs the same schema once at imaging.
|
|
|
|
This proposal turns the *manifest* into shopdb data: the authoritative manifest
|
|
lives in the shopdb database, is edited through the shopdb UI (an expansion of
|
|
`/settings/pctypemapping`), and is served to clients over HTTP as JSON. The
|
|
*payloads* (MSI/EXE/PS1/config bytes) stay on SMB, on HTTP, or both, referenced
|
|
by URL/path from the manifest rows. GE-Enforce.ps1 changes from "read a file on
|
|
W:" to "GET a manifest from shopdb, then fetch each payload from wherever the
|
|
row says."
|
|
|
|
The result: managing imaging PC types, their apps, scripts, files, registry
|
|
rules, and version gates becomes a first-class shopdb feature instead of hand-
|
|
edited JSON on a file share.
|
|
|
|
## 2. Why it fits shopdb
|
|
|
|
- shopdb already models the fleet (the collector ingests every PC's hostname,
|
|
pctype, installed software, versions). Making shopdb *also* own what SHOULD be
|
|
installed closes the loop: desired-state (manifest) and observed-state
|
|
(collector) live in one system and can be diffed.
|
|
- `/settings/pctypemapping` already maps `gea-shopfloor-*` PC types to
|
|
`ComputerType`. That page becomes the entry point for full imaging-PC-type
|
|
management.
|
|
- The plugin contract (per-plugin models, migrations, API prefix, settings
|
|
cards, collector hooks) is exactly the shape this needs.
|
|
- ADR-004 (per-site instances) matches: each site's shopdb owns each site's
|
|
manifest. No multi-tenant complication.
|
|
|
|
## 3. Grounding: the real manifest schema
|
|
|
|
Source of truth for these field names (do not invent others):
|
|
|
|
- Schema: `pxe-images/tsgwp00525-v2/shared/dt/shopfloor/_meta/manifest-schema.json`
|
|
- Engine: `pxe-images/common/lib/Install-FromManifest.ps1`
|
|
- Dispatcher: `.../shopfloor/common/GE-Enforce.ps1`
|
|
- Architecture: `pxe/docs/ge-enforce-v2-architecture.md`
|
|
|
|
A manifest is `{ "Version": str, "_comment": str, "Applications": [entry, ...] }`.
|
|
Only `Name` and `Type` are required per entry.
|
|
|
|
### Per-entry fields (complete set)
|
|
|
|
Identity / action:
|
|
- `Name` (required, unique, also the status-key `<scope>/<Name>`)
|
|
- `Type` (required): one of `MSI EXE CMD BAT PS1 INF File Registry`
|
|
- `_comment` (documentation, heavily used in practice)
|
|
|
|
Type-specific payload references (sparse; depends on Type):
|
|
- MSI/EXE/CMD/BAT/INF: `Installer` (relative path) + `InstallArgs`
|
|
- PS1: `Script` (relative path, falls back to `Installer`) + `Args`
|
|
- File: `Source` (relative) + `Destination` (absolute on-PC path)
|
|
- Registry: `RegPath` + `RegName` + `RegValue` + `RegType`
|
|
(`RegType` in `String DWord QWord MultiString ExpandString Binary`)
|
|
- Optional `LogFile`, `WaitTimeoutSec` (EXE hang kill), `InUseCheck`
|
|
|
|
Detection (decides whether the action fires / self-heals):
|
|
- `DetectionMethod`: one of
|
|
`Registry File FileVersion Hash MarkerFile ValueMatches pnputil Always`
|
|
- `DetectionPath`, `DetectionName`, `DetectionValue`, `DetectionPattern`
|
|
- Note: `DetectionValue` is method-dependent - SHA256 for Hash, a 4-part
|
|
version for FileVersion, a registry value for Registry, ignored for
|
|
Always/File. Same column, different meaning per method.
|
|
- No `DetectionMethod` = always installs.
|
|
|
|
Targeting filters (all ANDed; each is multi-value):
|
|
- `PCTypes` (array; `"*"` = all; alias graph expands old<->new names)
|
|
- `PCSubTypes` / subtype via `<pctype>-<subtype>` values
|
|
- `TargetHostnames` (array; exact + `-like WJS-*` wildcards)
|
|
- `TargetMachineNumbers` (array; per-bay)
|
|
- `_CmmVersion` (scalar; per-entry PC-DMIS version gate, needs lib >= 2.6)
|
|
|
|
Nested:
|
|
- `InUseCheck`: `{ Behavior, Processes: [{Name, ExePath, GracefulCloseTimeoutSec}] }`
|
|
Behavior in `Defer CloseAndReopen ForceClose ScheduleForReboot`
|
|
|
|
Parsed-but-inert today (model them, mark inert):
|
|
- `ApplyMode` (`Nightly Immediate ImmediateReboot`), `UpdateWindow` (`HH:MM-HH:MM`)
|
|
|
|
Preinstall-only extras (phase discriminator):
|
|
- `PreEnrollment`, `KillAfterDetection`, `PCTypesStrict`, `_pcTypesNote`
|
|
|
|
### Load-bearing behaviors the model must preserve
|
|
|
|
1. **Array order IS execution order.** Config-restore entries are deliberately
|
|
placed AFTER their vendor installer so a mid-cycle overwrite heals the same
|
|
cycle (eMxInfo.txt after eDNC; udc_webserver_settings after UDC). We MUST
|
|
store an explicit per-scope `sortorder`, not a set.
|
|
2. **PCTypes alias graph** is many-to-many old<->new names resolved by set
|
|
intersection, with a `PCTypesStrict` escape hatch. Not a simple FK.
|
|
3. **Polymorphic entry by Type** - sparse column set per type. Model as
|
|
single-table with nullable columns, or a typed-payload child. STI is simpler.
|
|
4. **Two manifest phases** - runtime (self-heal, per logon) and preinstall
|
|
(once at imaging) share the schema. One table with a `phase` discriminator.
|
|
|
|
## 4. Data model (new `geenforce` plugin)
|
|
|
|
Per-plugin Alembic chain (ADR-008). Tables (lowercase concatenated per naming
|
|
convention):
|
|
|
|
- `manifestscopes` - one row per imaging PC type / scope.
|
|
- `scopeid` PK
|
|
- `scopename` (e.g. `gea-shopfloor-cmm`; unique)
|
|
- `computertypeid` FK -> `computertypes` (this REPLACES the thin
|
|
`pctypemap_<pxetype>` setting; the mapping becomes a column here)
|
|
- `phase` enum (`runtime` | `preinstall`)
|
|
- `manifestversion` (string, mirrors manifest `Version`)
|
|
- `description`, `isactive`
|
|
- `iscommon` bool (the `common/` fleet-wide scope)
|
|
|
|
- `manifestentries` - one row per Applications[] entry.
|
|
- `entryid` PK, `scopeid` FK
|
|
- `sortorder` int (preserves array order; the ordering contract)
|
|
- `name`, `entrytype` (MSI/EXE/.../Registry), `comment`
|
|
- payload columns (nullable, per type): `installer`, `installargs`,
|
|
`scriptpath`, `scriptargs`, `sourcepath`, `destination`,
|
|
`regpath`, `regname`, `regvalue`, `regtype`
|
|
- `payloadsource` enum (`smb` | `http` | `inline`) + `payloadref`
|
|
(see section 5)
|
|
- detection columns: `detectionmethod`, `detectionpath`, `detectionname`,
|
|
`detectionvalue`, `detectionpattern`
|
|
- gates: `cmmversion`, plus child tables for the multi-value filters
|
|
- control: `logfile`, `waittimeoutsec`, `applymode`, `updatewindow`
|
|
- preinstall flags: `preenrollment`, `killafterdetection`, `pctypesstrict`
|
|
- `isactive`
|
|
|
|
- `manifestentrypctypes`, `manifestentryhostnames`, `manifestentrymachinenumbers`
|
|
- child rows for the ANDed multi-value filters (one value per row, wildcards
|
|
stored verbatim as patterns)
|
|
|
|
- `manifestinusechecks` + `manifestinusecheckprocesses`
|
|
- the nested InUseCheck object and its Processes[] child list
|
|
|
|
- `pctypealiases` - the old<->new name alias graph (seed from
|
|
`Install-FromManifest.ps1:463-475`), so the server can resolve/validate
|
|
PCTypes the same way the engine does.
|
|
|
|
The JSON the client receives is REBUILT from these rows in exact array order.
|
|
It must byte-for-byte match what the current engine expects (round-trip tested
|
|
against the live manifests).
|
|
|
|
## 5. Payloads: SMB and/or HTTP (both supported)
|
|
|
|
The user asked whether payloads can be SMB and/or HTTP. Yes - per entry:
|
|
|
|
- `payloadsource = smb`: `payloadref` is the current relative path
|
|
(`apps/eDNC_6-4-5.msi`); the client resolves it against the mounted scope
|
|
root exactly as today. Zero client behavior change for these rows. This is
|
|
the default and the migration target for large binaries (MSIs are hundreds of
|
|
MB; SMB streaming beats HTTP for those).
|
|
- `payloadsource = http`: `payloadref` is a URL (absolute, or relative to a
|
|
configured payload base). The client downloads to a local temp dir, verifies
|
|
the Hash/FileVersion detection value, then runs it. Good for small
|
|
config/script payloads and for sites with no SMB share.
|
|
- `payloadsource = inline`: for small text payloads (a `.ps1`, a config file, a
|
|
registry value), the bytes live in shopdb itself and are served in-band. No
|
|
external store at all. Best for scripts and File-type config drops.
|
|
|
|
Manifest generation emits, per entry, whatever the client needs to fetch the
|
|
bytes. The engine's existing "stage network EXE to local temp first" logic
|
|
(SYSTEM access-denied workaround) generalizes cleanly to HTTP download.
|
|
|
|
Hashing stays the integrity mechanism regardless of transport: `DetectionValue`
|
|
carries the SHA256, the client verifies after fetch. This is the security-
|
|
critical bit - shopdb serves a manifest that says "install X with hash H"; a
|
|
tampered payload from any transport fails the hash and does not run.
|
|
|
|
## 6. API surface (`/api/geenforce/...`)
|
|
|
|
Admin CRUD (gated by a new `geenforce.manage` permission via the plugin's
|
|
`get_permissions()` hook):
|
|
- `GET/POST /scopes`, `GET/PUT/DELETE /scopes/<id>` - imaging PC types
|
|
- `GET/POST /scopes/<id>/entries`, `PUT/DELETE /entries/<id>` - manifest entries
|
|
- `PUT /scopes/<id>/entries/reorder` - the ordering contract, drag-to-reorder
|
|
- `POST /entries/<id>/payload` - upload an inline/http payload (multipart),
|
|
compute + store its SHA256 into `detectionvalue`
|
|
- `GET /scopes/<id>/preview` - the exact JSON a client would receive (for review
|
|
before publish)
|
|
|
|
Client-facing (gated by a collector-style service token, `geenforce.fetch`
|
|
scope, reusing the PAT + `X-API-Key` machinery already built for the collector):
|
|
- `GET /manifest?pctype=<scope>&subtype=<s>&hostname=<h>&machinenumber=<n>`
|
|
Returns the assembled manifest JSON for that machine. The server can pre-apply
|
|
the PCTypes/hostname/machinenumber/cmmversion filters (thin client) OR return
|
|
the full scope and let the engine filter (fat client, matches today). Start
|
|
fat: return the scope manifest unchanged so the engine logic is untouched.
|
|
- Payload fetch for `http`/`inline` rows: `GET /payload/<entryid>` streaming the
|
|
bytes with the right content type.
|
|
|
|
## 7. Frontend: expand `/settings/pctypemapping`
|
|
|
|
The current page (`PCTypeMappingSettings.vue`, "Collector PC Types") is a read-
|
|
only-ish table of `pxetype -> ComputerType` dropdowns. It grows into the imaging-
|
|
PC-type manager:
|
|
|
|
- **Scopes list**: add/rename/delete imaging PC types; each still carries its
|
|
`ComputerType` mapping (that column moves from a setting into `manifestscopes`).
|
|
A `phase` toggle (runtime vs preinstall). Common scope flagged.
|
|
- **Scope detail / manifest editor**: an ordered, drag-reorderable list of
|
|
entries (the ordering contract made visible). Each entry is a typed form -
|
|
the visible fields switch on `entrytype` (MSI shows Installer+InstallArgs;
|
|
PS1 shows Script+Args; File shows Source+Destination; Registry shows the Reg*
|
|
quartet). Detection block with a method dropdown that reveals only the
|
|
relevant Detection* fields. Filter chips for PCTypes/hostnames/machine numbers.
|
|
InUseCheck sub-editor. Payload source selector (smb/http/inline) with upload
|
|
for the latter two.
|
|
- **Preview + publish**: show the generated JSON; "publish" bumps
|
|
`manifestversion` and writes a history snapshot (mirrors the current
|
|
`_meta/history/<date>-<scope>.json` backups).
|
|
- **Desired vs observed**: because the collector already reports
|
|
`installedversions`, the scope page can show, per entry, how many fleet PCs
|
|
match the expected detection value. This is the payoff of unifying the two.
|
|
|
|
This is an ADR-010 settings card contributed by the geenforce plugin, so it only
|
|
appears when the plugin is enabled.
|
|
|
|
## 8. Client change (minimal, staged)
|
|
|
|
`GE-Enforce.ps1` today: mount W:, read `<scope>\manifest.json`, hand to
|
|
`Install-FromManifest`. New path: GET the manifest from shopdb, write it to the
|
|
same local location the engine reads, then run the engine unchanged. That is the
|
|
smallest possible client delta - the engine, detection logic, self-heal, and
|
|
SMB payload resolution all stay identical. Only the *source of the JSON* moves
|
|
from file to HTTP.
|
|
|
|
Payloads: `smb` rows need no client change. `http`/`inline` rows need a small
|
|
fetch-and-verify helper (download to temp, check SHA256, then the existing
|
|
installer action runs against the local copy). The engine already stages network
|
|
EXEs to temp, so this is an extension, not a rewrite.
|
|
|
|
Auth: the client already has SFLD credentials in
|
|
`HKLM:\SOFTWARE\GE\SFLD\Credentials`. Add a shopdb service token (a
|
|
`geenforce.fetch` PAT) provisioned the same way (Azure DSC writes it to
|
|
registry), sent as `X-API-Key`. If shopdb is unreachable, the client falls back
|
|
to the last-known-good manifest cached locally (fail-safe: never leave a PC
|
|
unmanaged because the web app is down). This mirrors today's "creds missing =
|
|
exit 0, retry next cycle" resilience.
|
|
|
|
## 9. Cutover strategy
|
|
|
|
The manifest is desired-state that runs as SYSTEM and installs software fleet-
|
|
wide. A bad cutover = a fleet-wide mis-install. Stage it:
|
|
|
|
1. **Import + parity.** Write a one-shot importer that reads the current
|
|
on-share manifests (common + every `gea-shopfloor-*` + preinstall.json) into
|
|
the new tables. Then generate JSON back out and `diff` against the originals
|
|
until byte-identical for every scope. This proves the model is lossless
|
|
before anything depends on it. (This is the same discipline as the ADR-001
|
|
data migration.)
|
|
2. **Shadow mode.** shopdb serves the manifest at a new endpoint; a canary PC
|
|
fetches from shopdb but ALSO reads the share, and logs any diff. No install
|
|
behavior changes. Run across one of each PC type for a few cycles.
|
|
3. **Read cutover, payloads still SMB.** Flip GE-Enforce to source the JSON from
|
|
shopdb (payloads stay `smb`). The blast radius is only "where the JSON comes
|
|
from"; the bytes and engine are unchanged. Keep the share manifests as the
|
|
rollback (revert the dispatcher one-liner).
|
|
4. **Payload migration (optional, per entry).** Move small scripts/configs to
|
|
`inline`/`http` opportunistically. Leave big MSIs on SMB indefinitely - SMB
|
|
is the right transport for them.
|
|
5. **Author in shopdb.** Once read-cutover is stable, new manifest edits happen
|
|
in the shopdb UI and the on-share JSON is retired (or auto-exported as a
|
|
backup for break-glass).
|
|
|
|
Rollback at every stage is a one-line dispatcher revert, because the engine and
|
|
payload layout never stop working from the share.
|
|
|
|
## 10. Risks / open questions
|
|
|
|
- **The engine is the contract.** Any drift between shopdb's generated JSON and
|
|
what `Install-FromManifest.ps1` expects is a fleet-wide install bug. The
|
|
byte-identical round-trip test (step 1) is non-negotiable, and the plugin must
|
|
pin which engine lib version it targets (>= 2.6 for `_CmmVersion`).
|
|
- **PCTypes alias graph** must be seeded and kept in sync with
|
|
`Install-FromManifest.ps1:463-475`. If the engine's alias map changes, ours
|
|
must too. Consider having the engine fetch the alias map from shopdb as well,
|
|
so there is one source.
|
|
- **Availability coupling.** GE-Enforce currently depends only on SMB. Adding an
|
|
HTTP dependency on shopdb means shopdb downtime could stall enforcement -
|
|
hence the last-known-good local cache in section 8. Must be built in from day
|
|
one, not bolted on.
|
|
- **Secrets in payloads.** Some config drops (site-config, credentials) may
|
|
contain secrets. `inline` payloads live in the shopdb DB - those must respect
|
|
the existing "secrets stay in .env, not the settings table" rule. Likely keep
|
|
any secret-bearing payload on SMB with ACLs, never inline.
|
|
- **Preinstall runner** is a separate consumer (`00-PreInstall-*` at imaging,
|
|
before enrollment). It may not have a shopdb token yet at that point in the
|
|
imaging sequence. Preinstall may need to stay share-sourced longer than
|
|
runtime, or fetch a bootstrap manifest anonymously over HTTP.
|
|
- **This is a big build.** Realistically phased: (P1) model + importer + parity
|
|
test; (P2) admin API + CRUD; (P3) frontend editor on /settings/pctypemapping;
|
|
(P4) client fetch + shadow mode; (P5) read cutover; (P6) payload migration.
|
|
P1 is the gating de-risk - if the round-trip is not lossless, stop.
|
|
|
|
## 11. Relationship to existing work
|
|
|
|
- Replaces `plugins/computers/pctypemap.py` (the thin `pctypemap_<pxetype>`
|
|
settings) - the pctype -> ComputerType mapping becomes the `computertypeid`
|
|
column on `manifestscopes`. Migrate those settings in, then retire them.
|
|
- Also folds in the metrology mapping now living in `pctypemap.py`
|
|
(`METROLOGY_TOOL_MAP`). The collector already auto-creates a MeasuringTool
|
|
asset and a directional PC->tool `controls` relationship when it sees a
|
|
metrology pctype (CMM / Keyence / Genspect / wax-and-trace); the PC stays a
|
|
shopfloor PC. A metrology scope in the manifest model should carry the
|
|
attached-measuring-tool type alongside its ComputerType so imaging and
|
|
collector agree on what device the scope implies.
|
|
- Reuses the collector's token machinery (PAT + `X-API-Key` + scopes) for the
|
|
client-facing endpoints.
|
|
- Reuses `get_permissions()` (contract 0.10.0) for `geenforce.manage` /
|
|
`geenforce.fetch`.
|
|
- Pairs with the collector: desired-state (this plugin) + observed-state
|
|
(collector) enable a fleet compliance view.
|
|
|
|
## 12. Recommendation
|
|
|
|
Feasible and a strong architectural fit, but it is a multi-phase build with a
|
|
fleet-wide blast radius. The single most important gate is P1: import the real
|
|
manifests and prove byte-identical round-trip. Do not build the UI or touch a
|
|
client until that parity test is green. If and when we proceed, this warrants a
|
|
new ADR (ADR-012: GE-Enforce manifest ownership) capturing the desired-state
|
|
model, the SMB/HTTP/inline payload contract, and the fail-safe cache.
|