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>
318 lines
15 KiB
Markdown
318 lines
15 KiB
Markdown
# 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 `\\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:
|
|
```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
|