BIOS check fix, parallel downloads, shopfloor hardening

- Fix check-bios.cmd: replace parenthesized if blocks with goto labels
  (cmd.exe fails silently with if/else on network-mapped drives)
- Move BIOS check files to winpeapps/_shared/BIOS for reliable SMB access
- Add network wait loop before BIOS check in startnet.cmd
- Show firmware status in WinPE menu header (BIOS_STATUS variable)
- Add BypassNRO registry key to skip OOBE network requirement
- Refactor download-drivers.py with --parallel N flag (ThreadPoolExecutor)
- Set SupportUser AutoLogonCount to 3 in shopfloor unattend
- Add shutdown -a at start + shutdown /r /t 10 at end of Run-ShopfloorSetup.ps1
- Switch download-drivers.py from wget to curl for reliable stall detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-03-23 11:02:36 -04:00
parent 86660a0939
commit 6d0e6ee284
6 changed files with 1538 additions and 42 deletions

814
download-drivers.py Executable file
View File

@@ -0,0 +1,814 @@
#!/usr/bin/env python3
"""
download-drivers.py — Download Dell drivers (+ BIOS) and push to PXE server
Downloads driver packs directly from Dell's public catalog (downloads.dell.com).
Matches models from user_selections.json / HardwareDriver.json against Dell's
DriverPackCatalog. No GE network or Media Creator Lite required.
Usage:
./download-drivers.py # download + push selected drivers
./download-drivers.py --list # preview without downloading
./download-drivers.py --bios # also download BIOS updates
./download-drivers.py --image gea-standard # push directly to an image
./download-drivers.py --force # re-download even if on server
./download-drivers.py --parallel 4 # process 4 packs concurrently
Requires: curl, 7z, sshpass, rsync
"""
import argparse
import concurrent.futures
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
import threading
import xml.etree.ElementTree as ET
from pathlib import Path
REPO_DIR = Path(__file__).resolve().parent
PXE_HOST = "10.9.100.1"
PXE_USER = "pxe"
PXE_PASS = "pxe"
UPLOAD_DEST = "/home/pxe/image-upload"
IMAGE_BASE = "/srv/samba/winpeapps"
DELL_DRIVER_CATALOG = "https://downloads.dell.com/catalog/DriverPackCatalog.cab"
DELL_BIOS_CATALOG = "https://downloads.dell.com/catalog/DellSDPCatalogPC.cab"
DELL_BASE = "https://downloads.dell.com"
NS = {"d": "openmanage/cm/dm"}
SDP_CAT_NS = "http://schemas.microsoft.com/sms/2005/04/CorporatePublishing/SystemsManagementCatalog.xsd"
SDP_PKG_NS = "http://schemas.microsoft.com/wsus/2005/04/CorporatePublishing/SoftwareDistributionPackage.xsd"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def format_size(n):
if n >= 1024**3: return f"{n / 1024**3:.1f} GB"
if n >= 1024**2: return f"{n / 1024**2:.0f} MB"
return f"{n / 1024:.0f} KB"
def resolve_dest_dir(d):
"""Convert *destinationdir*\\Deploy\\... to Deploy/..."""
return d.replace("*destinationdir*\\", "").replace("*destinationdir*", "").replace("\\", "/")
def ssh_cmd(host, cmd):
return subprocess.run(
["sshpass", "-p", PXE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
"-o", "LogLevel=ERROR", f"{PXE_USER}@{host}", cmd],
capture_output=True, text=True)
def verify_sha256(filepath, expected):
sha = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
sha.update(chunk)
return sha.hexdigest().upper() == expected.upper()
def extract_model_ids(name):
"""Extract model identifiers like '5450', 'PC14250', 'QCM1250'."""
ids = set(re.findall(r'\b([A-Z]*\d[\w]{2,})\b', name, re.I))
# Dell uses Qx* codenames where GE uses QC*/QB* (e.g. QxM1250 = QCM1250)
extras = set()
for mid in ids:
if re.match(r'^Q[A-Z][A-Z]\d', mid, re.I):
extras.add("Qx" + mid[2:]) # QCM1250 -> QxM1250
elif re.match(r'^Qx[A-Z]\d', mid, re.I):
pass # already in Qx form, will match directly
return ids | extras
def get_brand(name):
lower = name.lower()
for b in ["latitude", "precision", "optiplex", "pro max", "pro"]:
if b in lower:
return b
return None
# ---------------------------------------------------------------------------
# Catalog download + parsing
# ---------------------------------------------------------------------------
def download_and_extract_cab(url, tmpdir):
"""Download a .cab, extract with 7z, return path to XML inside."""
cab = os.path.join(tmpdir, os.path.basename(url))
print(f" Fetching {os.path.basename(url)}...", end=" ", flush=True)
r = subprocess.run(["wget", "-q", "-O", cab, url])
if r.returncode != 0:
print("FAILED"); return None
subprocess.run(["7z", "x", "-y", f"-o{tmpdir}", cab],
capture_output=True, text=True)
os.remove(cab)
xml_name = os.path.basename(url).replace(".cab", ".xml")
xml_path = os.path.join(tmpdir, xml_name)
if os.path.exists(xml_path):
print("OK"); return xml_path
print("FAILED (XML not found)"); return None
def parse_driver_catalog(xml_path, os_filter=None):
"""Parse DriverPackCatalog.xml → list of driver pack dicts.
os_filter: list of OS prefixes to match, e.g. ["Windows10", "Windows11"].
Defaults to ["Windows10", "Windows11"] (both).
"""
if os_filter is None:
os_filter = ["Windows10", "Windows11"]
tree = ET.parse(xml_path)
packs = []
for pkg in tree.getroot().findall(".//d:DriverPackage", NS):
if pkg.get("type") != "win":
continue
os_codes = [o.get("osCode", "") for o in pkg.findall(".//d:OperatingSystem", NS)]
if not any(code.startswith(prefix) for code in os_codes for prefix in os_filter):
continue
models = []
for m in pkg.findall(".//d:Model", NS):
d = m.find("d:Display", NS)
models.append({
"name": m.get("name", ""),
"display": d.text.strip() if d is not None and d.text else ""
})
sha256 = ""
for h in pkg.findall(".//d:Cryptography/d:Hash", NS):
if h.get("algorithm") == "SHA256":
sha256 = h.text; break
path = pkg.get("path", "")
packs.append({
"url": f"{DELL_BASE}/{path}",
"filename": path.split("/")[-1],
"size": int(pkg.get("size", 0)),
"sha256": sha256,
"models": models,
})
return packs
def parse_bios_catalog(xml_path, model_names):
"""Parse DellSDPCatalogPC.xml → list of latest BIOS update dicts for given models."""
tree = ET.parse(xml_path)
root = tree.getroot()
bios = {} # model_key → best entry
for pkg in root.iter(f"{{{SDP_CAT_NS}}}SoftwareDistributionPackage"):
title_elem = pkg.find(f".//{{{SDP_PKG_NS}}}Title")
if title_elem is None or not title_elem.text:
continue
title = title_elem.text
if "BIOS" not in title:
continue
# Find which of our models this BIOS applies to
matched_model = None
for mname in model_names:
for mid in extract_model_ids(mname):
if mid in title:
matched_model = mname
break
if matched_model:
break
if not matched_model:
continue
# Extract version from title (e.g., "...BIOS,1.20.1,1.20.1")
ver_match = re.search(r",(\d+\.\d+\.\d+)", title)
version = ver_match.group(1) if ver_match else "0.0.0"
# Get download URL
origin = pkg.find(f".//{{{SDP_PKG_NS}}}OriginFile")
if origin is None:
continue
entry = {
"title": title,
"version": version,
"filename": origin.get("FileName", ""),
"url": origin.get("OriginUri", ""),
"size": int(origin.get("Size", 0)),
"model": matched_model,
}
# Keep latest version per model
key = matched_model
if key not in bios or version > bios[key]["version"]:
bios[key] = entry
return list(bios.values())
# ---------------------------------------------------------------------------
# Model matching
# ---------------------------------------------------------------------------
def find_dell_packs(our_model_name, dell_packs):
"""Find Dell driver pack(s) matching one of our model names."""
our_ids = extract_model_ids(our_model_name)
our_brand = get_brand(our_model_name)
our_rugged = "rugged" in our_model_name.lower()
if not our_ids:
return []
matches = []
for pack in dell_packs:
for dm in pack["models"]:
dell_ids = extract_model_ids(dm["name"]) | extract_model_ids(dm["display"])
if not (our_ids & dell_ids):
continue
# Brand check: if we specify a brand, Dell must match (or have none)
if our_brand:
dell_brand = get_brand(dm["name"])
if dell_brand and dell_brand != our_brand:
continue
# Rugged check: if Dell explicitly labels pack as Rugged,
# only match our Rugged models (prevents non-rugged 5430 matching
# Rugged 5430 pack). If Dell doesn't say Rugged, allow any match
# (handles 7220/7230 which are Rugged-only but unlabeled in catalog).
dell_rugged = "rugged" in dm["name"].lower() or "rugged" in pack["filename"].lower()
if dell_rugged and not our_rugged:
continue
matches.append(pack)
break
# Deduplicate by URL
seen = set()
return [m for m in matches if m["url"] not in seen and not seen.add(m["url"])]
# ---------------------------------------------------------------------------
# Download + push
# ---------------------------------------------------------------------------
def make_zip_name(filename, dest_dir):
"""Generate a zip filename matching GE convention: win11_<model>_<ver>.zip"""
# Strip extension and version suffix to get base name
base = re.sub(r'[_-]Win1[01][_.].*', '', filename, flags=re.I)
base = re.sub(r'[-_]', '', base).lower()
# Extract version from filename (e.g., A04, A13)
ver_match = re.search(r'_A(\d+)', filename, re.I)
ver = f"a{ver_match.group(1)}" if ver_match else "a00"
return f"win11_{base}_{ver}.zip"
def process_download(args, url, filename, sha256, size, target_dir, label, tmpdir):
"""Download, verify, extract, re-zip, and push one driver pack. Returns True on success.
Each caller should pass a unique tmpdir to avoid collisions in parallel mode."""
local_file = os.path.join(tmpdir, filename)
# Download
print(f" [{label}] Downloading {format_size(size)}...")
r = subprocess.run(["curl", "-L", "-s", "-S",
"--speed-limit", "1000", "--speed-time", "30",
"--retry", "3", "--retry-delay", "5",
"-o", local_file, url])
if r.returncode != 0 or not os.path.exists(local_file):
print(f" [{label}] ERROR: Download failed (curl exit {r.returncode})")
if os.path.exists(local_file): os.remove(local_file)
return False
# Verify hash (if provided)
if sha256:
print(f" [{label}] Verifying SHA256...", end=" ", flush=True)
if not verify_sha256(local_file, sha256):
print("MISMATCH!")
os.remove(local_file)
return False
print("OK")
# Extract locally with 7z (unique subdir per worker)
extract_dir = os.path.join(tmpdir, "extract")
os.makedirs(extract_dir, exist_ok=True)
print(f" [{label}] Extracting...", end=" ", flush=True)
r = subprocess.run(["7z", "x", "-y", f"-o{extract_dir}", local_file],
capture_output=True, text=True)
os.remove(local_file)
if r.returncode != 0:
print(f"FAILED: {r.stderr[:200]}")
subprocess.run(["rm", "-rf", extract_dir])
return False
print("OK")
# Re-zip for PESetup.exe (expects zipped driver packs, not loose files)
zip_name = make_zip_name(filename, target_dir)
zip_path = os.path.join(tmpdir, zip_name)
print(f" [{label}] Zipping as {zip_name}...", end=" ", flush=True)
r = subprocess.run(["zip", "-r", "-q", zip_path, "."],
cwd=extract_dir)
subprocess.run(["rm", "-rf", extract_dir])
if r.returncode != 0:
print("FAILED")
return False
zip_size = os.path.getsize(zip_path)
print(f"OK ({format_size(zip_size)})")
# Push zip to PXE server
print(f" [{label}] Pushing to {target_dir}/{zip_name}...")
ssh_cmd(args.server, f"mkdir -p '{target_dir}'")
r = subprocess.run([
"rsync", "-a",
"-e", f"sshpass -p {PXE_PASS} ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR",
zip_path, f"{PXE_USER}@{args.server}:{target_dir}/"
])
os.remove(zip_path)
if r.returncode != 0:
print(f" [{label}] ERROR: rsync failed")
return False
return True
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Download Dell drivers (+ BIOS) and push to PXE server")
parser.add_argument("--list", action="store_true",
help="Preview without downloading")
parser.add_argument("--bios", action="store_true",
help="Also download BIOS updates")
parser.add_argument("--image",
help="Push directly to image type (e.g. gea-standard)")
parser.add_argument("--server", default=PXE_HOST,
help=f"PXE server IP (default: {PXE_HOST})")
parser.add_argument("--force", action="store_true",
help="Re-download even if already on server")
parser.add_argument("--cache-path",
help="Path to local image dir with Deploy/Control/ and Tools/")
parser.add_argument("--local",
help="Download to local directory (no server needed)")
parser.add_argument("--parallel", type=int, default=1, metavar="N",
help="Process N packs concurrently (default: 1)")
args = parser.parse_args()
# --- Load our model selections ---
control_dir = tools_dir = None
if args.cache_path:
p = Path(args.cache_path)
control_dir = p / "Deploy" / "Control"
tools_dir = p / "Tools" if (p / "Tools").is_dir() else p.parent / "Tools"
else:
for d in sorted(REPO_DIR.iterdir()):
if d.is_dir() and (d / "Deploy" / "Control" / "HardwareDriver.json").exists():
control_dir = d / "Deploy" / "Control"
tools_dir = d / "Tools"
break
if not control_dir or not (control_dir / "HardwareDriver.json").exists():
sys.exit("ERROR: HardwareDriver.json not found. Use --cache-path or ensure a local image dir exists.")
if not (tools_dir / "user_selections.json").exists():
sys.exit("ERROR: user_selections.json not found")
with open(control_dir / "HardwareDriver.json") as f:
hw_drivers = json.load(f)
with open(tools_dir / "user_selections.json") as f:
selections = json.load(f)[0]
os_id = selections["OperatingSystemSelection"]
selected_families = set(m["Id"] for m in selections["HardwareModelSelection"])
# Filter to selected + matching OS
our_entries = [d for d in hw_drivers
if d["family"] in selected_families and os_id in d.get("aOsIds", [])]
# Collect unique model names and their DestinationDirs
model_dest_map = {} # model_name → dest_dir
for entry in our_entries:
dest = resolve_dest_dir(entry["DestinationDir"])
for m in entry["models"].split(","):
m = m.strip()
if m not in model_dest_map:
model_dest_map[m] = dest
base_path = f"{IMAGE_BASE}/{args.image}" if args.image else UPLOAD_DEST
print()
print("=" * 60)
print(" Dell Driver Downloader for PXE Server")
print("=" * 60)
# --- Download Dell catalog ---
with tempfile.TemporaryDirectory(prefix="dell-catalog-") as catdir:
xml_path = download_and_extract_cab(DELL_DRIVER_CATALOG, catdir)
if not xml_path:
sys.exit("ERROR: Could not download Dell driver catalog")
dell_packs = parse_driver_catalog(xml_path)
print(f" Catalog: {len(dell_packs)} Win11 driver packs available")
bios_updates = []
if args.bios:
bios_xml = download_and_extract_cab(DELL_BIOS_CATALOG, catdir)
if bios_xml:
bios_updates = parse_bios_catalog(bios_xml, list(model_dest_map.keys()))
print(f" BIOS: {len(bios_updates)} update(s) found")
# --- Match our models to Dell catalog ---
# Group: dest_dir → list of Dell packs to download
download_plan = [] # list of {dell_pack, dest_dir, our_models}
unmatched = []
seen_urls = set()
dest_seen = {} # dest_dir → set of URLs already planned
for model_name, dest_dir in model_dest_map.items():
matches = find_dell_packs(model_name, dell_packs)
if not matches:
unmatched.append(model_name)
continue
for pack in matches:
if pack["url"] in seen_urls:
continue
seen_urls.add(pack["url"])
download_plan.append({
"pack": pack,
"dest_dir": dest_dir,
"model": model_name,
})
# --- Display plan ---
print()
total_drv_size = sum(d["pack"]["size"] for d in download_plan)
print(f" Drivers: {len(download_plan)} pack(s) to download ({format_size(total_drv_size)})")
print(f" Target: {args.server}:{base_path}")
if unmatched:
print(f" No Dell match: {len(unmatched)} model(s)")
print()
for i, d in enumerate(download_plan, 1):
p = d["pack"]
print(f" {i:3}. {d['model']:<38} {format_size(p['size']):>8} {p['filename']}")
print(f" -> {d['dest_dir']}")
if unmatched:
print()
print(f" Unmatched models (not in Dell public catalog):")
for m in unmatched:
print(f" - {m}")
if bios_updates:
total_bios = sum(b["size"] for b in bios_updates)
print()
print(f" BIOS updates: {len(bios_updates)} ({format_size(total_bios)})")
for b in bios_updates:
print(f" {b['model']:<35} v{b['version']} {b['filename']}")
print()
if args.list:
print(" (--list mode, nothing downloaded)")
return
# --- LOCAL MODE: download to local directory ---
if args.local:
local_dir = Path(args.local)
drv_dir = local_dir / "drivers"
bios_local_dir = local_dir / "bios"
drv_dir.mkdir(parents=True, exist_ok=True)
# Load local manifest
manifest_path = local_dir / "manifest.json"
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
# Thread-safe counters and manifest access
_lock = threading.Lock()
counters = {"completed": 0, "skipped": 0, "errors": 0}
# Build GE filename mapping from our HardwareDriver.json entries
ge_filename_map = {} # model_name → GE FileName
for entry in our_entries:
fn = entry.get("FileName") or entry.get("fileName", "")
dest = resolve_dest_dir(entry.get("DestinationDir") or entry.get("destinationDir", ""))
for m in (entry.get("models") or entry.get("modelswminame", "")).split(","):
m = m.strip()
if m and fn:
ge_filename_map[m] = {"filename": fn, "dest_dir": dest}
def _download_one_local(i, d):
"""Download a single driver pack (local mode). Thread-safe."""
pack = d["pack"]
tag = f"[{i}/{len(download_plan)}]"
with _lock:
print(f"{'=' * 60}")
print(f"{tag} {d['model']} ({format_size(pack['size'])})")
print(f"{'=' * 60}")
# Check if already downloaded (manifest or file size match)
local_file = drv_dir / pack["filename"]
if not args.force:
with _lock:
existing_hash = manifest.get("drivers", {}).get(pack["url"])
if existing_hash == pack["sha256"]:
with _lock:
print(f"{tag} Already downloaded (hash matches)")
counters["skipped"] += 1
return
if local_file.exists() and local_file.stat().st_size == pack["size"]:
with _lock:
print(f"{tag} Already downloaded (size matches)")
manifest.setdefault("drivers", {})[pack["url"]] = pack["sha256"]
counters["skipped"] += 1
return
# Download raw .exe to drivers/
with _lock:
print(f"{tag} Downloading {format_size(pack['size'])}...")
r = subprocess.run(["curl", "-L", "-s", "-S",
"--speed-limit", "1000", "--speed-time", "30",
"--retry", "3", "--retry-delay", "5",
"-o", str(local_file), pack["url"]])
if r.returncode != 0 or not local_file.exists():
with _lock:
print(f"{tag} ERROR: Download failed (curl exit {r.returncode})")
counters["errors"] += 1
if local_file.exists(): local_file.unlink()
return
# Verify size first
actual_size = local_file.stat().st_size
if pack["size"] and actual_size != pack["size"]:
with _lock:
print(f"{tag} ERROR: Size mismatch (got {format_size(actual_size)}, expected {format_size(pack['size'])})")
counters["errors"] += 1
local_file.unlink()
return
# Verify hash
if pack["sha256"]:
with _lock:
print(f"{tag} Verifying SHA256...", end=" ", flush=True)
if not verify_sha256(str(local_file), pack["sha256"]):
with _lock:
print("MISMATCH!")
counters["errors"] += 1
local_file.unlink()
return
with _lock:
print("OK")
ge_info = ge_filename_map.get(d["model"], {})
with _lock:
counters["completed"] += 1
manifest.setdefault("drivers", {})[pack["url"]] = pack["sha256"]
manifest.setdefault("mapping", {})[pack["filename"]] = {
"model": d["model"],
"dell_filename": pack["filename"],
"ge_filename": ge_info.get("filename", ""),
"dest_dir": d["dest_dir"],
"sha256": pack["sha256"],
"size": pack["size"],
}
print(f"{tag} Done.")
workers = max(1, args.parallel)
if workers > 1:
print(f" Downloading with {workers} parallel workers")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_download_one_local, i, d)
for i, d in enumerate(download_plan, 1)]
concurrent.futures.wait(futures)
# --- Download BIOS ---
bios_ok = bios_err = 0
if bios_updates:
bios_local_dir.mkdir(parents=True, exist_ok=True)
print(f"{'=' * 60}")
print(f" BIOS Updates -> {bios_local_dir}")
print(f"{'=' * 60}")
def _download_one_bios(b):
nonlocal bios_ok, bios_err
with _lock:
print(f"\n {b['model']} v{b['version']}")
if not args.force:
with _lock:
existing = manifest.get("bios", {}).get(b["model"])
if existing == b["version"]:
with _lock:
print(f" Already downloaded (v{b['version']})")
return
local_file = bios_local_dir / b["filename"]
with _lock:
print(f" [{b['model']}] Downloading {format_size(b['size'])}...")
r = subprocess.run(["curl", "-L", "-s", "-S",
"--speed-limit", "1000", "--speed-time", "30",
"--retry", "3", "--retry-delay", "5",
"-o", str(local_file), b["url"]])
if r.returncode != 0:
with _lock:
print(f" [{b['model']}] ERROR: Download failed")
bios_err += 1
if local_file.exists(): local_file.unlink()
return
with _lock:
bios_ok += 1
manifest.setdefault("bios", {})[b["model"]] = b["version"]
manifest.setdefault("bios_mapping", {})[b["filename"]] = {
"model": b["model"],
"version": b["version"],
"filename": b["filename"],
}
print(f" [{b['model']}] Done.")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_download_one_bios, b) for b in bios_updates]
concurrent.futures.wait(futures)
# Save manifest
manifest_path.write_text(json.dumps(manifest, indent=2))
# --- Summary ---
print()
print(f"{'=' * 60}")
print(f" Summary — Local Download")
print(f"{'=' * 60}")
print(f" Drivers downloaded: {counters['completed']}")
if counters["skipped"]: print(f" Drivers skipped: {counters['skipped']} (already have)")
if counters["errors"]: print(f" Drivers failed: {counters['errors']}")
if bios_updates:
print(f" BIOS downloaded: {bios_ok}")
if bios_err: print(f" BIOS failed: {bios_err}")
print(f" Saved to: {local_dir}")
print(f" Manifest: {manifest_path}")
print()
print(f" To push to server later:")
print(f" python3 download-drivers.py --push-local {local_dir}")
print()
return
# --- REMOTE MODE: download and push to server ---
# --- Verify SSH ---
print(f" Testing SSH to {args.server}...", end=" ", flush=True)
r = ssh_cmd(args.server, "echo OK")
if r.stdout.strip() != "OK":
print("FAILED")
sys.exit(f" Cannot SSH to {PXE_USER}@{args.server}: {r.stderr.strip()}")
print("OK")
print()
# --- Load manifest (tracks what's been downloaded by hash) ---
manifest_path = f"{base_path}/.driver-manifest.json"
r = ssh_cmd(args.server, f"cat '{manifest_path}' 2>/dev/null")
manifest = json.loads(r.stdout) if r.stdout.strip() else {}
# Thread-safe counters and manifest access
_lock = threading.Lock()
counters = {"completed": 0, "skipped": 0, "errors": 0}
# --- Download drivers ---
with tempfile.TemporaryDirectory(prefix="pxe-drivers-") as tmpdir:
def _process_one_remote(i, d):
"""Download, extract, re-zip, and push one driver pack. Thread-safe."""
pack = d["pack"]
target = f"{base_path}/{d['dest_dir']}"
tag = f"[{i}/{len(download_plan)}]"
with _lock:
print(f"{'=' * 60}")
print(f"{tag} {d['model']} ({format_size(pack['size'])})")
print(f"{'=' * 60}")
if not args.force:
with _lock:
existing_hash = manifest.get(d["dest_dir"], {}).get(pack["filename"])
if existing_hash == pack["sha256"]:
with _lock:
print(f"{tag} Up to date (hash matches manifest)")
counters["skipped"] += 1
return
# Each worker gets its own temp subdirectory
worker_tmp = os.path.join(tmpdir, f"worker-{i}")
os.makedirs(worker_tmp, exist_ok=True)
ok = process_download(args, pack["url"], pack["filename"],
pack["sha256"], pack["size"], target,
d["model"], worker_tmp)
with _lock:
if ok:
counters["completed"] += 1
manifest.setdefault(d["dest_dir"], {})[pack["filename"]] = pack["sha256"]
print(f"{tag} Done.")
else:
counters["errors"] += 1
workers = max(1, args.parallel)
if workers > 1:
print(f" Processing with {workers} parallel workers")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_process_one_remote, i, d)
for i, d in enumerate(download_plan, 1)]
concurrent.futures.wait(futures)
# --- Download BIOS (goes to enrollment share, shared across all images) ---
bios_ok = bios_err = 0
bios_dir = "/srv/samba/enrollment/BIOS"
if bios_updates:
print(f"{'=' * 60}")
print(f" BIOS Updates -> {bios_dir}")
print(f"{'=' * 60}")
ssh_cmd(args.server, f"mkdir -p '{bios_dir}'")
models_txt = [] # lines for models.txt manifest
def _process_one_bios(b):
nonlocal bios_ok, bios_err
target = f"{bios_dir}/{b['filename']}"
with _lock:
print(f"\n {b['model']} v{b['version']}")
if not args.force:
with _lock:
existing = manifest.get("BIOS", {}).get(b["model"])
if existing == b["version"]:
with _lock:
print(f" Up to date (v{b['version']})")
models_txt.append(f"{b['model']}|{b['filename']}")
return
# BIOS .exe goes as-is (not extracted)
bios_tmp = os.path.join(tmpdir, f"bios-{b['filename']}")
with _lock:
print(f" [{b['model']}] Downloading {format_size(b['size'])}...")
r = subprocess.run(["curl", "-L", "-s", "-S",
"--speed-limit", "1000", "--speed-time", "30",
"--retry", "3", "--retry-delay", "5",
"-o", bios_tmp, b["url"]])
if r.returncode != 0:
with _lock:
print(f" [{b['model']}] ERROR: Download failed")
bios_err += 1
if os.path.exists(bios_tmp): os.remove(bios_tmp)
return
r = subprocess.run([
"rsync", "-a",
"-e", f"sshpass -p {PXE_PASS} ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR",
bios_tmp, f"{PXE_USER}@{args.server}:{target}"
])
os.remove(bios_tmp)
if r.returncode != 0:
with _lock:
print(f" [{b['model']}] ERROR: Push failed")
bios_err += 1
else:
with _lock:
print(f" [{b['model']}] Done.")
bios_ok += 1
manifest.setdefault("BIOS", {})[b["model"]] = b["version"]
models_txt.append(f"{b['model']}|{b['filename']}")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_process_one_bios, b) for b in bios_updates]
concurrent.futures.wait(futures)
# Generate models.txt for check-bios.cmd
if models_txt:
manifest_content = "# ModelSubstring|BIOSFile\\n" + "\\n".join(models_txt) + "\\n"
ssh_cmd(args.server,
f"printf '{manifest_content}' > '{bios_dir}/models.txt'")
print(f"\n models.txt updated ({len(models_txt)} entries)")
# --- Save manifest ---
completed, skipped, errors = counters["completed"], counters["skipped"], counters["errors"]
if completed > 0 or bios_ok > 0:
manifest_json = json.dumps(manifest, indent=2)
ssh_cmd(args.server,
f"cat > '{manifest_path}' << 'MANIFEST_EOF'\n{manifest_json}\nMANIFEST_EOF")
print(f" Manifest saved to {manifest_path}")
# --- Summary ---
print()
print(f"{'=' * 60}")
print(f" Summary")
print(f"{'=' * 60}")
print(f" Drivers downloaded: {completed}")
if skipped: print(f" Drivers skipped: {skipped} (up to date)")
if errors: print(f" Drivers failed: {errors}")
if bios_updates:
print(f" BIOS downloaded: {bios_ok}")
if bios_err: print(f" BIOS failed: {bios_err}")
print()
if completed > 0 and not args.image:
print(f" Drivers staged in {base_path}/Deploy/Out-of-box Drivers/")
print(f" Use the webapp (http://{args.server}:9009) to import,")
print(f" or re-run with --image <type> to push directly.")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,166 @@
<?xml version="1.0"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="windowsPE">
</settings>
<settings pass="offlineServicing">
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DriverPaths>
<PathAndCredentials xmlns="" wcm:keyValue="1" action="add">
<Path>W:\Drivers</Path>
</PathAndCredentials>
</DriverPaths>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<ComputerName>H%serialnumber%</ComputerName>
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
<RegisteredOwner>GE</RegisteredOwner>
<TimeZone>Eastern Standard Time</TimeZone>
</component>
<component name="Microsoft-Windows-Deployment" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
<Description>Install External Root Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>2</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
<Description>Install External Intermediate Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>3</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
<Description>Install Enterprise Root Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
<Description>Install Enterprise Device Issuing Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>5</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
<Description>Install Enterprise Server Issuing Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>6</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
<Description>Install Enterprise SmartCard Issuing Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>7</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
<Description>Install Enterprise User Issuing Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>8</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
<Description>Install Aerospace Enterprise Root Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>9</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
<Description>Install Zscaler Commercial Certificate</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Path>reg.exe add "HKLM\System\CurrentControlSet\Control\Network\NewNetworkWindowOff" /f </Path>
<Description>Disable Network Windows</Description>
<Order>10</Order>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>11</Order>
<Path>netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
<Description>Install INTERNETACCESS WiFi Profile</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>12</Order>
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck"</Path>
<Description>Enable WinRM</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>13</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
<Description>Bypass OOBE network requirement</Description>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<UserAccounts>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Password>
<Value>Pa55word</Value>
<PlainText>true</PlainText>
</Password>
<Name>SupportUser</Name>
<Group>Administrators</Group>
<DisplayName>SupportUser</DisplayName>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Password>
<Value>Pa55word</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<Username>SupportUser</Username>
<LogonCount>7</LogonCount>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>cmd.exe /c powercfg /change monitor-timeout-ac 0 &amp; powercfg /change monitor-timeout-dc 0 &amp; powercfg /change standby-timeout-ac 0 &amp; powercfg /change standby-timeout-dc 0</CommandLine>
<Description>Disable display and sleep timeout during setup</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -match 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 10; Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 100"</CommandLine>
<Description>Prioritize WiFi over ethernet</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private"</CommandLine>
<Description>Set network profile to Private</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>4</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Write-Host 'Waiting for internet connectivity...'; while (-not (Test-Connection -ComputerName login.microsoftonline.us -Count 1 -Quiet -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }; Write-Host 'Internet connected.'"</CommandLine>
<Description>Wait for internet connectivity</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>5</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
<Description>Run GCCH Enrollment</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>6</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
<Description>Run shopfloor PC type setup</Description>
</SynchronousCommand>
</FirstLogonCommands>
<TimeZone>Eastern Standard Time</TimeZone>
</component>
</settings>
</unattend>

148
playbook/check-bios.cmd Normal file
View File

@@ -0,0 +1,148 @@
@echo off
REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
REM Sets BIOS_STATUS for startnet.cmd menu display
set "BIOSDIR=%~dp0"
set "FLASH=%BIOSDIR%Flash64W.exe"
set "MANIFEST=%BIOSDIR%models.txt"
if exist "%FLASH%" goto :flash_ok
echo Flash64W.exe not found, skipping BIOS check.
set "BIOS_STATUS=Skipped (Flash64W.exe missing)"
exit /b 0
:flash_ok
if exist "%MANIFEST%" goto :manifest_ok
echo models.txt not found, skipping BIOS check.
set "BIOS_STATUS=Skipped (models.txt missing)"
exit /b 0
:manifest_ok
REM --- Get system model from WMI ---
set SYSMODEL=
for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
if not defined SYSMODEL set "SYSMODEL=%%M"
)
for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
if "%SYSMODEL%"=="" goto :no_model
goto :got_model
:no_model
echo Could not detect system model, skipping BIOS check.
set "BIOS_STATUS=Skipped (model not detected)"
exit /b 0
:got_model
echo Model: %SYSMODEL%
REM --- Get current BIOS version ---
set BIOSVER=
for /f "skip=1 tokens=*" %%V in ('wmic bios get smbiosbiosversion 2^>NUL') do (
if not defined BIOSVER set "BIOSVER=%%V"
)
for /f "tokens=*" %%a in ("%BIOSVER%") do set "BIOSVER=%%a"
echo Current BIOS: %BIOSVER%
REM --- Read manifest and find matching BIOS file ---
set BIOSFILE=
set TARGETVER=
for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
echo "%SYSMODEL%" | find /I "%%A" >NUL
if not errorlevel 1 (
set "BIOSFILE=%%B"
set "TARGETVER=%%C"
goto :found_bios
)
)
echo No BIOS update available for this model.
set "BIOS_STATUS=%SYSMODEL% - no update in catalog"
exit /b 0
:found_bios
if not exist "%BIOSDIR%%BIOSFILE%" goto :bios_file_missing
goto :bios_file_ok
:bios_file_missing
echo WARNING: %BIOSFILE% not found in BIOS folder.
set "BIOS_STATUS=%SYSMODEL% - %BIOSFILE% missing"
exit /b 0
:bios_file_ok
REM --- Skip if already at target version ---
echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
if not errorlevel 1 goto :already_current
REM --- Compare versions to prevent downgrade ---
call :compare_versions "%BIOSVER%" "%TARGETVER%"
if "%VERCMP%"=="newer" goto :already_newer
goto :do_flash
:already_current
echo BIOS is already up to date - %TARGETVER%
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
exit /b 0
:already_newer
echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
set "BIOS_STATUS=%SYSMODEL% v%BIOSVER% (up to date)"
exit /b 0
:do_flash
echo Update: %BIOSVER% -^> %TARGETVER%
echo Applying BIOS update (this may take a few minutes, do not power off)...
pushd "%BIOSDIR%"
Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
set FLASHRC=%ERRORLEVEL%
popd
echo Flash complete (exit code %FLASHRC%).
if "%FLASHRC%"=="3" goto :already_current
if "%FLASHRC%"=="0" goto :flash_done
if "%FLASHRC%"=="2" goto :staged
if "%FLASHRC%"=="6" goto :staged
echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
set "BIOS_STATUS=%SYSMODEL% flash error (code %FLASHRC%)"
exit /b 0
:flash_done
echo BIOS update complete.
set "BIOS_STATUS=%SYSMODEL% updated %BIOSVER% -^> %TARGETVER%"
exit /b 0
:staged
echo.
echo ========================================
echo BIOS update staged successfully.
echo It will flash during POST after the
echo post-imaging reboot.
echo ========================================
echo.
set "BIOS_STATUS=%SYSMODEL% STAGED %BIOSVER% -^> %TARGETVER% (flashes on reboot)"
exit /b 0
:compare_versions
set "VERCMP=equal"
set "_CV=%~1"
set "_TV=%~2"
for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
set /a "C1=%%a" 2>NUL
set /a "C2=%%b" 2>NUL
set /a "C3=%%c" 2>NUL
)
for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
set /a "T1=%%a" 2>NUL
set /a "T2=%%b" 2>NUL
set /a "T3=%%c" 2>NUL
)
if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
if %C2% LSS %T2% ( set "VERCMP=older" & goto :eof )
if %C3% GTR %T3% ( set "VERCMP=newer" & goto :eof )
if %C3% LSS %T3% ( set "VERCMP=older" & goto :eof )
set "VERCMP=equal"
goto :eof

