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.
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""Updates > iPXE: the Secure Boot boot files on the TFTP root.
|
|
|
|
Shows what is installed (version, signer chain, Microsoft CA warning), how
|
|
dnsmasq routes clients to those files, and checks GitHub for a newer
|
|
upstream release. Download and verify runs as a background job; install
|
|
and rollback copy a few MB and run inline. Logic lives in
|
|
services/ipxe_update.py; this module is the route surface.
|
|
"""
|
|
|
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
|
|
|
from services import ipxe_update, updates_common
|
|
from services.audit import audit
|
|
from views import save_speed_cap
|
|
|
|
bp = Blueprint("ipxe", __name__, url_prefix="/updates/ipxe")
|
|
|
|
|
|
def _back():
|
|
return redirect(url_for("ipxe.page"))
|
|
|
|
|
|
@bp.route("/")
|
|
def page():
|
|
rows = ipxe_update.installed()
|
|
by_name = {r["name"]: r["info"] for r in rows}
|
|
latest = ipxe_update.last_check()
|
|
ipxe_efi = by_name.get("ipxe.efi") or {}
|
|
status = ipxe_update.compare(latest["tag"], ipxe_efi.get("version")) if latest else None
|
|
return render_template(
|
|
"updates_ipxe.html",
|
|
rows=rows,
|
|
installed_sha={n: (i or {}).get("sha256") for n, i in by_name.items()},
|
|
routing=ipxe_update.dnsmasq_routing(),
|
|
latest=latest,
|
|
status=status,
|
|
job=ipxe_update.job_state(),
|
|
staged=ipxe_update.staged(),
|
|
backups=ipxe_update.backups(),
|
|
settings=updates_common.settings(),
|
|
default_install=ipxe_update.DEFAULT_INSTALL,
|
|
tarball_url=ipxe_update.LATEST_TARBALL_URL,
|
|
releases_page=ipxe_update.RELEASES_PAGE,
|
|
)
|
|
|
|
|
|
@bp.route("/status.json")
|
|
def status():
|
|
return jsonify(ipxe_update.job_state())
|
|
|
|
|
|
@bp.route("/check", methods=["POST"])
|
|
def check():
|
|
try:
|
|
info = ipxe_update.check_latest()
|
|
except Exception as ex:
|
|
flash(f"Could not check GitHub: {ex}", "danger")
|
|
return _back()
|
|
if not info["sha256"]:
|
|
flash(f"Latest release is {info['tag']}, but GitHub lists no sha256 for {ipxe_update.TARBALL}; "
|
|
"install it by hand (see below).", "warning")
|
|
else:
|
|
flash(f"Latest upstream release: {info['tag']}.", "info")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/stage", methods=["POST"])
|
|
def stage():
|
|
try:
|
|
ipxe_update.start_stage()
|
|
except Exception as ex:
|
|
flash(f"Download not started: {ex}", "danger")
|
|
return _back()
|
|
latest = ipxe_update.last_check() or {}
|
|
audit("IPXE_STAGE", f"download and verify {latest.get('tag')}")
|
|
flash(f"Downloading {latest.get('tag')}; this page updates when it is verified.", "info")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/install", methods=["POST"])
|
|
def install():
|
|
tag = request.form.get("tag", "")
|
|
# Only known names reach the audit log (form values could carry newlines).
|
|
names = [n for n in ipxe_update.MANAGED if n in request.form.getlist("names")]
|
|
try:
|
|
backup = ipxe_update.install(tag, names)
|
|
except Exception as ex:
|
|
flash(f"Install refused: {ex}", "danger")
|
|
return _back()
|
|
audit("IPXE_INSTALL", f"{tag}: {', '.join(names)} (backup {backup.rsplit('/', 1)[-1]})")
|
|
flash(f"Installed {', '.join(names)} from {tag}. "
|
|
"The next client to boot gets them; the old files are in the backups list.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/rollback", methods=["POST"])
|
|
def rollback():
|
|
# rollback() looks the id up by its last path part only; log that.
|
|
backup_id = request.form.get("backup", "").replace("\\", "/").rsplit("/", 1)[-1]
|
|
try:
|
|
files = ipxe_update.rollback(backup_id)
|
|
except Exception as ex:
|
|
flash(f"Rollback failed: {ex}", "danger")
|
|
return _back()
|
|
audit("IPXE_ROLLBACK", f"{backup_id}: {', '.join(files)}")
|
|
flash(f"Restored {', '.join(files)} from backup {backup_id}.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/settings", methods=["POST"])
|
|
def save_settings():
|
|
save_speed_cap() # shared with the other Updates pages (views/__init__.py)
|
|
return _back()
|