Files
pxe-server/scripts/preflight.py
cproudlock fa79f76951 Share accounts: per-site Samba passwords from the webapp
Tools > Share accounts sets the pxe-upload, blancco and clonezilla Samba
passwords. Saving one runs smbpasswd and writes it everywhere it is used:
startnet.cmd in boot.wim (pxe-upload), the served Blancco preferences and
the live config.img (blancco), the iPXE menu's Clonezilla entries
(clonezilla). Refused while a PC is imaging unless forced. Stored 0600 in
UPDATES_DIR/shares/accounts.json, never shown back.

- The stored values are the authority: the playbook only creates the
  Samba accounts, seeds the store from site-vars (pxe_upload_password,
  blancco_smb_password, clonezilla_smb_password) for accounts it does not
  hold, and runs "share_accounts apply" at the end of every run.
- Every startnet save re-pins SHAREPASS, so a restored snapshot cannot
  bring an old password back; new menu blocks use %SHAREUSER%/%SHAREPASS%.
- Shop floor unattend Order 4 reads the login from fetch-source.txt;
  Invoke-PostPpkg deletes that file once the PPKG is applied.
- Upload-Image.ps1 asks for the password unless -Pass is given.
- pxe_admin_password: the Linux pxe login. Required while the box still
  has the shipped "pxe"; set with chpasswd when it differs.
- Blancco site settings now also reach the live config.img (BIOS path).
2026-10-17 12:29:06 -04:00

136 lines
4.8 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 = os.environ.get("PXE_PASS") or "pxe" # the site's pxe_admin_password
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())