Files
pxe-server/webapp/services/driver_lint.py
cproudlock 397063f987 Webapp: Updates pages, image page tabs with review-before-save, BPRT sync
Updates group:
- Dell drivers and BIOS: check Dell's catalogs against all three driver maps
  and models.txt, download with a shared speed cap, repack, preview, apply
  with backups and rollback.
- iPXE boot files: check GitHub for the signed Secure Boot set, verify the
  sha256 and each file's signer, install atomically, roll back. Includes the
  by-hand procedure.
- Blancco: upload a release, stage it, test-boot it from the menu, activate,
  roll back.
- BPRT sync: validate a batch uploaded from the MediaCreator Lite cache
  (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site
  overlay files kept.

Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and
Packages tabs. Every save shows a diff first, keeps the file's format, makes a
backup and can be undone. Driver lint runs inline.

Enrollment: upload standard/engineer MCL packages, one per tenant/purpose,
with archive and restore. Hardware coverage counts engineer as Win11.
2026-10-13 16:58:55 -04:00

322 lines
14 KiB
Python

"""Lint an image's HardwareDriver.json against PESetup's real driver matcher.
A port of scripts/lint-driver-catalogue.py for the webapp (the script is
left as it is: it still gates deploys from a workstation). Same matcher,
same checks, with three differences that come from running on the server:
* zips are looked up on the share directly, and a zip found only with
different letter case is a WARN, not an ERROR, when WinPE still finds
it: WinPE reads the share over SMB and Samba matches each folder name
case-insensitively for Windows clients (default "case sensitive =
auto"). It stays an ERROR when a folder with the exact spelling exists
and the zip sits in its other-case twin;
* a missing (null) modelswminame or family is CRITICAL, not just "no
tokens": the C# calls .Split / .ToLower on them, which throws for every
model the family filter lets through;
* "win10-first": a Win11 entry's model is answered by a Win10 entry
earlier in the list. Every GE image deploys Windows 11 and PESetup
has no OS filter, so those PCs get Windows 10 drivers.
Positions in messages are 1-based (#5 = fifth entry), as on the Drivers tab.
lint_entries(entries, image_type) -> findings, each a dict:
{"level", "check", "message", "index" (0-based or None),
"indexes" (every 0-based entry the finding is about)}
lint_image(image_type) reads the map from disk (used by the Overview tab).
"""
import json
import os
from pathlib import PurePosixPath
from services import fs, image_pages
CRITICAL, ERROR, WARN, INFO = "CRITICAL", "ERROR", "WARN", "INFO"
LEVELS = (CRITICAL, ERROR, WARN, INFO)
LEVEL_ORDER = {lvl: i for i, lvl in enumerate(LEVELS)}
# Short names for the checks, shown as badges on the Drivers tab.
CHECK_LABELS = {
"virtual-platform": "virtual platform",
"null-tokens": "no modelswminame",
"null-family": "no family",
"empty-token": "empty token",
"token-whitespace": "token whitespace",
"no-tokens": "no tokens",
"family-mismatch": "family mismatch",
"no-filename": "no file name",
"missing-zip": "zip missing",
"case-mismatch": "zip case differs",
"win10-first": "Win10 entry wins",
"shadowed": "shadowed",
"duplicate-token": "duplicate token",
"bad-entry": "not an object",
}
# --- PESetup's matcher (docs/PESETUP-INTERNALS.md, GetDriverByModel) ---------
def model_family(model):
"""Three Contains() tests, the last hit wins, "" when none hit."""
upper = model.upper()
family = ""
if "LATITUDE" in upper:
family = "Latitude"
if "OPTIPLEX" in upper:
family = "Optiplex"
if "PRECISION" in upper:
family = "Precision"
return family
def entry_tokens(entry):
"""modelswminame split on comma. NOT trimmed: the C# does not trim.
A missing (null) modelswminame gives no tokens here; PESetup throws on
it instead, which the null-tokens check reports."""
if entry.get("modelswminame") is None:
return []
return str(entry.get("modelswminame")).split(",")
def entry_matches(entry, model):
family = model_family(model)
if family and family.lower() not in str(entry.get("family") or "").lower():
return False
# model.ToLower().Contains(token.ToLower()); "" is in every string.
return any(t.lower() in model.lower() for t in entry_tokens(entry))
def select_driver(entries, model):
"""First match wins, like GetDriverByModel: (index, entry) or (None, None)."""
for i, entry in enumerate(entries):
if entry_matches(entry, model):
return i, entry
return None, None
# --- helpers -------------------------------------------------------------------
def label(entry, index):
name = entry.get("modelsfriendlyname") or entry.get("modelswminame") or "?"
return "#%d %s" % (index + 1, name)
def _smb_lookup(root, rel):
"""Resolve rel the way Samba does for a Windows client: per folder, the
exact name when it exists, else the one name that differs only in case.
No backtracking: once a folder is picked, a file under its other-case
twin is not found (Dell_11 has held both Optiplex and OptiPlex)."""
cur = root
for part in PurePosixPath(rel).parts:
nxt = os.path.join(cur, part)
if not os.path.lexists(nxt):
try:
names = [n for n in os.listdir(cur) if n.lower() == part.lower()]
except OSError:
return None
if len(names) != 1:
return None
nxt = os.path.join(cur, names[0])
cur = nxt
return cur if os.path.isfile(cur) else None
def zip_state(image_type, entry):
"""Where an entry's zip is: {"rel", "status", "path"}. rel is relative
to the image root (Deploy/Out-of-box Drivers/...). status:
ok the exact path exists
case only with other letter case, and WinPE (SMB) still finds it
trap on disk under another-case folder that SMB does not pick
missing nowhere
none the entry names no file"""
rel = image_pages.hardware_coverage._zip_relpath(entry)
if not rel:
return {"rel": "", "status": "none", "path": None}
if ".." in PurePosixPath(rel).parts:
# Never look outside the image tree: /drivers/check takes any
# posted entry, and no real entry climbs out with "..".
return {"rel": rel, "status": "missing", "path": None}
root = fs.image_root(image_type)
exact = os.path.join(root, rel)
if os.path.isfile(exact):
return {"rel": rel, "status": "ok", "path": exact}
smb = _smb_lookup(root, rel)
if smb:
return {"rel": rel, "status": "case", "path": smb, "actual": os.path.relpath(smb, root)}
found = image_pages.zip_path(image_type, entry)
if found:
return {"rel": rel, "status": "trap", "path": found, "actual": os.path.relpath(found, root)}
return {"rel": rel, "status": "missing", "path": None}
def _add(out, level, check, message, index=None, indexes=None):
idx = list(indexes) if indexes is not None else ([] if index is None else [index])
out.append({"level": level, "check": check, "message": message,
"index": index, "indexes": idx})
# --- checks --------------------------------------------------------------------
def lint_entries(entries, image_type=None, zips=None):
"""Every finding for a list of entries. image_type enables the zip
checks; zips (a list of zip_state results, one per entry) saves
looking them up again."""
out = []
rows = [e if isinstance(e, dict) else {} for e in entries]
for i, e in enumerate(entries):
if not isinstance(e, dict):
_add(out, CRITICAL, "bad-entry",
"#%d is not a JSON object; PESetup cannot read the map." % (i + 1), i)
for i, entry in enumerate(rows):
tag = label(entry, i)
# Virtual platform hijack: one such entry anywhere flips PESetup into
# virtual-only mode and every physical machine fails.
manufacturer = str(entry.get("manufacturer") or "")
if "virtual platform" in manufacturer.lower():
_add(out, CRITICAL, "virtual-platform",
"%s manufacturer is %r. PESetup then takes the first entry as the driver and "
"REQUIRES the machine to look virtual, so every physical PC fails." % (tag, manufacturer), i)
# Null fields the C# dereferences without a check.
if entry.get("modelswminame") is None:
_add(out, CRITICAL, "null-tokens",
"%s has no modelswminame. PESetup calls Split() on it and throws for every "
"model the family filter lets through, so driver selection fails." % tag, i)
if entry.get("family") is None:
_add(out, CRITICAL, "null-family",
"%s has no family. PESetup calls ToLower() on it for every Latitude, OptiPlex "
"and Precision model and throws." % tag, i)
# Empty token: Contains("") is true for every model. An empty
# modelswminame is one empty token, so it matches everything too.
tokens = entry_tokens(entry)
if tokens == [""]:
_add(out, CRITICAL, "empty-token",
"%s modelswminame is empty. Split gives one empty token, which matches every "
"model, so this entry takes over the map." % tag, i)
elif any(t == "" for t in tokens):
_add(out, CRITICAL, "empty-token",
"%s modelswminame %r has an EMPTY token (trailing or doubled comma). An empty "
"token matches every model, so this entry takes over the map."
% (tag, entry.get("modelswminame")), i)
# Whitespace around a token is kept by Split(','), so it must be in the model.
for token in tokens:
if token and token != token.strip():
_add(out, ERROR, "token-whitespace",
"%s token %r carries whitespace. PESetup does not trim, so the model "
"string must contain the space too; this token will usually miss." % (tag, token), i)
if tokens and tokens != [""] and not str(entry.get("modelswminame")).strip():
_add(out, ERROR, "no-tokens",
"%s has an empty modelswminame, so no model can ever select it." % tag, i)
# The family filter runs on the MODEL first, then wants entry.family
# to contain it: a token implying another family can never match.
family_field = str(entry.get("family") or "")
for token in tokens:
token = token.strip()
implied = model_family(token) if token else ""
if implied and implied.lower() not in family_field.lower():
_add(out, ERROR, "family-mismatch",
"%s token %r implies family %s, but family is %r. The filter runs before "
"the token test, so this token can never match." % (tag, token, implied, family_field), i)
# The referenced zip.
if not str(entry.get("fileName") or entry.get("FileName") or ""):
_add(out, ERROR, "no-filename",
"%s has no fileName, so PESetup has nothing to unzip." % tag, i)
elif image_type:
z = zips[i] if zips is not None else zip_state(image_type, entry)
if z["status"] == "missing":
_add(out, ERROR, "missing-zip",
"%s references %s, which is not on the share. A driver miss is only a "
"warning in PESetup, so imaging finishes with no drivers." % (tag, z["rel"]), i)
elif z["status"] == "case":
_add(out, WARN, "case-mismatch",
"%s references %s but the share holds %s. WinPE finds it (Samba matches "
"case-insensitively), Linux tools and the lint script do not."
% (tag, z["rel"], z.get("actual", "")), i)
elif z["status"] == "trap":
_add(out, ERROR, "case-mismatch",
"%s references %s but the zip is at %s. A folder with the exact spelling "
"exists and does not hold it, so WinPE will not find it."
% (tag, z["rel"], z.get("actual", "")), i)
# Shadowing: replay each entry's own tokens through the matcher. When a
# Win10 entry answers a Win11 entry's model, that is worse than dead
# weight: Win11 PCs get Windows 10 drivers.
os_of = [image_pages.driver_os(e) for e in rows]
for i, entry in enumerate(rows):
for token in entry_tokens(entry):
token = token.strip()
if not token:
continue
win, _ = select_driver(rows, token)
if win is None or win == i:
continue
if os_of[i] == "Win11" and os_of[win] == "Win10":
_add(out, ERROR, "win10-first",
"%s token %r is answered by %s, a Windows 10 pack earlier in the list. "
"PESetup has no OS filter, so Windows 11 PCs get Windows 10 drivers. "
"Move the Win11 entry above it." % (label(entry, i), token, label(rows[win], win)),
i, indexes=[i, win])
else:
_add(out, WARN, "shadowed",
"%s token %r is answered by %s instead (first match wins)."
% (label(entry, i), token, label(rows[win], win)), i)
# One token claimed by several entries: the first one wins whatever the OS.
by_token = {}
for i, entry in enumerate(rows):
for token in entry_tokens(entry):
token = token.strip().lower()
if token:
by_token.setdefault(token, [])
if i not in by_token[token]:
by_token[token].append(i)
for token, idxs in sorted(by_token.items()):
if len(idxs) > 1:
detail = ", ".join("%s (%s, osId %s)" % (label(rows[i], i), os_of[i], rows[i].get("osId"))
for i in idxs)
_add(out, WARN, "duplicate-token",
"token %r is claimed by %d entries: %s. osId is not part of the match, so %s "
"wins whatever OS is applied." % (token, len(idxs), detail, label(rows[idxs[0]], idxs[0])),
None, indexes=idxs)
out.sort(key=lambda f: (LEVEL_ORDER[f["level"]], f["indexes"][0] if f["indexes"] else -1))
return out
def counts(findings):
"""{"total", "CRITICAL": n, "ERROR": n, "WARN": n, "INFO": n}."""
c = {lvl: 0 for lvl in LEVELS}
for f in findings:
c[f["level"]] += 1
c["total"] = sum(c[lvl] for lvl in LEVELS)
return c
def map_path(image_type):
return image_pages.control_file(image_type, "HardwareDriver.json")
def lint_image(image_type):
"""Findings for the map on disk. [] when the image has no map; one
CRITICAL finding when it does not parse or is not a list."""
path = map_path(image_type)
if not os.path.isfile(path):
return []
try:
with open(path, encoding="utf-8-sig") as fh:
entries = json.load(fh)
except (OSError, ValueError) as exc:
return [{"level": CRITICAL, "check": "parse", "index": None, "indexes": [],
"message": "HardwareDriver.json does not parse: %s" % exc}]
if not isinstance(entries, list):
return [{"level": CRITICAL, "check": "parse", "index": None, "indexes": [],
"message": "HardwareDriver.json is a %s, expected a list" % type(entries).__name__}]
return lint_entries(entries, image_type)