Files
pxe-server/docs/ge-enforce-v2-architecture.md
cproudlock ce604adcda Renumber PXE LAN from 10.9.100.0/24 to 172.16.9.0/24
Single-site bay-stuck issue at WJ: GE Intune Report IP script filters
Get-NetIPAddress on StartsWith("10.") and posts everything matching
to the GE Tines webhook. Bays at WJ get the PXE LAN 10.9.100.x IP
captured and reported -> GE backend tags bays as on a non-corp 10.x
subnet -> dynamic group eligibility for SFLD policy never matches.
Other GE sites work because their PXE LANs aren't on 10.x at all.

Renumber PXE LAN to RFC1918 172.16.9.0/24 so the GE filter naturally
skips wired PXE addresses without any disable-NIC dance.

Server-side already in flight (netplan dual-bound, dnsmasq scope +
boot URL repointed, blancco preferences + grub.cfg + iPXE GetPxeScript
all sed'd to 172.16.9.1). This commit is the playbook / scripts /
docs side: 109 hits across 35 files sed'd in one shot.

After this lands + boot.wim is rebuilt + bays renumber off DHCP,
the 10.9.100.1 binding will be dropped from netplan as the final
cleanup step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:30:32 -04:00

15 KiB

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 uses 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

{
  "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 \\172.16.9.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:
    (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