webapp: imaging progress dashboard + serial column on reports list
Adds end-to-end progress tracking for PXE imaging sessions and surfaces
each Blancco report's BIOS serial in the report list.
webapp:
* services/imaging_status.py - JSON-per-serial state store under
IMAGING_DIR (default /var/log/pxe-imaging). Atomic write via
tempfile + rename. log_tail capped at 50 lines. Merges partial
updates so clients can post just the current_stage tick.
* config.py - new IMAGING_DIR env-overridable path.
* services/csrf.py - explicit exempt list for machine-to-machine
endpoints; /imaging/status is the first entry. Air-gapped LAN;
trust-by-network for client posts.
* app.py - four new routes:
GET /imaging dashboard (renders all sessions)
POST /imaging/status client status push (JSON body)
GET /imaging/<serial>.json raw session JSON for ad-hoc polling
POST /imaging/delete/<s> clear a session from the dashboard
Also parses each Blancco XML in the /reports list to surface
system.serial + system.model columns.
* templates/imaging.html - Bootstrap dashboard with per-session
cards (state badge, progress bar, stage idx/total, mac, elapsed,
log tail). meta http-equiv refresh=5 for auto-tick.
* templates/base.html - new "Imaging Progress" nav entry.
* templates/reports.html - Serial + Model columns added.
playbook:
* shopfloor-setup/Shopfloor/lib/Send-PxeStatus.ps1 - new helper.
Dot-source this then call Send-PxeStatus -Stage X -StageIndex N
-StageTotal M from any stage script. BIOS serial via CIM, MAC via
Get-NetAdapter, pctype + machinenumber from C:\Enrollment.
Failures are swallowed to a local log so a network blip doesn't
block imaging.
* shopfloor-setup/Run-ShopfloorSetup.ps1 - dot-sources helper +
posts at three coarse milestones (start, PPKG enrollment,
handoff to Monitor-IntuneProgress).
* shopfloor-setup/gea-shopfloor-keyence/09-Setup-Keyence.ps1 -
posts at session start + after Install-FromManifest with
succeeded/failed status derived from $rc. Other 09-Setup-*.ps1
scripts can follow the same pattern.
ID is BIOS serial (stable across WinPE -> Windows transition and
across reboots, unlike hostname which is random pre-PPKG). Operator
already knows the serial of the bay they imaged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ from lxml import etree
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import config
|
||||
from services import blancco_report, deploy, fs, images, system, unattend, wim
|
||||
from services import blancco_report, deploy, fs, images, imaging_status, system, unattend, wim
|
||||
from services.audit import audit
|
||||
from services.csrf import init_csrf
|
||||
|
||||
@@ -363,15 +363,32 @@ def blancco_reports():
|
||||
if os.path.isdir(config.BLANCCO_REPORTS):
|
||||
for f in sorted(os.listdir(config.BLANCCO_REPORTS), reverse=True):
|
||||
fpath = os.path.join(config.BLANCCO_REPORTS, f)
|
||||
if os.path.isfile(fpath):
|
||||
stat = os.stat(fpath)
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
reports.append({
|
||||
"filename": f,
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime,
|
||||
"type": ext.lstrip(".").upper() or "FILE",
|
||||
})
|
||||
if not os.path.isfile(fpath):
|
||||
continue
|
||||
stat = os.stat(fpath)
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
# Surface BIOS serial + system model so operators can find a
|
||||
# report for a specific bay without opening each one. Parse
|
||||
# cost is one XML walk per .xml report (~50KB-3MB each;
|
||||
# negligible at fleet sizes).
|
||||
serial = ""
|
||||
model = ""
|
||||
if ext == ".xml":
|
||||
try:
|
||||
data = blancco_report.parse(fpath)
|
||||
sysinfo = (data.get("hardware") or {}).get("system") or {}
|
||||
serial = sysinfo.get("serial", "") or ""
|
||||
model = sysinfo.get("model", "") or ""
|
||||
except Exception:
|
||||
pass
|
||||
reports.append({
|
||||
"filename": f,
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime,
|
||||
"type": ext.lstrip(".").upper() or "FILE",
|
||||
"serial": serial,
|
||||
"model": model,
|
||||
})
|
||||
return render_template(
|
||||
"reports.html",
|
||||
reports=reports,
|
||||
@@ -421,6 +438,50 @@ def blancco_delete_report(filename):
|
||||
return redirect(url_for("blancco_reports"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes - Imaging Progress
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/imaging")
|
||||
def imaging_dashboard():
|
||||
sessions = imaging_status.list_sessions()
|
||||
return render_template("imaging.html", sessions=sessions)
|
||||
|
||||
|
||||
@app.route("/imaging/status", methods=["POST"])
|
||||
def imaging_status_post():
|
||||
# CSRF-exempt machine-to-machine endpoint; see services/csrf.py exempt list.
|
||||
payload = request.get_json(silent=True) or {}
|
||||
if not payload.get("serial"):
|
||||
return jsonify({"error": "missing serial"}), 400
|
||||
try:
|
||||
state = imaging_status.update_session(payload)
|
||||
except Exception as ex:
|
||||
audit("IMAGING_STATUS_ERROR", str(ex))
|
||||
return jsonify({"error": str(ex)}), 500
|
||||
return jsonify({"ok": True, "serial": state["serial"]}), 200
|
||||
|
||||
|
||||
@app.route("/imaging/<serial>.json")
|
||||
def imaging_session_json(serial):
|
||||
serial = secure_filename(serial)
|
||||
s = imaging_status.get_session(serial)
|
||||
if not s:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(s)
|
||||
|
||||
|
||||
@app.route("/imaging/delete/<serial>", methods=["POST"])
|
||||
def imaging_delete_session(serial):
|
||||
serial = secure_filename(serial)
|
||||
if imaging_status.delete_session(serial):
|
||||
audit("IMAGING_DELETE", serial)
|
||||
flash(f"Cleared imaging session {serial}.", "success")
|
||||
else:
|
||||
flash(f"Session not found: {serial}", "danger")
|
||||
return redirect(url_for("imaging_dashboard"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes - Enrollment Packages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user