A fresh server gets boot-tools/blancco from the USB, which can be an older Blancco (7.15.1) than our preferences (7.20). The version guard refused to patch its config.img and the exception failed the whole share apply, so the first-boot playbook failed. refresh_live_prefs() now returns a warning for config.img and still writes the served preferences.xml (UEFI path).
242 lines
8.6 KiB
Python
242 lines
8.6 KiB
Python
"""Updates > Blancco: install a new Blancco Drive Eraser release, test first.
|
|
|
|
Upload (or pick) the vendor ISO / USB image, stage it as a background job
|
|
into WEB_ROOT/blancco/staged/<version>/, boot it from a test entry in the
|
|
iPXE menu, then activate it (backup + atomic swap) or roll back. Staging is
|
|
a background job (systemd-run) whose progress JSON the page polls; the rest
|
|
are renames and small file writes and run inline. Logic lives in
|
|
services/blancco_update.py; this module is the route surface.
|
|
"""
|
|
|
|
from flask import (Blueprint, abort, flash, jsonify, redirect, render_template, request,
|
|
send_file, url_for)
|
|
|
|
from services import blancco_prefs
|
|
from services import blancco_update as bu
|
|
from services.audit import audit
|
|
|
|
bp = Blueprint("blancco_update", __name__, url_prefix="/updates/blancco")
|
|
|
|
|
|
def _back():
|
|
return redirect(url_for("blancco_update.page"))
|
|
|
|
|
|
@bp.route("/")
|
|
def page():
|
|
job = bu.job_state()
|
|
return render_template(
|
|
"updates_blancco.html",
|
|
current=bu.current(),
|
|
staged=bu.staged(),
|
|
backups=bu.backups(),
|
|
sources=bu.sources(),
|
|
job=job,
|
|
job_running=job.get("state") == "running",
|
|
test=bu.test_entry(),
|
|
initrd=bu.initrd_supports_base(),
|
|
upload_dir=bu.upload_dir(),
|
|
source_dirs=bu.source_dirs(),
|
|
server=bu.SERVER,
|
|
site=blancco_prefs.public(),
|
|
)
|
|
|
|
|
|
@bp.route("/site-settings", methods=["POST"])
|
|
def site_settings():
|
|
"""Business location and licensing Wi-Fi: saved, then the served
|
|
preferences.xml and the live config.img are written again. The password
|
|
is never shown back."""
|
|
try:
|
|
with bu._lock:
|
|
blancco_prefs.save(request.form.get("business_location", ""),
|
|
request.form.get("wifi_ssid", ""),
|
|
request.form.get("wifi_password", ""))
|
|
# Served preferences.xml and the live config.img (BIOS path).
|
|
changed, warning = bu.refresh_live_prefs()
|
|
except (OSError, ValueError) as e:
|
|
flash(f"Blancco site settings not saved: {e}", "danger")
|
|
return _back()
|
|
s = blancco_prefs.public()
|
|
audit("BLANCCO_SITE_SETTINGS", f"location={s['business_location']!r} ssid={s['wifi_ssid']!r} "
|
|
f"password={'set' if s['wifi_password_set'] else 'empty'}")
|
|
flash("Blancco site settings saved" + (" and preferences.xml updated." if changed else " (no change)."),
|
|
"success")
|
|
if warning:
|
|
flash(f"Blancco: {warning}", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/status.json")
|
|
def status():
|
|
job = bu.job_state()
|
|
return jsonify({k: job.get(k) for k in ("state", "step", "percent", "message", "log",
|
|
"version", "source", "started", "finished")})
|
|
|
|
|
|
@bp.route("/upload", methods=["POST"])
|
|
def upload():
|
|
"""Raw body (the page's fetch upload, name in ?name=) streams straight to
|
|
disk; a plain multipart form works too."""
|
|
f = request.files.get("release_file")
|
|
try:
|
|
if f is not None:
|
|
name, size = bu.save_upload(f.stream, f.filename)
|
|
else:
|
|
name, size = bu.save_upload(request.stream, request.args.get("name", ""),
|
|
request.content_length)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
if f is None:
|
|
return jsonify({"ok": False, "error": str(ex)}), 400
|
|
flash(f"Upload failed: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_UPLOAD", f"{name} ({size} bytes)")
|
|
if f is None:
|
|
return jsonify({"ok": True, "name": name, "size": size})
|
|
flash(f"Uploaded {name}.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/stage", methods=["POST"])
|
|
def stage():
|
|
key, _, name = request.form.get("source", "").partition(":")
|
|
try:
|
|
path = bu.resolve_source(key, name)
|
|
bu.start_stage(path)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Staging not started: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_STAGE", path)
|
|
flash(f"Staging {name}. This takes a minute or two; the page follows the job.", "info")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/cancel", methods=["POST"])
|
|
def cancel():
|
|
bu.cancel_stage()
|
|
audit("BLANCCO_STAGE_CANCEL", "")
|
|
flash("Staging cancelled; partial files removed.", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/prefs/<version>", methods=["POST"])
|
|
def merged_prefs(version):
|
|
f = request.files.get("prefs_file")
|
|
if not f or not f.filename:
|
|
flash("Choose the merged preferences.xml first.", "danger")
|
|
return _back()
|
|
try:
|
|
meta = bu.save_merged_prefs(version, f.read(4 << 20))
|
|
except bu.StageBlocked as ex:
|
|
flash(str(ex), "danger")
|
|
return _back()
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Preferences not accepted: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_PREFS_MERGE", f"{version} preferences {meta.get('prefs_version')}")
|
|
flash(f"Merged preferences accepted; Blancco {version} is staged and ready to test. "
|
|
"Commit the same file to playbook/blancco-preferences.xml so a playbook run "
|
|
"does not put the old one back.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/vendor-prefs/<version>.xml")
|
|
def vendor_prefs(version):
|
|
try:
|
|
path = bu.vendor_prefs_file(version)
|
|
except ValueError:
|
|
abort(404)
|
|
if not path:
|
|
abort(404)
|
|
return send_file(path, mimetype="application/xml", as_attachment=True,
|
|
download_name=f"vendor-preferences-{version}.xml")
|
|
|
|
|
|
@bp.route("/test/<version>", methods=["POST"])
|
|
def test_add(version):
|
|
try:
|
|
backup = bu.add_test_entry(version)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Test entry not added: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_TEST_ENTRY_ADD", f"{version} (menu backup {backup})")
|
|
flash(f"Boot menu now has \"Blancco Drive Eraser {version} (STAGED - TEST)\". "
|
|
"PXE boot a test machine and pick it.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/test/remove", methods=["POST"])
|
|
def test_remove():
|
|
try:
|
|
backup = bu.remove_test_entry()
|
|
except (RuntimeError, OSError) as ex:
|
|
flash(f"Test entry not removed: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_TEST_ENTRY_REMOVE", f"menu backup {backup}" if backup else "none present")
|
|
flash("Test entry removed from the boot menu." if backup else "No test entry in the menu.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/activate/<version>", methods=["POST"])
|
|
def activate(version):
|
|
try:
|
|
backup, files, removed = bu.activate(version)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Not activated: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_ACTIVATE", f"{version}; previous release saved in {backup}")
|
|
flash(f"Blancco {version} is live ({len(files)} files swapped). The previous release is in "
|
|
f"{backup}." + (" The test menu entry was removed." if removed is True else ""), "success")
|
|
if isinstance(removed, str):
|
|
flash(f"Blancco {version} is live, but {removed}.", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/rollback/<name>", methods=["POST"])
|
|
def rollback(name):
|
|
try:
|
|
backup, files = bu.rollback(name)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Not rolled back: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_ROLLBACK", f"restored {name}; the replaced release saved in {backup}")
|
|
flash(f"Restored {name} ({len(files)} files); its folder is released now that the files are "
|
|
f"live. What was live before is saved in {backup}.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/staged/<version>/delete", methods=["POST"])
|
|
def delete_staged(version):
|
|
try:
|
|
bu.delete_staged(version)
|
|
except (ValueError, RuntimeError, OSError) as ex:
|
|
flash(f"Not deleted: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_DELETE_STAGED", version)
|
|
flash(f"Deleted staged {version}.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/backup/<name>/delete", methods=["POST"])
|
|
def delete_backup(name):
|
|
try:
|
|
bu.delete_backup(name)
|
|
except (ValueError, OSError) as ex:
|
|
flash(f"Not deleted: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_DELETE_BACKUP", name)
|
|
flash(f"Deleted {name}.", "success")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/uploads/<name>/delete", methods=["POST"])
|
|
def delete_upload(name):
|
|
try:
|
|
bu.delete_upload(name)
|
|
except (ValueError, OSError) as ex:
|
|
flash(f"Not deleted: {ex}", "danger")
|
|
return _back()
|
|
audit("BLANCCO_DELETE_UPLOAD", name)
|
|
flash(f"Deleted {name}.", "success")
|
|
return _back()
|