Files
pxe-server/webapp/views/ipxe.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

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()