Image import keeps the shared BPRT link and seeds BPRT on a fresh box

An MCL import merged Deploy/Applications with rsync -a, which replaced each
image's BPRT symlink with a plain copy of the vendor folder. That copy has
no site overlay, so standard/engineer enrollment lost
Register-BulkEnrollTask-Standard.ps1 (specialize Order 39).

Now Deploy/Applications is imported first and without BPRT:
- where _shared/Applications/BPRT exists it is left alone (the BPRT page
  keeps it current) and the image link is put back;
- on a box with none yet, the imported BPRT seeds it: vendor
  Select-PCConfig.ps1 kept as -vendor, our overlay added, BOM fix applied;
- the import refuses before anything moves when that folder would not work
  (vendor scripts missing, or no overlay copy to add).

The two Register scripts join Select-PCConfig.ps1 in the repo. The playbook
already syncs shopfloor-setup to the enrollment share, so
config.BPRT_OVERLAY_DIR finds all three there; overlay_sources now falls
back to it for every overlay file, not only the shim.
This commit is contained in:
cproudlock
2026-10-16 14:56:33 -04:00
parent 821a927365
commit d63cc5761b
6 changed files with 376 additions and 13 deletions

View File

@@ -0,0 +1,48 @@
# Register-BulkEnrollTask-Standard.ps1 - register the BulkEnrollmentOrchestrator
# logon task for the gea-standard and gea-engineer images.
#
# WHY THIS IS A FILE AND NOT AN INLINE COMMAND
# This ran inline as "powershell.exe -Command ..." in the unattend's specialize
# pass at RunSynchronousCommand Order 39. RunSynchronousCommand/Path is capped at
# 259 characters by the unattend schema; the inline form was 604, so Windows
# Setup rejected the ENTIRE answer file:
#
# /settings/RunSynchronous/RunSynchronousCommand/[Order="39"]/Path
# Description = Value is invalid. hrResult = 0x80220005 pass = specialize
#
# One over-length value invalidates every pass, so the machine stops at a dialog
# with nothing configured.
#
# WHY THERE ARE TWO OF THESE SCRIPTS
# gea-shopfloor uses Register-BulkEnrollTask.ps1, which launches the orchestrator
# as a PowerShell script with -ManualFallback: shopfloor bays are vanilla by
# design, so the orchestrator skips BPRT injection and the provisioning package.
# gea-standard and gea-engineer enrol normally and call the .cmd wrapper instead.
# All three image types share ONE Deploy\Applications\BPRT directory (a symlink
# to _shared\Applications\BPRT), so the two variants need distinct filenames.
#
# PESetup's CopyPackages copies the whole of <media>\Deploy\Applications to the
# target before the image is applied, so this script is present at
# C:\Deploy\Applications\BPRT\ by the time specialize runs. See
# docs/PESETUP-INTERNALS.md.
$ErrorActionPreference = 'Stop'
$taskName = 'BulkEnrollmentOrchestrator'
$orchestrator = 'C:\Deploy\Applications\BPRT\Start-BulkEnrollOrchestrator.cmd'
# Self-deleting task: schtasks /Delete drops the registration, then the
# orchestrator runs. cmd's & chains them so the delete happens even if the
# orchestrator later reboots the machine.
$argument = '/c schtasks /Delete /TN {0} /F & "{1}"' -f $taskName, $orchestrator
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $argument
$trigger = New-ScheduledTaskTrigger -AtLogOn
# S-1-5-32-544 = BUILTIN\Administrators. GroupId + Highest so it runs elevated
# for whichever admin logs on first, without naming an account.
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 3)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
Write-Host "Registered scheduled task '$taskName'."

View File

