Staging fetches ipxe-shimx64.efi from the ipxe/shim release (sha256 from GitHub), and uses it instead of the tarball's 2011-only shim when it is signed under the Microsoft UEFI CA 2023 and trusts the same iPXE CA; otherwise the tarball shim stays, with the reason shown. snponly.efi and snponly-shim.efi are managed alongside ipxe.efi and ipxe-shim.efi, each iPXE build says whether it drives USB itself (kills USB keyboards), and tftp-unique-root per-MAC folders are listed read-only.
115 lines
3.9 KiB
Python
115 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(),
|
|
mac_folders=ipxe_update.mac_folders(),
|
|
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()
|