Files
pxe-server/webapp/services/mcl_ppkg.py
cproudlock 397063f987 Webapp: Updates pages, image page tabs with review-before-save, BPRT sync
Updates group:
- Dell drivers and BIOS: check Dell's catalogs against all three driver maps
  and models.txt, download with a shared speed cap, repack, preview, apply
  with backups and rollback.
- iPXE boot files: check GitHub for the signed Secure Boot set, verify the
  sha256 and each file's signer, install atomically, roll back. Includes the
  by-hand procedure.
- Blancco: upload a release, stage it, test-boot it from the menu, activate,
  roll back.
- BPRT sync: validate a batch uploaded from the MediaCreator Lite cache
  (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site
  overlay files kept.

Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and
Packages tabs. Every save shows a diff first, keeps the file's format, makes a
backup and can be undone. Driver lint runs inline.

Enrollment: upload standard/engineer MCL packages, one per tenant/purpose,
with archive and restore. Hardware coverage counts engineer as Win11.
2026-10-13 16:58:55 -04:00

372 lines
13 KiB
Python

"""MCL provisioning packages for gea-standard and gea-engineer.
The vendor's bulk-enroll scripts in _shared/Applications/BPRT (every image's
Deploy/Applications/BPRT is a symlink to it) pick the package by filename:
* the name must match ^((?:\\w|-)+?_){5,}v\\d+\\.\\d+.ppkg$ (PowerShell
-Match, so case-insensitive), e.g. GCCH_Prod_MCL-PERS_Exp_XXXXXXXX_v2.19.ppkg;
* Insert-BPRTToPPKG.ps1 then keeps the names that are -like
"<Tenant>_<Environment>_<Purpose>*" (a case-insensitive startswith, with
no trailing underscore) and FAILS enrollment when more than one is left.
So an upload replaces the package with the same first three fields, and the
old one moves to BPRT-archive/<timestamp>/ - outside BPRT, because PESetup
copies the whole folder to every PC. Restore swaps an archived package back
the same way. Either is refused when the result would leave two packages that
one prefix claims (e.g. MCL-SH and MCL-SHX).
A package is a WIM (Insert-BPRTToPPKG.ps1 renames it to .wim and mounts it),
so an upload must start with the WIM magic. The upload is written under the
archive folder first and renamed into BPRT only when complete, so a bay
copying BPRT mid-upload never sees a partial file.
"""
from __future__ import annotations
import errno
import os
import re
import shutil
import threading
import time
from datetime import datetime
import config
# The vendor's own pattern (Insert-BPRTToPPKG.ps1 / Start-BulkEnrollOrchestrator.ps1).
VENDOR_PATTERN = re.compile(r"^((?:\w|-)+?_){5,}v\d+\.\d+.ppkg$", re.IGNORECASE)
_VERSION = re.compile(r"_(v\d+\.\d+)\.ppkg$", re.IGNORECASE)
# Insert-BPRTToPPKG.ps1 swaps this placeholder for the token's expiry date on
# the PC's copy; a name without it is a package already used on a PC.
_PLACEHOLDER = "_exp_xxxxxxxx_"
_BATCH = re.compile(r"^\d{8}-\d{6}(-\d+)?$")
WIM_MAGIC = b"MSWIM\0\0\0"
# Serialises the archive-and-place step (uploads and restores).
_lock = threading.Lock()
def bprt_dir():
return os.path.join(config.SHARED_DIR, "Applications", "BPRT")
def archive_dir():
return os.path.join(config.SHARED_DIR, "Applications", "BPRT-archive")
def clean_name(name):
"""Bare file name; some browsers send a full Windows path."""
return os.path.basename(str(name or "").replace("\\", "/")).strip()
def is_mcl_name(name):
"""True for a name the bulk-enroll scripts would treat as an MCL package
(the shopfloor SFLD names, e.g. GCCH_Prod_SFLD_v4.18.ppkg, are not)."""
return bool(VENDOR_PATTERN.match(clean_name(name)))
def parse(name, strict=True):
"""{"prefix", "version"} for a usable package name, else ValueError.
The vendor regex ends in `.ppkg` with an unescaped dot, so it also takes
e.g. ..._v2.19-ppkg; strict (uploads, restores) wants the real extension,
while the listing (strict=False) shows what the enroll scripts would see."""
if not VENDOR_PATTERN.match(name) or (strict and not name.lower().endswith(".ppkg")):
raise ValueError(
f"{name} does not match the vendor naming the enroll scripts need, "
"e.g. GCCH_Prod_MCL-PERS_Exp_XXXXXXXX_v2.19.ppkg")
m = _VERSION.search(name)
return {"prefix": "_".join(name.split("_")[:3]),
"version": m.group(1) if m else ""}
def _claims(prefix, name):
"""What the vendor's `-like "$packagePrefix*"` does."""
return name.lower().startswith(prefix.lower())
def _clashes(names):
"""{name: [other names its prefix also claims]} for a set of vendor-valid
names; empty when every prefix finds exactly one package."""
out = {}
for n in names:
others = [m for m in names if m != n and _claims(parse(n, strict=False)["prefix"], m)]
if others:
out[n] = others
return out
def _check_unique(names, name):
"""Refuse when `name` would be part of a clash: its prefix claims another
package, or another's prefix claims it. A clash among other packages
(made by hand) is flagged on the page but does not block this one."""
clashes = {n: o for n, o in _clashes(names).items() if n == name or name in o}
if clashes:
n, others = next(iter(clashes.items()))
raise ValueError(
f"{parse(n, strict=False)['prefix']} would match {n} and {', '.join(others)}; "
"the enroll scripts fail when one prefix finds two packages")
def _pkg(folder, name):
st = os.stat(os.path.join(folder, name))
info = {"filename": name, "size": st.st_size, "modified": st.st_mtime,
"placeholder": _PLACEHOLDER in name.lower()}
try:
info.update(parse(name, strict=False))
except ValueError:
info.update(prefix="", version="")
return info
def _listed(folder, name):
return ((name.lower().endswith(".ppkg") or VENDOR_PATTERN.match(name))
and os.path.isfile(os.path.join(folder, name)))
def packages():
"""Current packages in BPRT, sorted by prefix."""
folder = bprt_dir()
out = []
if os.path.isdir(folder):
for name in os.listdir(folder):
if _listed(folder, name):
out.append(_pkg(folder, name))
out.sort(key=lambda p: (p["prefix"].lower(), p["filename"].lower()))
# A prefix that finds two packages breaks enrollment for that tenant/purpose.
clashes = _clashes([p["filename"] for p in out if p["prefix"]])
for p in out:
p["conflict_with"] = clashes.get(p["filename"], [])
p["conflict"] = bool(p["conflict_with"])
return out
def archived():
"""Archived packages, newest batch first: [{"batch", "filename", ...}]."""
root = archive_dir()
out = []
if os.path.isdir(root):
for batch in sorted(os.listdir(root), reverse=True):
folder = os.path.join(root, batch)
if not _BATCH.match(batch) or not os.path.isdir(folder):
continue # .incoming (uploads in progress) and strays
for name in sorted(os.listdir(folder)):
if _listed(folder, name):
out.append(dict(_pkg(folder, name), batch=batch))
return out
# --- moving files -----------------------------------------------------------
def _move(src, dst):
"""Rename src onto dst. BPRT and its archive sit on one filesystem, so this
is a rename; the copy fallback only covers a share split across mounts."""
try:
os.replace(src, dst)
except OSError as exc:
if exc.errno != errno.EXDEV:
raise
tmp = os.path.join(os.path.dirname(dst), f".{os.path.basename(dst)}.part")
try:
shutil.copy2(src, tmp)
os.replace(tmp, dst)
finally:
if os.path.exists(tmp):
os.remove(tmp)
os.remove(src)
def _new_batch():
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
path = os.path.join(archive_dir(), stamp)
n = 1
while os.path.exists(path): # two changes in one second must not share a batch
n += 1
path = os.path.join(archive_dir(), f"{stamp}-{n}")
os.makedirs(path)
_match_perms(path, directory=True)
return path
def _archive(names):
"""Move these BPRT packages into a new archive batch. Returns the batch
path (None when there was nothing to move)."""
if not names:
return None
batch = _new_batch()
done = []
try:
for name in names:
_move(os.path.join(bprt_dir(), name), os.path.join(batch, name))
done.append(name)
except OSError:
_unarchive(batch, done)
raise
return batch
def _unarchive(batch, names):
"""Undo _archive after a failed placement: best effort, every file back."""
for name in names:
try:
_move(os.path.join(batch, name), os.path.join(bprt_dir(), name))
except OSError:
pass
try:
os.rmdir(batch)
except OSError:
pass
def _same_prefix(prefix):
"""Valid BPRT package names whose own prefix equals this one."""
return [p["filename"] for p in packages()
if p["prefix"] and p["prefix"].lower() == prefix.lower()]
def _plan(name, info):
"""(names to archive) for placing `name`, or ValueError if the result
would leave a prefix finding two packages."""
current = [p["filename"] for p in packages() if p["prefix"]]
replaced = _same_prefix(info["prefix"])
_check_unique([n for n in current if n not in replaced] + [name], name)
return replaced
def _place(src, name, info):
"""Archive what `name` replaces, then rename src into BPRT. Returns the
archived names."""
with _lock:
replaced = _plan(name, info)
batch = _archive(replaced)
dest = os.path.join(bprt_dir(), name)
try:
_move(src, dest)
except OSError:
if batch:
_unarchive(batch, replaced)
raise
return replaced
# --- upload and restore -----------------------------------------------------
def check_upload(name):
"""Validate a name before the body is saved: (clean name, info)."""
name = clean_name(name)
info = parse(name)
_plan(name, info)
return name, info
def install_upload(fileobj, name):
"""Save an uploaded package into BPRT, archiving the one it replaces.
Returns (info, archived_names); info["filename"] is the name used and
info["warnings"] lists things worth telling the user."""
name, info = check_upload(name)
os.makedirs(bprt_dir(), exist_ok=True)
incoming = os.path.join(archive_dir(), ".incoming")
os.makedirs(incoming, exist_ok=True)
_sweep(incoming)
_match_perms(archive_dir(), directory=True)
_match_perms(incoming, directory=True)
tmp = os.path.join(incoming, f"{os.getpid()}-{threading.get_ident()}-{name}.part")
try:
fileobj.save(tmp)
if os.path.getsize(tmp) == 0:
raise ValueError("uploaded file is empty")
with open(tmp, "rb") as f:
if f.read(len(WIM_MAGIC)) != WIM_MAGIC:
raise ValueError(f"{name} is not a provisioning package (no WIM header)")
moved = _place(tmp, name, info)
_match_perms(os.path.join(bprt_dir(), name))
finally:
if os.path.exists(tmp):
os.remove(tmp)
info["filename"] = name
info["warnings"] = []
if _PLACEHOLDER not in name.lower():
info["warnings"].append(
f"{name} has no Exp_XXXXXXXX placeholder; a package already used on a PC "
"carries its BPRT and the enroll script cannot inject a new one")
return info, moved
def _sweep(incoming, age=86400):
"""Drop temp files a crashed upload left behind (300 MB each, invisible
on the page). A live upload's file is seconds old."""
cutoff = time.time() - age
for n in os.listdir(incoming):
path = os.path.join(incoming, n)
try:
if n.endswith(".part") and os.path.isfile(path) and os.path.getmtime(path) < cutoff:
os.remove(path)
except OSError:
pass
def archive(name):
"""Move one package out of BPRT into a new archive batch (clears a
duplicate made by hand). Returns the bare name."""
name = clean_name(name)
with _lock:
if name not in [p["filename"] for p in packages()]:
raise ValueError(f"{name or '(blank)'} is not a package in BPRT")
_archive([name])
return name
def restore(batch, name):
"""Put an archived package back, archiving whatever holds its prefix now."""
batch = str(batch or "")
if not _BATCH.match(batch):
raise ValueError(f"{batch or '(blank)'} is not an archive batch")
name = clean_name(name)
folder = os.path.join(archive_dir(), batch)
src = os.path.join(folder, name)
if not name or not os.path.isfile(src):
raise ValueError(f"{batch}/{name} is not in the archive")
info = parse(name)
moved = _place(src, name, info)
try:
os.rmdir(folder)
except OSError:
pass # batch still holds other packages
info["filename"] = name
return info, moved
def _match_perms(path, directory=False):
"""Uploads run as root; keep the share's pxe ownership so the Samba side
can still manage the file."""
try:
st = os.stat(bprt_dir())
os.chown(path, st.st_uid, st.st_gid)
os.chmod(path, 0o755 if directory else 0o744)
except OSError:
pass
def image_links():
"""[{"image", "state", "target"}] for every image with a
Deploy/Applications folder: state "ok" when its BPRT is the link to the
shared folder, "copy" when it is a real folder (an image import rsync
replaces the link with one, and then that image's PCs never see a
package changed here), "elsewhere" when it links somewhere else or
dangles, "none" when the image has no BPRT."""
shared = os.path.realpath(bprt_dir())
out = []
for image in config.IMAGE_TYPES:
apps = os.path.join(config.SAMBA_SHARE, image, "Deploy", "Applications")
if not os.path.isdir(apps):
continue
path = os.path.join(apps, "BPRT")
target = ""
if os.path.islink(path):
target = os.readlink(path)
state = "ok" if os.path.realpath(path) == shared and os.path.isdir(path) else "elsewhere"
elif os.path.isdir(path):
state = "copy"
else:
state = "none"
out.append({"image": image, "state": state, "target": target, "path": path})
return out