"""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 import config 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(), mac_folders=ipxe_update.mac_folders(), tftp_root=config.TFTP_ROOT.rstrip("/") or "/", 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, confirm=request.form.get("confirm") == "1") 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()