From 6d0e6ee284294edaf997912d00d6b5a4b7a901bb Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 23 Mar 2026 11:02:36 -0400 Subject: [PATCH] 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 --- download-drivers.py | 814 ++++++++++++++++++ playbook/FlatUnattendW10-shopfloor.xml | 166 ++++ playbook/check-bios.cmd | 148 ++++ .../shopfloor-setup/Run-ShopfloorSetup.ps1 | 60 ++ playbook/startnet.cmd | 196 ++++- startnet-template.cmd | 196 ++++- 6 files changed, 1538 insertions(+), 42 deletions(-) create mode 100755 download-drivers.py create mode 100644 playbook/FlatUnattendW10-shopfloor.xml create mode 100644 playbook/check-bios.cmd create mode 100644 playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 diff --git a/download-drivers.py b/download-drivers.py new file mode 100755 index 0000000..e49b64d --- /dev/null +++ b/download-drivers.py @@ -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__.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 to push directly.") + print() + + +if __name__ == "__main__": + main() diff --git a/playbook/FlatUnattendW10-shopfloor.xml b/playbook/FlatUnattendW10-shopfloor.xml new file mode 100644 index 0000000..df26d82 --- /dev/null +++ b/playbook/FlatUnattendW10-shopfloor.xml @@ -0,0 +1,166 @@ + + + + + + + + + W:\Drivers + + + + + + + H%serialnumber% + GE Aerospace + GE + Eastern Standard Time + + + + + 1 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'" + Install External Root Certificate + + + 2 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'" + Install External Intermediate Certificate + + + 3 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'" + Install Enterprise Root Certificate + + + 4 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'" + Install Enterprise Device Issuing Certificate + + + 5 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'" + Install Enterprise Server Issuing Certificate + + + 6 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'" + Install Enterprise SmartCard Issuing Certificate + + + 7 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'" + Install Enterprise User Issuing Certificate + + + 8 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'" + Install Aerospace Enterprise Root Certificate + + + 9 + powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt' -CertStoreLocation 'Cert:\LocalMachine\Root'" + Install Zscaler Commercial Certificate + + + reg.exe add "HKLM\System\CurrentControlSet\Control\Network\NewNetworkWindowOff" /f + Disable Network Windows + 10 + + + 11 + netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all + Install INTERNETACCESS WiFi Profile + + + 12 + powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck" + Enable WinRM + + + 13 + reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f + Bypass OOBE network requirement + + + + + + + en-US + en-US + en-US + en-US + + + + true + true + true + true + true + 3 + + + + + + Pa55word + 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> diff --git a/playbook/check-bios.cmd b/playbook/check-bios.cmd new file mode 100644 index 0000000..c89d698 --- /dev/null +++ b/playbook/check-bios.cmd @@ -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 diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 new file mode 100644 index 0000000..151a624 --- /dev/null +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -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 diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd index 38fd01e..0116090 100644 --- a/playbook/startnet.cmd +++ b/playbook/startnet.cmd @@ -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... diff --git a/startnet-template.cmd b/startnet-template.cmd index 38fd01e..0116090 100644 --- a/startnet-template.cmd +++ b/startnet-template.cmd @@ -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...