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.
269 lines
11 KiB
Python
269 lines
11 KiB
Python
"""GE Aerospace image pages: the Drivers tab (Deploy/Control/HardwareDriver.json).
|
|
|
|
The page holds the whole driver map in the browser: search and filters,
|
|
move up/down (PESetup takes the FIRST matching entry, so order is policy),
|
|
edit an entry in a modal, remove, add an unregistered zip. Nothing is
|
|
written until Review and save:
|
|
|
|
POST /drivers/check the staged map -> zip status + lint findings per
|
|
row (services/driver_lint), nothing written
|
|
POST /drivers/preview per-entry changes with old vs new JSON, the whole
|
|
file diff, lint before/after; nothing written
|
|
POST /drivers/save the same body + the previewed hash; backup to
|
|
HardwareDriver.json.bak-<ts>-webui, atomic write in
|
|
the file's own format (services/control_files)
|
|
POST /drivers/undo put the last save back (only while the file still
|
|
holds what that save wrote)
|
|
POST /drivers/orphans/remove delete unregistered zips no image uses
|
|
|
|
Every JSON body carries "base", the map's sha256 when the page was loaded:
|
|
a map rewritten since (another tab, the Dell updater) is never overwritten
|
|
with a stale copy. Uploads stay on /drivers/upload in app.py.
|
|
|
|
The endpoint images_drivers.page is the "drivers" row of
|
|
services/image_pages.IMAGE_TABS; keep the name, or change that row.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
|
from markupsafe import Markup
|
|
|
|
import config
|
|
from services import control_files, driver_lint, driver_map, hardware_coverage, image_pages
|
|
from services.audit import audit
|
|
|
|
bp = Blueprint("images_drivers", __name__, url_prefix="/images")
|
|
|
|
UNDO_SCOPE = "drivers"
|
|
|
|
|
|
def _unknown():
|
|
flash("Unknown image type.", "danger")
|
|
return redirect(url_for("dashboard"))
|
|
|
|
|
|
def _script_json(value):
|
|
"""JSON for a <script type="application/json"> block, key order kept
|
|
(Flask's tojson sorts keys). <, > and & are escaped so file content can
|
|
never close the script tag."""
|
|
text = json.dumps(value, ensure_ascii=False)
|
|
text = text.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
|
|
return Markup(text)
|
|
|
|
|
|
def _folders(image_type):
|
|
"""Driver folders under Out-of-box Drivers, as destinationDir values,
|
|
for the edit modal's suggestions."""
|
|
root = driver_map._drivers_root(image_type)
|
|
out = []
|
|
if not root:
|
|
return out
|
|
for dirpath, dirnames, _files in os.walk(root):
|
|
dirnames.sort()
|
|
rel = os.path.relpath(dirpath, root)
|
|
if rel != ".":
|
|
out.append(driver_map.DEST_PREFIX + rel.replace(os.sep, "\\"))
|
|
return out
|
|
|
|
|
|
@bp.route("/<image_type>/drivers")
|
|
def page(image_type):
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return _unknown()
|
|
entries, error = driver_map.load(image_type)
|
|
rows, _findings, counts = driver_map.rows(image_type, entries)
|
|
orphans = driver_map.orphans(image_type)
|
|
families = sorted({str(e.get("family")) for e in entries
|
|
if isinstance(e, dict) and e.get("family")}, key=str.lower)
|
|
hw_extra = image_pages.control_file(image_type, "hw_drivers.json")
|
|
data = {
|
|
"entries": entries,
|
|
"rows": rows,
|
|
"counts": counts,
|
|
"base": driver_map.file_sha(image_type),
|
|
"families": families,
|
|
"folders": _folders(image_type),
|
|
"urls": {
|
|
"check": url_for("images_drivers.check", image_type=image_type),
|
|
"preview": url_for("images_drivers.preview", image_type=image_type),
|
|
"save": url_for("images_drivers.save", image_type=image_type),
|
|
},
|
|
}
|
|
return render_template(
|
|
"image_drivers.html",
|
|
image_type=image_type,
|
|
friendly_name=config.FRIENDLY_NAMES.get(image_type, image_type),
|
|
active_tab="drivers",
|
|
page_json=_script_json(data),
|
|
load_error=error,
|
|
counts=counts,
|
|
entry_count=len(entries),
|
|
orphans=orphans,
|
|
hw_drivers_extra=os.path.isfile(hw_extra),
|
|
last_save=control_files.last_save(image_type, UNDO_SCOPE),
|
|
)
|
|
|
|
|
|
# --- staged map: check, preview, save ------------------------------------------------------
|
|
|
|
class Stale(Exception):
|
|
"""The map changed on disk since the page was loaded."""
|
|
|
|
|
|
def _body():
|
|
body = request.get_json(silent=True)
|
|
if not isinstance(body, dict):
|
|
raise ValueError("expected a JSON object")
|
|
return body
|
|
|
|
|
|
def _staged(image_type, body, check_base=True):
|
|
"""(old entries, new entries, origs, moved flags) for a posted map."""
|
|
old, error = driver_map.load(image_type)
|
|
if error:
|
|
raise ValueError(error + ". Fix the file by hand first.")
|
|
if check_base and body.get("base") != driver_map.file_sha(image_type):
|
|
raise Stale("HardwareDriver.json changed on disk after this page was loaded "
|
|
"(another save, an undo or the Dell updater). Reload the page and "
|
|
"make the edits again.")
|
|
new, origs, moved = driver_map.staged(body, len(old))
|
|
return old, new, origs, moved
|
|
|
|
|
|
def _blocking(image_type, old, new, origs):
|
|
"""Lint of the map before and after; a save may not add CRITICAL
|
|
findings (each one breaks imaging for many or all PCs)."""
|
|
before_findings = driver_lint.lint_entries(old, image_type)
|
|
after_findings = driver_lint.lint_entries(new, image_type)
|
|
before = driver_lint.counts(before_findings)
|
|
after = driver_lint.counts(after_findings)
|
|
# Compare the CRITICAL findings themselves, not their count: fixing one
|
|
# while adding another keeps the count and must still be refused. Every
|
|
# CRITICAL check is about one entry, named here by its position in the
|
|
# file (origs), so an entry that already had the finding may still be
|
|
# edited or moved; a new entry or a new check on an old one is refused.
|
|
had = {(f["check"], f["index"]) for f in before_findings if f["level"] == driver_lint.CRITICAL}
|
|
new_critical = []
|
|
for f in after_findings:
|
|
if f["level"] != driver_lint.CRITICAL:
|
|
continue
|
|
i = f["index"]
|
|
o = origs[i] if i is not None and 0 <= i < len(origs) else None
|
|
if o is None or (f["check"], o) not in had:
|
|
new_critical.append(f["message"])
|
|
return before, after, after_findings, new_critical
|
|
|
|
|
|
def _json_error(exc):
|
|
if isinstance(exc, Stale):
|
|
return jsonify({"error": str(exc), "stale": True}), 409
|
|
return jsonify({"error": str(exc)}), 400
|
|
|
|
|
|
@bp.route("/<image_type>/drivers/check", methods=["POST"])
|
|
def check(image_type):
|
|
"""Row facts for the staged map (the page re-lints after every edit)."""
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return jsonify({"error": "Unknown image type"}), 404
|
|
try:
|
|
_old, new, _origs, _moved = _staged(image_type, _body(), check_base=False)
|
|
except ValueError as exc:
|
|
return _json_error(exc)
|
|
rows, _findings, counts = driver_map.rows(image_type, new)
|
|
return jsonify({"rows": rows, "counts": counts})
|
|
|
|
|
|
@bp.route("/<image_type>/drivers/preview", methods=["POST"])
|
|
def preview(image_type):
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return jsonify({"error": "Unknown image type"}), 404
|
|
try:
|
|
old, new, origs, moved = _staged(image_type, _body())
|
|
except (ValueError, Stale) as exc:
|
|
return _json_error(exc)
|
|
plan = control_files.plan_json(driver_map.map_path(image_type), new, driver_map.FILE_NAME)
|
|
entry_changes, summary = driver_map.changes(old, new, origs, moved)
|
|
before, after, after_findings, new_critical = _blocking(image_type, old, new, origs)
|
|
return jsonify({
|
|
"files": [control_files.public(plan)],
|
|
"changes": entry_changes,
|
|
"summary": summary,
|
|
"lint": {"before": before, "after": after,
|
|
"after_errors": [f["message"] for f in after_findings
|
|
if f["level"] in (driver_lint.CRITICAL, driver_lint.ERROR)]},
|
|
"blocked": new_critical,
|
|
})
|
|
|
|
|
|
@bp.route("/<image_type>/drivers/save", methods=["POST"])
|
|
def save(image_type):
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return jsonify({"error": "Unknown image type"}), 404
|
|
try:
|
|
body = _body()
|
|
old, new, origs, moved = _staged(image_type, body)
|
|
_before, _after, _f, new_critical = _blocking(image_type, old, new, origs)
|
|
if new_critical:
|
|
raise ValueError("refused: this map adds a CRITICAL finding: " + new_critical[0])
|
|
plan = control_files.plan_json(driver_map.map_path(image_type), new, driver_map.FILE_NAME)
|
|
_changes, summary = driver_map.changes(old, new, origs, moved)
|
|
expect = body.get("expect") if isinstance(body.get("expect"), dict) else {}
|
|
if driver_map.FILE_NAME not in expect:
|
|
raise ValueError("missing the previewed file hash; review the changes first")
|
|
changed = control_files.commit(image_type, UNDO_SCOPE, [plan],
|
|
action="Drivers save", expect=expect)
|
|
except (ValueError, Stale) as exc:
|
|
return _json_error(exc)
|
|
except control_files.Conflict as exc:
|
|
return jsonify({"error": str(exc)}), 409
|
|
except OSError as exc:
|
|
return jsonify({"error": f"write failed: {exc}"}), 500
|
|
if changed:
|
|
hardware_coverage.invalidate()
|
|
detail = ", ".join(f"{k} {v}" for k, v in summary.items() if v) or "whitespace only"
|
|
audit("IMAGE_DRIVERS_SAVE", f"{image_type} {driver_map.FILE_NAME}: {detail}")
|
|
flash(f"Saved {driver_map.FILE_NAME} ({detail}). The previous file is next to it "
|
|
"(.bak-...-webui) and Undo last save puts it back.", "success")
|
|
else:
|
|
flash("Nothing changed, nothing written.", "info")
|
|
return jsonify({"ok": True, "saved": bool(changed)})
|
|
|
|
|
|
@bp.route("/<image_type>/drivers/undo", methods=["POST"])
|
|
def undo(image_type):
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return _unknown()
|
|
try:
|
|
rec = control_files.undo(image_type, UNDO_SCOPE)
|
|
hardware_coverage.invalidate()
|
|
audit("IMAGE_DRIVERS_UNDO", f"{image_type} {driver_map.FILE_NAME} (save of {rec.get('ts')})")
|
|
flash(f"Undid the last save: {driver_map.FILE_NAME} restored.", "success")
|
|
except control_files.Conflict as exc:
|
|
flash(f"Undo refused: {exc}", "danger")
|
|
except OSError as exc:
|
|
flash(f"Undo failed: {exc}", "danger")
|
|
return redirect(image_pages.tab_url(image_type, "drivers"))
|
|
|
|
|
|
# --- unregistered zips ---------------------------------------------------------------------
|
|
|
|
@bp.route("/<image_type>/drivers/orphans/remove", methods=["POST"])
|
|
def orphans_remove(image_type):
|
|
if image_type not in config.IMAGE_TYPES:
|
|
return _unknown()
|
|
rels = request.form.getlist("rel")
|
|
back = redirect(image_pages.tab_url(image_type, "drivers"))
|
|
if not rels:
|
|
flash("No files selected.", "warning")
|
|
return back
|
|
rec = driver_map.delete_orphans(image_type, rels)
|
|
audit("DRIVER_ORPHAN_REMOVE", f"{image_type} removed={len(rec['removed'])} "
|
|
f"refused={len(rec['refused'])} {', '.join(rec['removed'])}")
|
|
if rec["removed"]:
|
|
flash(f"Deleted {len(rec['removed'])} unregistered zip(s).", "success")
|
|
for rel, why in rec["refused"]:
|
|
flash(f"Not deleted: {rel} ({why}).", "warning")
|
|
return back
|