Engineer image: upload the Engineering Layer from the web app

New Engineering Layer tab on the engineer image page. A site uploads
Engineering_Install_v<ver>.exe (keeps prereqs) or a .zip of the whole
extra_engineering payload. The upload is streamed to disk, checked (MZ
header, no unsafe zip paths) and swapped in; the replaced payload is
archived (two kept) with a Restore button. Install-EngineeringLayer.ps1
stays repo-owned and now installs the newest Engineering_Install_v*.exe.

KB: Part 4 becomes Site Content (enrollment package, Engineering Layer,
Blancco site settings, Shop Floor apps) plus a new Adding Shop Floor
Apps section for preinstall.json/postppkg.json. WJ examples removed.
This commit is contained in:
cproudlock
2026-10-16 20:48:25 -04:00
parent 04db6d6605
commit b9a4d0230d
9 changed files with 775 additions and 9 deletions

View File

@@ -0,0 +1,58 @@
"""Engineer image > Engineering Layer tab: upload the Engineering Layer
installer (or the whole extra_engineering payload) the engineer unattend
installs. Logic in services/engineering_layer.py."""
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
import config
from services import engineering_layer as el
from services.audit import audit
bp = Blueprint("engineering_layer", __name__, url_prefix="/images")
def _back():
return redirect(url_for("engineering_layer.page", image_type=el.IMAGE))
@bp.route("/<image_type>/engineering-layer")
def page(image_type):
if image_type != el.IMAGE:
flash("The Engineering Layer belongs to the engineer image.", "warning")
return redirect(url_for("images.overview", image_type=image_type)) \
if image_type in config.IMAGE_TYPES else redirect(url_for("dashboard"))
return render_template(
"image_engineering_layer.html",
image_type=image_type,
friendly_name=config.FRIENDLY_NAMES.get(image_type, image_type),
active_tab="englayer",
st=el.status(),
)
@bp.route("/gea-engineer/engineering-layer/upload", methods=["POST"])
def upload():
"""Raw body (the page's XHR upload, name in ?name=) streams to disk."""
name = request.args.get("name", "")
try:
res = el.upload(request.stream, name, request.content_length)
except (ValueError, RuntimeError, OSError) as ex:
return jsonify({"ok": False, "error": str(ex)}), 400
audit("ENGINEERING_LAYER_UPLOAD",
f"{res['name']} ({res['size']} bytes): replaced {', '.join(res['replaced'])}; "
f"installer v{res['version']}; previous in archive {res['archived'] or '(none)'}")
flash(f"Engineering Layer updated from {res['name']}: installer v{res['version']}"
+ (f", previous kept as {res['archived']}." if res["archived"] else "."), "success")
return jsonify({"ok": True, **res})
@bp.route("/gea-engineer/engineering-layer/restore/<stamp>", methods=["POST"])
def restore(stamp):
try:
parts = el.restore(stamp)
except (ValueError, RuntimeError, OSError) as ex:
flash(f"Not restored: {ex}", "danger")
return _back()
audit("ENGINEERING_LAYER_RESTORE", f"{stamp}: {', '.join(parts)}")
flash(f"Restored the Engineering Layer from {stamp}; the replaced one is archived.", "success")
return _back()