View File

@@ -0,0 +1,60 @@
# Run-ShopfloorSetup.ps1 — Dispatcher for shopfloor PC type setup
# Runs Shopfloor baseline scripts first, then type-specific scripts on top.
# Cancel any pending reboot so it doesn't interrupt setup
shutdown -a 2>$null
$enrollDir = "C:\Enrollment"
$typeFile = Join-Path $enrollDir "pc-type.txt"
$setupDir = Join-Path $enrollDir "shopfloor-setup"
if (-not (Test-Path $typeFile)) {
Write-Host "No pc-type.txt found - skipping shopfloor setup."
exit 0
}
$pcType = (Get-Content $typeFile -First 1).Trim()
if (-not $pcType) {
Write-Host "pc-type.txt is empty - skipping shopfloor setup."
exit 0
}
Write-Host "Shopfloor PC Type: $pcType"
# --- Run Shopfloor baseline scripts first ---
$baselineDir = Join-Path $setupDir "Shopfloor"
if (Test-Path $baselineDir) {
$scripts = Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
foreach ($script in $scripts) {
shutdown /a 2>$null
Write-Host "Running baseline: $($script.Name)"
try {
& $script.FullName
} catch {
Write-Warning "Baseline script $($script.Name) failed: $_"
}
}
}
# --- Run type-specific scripts (if not just baseline Shopfloor) ---
if ($pcType -ne "Shopfloor") {
$typeDir = Join-Path $setupDir $pcType
if (Test-Path $typeDir) {
$scripts = Get-ChildItem -Path $typeDir -Filter "*.ps1" -File | Sort-Object Name
foreach ($script in $scripts) {
shutdown /a 2>$null
Write-Host "Running $pcType setup: $($script.Name)"
try {
& $script.FullName
} catch {
Write-Warning "Script $($script.Name) failed: $_"
}
}
} else {
Write-Host "No type-specific scripts found for $pcType."
}
}
Write-Host "Shopfloor setup complete for $pcType."
Write-Host "Rebooting in 10 seconds..."
shutdown /r /t 10