@@ -0,0 +1,42 @@
# Register-BulkEnrollTask.ps1 - register the BulkEnrollmentOrchestrator logon task.
#
# WHY THIS IS A FILE AND NOT AN INLINE COMMAND
# This ran as an inline "powershell.exe -Command ..." in the unattend's
# specialize pass, at RunSynchronousCommand Order 16. The unattend schema caps
# RunSynchronousCommand/Path at 259 characters; the inline form had grown to 676,
# so Windows Setup rejected the WHOLE answer file:
#
# /settings/RunSynchronous/RunSynchronousCommand/[Order="16"]/Path
# Description = Value is invalid. hrResult = 0x80220005 pass = specialize
# "Windows could not parse or process unattend answer file ... The answer file
# is invalid."
#
# One over-length value blocks every pass, so the machine stops at a dialog with
# nothing configured. Every shopfloor build from 2026-08-05 evening onward failed
# this way. Keep the unattend's Path short and put logic here.
#
# PESetup's CopyPackages copies the whole of <media>\Deploy\Applications to the
# target before the image is applied, so this script is present at
# C:\Deploy\Applications\BPRT\ by the time specialize runs. See
# docs/PESETUP-INTERNALS.md.
$ErrorActionPreference = 'Stop'
$taskName = 'BulkEnrollmentOrchestrator'
$orchestrator = 'C:\Deploy\Applications\BPRT\Start-BulkEnrollOrchestrator.ps1'
# Self-deleting task: schtasks /Delete removes the registration, then the
# orchestrator runs. cmd's & chains them so the delete happens even if the
# orchestrator later reboots the machine.
$argument = '/c schtasks /Delete /TN {0} /F & powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{1}" -ManualFallback' -f $taskName, $orchestrator
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $argument
$trigger = New-ScheduledTaskTrigger -AtLogOn
# S-1-5-32-544 = BUILTIN\Administrators. GroupId + Highest so it runs elevated
# for whichever admin logs on first, without naming an account.
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 3)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
Write-Host "Registered scheduled task '$taskName'."

View File

@@ -15,6 +15,11 @@ ENROLLMENT_SHARE = os.environ.get("ENROLLMENT_SHARE", "/srv/samba/enrollment")
ENROLLMENT_PPKG_DIR = os.environ.get( ENROLLMENT_PPKG_DIR = os.environ.get(
"ENROLLMENT_PPKG_DIR", os.path.join(ENROLLMENT_SHARE, "ppkgs") "ENROLLMENT_PPKG_DIR", os.path.join(ENROLLMENT_SHARE, "ppkgs")
) )
# Our BPRT overlay scripts (repo playbook/shopfloor-setup/BPRT/), which the
# playbook syncs here with the rest of shopfloor-setup.
BPRT_OVERLAY_DIR = os.environ.get(
"BPRT_OVERLAY_DIR", os.path.join(ENROLLMENT_SHARE, "shopfloor-setup", "BPRT")
)
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload") UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared") SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
WEB_ROOT = os.environ.get("WEB_ROOT", "/var/www/html") WEB_ROOT = os.environ.get("WEB_ROOT", "/var/www/html")

View File

