Eliminate USB requirement for WinPE PXE boot, add image upload script

- Add startnet.cmd: FlatSetupLoader.exe + Boot.tag/Media.tag eliminates
  physical USB requirement for WinPE PXE deployment
- Add Upload-Image.ps1: PowerShell script to robocopy MCL cached images
  to PXE server via SMB (Deploy, Tools, Sources)
- Add gea-shopfloor-mce image type across playbook, webapp, startnet
- Change webapp import to move (not copy) for upload sources to save disk
- Add Samba symlink following config for shared image directories
- Add Media.tag creation task in playbook for drive detection
- Update prepare-boot-tools.sh with Blancco config/initramfs patching
- Add grub-efi-amd64-bin to download-packages.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-12 16:40:27 -05:00
parent f4c158a5ac
commit 1a5c4f7124
7 changed files with 696 additions and 38 deletions

View File

@@ -72,6 +72,7 @@ IMAGE_TYPES = [
"gea-standard",
"gea-engineer",
"gea-shopfloor",
"gea-shopfloor-mce",
"ge-standard",
"ge-engineer",
"ge-shopfloor-lockdown",
@@ -82,6 +83,7 @@ FRIENDLY_NAMES = {
"gea-standard": "GE Aerospace Standard",
"gea-engineer": "GE Aerospace Engineer",
"gea-shopfloor": "GE Aerospace Shop Floor",
"gea-shopfloor-mce": "GE Aerospace Shop Floor MCE",
"ge-standard": "GE Legacy Standard",
"ge-engineer": "GE Legacy Engineer",
"ge-shopfloor-lockdown": "GE Legacy Shop Floor Lockdown",
@@ -213,8 +215,9 @@ def find_upload_sources():
return sources
def _import_deploy(src_deploy, dst_deploy, target=""):
"""Copy Deploy directory contents, merging shared subdirs into _shared."""
def _import_deploy(src_deploy, dst_deploy, target="", move=False):
"""Import Deploy directory contents, merging shared subdirs into _shared.
When move=True, files are moved instead of copied (saves disk space)."""
# Build list of scoped shared dirs for this target
scoped_shared = []
prefix_key = ""
@@ -224,20 +227,23 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
prefix_key = prefix
break
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
os.makedirs(dst_deploy, exist_ok=True)
for item in os.listdir(src_deploy):
src_item = os.path.join(src_deploy, item)
dst_item = os.path.join(dst_deploy, item)
if not os.path.isdir(src_item):
shutil.copy2(src_item, dst_item)
_transfer(src_item, dst_item)
continue
# Global shared (e.g., Out-of-box Drivers) — one copy for all
if item in SHARED_DEPLOY_GLOBAL:
shared_dest = os.path.join(SHARED_DIR, item)
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
@@ -245,14 +251,14 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
if item in scoped_shared:
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
# Normal copy
# Normal transfer
if os.path.exists(dst_item):
shutil.rmtree(dst_item)
shutil.copytree(src_item, dst_item)
_transfer_tree(src_item, dst_item)
def _replace_with_symlink(link_path, target_path):
@@ -264,21 +270,24 @@ def _replace_with_symlink(link_path, target_path):
os.symlink(target_path, link_path)
def _merge_tree(src, dst):
"""Recursively merge src tree into dst, overwriting existing files."""
def _merge_tree(src, dst, move=False):
"""Recursively merge src tree into dst, overwriting existing files.
When move=True, files are moved instead of copied."""
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
if os.path.isdir(d):
_merge_tree(s, d)
_merge_tree(s, d, move=move)
else:
if os.path.exists(d):
os.remove(d)
shutil.copytree(s, d)
_transfer_tree(s, d)
else:
os.makedirs(os.path.dirname(d), exist_ok=True)
shutil.copy2(s, d)
_transfer(s, d)
def allowed_import_source(source):
@@ -659,6 +668,11 @@ def images_import():
os.makedirs(dest, exist_ok=True)
src_items = os.listdir(source)
# Move files from network upload to save disk space; copy from USB
use_move = source == UPLOAD_DIR or source.startswith(UPLOAD_DIR + "/")
_transfer = shutil.move if use_move else shutil.copy2
_transfer_tree = shutil.move if use_move else shutil.copytree
# Detect layout: if source has Deploy/, Sources/, Tools/ at top
# level, it's the full image root structure (USB-style).
# Otherwise treat it as Deploy/ contents directly.
@@ -673,18 +687,18 @@ def images_import():
shared_root = dirs
break
# Full image root: copy Deploy contents + sibling dirs
# Full image root: import Deploy contents + sibling dirs
for item in src_items:
src_item = os.path.join(source, item)
if item == "Deploy":
_import_deploy(src_item, dest, target)
_import_deploy(src_item, dest, target, move=use_move)
elif os.path.isdir(src_item) and item in shared_root:
# Shared sibling: merge into _shared/{prefix}{item}
# and symlink from image root
prefix_key = target.split("-")[0] + "-"
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=use_move)
dst_item = os.path.join(root, item)
if os.path.islink(dst_item):
os.remove(dst_item)
@@ -696,12 +710,12 @@ def images_import():
dst_item = os.path.join(root, item)
if os.path.exists(dst_item):
shutil.rmtree(dst_item)
shutil.copytree(src_item, dst_item)
_transfer_tree(src_item, dst_item)
else:
shutil.copy2(src_item, os.path.join(root, item))
_transfer(src_item, os.path.join(root, item))
else:
# Flat layout: treat source as Deploy contents
_import_deploy(source, dest, target)
_import_deploy(source, dest, target, move=use_move)
audit("IMAGE_IMPORT", f"{source} -> {target}")
flash(