View File

@@ -3,37 +3,141 @@ echo Please wait while 'WinPE' is being processed. This may take a few seconds.
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
REM --- Wait for network (DHCP may take a moment after wpeinit) ---
echo Waiting for network...
:wait_net
ping -n 2 10.9.100.1 >NUL 2>&1
if errorlevel 1 goto wait_net
echo Network ready.
REM --- BIOS update check (runs before imaging menu) ---
set BIOS_STATUS=No BIOS check (share unavailable)
net use B: \\10.9.100.1\winpeapps\_shared /user:pxe-upload pxe /persistent:no 2>NUL
if exist B:\BIOS\check-bios.cmd (
echo.
echo Checking for BIOS updates...
call B:\BIOS\check-bios.cmd
REM If BIOS was flashed, check-bios.cmd reboots and we never reach here.
echo.
)
net use B: /delete 2>NUL
:menu
cls
echo.
echo ========================================
echo WinPE Setup Menu
echo ========================================
echo Firmware: %BIOS_STATUS%
echo.
echo Please select an option:
echo.
echo 1. GEA Standard
echo 2. GEA Engineer
echo 3. GEA Shopfloor
echo 4. GEA Shopfloor MCE
echo 5. GE Standard
echo 6. GE Engineer
echo 7. GE Shopfloor Lockdown
echo 8. GE Shopfloor MCE
echo 4. GE Standard
echo 5. GE Engineer
echo 6. GE Shopfloor Lockdown
echo 7. GE Shopfloor MCE
echo.
echo ========================================
echo.
set /p choice=Enter your choice (1-8):
set /p choice=Enter your choice (1-7):
REM --- Only shopfloor images (3,6,7) need GCCH enrollment ---
set PPKG=
if "%choice%"=="3" goto enroll_menu
if "%choice%"=="6" goto enroll_menu
if "%choice%"=="7" goto enroll_menu
goto enroll_staged
:enroll_menu
cls
echo.
echo ========================================
echo GCCH Enrollment Profile
echo ========================================
echo.
echo 1. No Office
echo 2. Standard Office (x86)
echo 3. Standard Office (x64)
echo 4. Pro Plus Office (x86) with Access
echo 5. Pro Plus Office (x64) with Access
echo 6. Skip enrollment
echo.
set /p enroll=Enter your choice (1-6):
if "%enroll%"=="1" set PPKG=GCCH_Prod_SFLD_NoOffice_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="2" set PPKG=GCCH_Prod_SFLD_StdOffice-x86_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="3" set PPKG=GCCH_Prod_SFLD_StdOffice-x64_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="4" set PPKG=GCCH_Prod_SFLD_ProPlusOffice-x86_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="5" set PPKG=GCCH_Prod_SFLD_ProPlusOffice-x64_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="6" set PPKG=
if "%enroll%"=="" goto enroll_menu
:pctype_menu
cls
echo.
echo ========================================
echo Shopfloor PC Type
echo ========================================
echo.
echo 1. CMM
echo 2. Wax and Trace
echo 3. Keyence
echo 4. Genspect
echo 5. Display
echo 6. Shopfloor (General)
echo.
set PCTYPE=
set /p pctype_choice=Enter your choice (1-6):
if "%pctype_choice%"=="1" set PCTYPE=CMM
if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace
if "%pctype_choice%"=="3" set PCTYPE=Keyence
if "%pctype_choice%"=="4" set PCTYPE=Genspect
if "%pctype_choice%"=="5" set PCTYPE=Display
if "%pctype_choice%"=="6" set PCTYPE=Shopfloor
if "%PCTYPE%"=="" goto pctype_menu
REM --- Display sub-type selection ---
set DISPLAYTYPE=
if not "%PCTYPE%"=="Display" goto skip_display_menu
:display_menu
cls
echo.
echo ========================================
echo Display Type
echo ========================================
echo.
echo 1. Lobby Display
echo 2. Dashboard
echo.
set /p display_choice=Enter your choice (1-2):
if "%display_choice%"=="1" set DISPLAYTYPE=Lobby
if "%display_choice%"=="2" set DISPLAYTYPE=Dashboard
if "%DISPLAYTYPE%"=="" goto display_menu
:skip_display_menu
REM --- Map enrollment share early (kept open for copy after imaging) ---
set NEED_ENROLL=0
if not "%PPKG%"=="" set NEED_ENROLL=1
if not "%PCTYPE%"=="" set NEED_ENROLL=1
if "%NEED_ENROLL%"=="0" goto enroll_staged
net use Y: \\10.9.100.1\enrollment /user:pxe-upload pxe /persistent:no
if "%PPKG%"=="" goto enroll_staged
if not exist "Y:\%PPKG%" (
echo WARNING: %PPKG% not found on server. Enrollment will be skipped.
set PPKG=
)
:enroll_staged
echo. > X:\Boot.tag
if "%choice%"=="1" goto gea-standard
if "%choice%"=="2" goto gea-engineer
if "%choice%"=="3" goto gea-shopfloor
if "%choice%"=="4" goto gea-shopfloor-mce
if "%choice%"=="5" goto ge-standard
if "%choice%"=="6" goto ge-engineer
if "%choice%"=="7" goto ge-shopfloor-lockdown
if "%choice%"=="8" goto ge-shopfloor-mce
if "%choice%"=="4" goto ge-standard
if "%choice%"=="5" goto ge-engineer
if "%choice%"=="6" goto ge-shopfloor-lockdown
if "%choice%"=="7" goto ge-shopfloor-mce
echo Invalid choice. Please try again.
pause
goto menu
@@ -62,14 +166,6 @@ for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor /user:pxe-upload pxe /persistent:no
goto end
:gea-shopfloor-mce
echo.
echo Starting GEA Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor-mce /user:pxe-upload pxe /persistent:no
goto end
:ge-standard
echo.
echo Starting GE Standard setup...
@@ -107,12 +203,70 @@ echo.
echo Waiting for PESetup.exe to start...
:wait_start
ping -n 3 127.0.0.1 >NUL
tasklist /FI "IMAGENAME eq PESetup.exe" 2>NUL | find /I "PESetup.exe" >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if errorlevel 1 goto wait_start
echo PESetup.exe is running. Waiting for imaging to complete...
REM --- Copy enrollment package and shopfloor setup as soon as Windows partition appears ---
if "%PPKG%"=="" if "%PCTYPE%"=="" goto wait_finish
echo Waiting for Windows partition at W: ...
:wait_enroll
ping -n 11 127.0.0.1 >NUL
if not exist W:\Windows\System32\config\system goto wait_enroll
echo Found Windows at W:
mkdir W:\Enrollment 2>NUL
REM --- Copy PPKG if selected ---
if "%PPKG%"=="" goto copy_pctype
copy /Y "Y:\%PPKG%" "W:\Enrollment\%PPKG%"
if errorlevel 1 (
echo WARNING: Failed to copy enrollment package.
goto copy_pctype
)
copy /Y "Y:\run-enrollment.ps1" "W:\run-enrollment.ps1"
REM --- Create enroll.cmd at drive root as manual fallback ---
> W:\enroll.cmd (
echo @echo off
echo echo Waiting for network...
echo :waitnet
echo ping -n 2 8.8.8.8 ^>NUL 2^>^&1
echo if errorlevel 1 goto waitnet
echo echo Network connected. Running enrollment...
echo powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"
)
echo Manual fallback created at W:\enroll.cmd
:copy_pctype
REM --- Copy shopfloor PC type setup scripts ---
if "%PCTYPE%"=="" goto cleanup_enroll
echo %PCTYPE%> W:\Enrollment\pc-type.txt
if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt
copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
REM --- Always copy Shopfloor baseline scripts ---
mkdir W:\Enrollment\shopfloor-setup 2>NUL
if exist "Y:\shopfloor-setup\Shopfloor" (
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
echo Copied Shopfloor baseline setup files.
)
REM --- Copy type-specific scripts on top of baseline ---
if "%PCTYPE%"=="Shopfloor" goto pctype_done
if exist "Y:\shopfloor-setup\%PCTYPE%" (
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\"
echo Copied %PCTYPE% setup files.
) else (
echo WARNING: No setup files found for PC type %PCTYPE%.
)
:pctype_done
:cleanup_enroll
net use Y: /delete 2>NUL
:wait_finish
ping -n 11 127.0.0.1 >NUL
tasklist /FI "IMAGENAME eq PESetup.exe" 2>NUL | find /I "PESetup.exe" >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if not errorlevel 1 goto wait_finish
echo.
echo Imaging complete. Rebooting in 15 seconds...

