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>
38 lines
1.1 KiB
Python
38 lines
1.1 KiB
Python
"""Session-based CSRF token: generate per-session, double-submit on POST."""
|
|
|
|
import secrets
|
|
|
|
from flask import abort, request, session
|
|
|
|
# Endpoint paths exempt from CSRF (machine-to-machine POSTs from PXE clients
|
|
# that have no browser session to carry a token). Air-gapped LAN; treated as
|
|
# trust-by-network. Keep the list short and explicit.
|
|
CSRF_EXEMPT_PATHS = {
|
|
"/imaging/status",
|
|
}
|
|
|
|
|
|
def generate_csrf_token():
|
|
"""Return the CSRF token for the current session, creating one if needed."""
|
|
if "_csrf_token" not in session:
|
|
session["_csrf_token"] = secrets.token_hex(32)
|
|
return session["_csrf_token"]
|
|
|
|
|
|
def init_csrf(app):
|
|
"""Wire CSRF protection into a Flask app: validator + template helper."""
|
|
|
|
@app.before_request
|
|
def _validate_csrf():
|
|
if request.method != "POST":
|
|
return
|
|
if request.path in CSRF_EXEMPT_PATHS:
|
|
return
|
|
token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token")
|
|
if not token or token != generate_csrf_token():
|
|
abort(403)
|
|
|
|
@app.context_processor
|
|
def _inject_csrf_token():
|
|
return {"csrf_token": generate_csrf_token}
|