Files
pxe-server/scripts/preflight.py
cproudlock 04db6d6605 Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo
(pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers,
CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the
WJ preinstall list, and the WJ shopfloor PC types (Collections,
NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace,
Display, PartMarker) with GE-Enforce, machine numbers and the desktop
layout. The part-marker bench tools moved to their own repo. Dead
scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models,
mirror-from-gold, pull-bios, startnet-template.cmd.

Shopfloor is one generic type, gea-shopfloor-standard, with today's
timing:
- PPKG at first logon (unattend Order 10, unchanged).
- Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup.
- A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once
  after the post-PPKG reboot. It also clears AutoLogonCount after the
  ShopFloor handover (the Users-level monitor can be denied that HKLM
  write) and removes the Intune Sync task when done.
- Both lists ship empty. A site adds entries and installers.
- WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal
  carry only the generic payload. The webapp's shopfloor sub-menu editor
  is gone, and the imaging tiles name the new stages.
- The playbook deploys the empty lists only if missing. It now deploys
  the PowerShell 7 MSI the shopfloor unattend installs, which it never
  did before.

Blancco site settings move into the webapp (Updates > Blancco releases):
business location, licensing Wi-Fi SSID and password. They are rendered
into the served preferences.xml and applied to every staged release, and
are seeded from site-vars (site_name, blancco_wifi_ssid,
blancco_wifi_password) only while none exist. The repo XML carries no
site values; the plaintext Wi-Fi password is gone from it.

New site_name and site_timezone settings: report and evidence-pack site
name, server clock and webapp time zone. No more West Jefferson defaults
in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the
server address filled in at download instead of WJ's GE address.

USB on Windows:
- scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It
  writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus
  payload) with diskpart and reads every file back by SHA-256.
- Tested with Windows PowerShell 5.1 in the win11 VM, and the layout
  installs and first-boots in the pxetest VM.
- scripts/stage-usb-share.sh stages the build folder on a share.
- site-content/ is optional for both builders.

KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment,
Windows only, with a site-vars.yml guide.

Checks: webapp tests pass (5 suites); the unattend lint and playbook
syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names
or PC types in playbook/, webapp/, scripts/ or autoinstall/.
2026-10-16 20:37:22 -04:00

136 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
preflight.py - run every server-side check before a build day.
The individual linters each catch one class of silent failure. This runs them
together so "is the PXE server fit to image from?" is one command and one exit
code, instead of four things somebody has to remember.
What it runs:
lint-driver-catalogue.py HardwareDriver.json vs PESetup's real matcher -
missing/case-mismatched packs, empty tokens,
shadowed entries. A driver miss is only a WARNING
to PESetup, so the bay images with no NIC.
lint-unattend.py answer files vs the schema limits. One over-length
value invalidates the whole file for its pass and
stops the bay at a dialog with nothing configured.
firmware coverage models in the driver catalogues that have no entry
in the BIOS models.txt, so their firmware is never
offered. This one is advisory - the manifest is a
curated set and most misses are out-of-fleet.
Usage:
./preflight.py # everything
./preflight.py --quick # skip the media-view verify (the slow one)
Exit code is non-zero if any blocking check fails.
"""
import argparse
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
PXE_HOST = "172.16.9.1"
PXE_USER = "pxe"
PXE_PASS = "pxe"
MODELS_TXT = "/srv/samba/winpeapps/_shared/BIOS/models.txt"
IMAGE_BASE = "/srv/samba/winpeapps"
def run(label, argv, blocking=True):
print("=" * 72)
print(label)
print("=" * 72)
try:
r = subprocess.run(argv, cwd=REPO)
rc = r.returncode
except OSError as e:
print(" could not run: %s" % e)
rc = 1
status = "PASS" if rc == 0 else ("FAIL" if blocking else "WARN")
print("-> %s (exit %d)\n" % (status, rc))
return rc if blocking else 0
def ssh(cmd):
return subprocess.run(
["sshpass", "-p", PXE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
"-o", "LogLevel=ERROR", "%s@%s" % (PXE_USER, PXE_HOST), cmd],
capture_output=True, text=True)
def firmware_coverage():
"""Models in the driver catalogues with no BIOS manifest entry.
Advisory only. models.txt is deliberately curated and most uncovered models
are legacy or not in this fleet - the value is spotting a model you DO image
whose firmware silently never updates, which is how the OptiPlex 7020 family
sat uncovered until 2026-08-06."""
print("=" * 72)
print("firmware coverage (advisory)")
print("=" * 72)
r = ssh("cat '%s'" % MODELS_TXT)
if r.returncode != 0:
print(" could not read %s\n-> WARN\n" % MODELS_TXT)
return 0
tokens = [ln.split("|")[0] for ln in r.stdout.splitlines()
if ln.strip() and not ln.startswith("#")]
r = ssh("cat %s/*/Deploy/Control/HardwareDriver.json" % IMAGE_BASE)
if r.returncode != 0:
print(" could not read the driver catalogues\n-> WARN\n")
return 0
import json, re
models = set()
for blob in re.findall(r"\[.*?\n\]", r.stdout, re.S):
try:
for e in json.loads(blob):
for t in str(e.get("modelswminame") or "").split(","):
if t.strip():
models.add(t.strip())
except json.JSONDecodeError:
continue
missing = [m for m in sorted(models)
if not any(tok.lower() in m.lower() for tok in tokens)]
print(" %d catalogued models, %d covered, %d with no firmware entry"
% (len(models), len(models) - len(missing), len(missing)))
if missing:
print(" uncovered (check any of these you actually image):")
for m in missing[:15]:
print(" %s" % m)
if len(missing) > 15:
print(" ... and %d more" % (len(missing) - 15))
print("-> WARN (advisory)\n" if missing else "-> PASS\n")
return 0
def main():
ap = argparse.ArgumentParser(description="Run every pre-build check.")
ap.add_argument("--quick", action="store_true",
help="skip the media-view verify (the slow one)")
args = ap.parse_args()
rc = 0
rc |= run("driver catalogue", [sys.executable, "scripts/lint-driver-catalogue.py", "--quiet"])
rc |= run("unattend answer files", [sys.executable, "scripts/lint-unattend.py", "--quiet"])
rc |= firmware_coverage()
print("=" * 72)
if rc:
print("PREFLIGHT FAILED - fix the blocking findings above before imaging.")
return 1
print("PREFLIGHT PASSED")
return 0
if __name__ == "__main__":
sys.exit(main())