Add GE-Enforce v2 architecture doc
Captures the full picture of how the manifest engine works, why scripts don't need self-heal entries (run from share), credential context (SYSTEM = computer account, requires Mount-SFLDShare for file-level reads), C:\Enrollment vs SFLD share copy distinction, and update workflows. Written in response to a session that wasted time adding redundant manifest entries because this wasn't documented. Companion to scripts/diagnostics/Capture-LockdownState.ps1 and the auditing script in pxe-images/Audit-SFLDShare.ps1.
This commit is contained in:
317
docs/ge-enforce-v2-architecture.md
Normal file
317
docs/ge-enforce-v2-architecture.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# GE-Enforce v2 architecture
|
||||
|
||||
Ongoing fleet enforcement layer for GE Aerospace shopfloor PCs. Replaces the
|
||||
v1 `Machine-Enforce.ps1` + `main\machineapps\machineapps-manifest.json`
|
||||
arrangement that DSC-based deployment retired.
|
||||
|
||||
## What it does
|
||||
|
||||
On every user logon (and on a periodic schedule), every shopfloor PC mounts
|
||||
the SFLD share with credentials provisioned by Azure DSC, reads the
|
||||
manifest(s) for its PC type, and runs `Install-FromManifest.ps1` to install,
|
||||
update, or repair anything whose detection rule fails. The detection-driven
|
||||
design means a freshly-imaged PC in a known-good state no-ops; an out-of-
|
||||
date or tampered PC self-heals on the next cycle.
|
||||
|
||||
The same entry-points handle:
|
||||
|
||||
- Initial shopfloor app install on first logon after PXE imaging.
|
||||
- Drift correction (e.g. user uninstalled UDC, vendor patch tampered with a
|
||||
config file, manifest version bumped fleet-wide).
|
||||
- One-shot per-bay actions like UDC data restore from a swap-source PC.
|
||||
|
||||
## Components
|
||||
|
||||
| Piece | Lives at | Role |
|
||||
|-------|----------|------|
|
||||
| `GE-Enforce.ps1` | `playbook/shopfloor-setup/common/GE-Enforce.ps1` | Logon dispatcher. Mounts share, finds manifests, calls Install-FromManifest. |
|
||||
| `Register-GEEnforce.ps1` | `playbook/shopfloor-setup/common/Register-GEEnforce.ps1` | Registers the scheduled task (`GE Shopfloor Machine Apps Enforce`) at imaging time. |
|
||||
| `Install-FromManifest.ps1` | `playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1` | Manifest interpreter. Iterates entries, runs detection, fires installers. |
|
||||
| `Mount-SFLDShare` | `playbook/shopfloor-setup/Shopfloor/lib/Restore-EDncReg.ps1` | Reads creds from `HKLM:\SOFTWARE\GE\SFLD\Credentials\*` and `net use`s the share with the SFLD user identity. |
|
||||
| Manifests | SFLD share `<scope>\manifest.json` | Declarative list of entries to enforce. |
|
||||
| Site config | `playbook/shopfloor-setup/site-config.json` (deployed to `C:\Enrollment\site-config.json`) | Per-PC-type share path overrides + startup/desktop/taskbar layout. |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
Imaging time (PXE)
|
||||
startnet.cmd xcopies shopfloor-setup -> W:\Enrollment\
|
||||
Run-ShopfloorSetup.ps1 runs the imaging-phase scripts
|
||||
Register-GEEnforce.ps1 registers the scheduled task
|
||||
|
||||
First user logon (post-imaging, post-SFLD-DSC creds delivered)
|
||||
Scheduled task fires GE-Enforce.ps1
|
||||
GE-Enforce reads HKLM:\SOFTWARE\GE\SFLD\Credentials -> Mount-SFLDShare W:
|
||||
Reads pc-type from C:\Enrollment\pc-type.txt
|
||||
For each scope (common, then <pctype>):
|
||||
Reads <scope>\manifest.json
|
||||
Calls Install-FromManifest.ps1 -ManifestPath ... -InstallerRoot W:\<scope>
|
||||
Install-FromManifest iterates each Application entry:
|
||||
Apply PCTypes filter (skip if entry doesn't apply to this PC)
|
||||
Apply TargetMachineNumbers filter (skip if applicable)
|
||||
Run Detection - if installed at expected version/hash/state, skip
|
||||
Else run installer per Type (MSI/EXE/CMD/PS1/File/INF/Registry)
|
||||
Log result to C:\Logs\Shopfloor\enforce-YYYYMMDD.log
|
||||
Unmounts W:
|
||||
|
||||
Every subsequent logon
|
||||
Same dispatcher fires. No-ops on already-correct state. Self-heals drift.
|
||||
```
|
||||
|
||||
## SFLD share layout (v2)
|
||||
|
||||
```
|
||||
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\
|
||||
_meta\
|
||||
README.md layout doc
|
||||
manifest-schema.json JSON schema for entries
|
||||
history\ timestamped backup of each manifest on push
|
||||
_outputs\
|
||||
ntlars-backups\ eDNC INI backups per machine number
|
||||
logs\ client-uploaded diagnostics
|
||||
|
||||
common\ applies to every PC type
|
||||
manifest.json
|
||||
apps\ MSI / EXE / CMD installers
|
||||
configs\ data files (XML, fonts, txt)
|
||||
scripts\ PS1 / BAT / CMD helpers
|
||||
|
||||
standard-machine\ Standard PC type, subtype Machine
|
||||
manifest.json
|
||||
apps\ configs\ scripts\
|
||||
|
||||
cmm\ display\ genspect\ keyence\ lab\ waxandtrace\
|
||||
per-PC-type
|
||||
```
|
||||
|
||||
`_meta`, `_outputs`, and per-PC-type dirs each have `apps/`, `configs/`,
|
||||
`scripts/` siblings to manifest.json, referenced by relative paths in manifest
|
||||
entries (`Source: scripts/Foo.ps1` => `<scope>\scripts\Foo.ps1`).
|
||||
|
||||
### v1 vs v2
|
||||
|
||||
v1 used `\shared\dt\shopfloor\main\machineapps\machineapps-manifest.json` as
|
||||
the single Standard-Machine manifest. v2 reorganized to per-PC-type
|
||||
manifests so CMM/Keyence/Display PCs get their own scope without filtering
|
||||
the whole fleet manifest by `PCTypes`. The v1 path may still exist on the
|
||||
share for backwards compat; v2 GE-Enforce ignores it. Do not edit the v1
|
||||
manifest for v2-deployed sites.
|
||||
|
||||
## Manifest entry schema
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2.0",
|
||||
"_comment": "...",
|
||||
"Applications": [
|
||||
{
|
||||
"_comment": "Per-entry context: install path, version pin reasons, rotation procedure",
|
||||
"Name": "<unique label, also shown in log>",
|
||||
"Type": "<MSI|EXE|CMD|PS1|File|INF|Registry>",
|
||||
"Installer": "<path-relative-to-scope>",
|
||||
"Source": "<for Type=File: path-relative-to-scope>",
|
||||
"Destination": "<for Type=File: absolute disk path>",
|
||||
"Script": "<for Type=PS1: path-relative-to-scope>",
|
||||
"Args": "<optional: extra args>",
|
||||
"InstallArgs": "<optional: msiexec/exe args>",
|
||||
"DetectionMethod": "<File|Registry|FileVersion|Hash|MarkerFile|ValueMatches|Always>",
|
||||
"DetectionPath": "<absolute disk path, or HKLM:\\... for Registry>",
|
||||
"DetectionName": "<for Registry: value name>",
|
||||
"DetectionValue": "<expected value or hash>",
|
||||
"PCTypes": ["Standard", "CMM", "..."],
|
||||
"PCSubTypes": ["Machine"],
|
||||
"TargetMachineNumbers": ["1234", "5678"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Type semantics (the part that's easy to get wrong)
|
||||
|
||||
| Type | What it does | Local artifact? | Self-heal pattern |
|
||||
|------|-------------|-----------------|-------------------|
|
||||
| `MSI` | `msiexec /i <share path>` + InstallArgs | Vendor footprint (uninstall reg key, files) | Detection on uninstall reg DisplayVersion or product File |
|
||||
| `EXE` | Direct exec of installer EXE | Vendor footprint | Same |
|
||||
| `CMD` | `cmd /c <share path>` (a wrapper script) | Whatever the wrapper deploys | Same |
|
||||
| `PS1` | `powershell.exe -File <share path>` - **executes from share, no local copy** | None - runs in place | NOT NEEDED. Update share file = next logon runs new code. No Hash entry required because there's nothing on disk to drift. |
|
||||
| `File` | `Copy-Item -Source <share> -Destination <disk>` | The file at Destination | **Hash detection IS needed.** Drift = re-copy. |
|
||||
| `INF` | `pnputil /add-driver` | Driver staged | Detection via PnP |
|
||||
| `Registry` | Detection-only (no install side; entry is purely a no-op trigger) | Whatever wrote the key | n/a |
|
||||
|
||||
**`Type=PS1` is for scripts that need to RUN every cycle, not be DEPLOYED.**
|
||||
Common pattern: `DetectionMethod: Always` (always fires). Used for one-shot
|
||||
cleanups (e.g. `Restore-UDCData.ps1` consumes a swap backup and self-no-ops
|
||||
when none is waiting).
|
||||
|
||||
If you want a PS1 to ALSO be deployed to disk somewhere, that's a separate
|
||||
`Type=File` entry copying it - very rarely useful since GE-Enforce already
|
||||
runs it from the share.
|
||||
|
||||
### Detection methods
|
||||
|
||||
- **`File`** - `Test-Path` on `DetectionPath`. Optional `DetectionName` for property.
|
||||
- **`Registry`** - `Test-Path` on registry key. With `DetectionName` + `DetectionValue`, must match.
|
||||
- **`FileVersion`** - PE FileVersion of `DetectionPath` matches `DetectionValue` exactly.
|
||||
- **`Hash`** - SHA256 of `DetectionPath` matches `DetectionValue`. Case-insensitive.
|
||||
- **`MarkerFile`** - `Test-Path` on a marker file (engine creates it after a successful PS1 run). For PS1 entries that should NOT re-fire after one success.
|
||||
- **`ValueMatches`** - Registry value exact match.
|
||||
- **`Always`** - Never matches; entry fires every cycle. For per-cycle cleanup/check scripts.
|
||||
|
||||
### Filters
|
||||
|
||||
- `PCTypes` - applies only if `C:\Enrollment\pc-type.txt` matches one of the listed values. Skips otherwise.
|
||||
- `PCSubTypes` - same, against `C:\Enrollment\pc-subtype.txt`.
|
||||
- `TargetMachineNumbers` - applies only if local `udc_settings.json` MachineNumber is in the list. Used for variant-specific (Fanuc/Okuma/Makino) entries.
|
||||
|
||||
## Credential context (the trap that bit us 2026-05-01)
|
||||
|
||||
GE-Enforce runs as `NT AUTHORITY\SYSTEM` (scheduled task at startup +
|
||||
logon). SYSTEM authenticates to remote SMB as the **computer account**
|
||||
(`DOMAIN\HOSTNAME$`), not as a user.
|
||||
|
||||
The SFLD share's ACL grants top-level enumeration to authenticated computers
|
||||
but file-level reads only to a specific SFLD user. So `Test-Path` on a UNC
|
||||
path from SYSTEM:
|
||||
|
||||
- **Top-level dir** like `\\tsgwp00525...\shared\dt\shopfloor\backup\udc\` -> True
|
||||
- **Bay-level subdir** like `...\backup\udc\3207\` -> True (depending on ACL)
|
||||
- **File-level** like `...\backup\udc\3207\CurrentData.json` -> **`$false`**, indistinguishable from "file not found"
|
||||
|
||||
Symptom: scripts that `Test-Path` raw UNC paths from SYSTEM context silently
|
||||
log "absent" / "no work" while the files actually exist. Real cause is
|
||||
access denied returning False. Failure mode is invisible.
|
||||
|
||||
**Always** use `Mount-SFLDShare` (in `Shopfloor\lib\Restore-EDncReg.ps1`)
|
||||
before file access. It reads creds from `HKLM:\SOFTWARE\GE\SFLD\Credentials\*`
|
||||
and `net use W: \\... /user:<sflduser> <pw> /persistent:no`. Subsequent
|
||||
file operations on `W:\...` succeed.
|
||||
|
||||
If creds are missing in the registry (e.g. SFLD-DSC bootstrap didn't run),
|
||||
`Mount-SFLDShare` returns `$false`. Scripts should fail-fast with a clear
|
||||
ERROR rather than continue with raw-UNC access.
|
||||
|
||||
## C:\Enrollment\shopfloor-setup\ vs SFLD share
|
||||
|
||||
Two separate copies of overlapping content with different roles:
|
||||
|
||||
| Path | Source | Used by | Updated when |
|
||||
|------|--------|---------|--------------|
|
||||
| `C:\Enrollment\shopfloor-setup\` | PXE imaging copy from `\\10.9.100.1\enrollment\shopfloor-setup\` | Imaging-flow scripts: `Run-ShopfloorSetup.ps1`, `Stage-Dispatcher.ps1`, `Set-MachineNumber.ps1` -> `Update-MachineNumber.ps1` | Re-image only |
|
||||
| SFLD share `\<scope>\` | Direct upload | GE-Enforce.ps1 / Install-FromManifest.ps1 (every logon) | Direct file upload to share |
|
||||
|
||||
Implication for hot-fixing scripts: a fix to `Restore-UDCData.ps1` needs to
|
||||
land on the SFLD share (`standard-machine/scripts/Restore-UDCData.ps1`) for
|
||||
the manifest engine to pick it up. Updating only the PXE enrollment share
|
||||
helps NEW imaging but not already-imaged PCs.
|
||||
|
||||
## site-config.json paths
|
||||
|
||||
Defines per-PC-type share roots used by Update-MachineNumber.ps1 and
|
||||
Backup-UDCData.ps1 logic outside the manifest engine:
|
||||
|
||||
| Field (under `pcProfiles.Standard-Machine`) | Used for |
|
||||
|---------------------------------------------|----------|
|
||||
| `machineappsSharePath` | v1 legacy machineapps share (mostly unused in v2) |
|
||||
| `ntlarsBackupSharePath` | per-machine `.reg` backups (eDNC) |
|
||||
| `udcBackupSharePath` | per-bay live UDC data backup (CurrentData.json + ArchivedData/) |
|
||||
| `udcSettingsSharePath` | per-bay `udc_settings_<n>.json` (UDC settings overrides) |
|
||||
|
||||
These paths are read by Update-MachineNumber.ps1 placeholder->real
|
||||
transition logic to mount the right share + restore the right data when a
|
||||
tech sets a real machine number on a previously-9999 PC.
|
||||
|
||||
## Update workflow
|
||||
|
||||
### Bumping a vendor app version
|
||||
|
||||
1. Drop new `.msi` / `.exe` in `<scope>\apps\` on the share.
|
||||
2. Update the matching manifest entry: `Installer` filename + `DetectionValue`.
|
||||
3. Save `<scope>\manifest.json`.
|
||||
4. Optional: copy current manifest to `_meta\history\<date>-<scope>.json`.
|
||||
5. Every PC of that scope picks up the change next logon.
|
||||
|
||||
### Rotating a deployed config (eMxInfo.txt, udc_webserver_settings.json)
|
||||
|
||||
1. Overwrite the file in `<scope>\configs\` on the share.
|
||||
2. Recompute SHA256:
|
||||
```powershell
|
||||
(Get-FileHash .\udc_webserver_settings.json -Algorithm SHA256).Hash
|
||||
```
|
||||
3. Paste into the manifest entry's `DetectionValue`.
|
||||
4. Save manifest.
|
||||
|
||||
### Hot-fixing a script (Restore-UDCData.ps1, etc.)
|
||||
|
||||
1. Patch script in repo.
|
||||
2. Copy to `<scope>\scripts\<name>.ps1` on the SFLD share.
|
||||
3. (No manifest edit needed - `Type=PS1` runs from share.)
|
||||
4. Every PC of that scope runs the new version next logon.
|
||||
|
||||
### Adding a new entry
|
||||
|
||||
1. Decide scope: cross-PC-type goes in `common/`, PC-type-specific in
|
||||
`<pctype>/`. Verify the entry isn't already present in either.
|
||||
2. Pick `Type` based on what the entry is: vendor installer, config file
|
||||
deploy, or per-cycle script run.
|
||||
3. For `File` type: include source on the share; pick `DetectionMethod=Hash`
|
||||
with a precomputed SHA256 so drift triggers re-deploy.
|
||||
4. For `PS1` type: place script in `<scope>\scripts\`; use
|
||||
`DetectionMethod=Always` for per-cycle, or `MarkerFile` for one-shot.
|
||||
5. For installer types: provide `Installer` path, `Type`, `InstallArgs`,
|
||||
detection rule that distinguishes "installed at this version" from
|
||||
"missing or wrong version".
|
||||
6. Add a `_comment` field documenting WHY this entry exists, where the
|
||||
target installs, and how to rotate the version.
|
||||
7. Save + push.
|
||||
|
||||
### Rolling back
|
||||
|
||||
History snapshots live at `_meta\history\<date>-<scope>.json`. Manual revert:
|
||||
copy old snapshot back over `<scope>\manifest.json`. Engine picks it up next
|
||||
cycle.
|
||||
|
||||
## Logs
|
||||
|
||||
Per-PC log file: `C:\Logs\Shopfloor\enforce-YYYYMMDD.log`
|
||||
|
||||
Format:
|
||||
|
||||
```
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] Mounted \\... as W:
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] standard\manifest.json not on share - no type-specific apps
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] ---- Processing scope: common ----
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] Manifest lists N app(s)
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] ==> Adobe Acrobat Reader DC
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] Already installed at expected version - skipping
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] ==> WJF Defect Tracker
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] msiexec: W:\common\apps\WJF_Defect_Tracker.msi
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] verbose log: C:\Logs\Shopfloor\msi-WJF_Defect_Tracker.log
|
||||
[YYYY-MM-DD HH:MM:SS] [INFO] Exit 0 - SUCCESS
|
||||
```
|
||||
|
||||
Per-installer logs go alongside (`msi-*.log`, per-script transcripts).
|
||||
PS1 entry stdout/stderr surfaces in the main enforce log.
|
||||
|
||||
If detection succeeds first time on a freshly-imaged PC, every entry logs
|
||||
"Already installed at expected version - skipping" - the no-op path
|
||||
proves the manifest is in sync with the image.
|
||||
|
||||
## Common audit failures and their cause
|
||||
|
||||
| Symptom | Likely cause |
|
||||
|---------|-------------|
|
||||
| `<vendor>` MSI redeploys every cycle | Detection rule's path or DetectionValue doesn't match the actual install footprint. Verify PE FileVersion is 4-part vs DisplayVersion is 3-part. Verify 32-bit installers go under `WOW6432Node`. |
|
||||
| Script logs "absent" or "no work" repeatedly while file is visible interactively | Running as SYSTEM with raw-UNC access. Switch to `Mount-SFLDShare`. |
|
||||
| New script change doesn't take effect | Edited the wrong copy. C:\Enrollment\ is imaging-time; SFLD share is runtime. |
|
||||
| Manifest entry skipped silently | `PCTypes` / `PCSubTypes` filter excludes this PC. Check `C:\Enrollment\pc-type.txt`. |
|
||||
| Two PCs racing for the same per-bay backup | Both have the same `udc_settings.json` MachineNumber. Find + decommission the second one. |
|
||||
|
||||
## See also
|
||||
|
||||
- `playbook/shopfloor-setup/_meta/README.md` (on the SFLD share local mirror) - canonical share layout doc
|
||||
- `pxe-images/Audit-SFLDShare.ps1` - audit script that walks the share and
|
||||
validates layout + canonical hashes + detection-rule sanity
|
||||
- `playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1`
|
||||
- imaging-time progress monitor that watches for the upstream signals
|
||||
(SFLD reg key, DSCDeployment.log, Consume Credentials task) before
|
||||
GE-Enforce can even start
|
||||
Reference in New Issue
Block a user