REFERENCES (section 13, was six lines, now a map) Where every referenced thing lives, so the document is usable by someone without today's context: this repo, the PXE server share, shopdb-flask, the on-bay log locations, and the evidence bundles each claim came from. Plus re-derivation recipes - pulling startnet out of boot.wim, extracting customizations.xml from an 8 GB .ppkg without unpacking it, reading the provisioning evtx, and the two validation commands - and the 2026-08-06 commit list. LTSC AUTOSTART - CORRECTED Section 7.1 previously argued PESetup might already auto-start: AutoStartCountDown is 31, disableauto.json is absent, and the operator on 579C144 clicked Next at 11 seconds, so nobody had waited long enough to find out. Per the team's operational experience that is wrong for our media. LTSC builds cannot autostart - the SELECT OS confirmation is mandatory regardless of the countdown, and every image we produce is Windows 11 LTSC 24H2. So the countdown is not a route to zero-touch. That promotes re-implementing the imaging step from a contingency to the actual decision, and the risk table and open questions now frame it as "re-implement, or accept one click per build" - zero-touch being the only thing that requires it. The hands-off test is kept, but only to confirm and record the behaviour first-hand.
593 lines
28 KiB
Markdown
593 lines
28 KiB
Markdown
# Zero-touch, ShopDB-driven imaging - proposal
|
|
|
|
**Status:** proposal, nothing built
|
|
**Written:** 2026-08-06
|
|
**Author's note:** every claim about current behaviour below is from evidence
|
|
gathered on 2026-08-06 - decompiled binaries, live logs off bay 579C144, and the
|
|
live share. Where something is inferred rather than observed it says so.
|
|
|
|
---
|
|
|
|
## 1. What we want
|
|
|
|
Two ideas, one architecture.
|
|
|
|
**A. The image becomes thin.** It carries only what must happen at image time -
|
|
partition, apply the WIM, join Entra, install the enforce client. Everything else
|
|
(PC type, subtype, tool association, apps, settings) is data, pulled at first
|
|
boot from the ShopDB-Flask API and enforced continuously by GE-Enforce.
|
|
|
|
**B. Imaging becomes a job, not a menu.** Open a machine in ShopDB, click
|
|
*Create this PC*, pick the zeroed box that is sitting in WinPE waiting, and it
|
|
images start to finish with nobody touching the keyboard - reporting progress
|
|
onto that machine's record in real time.
|
|
|
|
Between them: no menus in WinPE, no per-type media, no decisions baked into text
|
|
files by whoever happened to be standing at the bay.
|
|
|
|
---
|
|
|
|
## 2. Why - what today actually costs
|
|
|
|
Today every decision is made in WinPE, written to files, and carried through a
|
|
chain where each link can fail silently:
|
|
|
|
```
|
|
startnet.cmd -> unattend (4 passes) -> FirstLogonCommands (18 orders)
|
|
-> ppkg -> DSC -> GE-Enforce
|
|
```
|
|
|
|
On 2026-08-06 alone, that chain produced:
|
|
|
|
| Failure | Root cause | How long it hid |
|
|
|---|---|---|
|
|
| Every build stopped at a dialog | one unattend `Path` at 676 chars against a 259 limit | ~1 day, and it masked the next two |
|
|
| Bays staged nothing, silently | a volume scan raced PESetup's `diskpart` and staged into a partition being erased | weeks (4 Display bays sat "green") |
|
|
| Deployment destroyed 4s after autologon | an at-logon task ran `-ManualFallback`, which is `sysprep /oobe /reboot` | until traced through Panther |
|
|
| Machines named `E<serial>` not `F<serial>` | our script re-applied a rename over the package's own | unknown |
|
|
| Six copies reported success having moved 0 bytes | unconditional `echo` after `robocopy` | weeks |
|
|
|
|
The pattern is identical in every case: **a decision made early, carried far, and
|
|
verified nowhere.** Moving decisions to a running, networked machine with an API
|
|
and a UI makes them recoverable - re-runnable, correctable, and observable.
|
|
|
|
---
|
|
|
|
## 3. What we already have to build on
|
|
|
|
This is not a greenfield project. Most of the mechanism exists.
|
|
|
|
### 3.1 ShopDB-Flask
|
|
- Assets keyed by serial; CMM bays, measuring tools, machine numbers already modelled
|
|
- Collector API: `POST /shopdb/api/collector/computers` (X-API-Key), ingests inventory by serial
|
|
- GE-Enforce plugin: scopes, manifests, **content-addressed payload store**
|
|
(`instance/geenforce/payloads/<sha256>`), `GET /api/geenforce/payload/<sha256>`
|
|
- Display scope (`plugins/geenforce/seed_display_scope.py`) already proves the pattern:
|
|
read a local subtype, prefer a **server-side role resolved by device IP**, pull
|
|
payloads over HTTPS, write the kiosk shortcut, sweep stale ones, self-heal each cycle
|
|
- Per-device role API: `GET /api/dashboarddefaults/display-role`
|
|
|
|
### 3.2 PXE server
|
|
- Per-bay state: `/var/log/pxe-imaging/<serial>.json` with stage history
|
|
- `winpe-status-push.ps1` already POSTs stage updates from WinPE
|
|
- Data-driven menus already: `menu.json` on the share, rendered by
|
|
`select-shopfloor-type.ps1`, edited by the webapp
|
|
- Imaging dashboard with per-bay tiles
|
|
- As of today: staging verification, imaging-log harvest to
|
|
`enrollment/imaging-logs/<serial>/`, `preflight.ps1` at the bay,
|
|
`preflight.py` + `lint-unattend.py` + `lint-driver-catalogue.py` +
|
|
`share-drift.py` on the server
|
|
|
|
### 3.3 Hardware
|
|
- Second NIC already present and unused: `enp0s31f6` (onboard, `e1000e`, currently down)
|
|
- PXE LAN currently on a USB adapter, `enx34c8d6b11010`
|
|
- That adapter was silently capped at USB 2.0 (327 Mb/s measured); moved to a
|
|
USB 3 port on 2026-08-06 and now runs at line-rate gigabit (**937 Mb/s measured**)
|
|
|
|
---
|
|
|
|
## 4. Target architecture
|
|
|
|
### 4.1 Three (or one) pre-imaged types
|
|
|
|
Proposed: **Displays, Inspection, Shopfloor** as pre-imaged stock, with the
|
|
subtype chosen later by the user at first boot.
|
|
|
|
**Open question worth settling early:** if the base image is identical and all
|
|
configuration comes from the API, what actually differs between the three?
|
|
Drivers are selected by PESetup per *model*, not per type. Unless disk layout or
|
|
a must-be-offline application differs, **one image** is better - three media trees
|
|
mean three things to keep in sync, and drift is what caused the day-long outage.
|
|
|
|
### 4.2 First-boot selection
|
|
|
|
A friendly PS1 runs when no PC type is recorded yet:
|
|
|
|
```
|
|
Inspection -> CMM | Genspect | Wax Trace | Keyence
|
|
└─ CMM / Keyence / WaxTrace -> which measuring tool?
|
|
(list served from ShopDB assets, not a CSV)
|
|
Displays -> Lobby | Dashboard | 3D Print Kiosk
|
|
Shopfloor -> Collections | No Collections | Common | Heattreat | Part Marker | ...
|
|
```
|
|
|
|
The tool selection is the important half: it drives PC-DMIS version, FormTracePak
|
|
version, DODA flag, NTLARS/eDNC registry, UDC settings, machine number.
|
|
|
|
**Design points:**
|
|
|
|
- **Server is the source of truth.** The selection POSTs to ShopDB keyed on
|
|
serial; the local file is a cache. A mis-selection is then fixable from a
|
|
browser, not by walking to the bay.
|
|
- **Re-runnable.** Available from the Start menu, shows what was chosen and what
|
|
was installed. People pick wrong; that must not mean re-imaging.
|
|
- **Audited.** ShopDB records who chose what and when - that is what separates
|
|
"misconfigured" from "chose wrong" six months later.
|
|
- **Do not require the LAPS password.** Handing a shop-floor user the local admin
|
|
password to run a selection dialog is a real control change, and the app runs
|
|
elevated. Prefer a SYSTEM-run UI on the logon desktop, or a kiosk shell.
|
|
|
|
### 4.3 Per-machine configuration records
|
|
|
|
Today a machine's identity is scattered:
|
|
|
|
| What | Where today | Size |
|
|
|---|---|---|
|
|
| eDNC / NTLARS registry | `_ntlars-backups/` - 147 per-bay `.reg` files | ~1.5 MB total |
|
|
| WaxTrace bay config | `bay-config.csv` - 15 bays, 7 FTPak versions, per-bay user ID | small |
|
|
| CMM bay config | `cmm-bay-config.csv` -> version.txt, doda.txt | small |
|
|
| UDC settings | `pre-install/udc-backups/`, per-bay | ~240 MB per bay |
|
|
| Machine number | `machine-number.txt` written by startnet | trivial |
|
|
|
|
Every bay gets **all** of it staged, then picks its own.
|
|
|
|
Proposed: one record per machine in ShopDB.
|
|
|
|
```
|
|
ShopDB asset (key: serial and/or machine number)
|
|
├─ pctype + subtype -> selects the GE-Enforce scope
|
|
├─ tool association -> CMM bay, Keyence model, FTPak version, DODA
|
|
├─ machine number -> UDC / eDNC naming
|
|
└─ payload refs (sha256) -> NTLARS .reg, UDC backup, per-bay settings
|
|
```
|
|
|
|
**Reference payloads, do not embed them.** NTLARS `.reg` files carry binary
|
|
values and UDC backups are hundreds of MB. GE-Enforce already has a
|
|
content-addressed blob store with an HTTP fetch endpoint - the record names the
|
|
payload, the store holds it.
|
|
|
|
This also removes a live bug class. `Restore-UDCData.ps1` and
|
|
`Update-MachineNumber.ps1` each mount SMB backup shares on their own drive
|
|
letters, which collided with GE-Enforce's `W:` and killed the manifest entry
|
|
running next (fixed in `66c24b5` by moving them to `R:` and `N:`). Payloads over
|
|
HTTPS remove drive-letter juggling entirely.
|
|
|
|
Migration is a one-time import of two CSVs and 147 `.reg` files. A script, not a
|
|
project.
|
|
|
|
---
|
|
|
|
## 5. Zero-touch: ShopDB-triggered imaging
|
|
|
|
### 5.1 Flow
|
|
|
|
```
|
|
1. Zeroed PC, boot order = IPv4 only. PXE boots into WinPE.
|
|
2. WinPE reports serial + MAC + model, then WAITS.
|
|
3. ShopDB shows it under "awaiting assignment".
|
|
4. Operator opens the target machine -> "Create this PC" -> picks the waiting box.
|
|
5. WinPE polls, receives a job:
|
|
{ pctype, subtype, tool/bay, tenant, purpose, target asset id }
|
|
and images with NO menus.
|
|
6. Progress streams to that machine's record in real time.
|
|
7. On completion the PC binds to the asset record.
|
|
```
|
|
|
|
Every menu in `startnet.cmd` becomes a field in the job. If no job exists, it
|
|
falls back to today's interactive menu and displays its own serial so one can be
|
|
created.
|
|
|
|
### 5.2 Safety - this is a remote-triggered disk wipe
|
|
|
|
Clicking a web button destroys a machine, and the target is chosen from a list.
|
|
The nightmare is picking the wrong row.
|
|
|
|
**Mitigation:** the waiting PC displays a short code on screen; the job cannot be
|
|
claimed unless that code matches what the operator confirms in ShopDB. Cheap, and
|
|
it makes wrong-machine wipes structurally hard rather than merely unlikely.
|
|
|
|
Additionally: a job may only target a machine currently in the waiting state, and
|
|
the wipe must be logged against both the operator and the asset.
|
|
|
|
### 5.3 Real-time record updates
|
|
|
|
Each phase already produces data; it simply is not joined up.
|
|
|
|
| Phase | Existing source | Lands on the record as |
|
|
|---|---|---|
|
|
| PXE boot | dnsmasq lease + WinPE check-in | serial, MAC, model, "imaging started" |
|
|
| WIM apply | `winpe-status-push.ps1` | live stage / progress |
|
|
| Staging | `winpe-staging.log` + harvested bundle | what payload actually landed |
|
|
| First boot | collector API | CPU, RAM, disk, OS build |
|
|
| Enrollment | `criticalChecks.json`, `TokenMatch.json` | Entra join state, tenant, purpose |
|
|
| Steady state | GE-Enforce report | installed apps, drift, self-heal events |
|
|
|
|
End state is one timeline per machine: *imaged 14:32 -> staged
|
|
gea-shopfloor-cmm, bay CMM4 -> enrolled GCCH_SH -> PC-DMIS 2019 R2 installed ->
|
|
enforcing clean since*.
|
|
|
|
**Two rules so it does not rot:**
|
|
|
|
1. **Push, best-effort.** If ShopDB is unreachable the bay must still image.
|
|
Queue events on the PXE server and drain them; the harvested logs are the
|
|
durable local copy.
|
|
2. **One writer per field.** Imaging status from the PXE server, inventory from
|
|
the collector, app state from GE-Enforce. Two writers on one field is the
|
|
exact bug that ate 2026-08-06.
|
|
|
|
---
|
|
|
|
## 6. Network design
|
|
|
|
### 6.1 Dual-homed PXE server
|
|
|
|
- **PXE side** - isolated `172.16.9.0/24`, dnsmasq DHCP bound to that adapter
|
|
- **GE side** - corporate, reachable from anywhere; carries ShopDB API traffic,
|
|
the job queue, status pushes, and (if wanted) the BPRT token fetch
|
|
|
|
The onboard `enp0s31f6` is free for this today.
|
|
|
|
### 6.2 Security prerequisites - these gate the whole thing
|
|
|
|
Checked 2026-08-06:
|
|
|
|
**Already safe.** dnsmasq has `listen-address=172.16.9.1` + `bind-dynamic`, so it
|
|
binds only to the interface holding that address. DHCP will **not** leak onto a
|
|
GE-side NIC. This was the failure I most expected and it is already handled.
|
|
|
|
**Not safe yet:**
|
|
|
|
```
|
|
smb.conf no `interfaces =` / `bind interfaces only` -> Samba listens on ALL interfaces
|
|
ufw every rule is ALLOW IN Anywhere, not interface-scoped:
|
|
22/tcp SSH (account is pxe/pxe)
|
|
445/tcp shares (pxe-upload, password in scripts)
|
|
69/udp TFTP
|
|
9009,9011 webapp
|
|
```
|
|
|
|
The moment a GE-side interface comes up, all of that is corporate-reachable.
|
|
|
|
**Required before dual-homing:**
|
|
1. Scope every UFW rule to the PXE interface (`ufw allow in on <pxe-if> ...`)
|
|
2. `interfaces = <pxe-if>` + `bind interfaces only = yes` in `smb.conf`
|
|
3. Expose deliberately on the GE side only what must be - realistically the
|
|
webapp, behind real authentication
|
|
4. Reconsider SSH credentials if the host becomes corporate-reachable
|
|
|
|
*(Correction to an earlier note: the shares are **not** guest-accessible -
|
|
`guest ok = no` on every share, with `map to guest = bad user`. They require the
|
|
`pxe-upload` account. That account's password is in scripts, so it is not a
|
|
control once 445 is corporate-reachable.)*
|
|
|
|
### 6.3 What dual-homing unlocks
|
|
|
|
- Job queue sync and live status without the share as intermediary
|
|
- BPRT token fetch (`mcl.dwcdn.geaerospace.com`) reachable at imaging time
|
|
- Possibly the Entra join during imaging rather than after re-cabling - see §8.2
|
|
|
|
---
|
|
|
|
## 7. The PESetup question
|
|
|
|
### 7.1 Autostart does not apply to our media
|
|
|
|
`AutoStartCountDown = 31` exists in the decompiled settings, and
|
|
`disableauto.json` - whose *presence* disables autostart - is absent from the
|
|
media. On the surface that suggests a 31-second countdown, and the observed log
|
|
is consistent with an operator simply clicking before it fired:
|
|
|
|
```
|
|
11:30:07.74 'Selection View' started (LTSC warning displayed)
|
|
11:30:18.63 Btn 'Next' pressed.
|
|
```
|
|
|
|
**However:** per the team's operational experience, **LTSC builds cannot
|
|
autostart**. The SELECT OS step warns when the media holds an LTSC image
|
|
("99% of the time only for ShopFloor. Are you sure you want 'LTSC'?") and that
|
|
confirmation is mandatory regardless of the countdown. Our media is
|
|
`Windows 11 LTSC 24H2` (`OperatingSystem.json`, build 26100, id 20), so every
|
|
image we produce hits it.
|
|
|
|
Consequence: **the countdown is not a route to zero-touch for us.** The 40-second
|
|
hands-off test is still worth doing once to confirm the behaviour first-hand and
|
|
record it - but plan on the answer being "it waits".
|
|
|
|
That promotes §7.2 from a contingency to the actual decision.
|
|
|
|
### 7.2 Re-implementation - now the likely path, and tractable
|
|
|
|
Nothing in the mechanism is proprietary - only the data, which we already own and
|
|
already parse.
|
|
|
|
| PESetup step | Equivalent |
|
|
|---|---|
|
|
| `PrepareDisk` | `diskpart /s diskpartEFI.txt` |
|
|
| `PrepareUnattend` | token substitution (`%serialnumber%`, `*arch*`) + `Packages.xml` merge |
|
|
| `CopyTools` / `CopyPackages` / `CopyHWPackages` | robocopy |
|
|
| `CopyDrivers` | select by model from `HardwareDriver.json`, unzip |
|
|
| `ApplyImage` | `DISM /Apply-Image` per `OperatingSystem.json` |
|
|
| `ApplyPackages` / `ApplyLanguagePacks` | `DISM /Add-Package` |
|
|
| `ApplyUnattend` | `DISM /Apply-Unattend` |
|
|
| `CreateBCD` / `CopyWinRE` | `bcdboot`, `reagentc` |
|
|
|
|
A few hundred lines of PowerShell. We would also get to fix the traps documented
|
|
in `docs/PESETUP-INTERNALS.md`: substring first-match-wins driver matching, a
|
|
family filter that knows only Latitude/OptiPlex/Precision, and a driver miss
|
|
being a **warning** rather than a failure.
|
|
|
|
**The cost is ownership, not effort.** `Release.json` points at MCL auto-update
|
|
URLs, so GE expects this media to self-update. Diverging means tracking their
|
|
format changes indefinitely, plus a likely compliance conversation about the
|
|
mandated imaging tool. Since the LTSC gate means autostart will not save us
|
|
(§7.1), this is a decision to take deliberately rather than a fallback.
|
|
|
|
### 7.3 What we would keep either way
|
|
|
|
Image-time preflight stays valuable regardless: secure boot (PESetup hard-fails
|
|
without it), >=120 GB disk (`MinRequiredSpaceWithoutCompression`), driver match
|
|
for the model, media age (30-day expiry). Already built as
|
|
`playbook/scripts/preflight.ps1`.
|
|
|
|
---
|
|
|
|
## 8. Enrollment
|
|
|
|
### 8.1 How it works today
|
|
|
|
- Six MCL packages on the media: `PERS` / `SH` / `LOAN` x `GCCH` / `RoW`, plus
|
|
the SFLD package for shopfloor
|
|
- `Start-BulkEnrollOrchestrator.ps1` asks tenant + purpose via
|
|
`Select-PCConfig.ps1` (a GUI), then applies the matching package
|
|
- `Insert-BPRTToPPKG.ps1` fetches an **encrypted token table** from
|
|
`mcl.dwcdn.geaerospace.com`, decrypts with a static passphrase
|
|
(PBKDF2 310k / SHA-256 / AES-CBC), matches on **Tenant AND Purpose**, injects
|
|
the token and stamps the real expiry into the filename
|
|
(`Exp_XXXXXXXX` -> `Exp_yyyyMMdd`)
|
|
- The package itself sets `<DNSComputerName>F%SERIAL%</DNSComputerName>` and
|
|
performs the Entra join
|
|
- A human then assigns the device category in Intune
|
|
|
|
As of 2026-08-06 the tenant/purpose choice can be pre-seeded at the PXE menu
|
|
(`C:\Enrollment\pc-config.txt`, consumed by a shim at the path the orchestrator
|
|
already calls). Injection deliberately stays at first logon - it needs internet.
|
|
|
|
### 8.2 The constraint that shapes pre-imaged stock
|
|
|
|
The BPRT token is stamped into the package with an expiry. A PC imaged, powered
|
|
off, and left in a cupboard for three months has a **dead token** and will never
|
|
join - and the failure looks identical to "not on the production network yet",
|
|
because `Entra ID Joined: false` right after imaging is normal.
|
|
|
|
Three options, pick deliberately:
|
|
|
|
1. **Join at imaging time** - requires the production network at the bench
|
|
(which dual-homing could provide), and pre-imaged stock is then already joined
|
|
2. **Join at first power-on** - cleaner logistically, but stock has a shelf life
|
|
bounded by token expiry
|
|
3. **Re-inject on demand** - first boot detects a dead token and fetches a fresh
|
|
one; needs the machine to reach the token blob
|
|
|
|
Whichever is chosen, the *silent* failure must be closed: a check that
|
|
distinguishes "token expired" from "no network yet".
|
|
|
|
---
|
|
|
|
## 9. Ownership model
|
|
|
|
Four times on 2026-08-06 two systems owned one thing and the last writer won
|
|
silently. A clean slate is the moment to fix that. See `docs/OWNERSHIP.md`.
|
|
|
|
| Concern | Owner | Not |
|
|
|---|---|---|
|
|
| Computer name | the provisioning package (`F%SERIAL%`) | any script in this repo |
|
|
| Drive letters during imaging | PESetup (`W:`, hardcoded 9 places) | anything running `diskpart` concurrently |
|
|
| Entra enrollment | the package + orchestrator | `run-enrollment.ps1` |
|
|
| Kiosk / display targets | GE-Enforce scope | `site-config.json` (backstop only) |
|
|
| Imaging status | PXE server | the collector |
|
|
| Inventory | collector API | the PXE server |
|
|
| Installed app state | GE-Enforce reports | anything else |
|
|
| Job queue | **ShopDB** (owns assets) | the PXE webapp (executor only) |
|
|
|
|
---
|
|
|
|
## 10. Phased delivery
|
|
|
|
Each phase is useful standing alone. No phase requires the next.
|
|
|
|
**Phase 0 - settle the unknowns (hours)**
|
|
- Confirm the LTSC autostart gate first-hand (one bay, hands off, 40 seconds)
|
|
and record it - expected to wait, per operational experience
|
|
- Decide: re-implement the imaging step, or accept one click per build
|
|
- Decide: one image or three
|
|
- Decide: join at imaging time, at first power-on, or re-inject
|
|
|
|
**Phase 1 - visibility (small)**
|
|
- WinPE reports serial / MAC / model on boot; "awaiting assignment" list in the webapp
|
|
- Relay imaging status to ShopDB over the GE side
|
|
- *Useful immediately: you can see what is booting and what it did.*
|
|
|
|
**Phase 2 - dual-home safely (small, gating)**
|
|
- Interface-scope UFW, bind Samba, bring up `enp0s31f6`
|
|
- Nothing else proceeds safely until this is done
|
|
|
|
**Phase 3 - job queue**
|
|
- Job model + API in ShopDB (create / claim / complete), with the on-screen
|
|
confirmation code
|
|
- WinPE polls once where the menu is today; falls back to the menu if no job
|
|
|
|
**Phase 4 - first-boot selection**
|
|
- Selection UI, server-backed lists, POST the choice, re-runnable
|
|
- Retire the WinPE sub-menus for subtype and tool association
|
|
|
|
**Phase 5 - per-machine records**
|
|
- Import the CSVs and `.reg` files into ShopDB with payload refs
|
|
- Retire `bay-config.csv`, `cmm-bay-config.csv`, and staging all 147 `.reg` files
|
|
to every bay
|
|
|
|
**Phase 6 - thin the image**
|
|
- Remove per-type media (`_media/<pctype>`), the staging block, the Office ppkg
|
|
variants, the CMM bay picker, the WaxTrace ISO cherry-pick
|
|
- WinPE's job becomes: partition, apply, join, install enforce client, reboot
|
|
|
|
---
|
|
|
|
## 11. Risks
|
|
|
|
| Risk | Why it matters | Mitigation |
|
|
|---|---|---|
|
|
| Remote-triggered wipe hits the wrong machine | destructive, irreversible | on-screen confirmation code; job may only target a waiting machine; log against operator and asset |
|
|
| ShopDB becomes a single point of failure | today a bay configures from the share with ShopDB down | GE-Enforce ETag / last-known-good cache; explicit failure UI, never a silent stall |
|
|
| Dual-homing exposes the PXE server | SSH `pxe/pxe`, SMB, TFTP currently `ALLOW IN Anywhere` | Phase 2 gates everything |
|
|
| BPRT expiry on shelf stock | silent, indistinguishable from "no network yet" | pick a join strategy (§8.2) and add an explicit expiry check |
|
|
| Selection UI becomes the new single point of correctness | wrong subtype = wrong PC-DMIS, wrong bay config | re-runnable, server-corrected, audited |
|
|
| Re-implementing PESetup | ongoing MCL format tracking + a likely compliance conversation | scope it against "one click per build" - zero-touch is the only thing that needs it |
|
|
| Big payloads over the corporate network | PC-DMIS, FormTracePak ISOs (~2 GB each), Keyence | GE-Enforce supports `smb` / `http` / `inline` per entry - decide per payload |
|
|
| Three images drift apart | drift caused the 2026-08-06 outage | prefer one image; `share-drift.py` guards what remains |
|
|
|
|
---
|
|
|
|
## 12. Open questions
|
|
|
|
1. One image or three? What genuinely differs between them?
|
|
2. Join at imaging time, at first power-on, or re-inject on demand?
|
|
3. Is the LTSC confirmation truly unskippable? (expected yes - it decides
|
|
whether zero-touch requires re-implementing the imaging step)
|
|
4. Is replacing the GE-mandated imaging tool acceptable, if it comes to that?
|
|
5. Does the selection UI run as SYSTEM (no LAPS password), or as an admin user?
|
|
6. Which payloads stay on SMB and which move to HTTPS?
|
|
7. Who operates the job queue day to day - is "Create this PC" a technician
|
|
action, or does it need approval?
|
|
|
|
---
|
|
|
|
## 13. Where everything referenced here lives
|
|
|
|
### 13.1 This repo (`/home/camp/projects/pxe`)
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `docs/PESETUP-INTERNALS.md` | decompiled imaging tool behaviour; the source for every PESetup claim above |
|
|
| `docs/OWNERSHIP.md` | who owns what, with the 2026-08-06 collisions |
|
|
| `playbook/startnet.cmd` | the WinPE script this proposal thins out - menus, staging, verify, log harvest |
|
|
| `playbook/scripts/preflight.ps1` | bay-side checks (secure boot, disk, driver match, media age) |
|
|
| `playbook/shopfloor-setup/run-enrollment.ps1` | applies the SFLD package via `provtool` |
|
|
| `playbook/shopfloor-setup/Run-ShopfloorSetup.ps1` | post-boot setup; the enrollment-model comment is here |
|
|
| `playbook/shopfloor-setup/Fetch-StagingPayload.ps1`, `Verify-And-Heal-Staging.ps1` | first-logon self-heal; read `C:\Enrollment\fetch-source.txt` |
|
|
| `playbook/shopfloor-setup/BPRT/Select-PCConfig.ps1` | the pre-seed shim (vendor GUI preserved as `-vendor.ps1` on the share) |
|
|
| `playbook/shopfloor-setup/BIOS/{check-bios.cmd,models.txt}` | firmware matching |
|
|
| `scripts/preflight.py` | runs every server-side lint in one command |
|
|
| `scripts/lint-unattend.py` | schema limits: Path 259, CommandLine 1024, Description 256 |
|
|
| `scripts/lint-driver-catalogue.py` | reimplements `GetDriverByModel` |
|
|
| `scripts/share-drift.py` | repo vs live share, git-owned vs unreconciled |
|
|
| `scripts/build-pctype-media.py` | per-PCTYPE media views (retired by Phase 6) |
|
|
|
|
### 13.2 PXE server (`pxe@172.16.9.1`)
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `/srv/samba/winpeapps/<image>/Deploy/Control/` | `HardwareDriver.json`, `OperatingSystem.json`, `Media.tag`, `Release.json` |
|
|
| `/srv/samba/winpeapps/<image>/Deploy/FlatUnattendW10.xml` | the answer file that actually boots machines |
|
|
| `/srv/samba/winpeapps/_shared/Applications/BPRT/` | the six MCL packages, `Insert-BPRTToPPKG.ps1`, `Start-BulkEnrollOrchestrator.ps1`, `Select-PCConfig*.ps1` |
|
|
| `/srv/samba/winpeapps/_shared/BIOS/` | firmware EXEs + `models.txt` (served as the `winpeapps_bios` share) |
|
|
| `/srv/samba/winpeapps/_media/<pctype>/` | per-PCTYPE media views |
|
|
| `/srv/samba/enrollment/scripts/` | `run-enrollment.ps1`, `preflight.ps1`, `winpe-status-push.ps1` |
|
|
| `/srv/samba/enrollment/shopfloor-setup/` | per-PC-type trees, `menu.json`, `_ntlars-backups/` (147 `.reg`) |
|
|
| `/srv/samba/enrollment/shopfloor-setup/gea-shopfloor-waxtrace/bay-config.csv` | 15 bays, FTPak versions |
|
|
| `/srv/samba/enrollment/installers-post/cmm/cmm-bay-config.csv` | CMM bay -> version, DODA |
|
|
| `/srv/samba/enrollment/pre-install/udc-backups/` | per-bay UDC settings |
|
|
| `/srv/samba/enrollment/ppkgs/` | the SFLD package (8.2 GB) |
|
|
| `/srv/samba/enrollment/imaging-logs/<serial>/` | harvested imaging logs (added 2026-08-06) |
|
|
| `/var/www/html/win11/sources/boot.wim` | the booted WinPE image |
|
|
| `/var/log/pxe-imaging/<serial>.json` | per-bay stage history |
|
|
| `/opt/pxe-webapp/` | Flask webapp (unit `pxe-webapp`, :9009) |
|
|
| `/etc/dnsmasq.conf`, `/etc/samba/smb.conf` | the binding config §6.2 depends on |
|
|
|
|
### 13.3 shopdb-flask (`/home/camp/projects/shopdb-flask`)
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `plugins/geenforce/seed_display_scope.py` | the pattern this proposal generalises - subtype map, server role by IP, sweep |
|
|
| `plugins/geenforce/client/` | `ShopdbEnforceClient.psm1`, `Invoke-ShopdbEnforce.ps1` |
|
|
| `shopdb/core/api/` | collector endpoint, dashboard-defaults display-role |
|
|
|
|
### 13.4 Evidence behind the claims in this document
|
|
|
|
| Path | What it shows |
|
|
|---|---|
|
|
| `/home/camp/pxe-images/now/pesetuplog.txt` | PESetup 4.0.0.20 run - media drive, disking, copy, driver selection, the `Btn 'Next'` timing in §7.1 |
|
|
| `/home/camp/pxe-images/now/debug.txt` | the WinPE console for the failed volume-finder run |
|
|
| `/home/camp/pxe-images/pxe2/Panther/` | `setupact.log` / `setuperr.log` naming the unattend `Path` failure |
|
|
| `/home/camp/pxe-images/debug2/` | the autologon registry state and the sysprep-loop timeline |
|
|
| `/home/camp/pxe-images/debug3/Logs/BPRT/` | the 13 package components, `criticalChecks.json`, `packageInfo.json` |
|
|
| `/home/camp/pxe-images/debug3/Logs/PPKG/` | the package itself, `provisioning-sessions.json`, the diagnostics evtx |
|
|
| `/home/camp/pxe-images/debug3/Logs/enrollment.log` | the double run and the rename race in §9 |
|
|
| `/home/camp/pxe-images/debug3/Logs/FilteredReportIP.log` | proves the bay had no route to Entra (§8.2) |
|
|
|
|
### 13.5 On-bay locations (for anyone diagnosing one)
|
|
|
|
```
|
|
C:\Enrollment\ staged payload, pc-type.txt, display-type.txt,
|
|
pc-config.txt, fetch-source.txt, winpe-staging.log
|
|
C:\Deploy\Applications\BPRT\ the packages + orchestrator, copied by PESetup
|
|
C:\Logs\BPRT\<Component>\ per-component package logs (UTF-16)
|
|
C:\Logs\BPRT\criticalChecks.json PPKG matched / Entra joined / OOBE execution
|
|
C:\Logs\PPKG\ provisioning diagnostics + session state
|
|
C:\Logs\SFLD\ DSC deployment, sync_intune, shopfloor-setup
|
|
C:\Logs\enrollment.log run-enrollment.ps1
|
|
C:\Windows\Panther\ setupact.log, setuperr.log, unattend.xml, PESetup*.log
|
|
C:\Windows\Panther\UnattendGC\ oobeSystem pass
|
|
```
|
|
|
|
### 13.6 How to re-derive things
|
|
|
|
```bash
|
|
# read a file off the share
|
|
sshpass -p pxe ssh -o StrictHostKeyChecking=no pxe@172.16.9.1 'cat <path>'
|
|
smbclient //172.16.9.1/enrollment -U pxe-upload%pxe -c 'ls'
|
|
|
|
# what is actually inside the booted WinPE
|
|
sudo wimextract /var/www/html/win11/sources/boot.wim 1 \
|
|
/Windows/System32/startnet.cmd --dest-dir=/tmp/x
|
|
sudo wimdir /var/www/html/win11/sources/boot.wim 1 | grep -i <name>
|
|
|
|
# a .ppkg is a WIM - pull out the spec without extracting 8 GB
|
|
7z l <pkg>.ppkg
|
|
7z e <pkg>.ppkg -o<dir> "Multivariant/0/customizations.xml" \
|
|
"Multivariant/0/Prov/RunTime/0__Accounts_Azure.provxml"
|
|
|
|
# provisioning diagnostics (python-evtx is installed)
|
|
python3 -c "from Evtx.Evtx import Evtx;
|
|
[print(r.xml()) for r in Evtx('Provisioning-Diagnostics-Admin.evtx').records()]"
|
|
|
|
# decompile PESetup - the bundle-extraction recipe is in PESETUP-INTERNALS.md
|
|
ilspycmd -t <TypeName> PESetup.dll
|
|
|
|
# validate before deploying
|
|
./scripts/preflight.py # every server-side lint
|
|
./scripts/share-drift.py --diff # repo vs live
|
|
```
|
|
|
|
### 13.7 The 2026-08-06 commits this document draws on
|
|
|
|
```
|
|
dfa026b lint-driver-catalogue c7b1769 build-pctype-media
|
|
049dd7b startnet per-PCTYPE media d5f7abc BIOS 7020 family
|
|
385acfe lint-unattend 2b6189f startnet W: wait + honest reporting
|
|
d04865d PESetup version + LogonCount b96ff7e retire shopfloor-dashboard route
|
|
36be60e run-enrollment stops renaming 8c21282 staging verify + log harvest
|
|
d2200e8 preflight (bay + server) 68df59e OWNERSHIP.md + share-drift
|
|
9d51c0b purpose menu + 3D kiosk a0aceb4 PXESERVER consolidation
|
|
bb08392 download-drivers case fix
|
|
```
|