@@ -50,6 +50,7 @@ import zipfile
from collections import Counter from collections import Counter
from datetime import datetime, timezone from datetime import datetime, timezone
import config
from services import mcl_ppkg from services import mcl_ppkg
from services import updates_common as uc from services import updates_common as uc
@@ -65,12 +66,14 @@ UNIT = "pxe-bprt-sync"
# Select-PCConfig-vendor.ps1 (VENDOR_RENAMED); # Select-PCConfig-vendor.ps1 (VENDOR_RENAMED);
# Register-BulkEnrollTask-Standard.ps1 registers the orchestrator; specialize Order 39 # Register-BulkEnrollTask-Standard.ps1 registers the orchestrator; specialize Order 39
# of playbook/FlatUnattendW10.xml and # of playbook/FlatUnattendW10.xml and
# FlatUnattendW10-engineer.xml runs it. It exists # FlatUnattendW10-engineer.xml runs it;
# only on the share (no repo copy);
# Register-BulkEnrollTask.ps1 the shopfloor -ManualFallback variant. Nothing # Register-BulkEnrollTask.ps1 the shopfloor -ManualFallback variant. Nothing
# calls it since the shopfloor unattend dropped # calls it since the shopfloor unattend dropped
# its Order 16 (2026-08-06); kept as it was. # its Order 16 (2026-08-06); kept as it was.
OVERLAY = ("Select-PCConfig.ps1", "Register-BulkEnrollTask-Standard.ps1", # All three are in the repo, playbook/shopfloor-setup/BPRT/; the playbook
# syncs them to config.BPRT_OVERLAY_DIR. A copy in the live BPRT wins; those
# copies fill in what it lacks (overlay_sources).
OVERLAY =("Select-PCConfig.ps1", "Register-BulkEnrollTask-Standard.ps1",
"Register-BulkEnrollTask.ps1") "Register-BulkEnrollTask.ps1")
# Vendor files the overlay displaces, and the name the vendor's copy keeps. # Vendor files the overlay displaces, and the name the vendor's copy keeps.
VENDOR_RENAMED = {"Select-PCConfig.ps1": "Select-PCConfig-vendor.ps1"} VENDOR_RENAMED = {"Select-PCConfig.ps1": "Select-PCConfig-vendor.ps1"}
@@ -332,22 +335,98 @@ def vendor_view(root, files):
return out return out
def overlay_fallbacks():
"""Folders holding our overlay outside BPRT: the copy the playbook syncs
to the enrollment share, then the repo when the webapp runs from a
checkout."""
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return [config.BPRT_OVERLAY_DIR,
os.path.abspath(os.path.join(here, "..", "playbook", "shopfloor-setup", "BPRT"))]
def overlay_sources(root, files): def overlay_sources(root, files):
"""{overlay name: absolute source path} found in a BPRT folder; our shim """{overlay name: absolute source path}: the copy in this BPRT folder,
falls back to the repo copy when the webapp runs from a checkout.""" else the first one in overlay_fallbacks(). A Select-PCConfig.ps1 counts
only when it is our shim."""
out = {} out = {}
for name in OVERLAY: for name in OVERLAY:
path = os.path.join(root, name) path = os.path.join(root, name)
if name in files and (name not in VENDOR_RENAMED or is_shim(path)): if name in files and (name not in VENDOR_RENAMED or is_shim(path)):
out[name] = path out[name] = path
if "Select-PCConfig.ps1" not in out: continue
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) for folder in overlay_fallbacks():
repo = os.path.join(here, "..", "playbook", "shopfloor-setup", "BPRT", "Select-PCConfig.ps1") path = os.path.join(folder, name)
if is_shim(repo): if os.path.isfile(path) and (name not in VENDOR_RENAMED or is_shim(path)):
out["Select-PCConfig.ps1"] = os.path.abspath(repo) out[name] = path
break
return out return out
def seed_from_import(src):
"""An image import brought a BPRT folder (src). On a box that has no
_shared/Applications/BPRT yet (a fresh install) it becomes that folder,
with the vendor's Select-PCConfig.ps1 kept as Select-PCConfig-vendor.ps1
and our overlay added, the way activate() builds one. Where BPRT exists
it is left alone: the BPRT page keeps it current. Returns "seeded" or
"kept"; raises before anything changed when the folder would not work."""
with _Lock():
_recover()
live = bprt_dir()
if os.path.isdir(live) and not os.path.islink(live):
return "kept"
if os.path.lexists(live):
raise RuntimeError(f"{live} exists but is not a folder; fix it on the server first")
files, _ = walk(src)
names = _lc(files)
missing = [r for r in REQUIRED if r.lower() not in names]
if missing:
raise RuntimeError("the imported BPRT folder lacks " + ", ".join(missing)
+ ": the bulk-enroll flow calls them by path")
srcs = overlay_sources(src, files)
lack = [n for n in ("Select-PCConfig.ps1",) + OVERLAY_REQUIRED if n not in srcs]
if lack:
raise RuntimeError(", ".join(lack) + " not found in " + config.BPRT_OVERLAY_DIR
+ ": run the playbook (it copies shopfloor-setup there), then import again")
new = new_dir()
if os.path.lexists(new):
shutil.rmtree(new) # left by a failed run; never live
os.makedirs(os.path.dirname(new), exist_ok=True)
try:
shutil.copytree(src, new, symlinks=False)
top = os.listdir(new)
for vendor, kept in VENDOR_RENAMED.items():
for n in top:
p = os.path.join(new, n)
if n.lower() == vendor.lower() and not is_shim(p):
os.replace(p, os.path.join(new, kept))
top = os.listdir(new)
for oname, s in srcs.items():
for n in top:
if n.lower() == oname.lower():
os.remove(os.path.join(new, n)) # the vendor shipped one (any case); ours wins
shutil.copy2(s, os.path.join(new, oname))
bom = []
for rel in walk(new)[0]:
path = os.path.join(new, *rel.split("/"))
if _is_script(rel) and _bom_less_non_ascii(path):
with open(path, "rb") as f:
data = f.read()
with open(path, "wb") as f:
f.write(BOM + data)
bom.append(rel)
for dirpath, dirs, fnames in os.walk(new):
_match_owner(dirpath, 0o755)
for n in fnames:
_match_owner(os.path.join(dirpath, n), _DEFAULT_FILE_MODE)
os.rename(new, live)
except BaseException:
shutil.rmtree(new, ignore_errors=True)
raise
_audit("BPRT_SEED", f"from image import {src}: {len(files)} files, overlay "
f"{', '.join(sorted(srcs))}, BOM added to {len(bom)}")
return "seeded"
def manifest(build=False, progress=None): def manifest(build=False, progress=None):
"""What manifest.json serves: every vendor file of the live BPRT with size """What manifest.json serves: every vendor file of the live BPRT with size
and sha256 (and sha256_nobom where we added a BOM). Returns None when a and sha256 (and sha256_nobom where we added a BOM). Returns None when a

