"""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 "__*" (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// - 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