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/.
136 lines
4.7 KiB
Python
Executable File
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())
|