Preflight: check what PESetup fails on, before it fails on it

TWO SCRIPTS, DIFFERENT AUDIENCES

playbook/scripts/preflight.ps1 runs at the bay, called by startnet once the
media is mapped. It checks the four things that come straight out of PESetup's
own behaviour:

  secure boot   GatherDataSelection fails outright when SecurebootEnabled != 1
  disk >= 120GB MinRequiredSpaceWithoutCompression is 128849018880
  driver match  reimplements GetDriverByModel - family filter, untrimmed
                comma-separated substring tokens, first match wins - and checks
                the pack is actually on the media
  media age     media expires 30 days after build; Media.tag's timestamp is the
                local proxy

The driver check is the one that earns it. A miss is only a WARNING to PESetup,
so the bay images with no NIC, DNS fails at first boot, and enrollment cannot
reach the CDN - a symptom three steps removed from the cause. Advisory by
design: it reports and pauses on a blocker, the tech decides. Lives on the
enrollment share so it can be fixed without rebuilding boot.wim.

scripts/preflight.py runs on the server before a build day and aggregates
everything already built - driver catalogue lint, unattend lint, per-PCTYPE
media view verify - plus a new advisory firmware-coverage check that lists
catalogued models with no BIOS models.txt entry. That last one is how the
OptiPlex 7020 family sat uncovered: 127 catalogued models, 58 covered today.

First run: driver catalogues clean, all three unattends clean, firmware coverage
advisory only. PREFLIGHT PASSED.

Verified: both scripts parse clean (PowerShell parser / python), startnet parens
balance, every goto resolves, 915 CRLF lines with no bare LF. Deployed -
boot.wim md5 99fd3132, preflight.ps1 on the share.
This commit is contained in:
cproudlock
2026-08-06 14:22:01 -04:00
parent 8c21282024
commit d2200e8522
3 changed files with 294 additions and 0 deletions

139
scripts/preflight.py Executable file
View File

@@ -0,0 +1,139 @@
#!/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.
build-pctype-media.py resolves the critical paths in every per-PCTYPE
--verify media view; a dangling symlink is a dead view.
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"])
if not args.quick:
rc |= run("per-PCTYPE media views", [sys.executable, "scripts/build-pctype-media.py", "--verify"])
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())