Tools > Share accounts sets the pxe-upload, blancco and clonezilla Samba passwords. Saving one runs smbpasswd and writes it everywhere it is used: startnet.cmd in boot.wim (pxe-upload), the served Blancco preferences and the live config.img (blancco), the iPXE menu's Clonezilla entries (clonezilla). Refused while a PC is imaging unless forced. Stored 0600 in UPDATES_DIR/shares/accounts.json, never shown back. - The stored values are the authority: the playbook only creates the Samba accounts, seeds the store from site-vars (pxe_upload_password, blancco_smb_password, clonezilla_smb_password) for accounts it does not hold, and runs "share_accounts apply" at the end of every run. - Every startnet save re-pins SHAREPASS, so a restored snapshot cannot bring an old password back; new menu blocks use %SHAREUSER%/%SHAREPASS%. - Shop floor unattend Order 4 reads the login from fetch-source.txt; Invoke-PostPpkg deletes that file once the PPKG is applied. - Upload-Image.ps1 asks for the password unless -Pass is given. - pxe_admin_password: the Linux pxe login. Required while the box still has the shipped "pxe"; set with chpasswd when it differs. - Blancco site settings now also reach the live config.img (BIOS path).
240 lines
8.5 KiB
Python
240 lines
8.5 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 = bu.refresh_live_prefs()
|
|
except (OSError, ValueError, bu.StageBlocked) 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")
|
|
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()
|