View File

@@ -3,37 +3,141 @@ echo Please wait while 'WinPE' is being processed. This may take a few seconds.
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
REM --- Wait for network (DHCP may take a moment after wpeinit) ---
echo Waiting for network...
:wait_net
ping -n 2 10.9.100.1 >NUL 2>&1
if errorlevel 1 goto wait_net
echo Network ready.
REM --- BIOS update check (runs before imaging menu) ---
set BIOS_STATUS=No BIOS check (share unavailable)
net use B: \\10.9.100.1\winpeapps\_shared /user:pxe-upload pxe /persistent:no 2>NUL
if exist B:\BIOS\check-bios.cmd (
echo.
echo Checking for BIOS updates...
call B:\BIOS\check-bios.cmd
REM If BIOS was flashed, check-bios.cmd reboots and we never reach here.
echo.
)
net use B: /delete 2>NUL
:menu
cls
echo.
echo ========================================
echo WinPE Setup Menu
echo ========================================
echo Firmware: %BIOS_STATUS%
echo.
echo Please select an option:
echo.
echo 1. GEA Standard
echo 2. GEA Engineer
echo 3. GEA Shopfloor
echo 4. GEA Shopfloor MCE
echo 5. GE Standard
echo 6. GE Engineer
echo 7. GE Shopfloor Lockdown
echo 8. GE Shopfloor MCE
echo 4. GE Standard
echo 5. GE Engineer
echo 6. GE Shopfloor Lockdown
echo 7. GE Shopfloor MCE
echo.
echo ========================================
echo.
set /p choice=Enter your choice (1-8):
set /p choice=Enter your choice (1-7):
REM --- Only shopfloor images (3,6,7) need GCCH enrollment ---
set PPKG=
if "%choice%"=="3" goto enroll_menu
if "%choice%"=="6" goto enroll_menu
if "%choice%"=="7" goto enroll_menu
goto enroll_staged
:enroll_menu
cls
echo.
echo ========================================
echo GCCH Enrollment Profile
echo ========================================
echo.
echo 1. No Office
echo 2. Standard Office (x86)
echo 3. Standard Office (x64)
echo 4. Pro Plus Office (x86) with Access
echo 5. Pro Plus Office (x64) with Access
echo 6. Skip enrollment
echo.
set /p enroll=Enter your choice (1-6):
if "%enroll%"=="1" set PPKG=GCCH_Prod_SFLD_NoOffice_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="2" set PPKG=GCCH_Prod_SFLD_StdOffice-x86_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="3" set PPKG=GCCH_Prod_SFLD_StdOffice-x64_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="4" set PPKG=GCCH_Prod_SFLD_ProPlusOffice-x86_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="5" set PPKG=GCCH_Prod_SFLD_ProPlusOffice-x64_US_Exp_20260430_v4.8.ppkg
if "%enroll%"=="6" set PPKG=
if "%enroll%"=="" goto enroll_menu
:pctype_menu
cls
echo.
echo ========================================
echo Shopfloor PC Type
echo ========================================
echo.
echo 1. CMM
echo 2. Wax and Trace
echo 3. Keyence
echo 4. Genspect
echo 5. Display
echo 6. Shopfloor (General)
echo.
set PCTYPE=
set /p pctype_choice=Enter your choice (1-6):
if "%pctype_choice%"=="1" set PCTYPE=CMM
if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace
if "%pctype_choice%"=="3" set PCTYPE=Keyence
if "%pctype_choice%"=="4" set PCTYPE=Genspect
if "%pctype_choice%"=="5" set PCTYPE=Display
if "%pctype_choice%"=="6" set PCTYPE=Shopfloor
if "%PCTYPE%"=="" goto pctype_menu
REM --- Display sub-type selection ---
set DISPLAYTYPE=
if not "%PCTYPE%"=="Display" goto skip_display_menu
:display_menu
cls
echo.
echo ========================================
echo Display Type
echo ========================================
echo.
echo 1. Lobby Display
echo 2. Dashboard
echo.
set /p display_choice=Enter your choice (1-2):
if "%display_choice%"=="1" set DISPLAYTYPE=Lobby
if "%display_choice%"=="2" set DISPLAYTYPE=Dashboard
if "%DISPLAYTYPE%"=="" goto display_menu
:skip_display_menu
REM --- Map enrollment share early (kept open for copy after imaging) ---
set NEED_ENROLL=0
if not "%PPKG%"=="" set NEED_ENROLL=1
if not "%PCTYPE%"=="" set NEED_ENROLL=1
if "%NEED_ENROLL%"=="0" goto enroll_staged
net use Y: \\10.9.100.1\enrollment /user:pxe-upload pxe /persistent:no
if "%PPKG%"=="" goto enroll_staged
if not exist "Y:\%PPKG%" (
echo WARNING: %PPKG% not found on server. Enrollment will be skipped.
set PPKG=
)
:enroll_staged
echo. > X:\Boot.tag
if "%choice%"=="1" goto gea-standard
if "%choice%"=="2" goto gea-engineer
if "%choice%"=="3" goto gea-shopfloor
if "%choice%"=="4" goto gea-shopfloor-mce
if "%choice%"=="5" goto ge-standard
if "%choice%"=="6" goto ge-engineer
if "%choice%"=="7" goto ge-shopfloor-lockdown
if "%choice%"=="8" goto ge-shopfloor-mce
if "%choice%"=="4" goto ge-standard
if "%choice%"=="5" goto ge-engineer
if "%choice%"=="6" goto ge-shopfloor-lockdown
if "%choice%"=="7" goto ge-shopfloor-mce
echo Invalid choice. Please try again.
pause
goto menu
@@ -62,14 +166,6 @@ for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor /user:pxe-upload pxe /persistent:no
goto end
:gea-shopfloor-mce
echo.
echo Starting GEA Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor-mce /user:pxe-upload pxe /persistent:no
goto end
:ge-standard
echo.
echo Starting GE Standard setup...
@@ -107,12 +203,70 @@ echo.
echo Waiting for PESetup.exe to start...
:wait_start
ping -n 3 127.0.0.1 >NUL
tasklist /FI "IMAGENAME eq PESetup.exe" 2>NUL | find /I "PESetup.exe" >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if errorlevel 1 goto wait_start
echo PESetup.exe is running. Waiting for imaging to complete...
REM --- Copy enrollment package and shopfloor setup as soon as Windows partition appears ---
if "%PPKG%"=="" if "%PCTYPE%"=="" goto wait_finish
echo Waiting for Windows partition at W: ...
:wait_enroll
ping -n 11 127.0.0.1 >NUL
if not exist W:\Windows\System32\config\system goto wait_enroll
echo Found Windows at W:
mkdir W:\Enrollment 2>NUL
REM --- Copy PPKG if selected ---
if "%PPKG%"=="" goto copy_pctype
copy /Y "Y:\%PPKG%" "W:\Enrollment\%PPKG%"
if errorlevel 1 (
echo WARNING: Failed to copy enrollment package.
goto copy_pctype
)
copy /Y "Y:\run-enrollment.ps1" "W:\run-enrollment.ps1"
REM --- Create enroll.cmd at drive root as manual fallback ---
> W:\enroll.cmd (
echo @echo off
echo echo Waiting for network...
echo :waitnet
echo ping -n 2 8.8.8.8 ^>NUL 2^>^&1
echo if errorlevel 1 goto waitnet
echo echo Network connected. Running enrollment...
echo powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"
)
echo Manual fallback created at W:\enroll.cmd
:copy_pctype
REM --- Copy shopfloor PC type setup scripts ---
if "%PCTYPE%"=="" goto cleanup_enroll
echo %PCTYPE%> W:\Enrollment\pc-type.txt
if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt
copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
REM --- Always copy Shopfloor baseline scripts ---
mkdir W:\Enrollment\shopfloor-setup 2>NUL
if exist "Y:\shopfloor-setup\Shopfloor" (
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
echo Copied Shopfloor baseline setup files.
)
REM --- Copy type-specific scripts on top of baseline ---
if "%PCTYPE%"=="Shopfloor" goto pctype_done
if exist "Y:\shopfloor-setup\%PCTYPE%" (
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\"
echo Copied %PCTYPE% setup files.
) else (
echo WARNING: No setup files found for PC type %PCTYPE%.
)
:pctype_done
:cleanup_enroll
net use Y: /delete 2>NUL
:wait_finish
ping -n 11 127.0.0.1 >NUL
tasklist /FI "IMAGENAME eq PESetup.exe" 2>NUL | find /I "PESetup.exe" >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if not errorlevel 1 goto wait_finish
echo.
echo Imaging complete. Rebooting in 15 seconds...