View File

@@ -22,7 +22,7 @@ def _replace_with_symlink(link_path, target_path):
os.symlink(target_path, link_path) os.symlink(target_path, link_path)
def sync_tree(src, dst, move=False, checksum=True): def sync_tree(src, dst, move=False, checksum=True, exclude=()):
"""Copy src -> dst with rsync, SKIPPING files whose content already """Copy src -> dst with rsync, SKIPPING files whose content already
matches and transferring only new or changed files. matches and transferring only new or changed files.
@@ -32,8 +32,10 @@ def sync_tree(src, dst, move=False, checksum=True):
set checksum=False for the faster size+mtime comparison. move=True set checksum=False for the faster size+mtime comparison. move=True
removes source files after a successful transfer (frees the upload dir). removes source files after a successful transfer (frees the upload dir).
Existing dst files not present in src are left untouched (merge, not Existing dst files not present in src are left untouched (merge, not
mirror - no --delete).""" mirror - no --delete). exclude: rsync patterns left out ("/BPRT" is
the top-level BPRT only)."""
flags = ["-a"] # recurse, preserve perms/times/symlinks flags = ["-a"] # recurse, preserve perms/times/symlinks
flags += [f"--exclude={pat}" for pat in exclude]
if checksum: if checksum:
flags.append("--checksum") # compare by content hash, not size+mtime flags.append("--checksum") # compare by content hash, not size+mtime
if move: if move:
@@ -75,10 +77,16 @@ def import_deploy(src_deploy, dst_deploy, target="", move=False):
_transfer_tree = shutil.move if move else shutil.copytree _transfer_tree = shutil.move if move else shutil.copytree
os.makedirs(dst_deploy, exist_ok=True) os.makedirs(dst_deploy, exist_ok=True)
for item in os.listdir(src_deploy): # Applications first: its BPRT check can refuse the import before
# anything else has moved.
for item in sorted(os.listdir(src_deploy), key=lambda n: n != "Applications"):
src_item = os.path.join(src_deploy, item) src_item = os.path.join(src_deploy, item)
dst_item = os.path.join(dst_deploy, item) dst_item = os.path.join(dst_deploy, item)
if item == "Applications" and os.path.isdir(src_item):
_import_applications(src_item, dst_item, move=move)
continue
if not os.path.isdir(src_item): if not os.path.isdir(src_item):
_transfer(src_item, dst_item) _transfer(src_item, dst_item)
continue continue
@@ -103,6 +111,41 @@ def import_deploy(src_deploy, dst_deploy, target="", move=False):
_transfer_tree(src_item, dst_item) _transfer_tree(src_item, dst_item)
def _import_applications(src_apps, dst_apps, move=False):
"""Deploy/Applications, except BPRT. Every image links its BPRT to
_shared/Applications/BPRT, which the BPRT page keeps current and which
carries our overlay (services/bprt_sync.py). rsync -a replaces that link
with a plain copy of the vendor's folder, without the overlay, and
standard/engineer enrollment then fails at specialize Order 39. So the
imported BPRT only seeds the shared folder on a box that has none yet,
and is otherwise left out; the link is put back either way."""
from services import bprt_sync
bprt = next((n for n in os.listdir(src_apps)
if n.lower() == "bprt" and os.path.isdir(os.path.join(src_apps, n))), None)
if bprt:
bprt_sync.seed_from_import(os.path.join(src_apps, bprt))
sync_tree(src_apps, dst_apps, move=move, exclude=[f"/{bprt}"] if bprt else ())
if bprt and move:
shutil.rmtree(os.path.join(src_apps, bprt), ignore_errors=True)
try:
os.rmdir(src_apps)
except OSError:
pass
shared = bprt_sync.bprt_dir()
if not os.path.isdir(shared) or os.path.islink(shared):
return
for n in os.listdir(dst_apps):
p = os.path.join(dst_apps, n)
if n.lower() == "bprt" and n != "BPRT":
if os.path.islink(p) or not os.path.isdir(p):
os.remove(p)
else:
shutil.rmtree(p)
link = os.path.join(dst_apps, "BPRT")
if not (os.path.islink(link) and os.path.realpath(link) == os.path.realpath(shared)):
_replace_with_symlink(link, shared)
def allowed_import_source(source): def allowed_import_source(source):
"""True if source is a USB mount or under the upload dir.""" """True if source is a USB mount or under the upload dir."""
# Compare real paths: "<upload dir>/../.." passes a plain prefix test. # Compare real paths: "<upload dir>/../.." passes a plain prefix test.

View File

@@ -0,0 +1,146 @@
"""Image import and the shared BPRT folder.
Run from the webapp folder:
PYTHONPATH=.:<deps> python -m unittest tests.test_bprt_import -v
An MCL import carries Deploy/Applications/BPRT. rsync used to replace each
image's BPRT link with that plain vendor copy, which has no site overlay, so
standard/engineer enrollment lost Register-BulkEnrollTask-Standard.ps1. Now
the import seeds _shared BPRT on a fresh box, leaves an existing one alone,
and always restores the link. Every config path points into a temp folder.
"""
import os
import shutil
import sys
import tempfile
import unittest
from unittest import mock
_T = tempfile.mkdtemp(prefix="pxe-bprt-import-test-")
for _k, _sub in {"SAMBA_SHARE": "samba", "ENROLLMENT_SHARE": "enr", "UPDATES_DIR": "upd",
"UPLOAD_DIR": "upl"}.items():
os.environ.setdefault(_k, os.path.join(_T, _sub))
os.makedirs(os.environ[_k], exist_ok=True)
os.environ.setdefault("AUDIT_LOG", os.path.join(_T, "audit_log"))
WEBAPP = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPO_OVERLAY = os.path.join(WEBAPP, "..", "playbook", "shopfloor-setup", "BPRT")
sys.path.insert(0, WEBAPP)
import config # noqa: E402
from services import bprt_sync as BS # noqa: E402
from services import deploy # noqa: E402
VENDOR_SELECT = b"# vendor dialog\r\nShow-Dialog\r\n"
def write(path, data=b"x"):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(data)
def vendor_bprt(root, version=b"v1", skip=()):
for name in BS.REQUIRED:
if name in skip:
continue
write(os.path.join(root, name), VENDOR_SELECT if name == "Select-PCConfig.ps1" else version)
write(os.path.join(root, "Translations", "de.psd1"), b"@{}")
def read(path):
with open(path, "rb") as f:
return f.read()
class BprtImport(unittest.TestCase):
def setUp(self):
for d in (config.SAMBA_SHARE, config.UPLOAD_DIR, config.ENROLLMENT_SHARE, config.UPDATES_DIR):
shutil.rmtree(d, ignore_errors=True)
os.makedirs(d)
shutil.copytree(REPO_OVERLAY, config.BPRT_OVERLAY_DIR)
self.src = os.path.join(config.UPLOAD_DIR, "Deploy")
vendor_bprt(os.path.join(self.src, "Applications", "BPRT"))
write(os.path.join(self.src, "Applications", "extra", "app.msi"), b"msi")
write(os.path.join(self.src, "Control", "OperatingSystem.json"), b"{}")
self.dest = os.path.join(config.SAMBA_SHARE, "gea-standard", "Deploy")
self.live = BS.bprt_dir()
self.link = os.path.join(self.dest, "Applications", "BPRT")
def assert_linked(self):
self.assertTrue(os.path.islink(self.link), "image BPRT is not a link")
self.assertEqual(os.path.realpath(self.link), os.path.realpath(self.live))
def test_fresh_box_seeds_shared_bprt_with_overlay(self):
deploy.import_deploy(self.src, self.dest, "gea-standard")
self.assertTrue(os.path.isdir(self.live) and not os.path.islink(self.live))
self.assertTrue(BS.is_shim(os.path.join(self.live, "Select-PCConfig.ps1")))
self.assertEqual(read(os.path.join(self.live, "Select-PCConfig-vendor.ps1")), VENDOR_SELECT)
for name in BS.OVERLAY:
self.assertEqual(read(os.path.join(self.live, name)),
read(os.path.join(REPO_OVERLAY, name)), name)
self.assertTrue(os.path.isfile(os.path.join(self.live, "Translations", "de.psd1")))
self.assert_linked()
self.assertEqual(read(os.path.join(self.dest, "Applications", "extra", "app.msi")), b"msi")
self.assertFalse(os.path.exists(BS.new_dir()))
def test_existing_bprt_is_kept_and_link_restored(self):
vendor_bprt(self.live, version=b"live")
shutil.copy2(os.path.join(REPO_OVERLAY, "Select-PCConfig.ps1"),
os.path.join(self.live, "Select-PCConfig.ps1"))
# what the old import left: a plain copy in place of the link
vendor_bprt(self.link, version=b"stale copy")
deploy.import_deploy(self.src, self.dest, "gea-standard")
self.assertEqual(read(os.path.join(self.live, "WCD-Helpers.ps1")), b"live")
self.assertTrue(BS.is_shim(os.path.join(self.live, "Select-PCConfig.ps1")))
self.assert_linked()
def test_differently_cased_copy_is_replaced(self):
vendor_bprt(self.live, version=b"live")
vendor_bprt(os.path.join(self.dest, "Applications", "bprt"), version=b"stale")
deploy.import_deploy(self.src, self.dest, "gea-standard")
self.assertEqual(sorted(n for n in os.listdir(os.path.join(self.dest, "Applications"))
if n.lower() == "bprt"), ["BPRT"])
self.assert_linked()
def test_fresh_box_without_overlay_refuses_before_anything_moves(self):
empty = os.path.join(_T, "no-overlay")
os.makedirs(empty, exist_ok=True)
with mock.patch.object(BS, "overlay_fallbacks", return_value=[empty]):
with self.assertRaisesRegex(RuntimeError, "Register-BulkEnrollTask-Standard.ps1"):
deploy.import_deploy(self.src, self.dest, "gea-standard", move=True)
self.assertFalse(os.path.lexists(self.live))
self.assertFalse(os.path.exists(os.path.join(self.dest, "Applications")))
self.assertFalse(os.path.exists(os.path.join(self.dest, "Control")))
self.assertTrue(os.path.isfile(os.path.join(self.src, "Applications", "extra", "app.msi")))
def test_incomplete_vendor_bprt_refuses(self):
shutil.rmtree(os.path.join(self.src, "Applications", "BPRT"))
vendor_bprt(os.path.join(self.src, "Applications", "BPRT"), skip=("WCD-Helpers.ps1",))
with self.assertRaisesRegex(RuntimeError, "WCD-Helpers.ps1"):
deploy.import_deploy(self.src, self.dest, "gea-standard")
self.assertFalse(os.path.lexists(self.live))
def test_move_clears_the_upload_bprt(self):
deploy.import_deploy(self.src, self.dest, "gea-standard", move=True)
self.assertFalse(os.path.exists(os.path.join(self.src, "Applications")))
self.assert_linked()
def test_overlay_sources_fill_in_register_scripts(self):
vendor_bprt(self.live)
files, _ = BS.walk(self.live)
srcs = BS.overlay_sources(self.live, files)
self.assertEqual(srcs["Register-BulkEnrollTask-Standard.ps1"],
os.path.join(config.BPRT_OVERLAY_DIR, "Register-BulkEnrollTask-Standard.ps1"))
self.assertEqual(os.path.dirname(srcs["Select-PCConfig.ps1"]), config.BPRT_OVERLAY_DIR)
def test_vendor_select_in_overlay_dir_is_not_our_shim(self):
os.remove(os.path.join(config.BPRT_OVERLAY_DIR, "Select-PCConfig.ps1"))
write(os.path.join(config.BPRT_OVERLAY_DIR, "Select-PCConfig.ps1"), VENDOR_SELECT)
with mock.patch.object(BS, "overlay_fallbacks", return_value=[config.BPRT_OVERLAY_DIR]):
with self.assertRaisesRegex(RuntimeError, "Select-PCConfig.ps1"):
deploy.import_deploy(self.src, self.dest, "gea-standard")
if __name__ == "__main__":
unittest.main()