#!/usr/bin/env python3 """Sync HardwareDriver.json and user_selections.json across all PXE image types. Reads all HardwareDriver.json files, builds a unified driver catalog, then updates each image to include all known hardware models. Run after adding new driver packs to the shared Out-of-box Drivers directory. """ import json import os import sys from pathlib import Path from collections import OrderedDict WINPEAPPS = Path("/srv/samba/winpeapps") SHARED_DRIVERS = WINPEAPPS / "_shared" / "Out-of-box Drivers" def normalize_entry(entry): """Normalize a HardwareDriver.json entry to a consistent format.""" norm = {} norm["manufacturer"] = entry.get("manufacturer", "Dell") norm["product"] = entry.get("product") or entry.get("manufacturerfriendlyname", "Dell") norm["family"] = entry.get("family", "") norm["modelswminame"] = entry.get("modelswminame") or entry.get("models", "") norm["modelsfriendlyname"] = entry.get("modelsfriendlyname", "") norm["fileName"] = entry.get("fileName") or entry.get("FileName", "") norm["destinationDir"] = entry.get("destinationDir") or entry.get("DestinationDir", "") norm["url"] = entry.get("url", "") norm["hash"] = entry.get("hash", "") norm["size"] = entry.get("size", 0) norm["modifiedDate"] = entry.get("modifiedDate", "0001-01-01T00:00:00") norm["osId"] = entry.get("osId", "") norm["imagedisk"] = entry.get("imagedisk", 0) return norm def merge_os_ids(a, b): """Merge two osId strings (e.g., '18' + '20,21' -> '18,20,21').""" ids = set() for oid in [a, b]: for part in str(oid).split(","): part = part.strip() if part: ids.add(part) return ",".join(sorted(ids, key=lambda x: int(x) if x.isdigit() else 0)) def check_driver_exists(entry): """Check if the driver zip actually exists in the shared directory.""" dest = entry["destinationDir"] dest = dest.replace("*destinationdir*", "") dest = dest.lstrip("\\") dest = dest.replace("\\", "/") # Strip leading path components that are already in SHARED_DRIVERS for prefix in ["Deploy/Out-of-box Drivers/", "Out-of-box Drivers/"]: if dest.startswith(prefix): dest = dest[len(prefix):] break dest = dest.lstrip("/") zip_path = SHARED_DRIVERS / dest / entry["fileName"] return zip_path.exists() def main(): print("=== PXE Hardware Model Sync ===") print() # Step 1: Build unified catalog from all images print("Reading driver catalogs...") catalog = OrderedDict() image_dirs = sorted( [d for d in WINPEAPPS.iterdir() if d.is_dir() and not d.name.startswith("_")] ) for img_dir in image_dirs: hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json" if not hw_file.exists(): continue with open(hw_file) as f: entries = json.load(f) print(" Read {} entries from {}".format(len(entries), img_dir.name)) for entry in entries: norm = normalize_entry(entry) key = (norm["family"], norm["fileName"]) if key in catalog: catalog[key]["osId"] = merge_os_ids( catalog[key]["osId"], norm["osId"] ) # Prefer longer/more complete model names if len(norm["modelswminame"]) > len(catalog[key]["modelswminame"]): catalog[key]["modelswminame"] = norm["modelswminame"] if len(norm["modelsfriendlyname"]) > len( catalog[key]["modelsfriendlyname"] ): catalog[key]["modelsfriendlyname"] = norm["modelsfriendlyname"] else: catalog[key] = norm unified = list(catalog.values()) print() print("Unified catalog: {} unique driver entries".format(len(unified))) # Step 2: Check which drivers actually exist on disk missing = [] found = 0 for entry in unified: if check_driver_exists(entry): found += 1 else: missing.append( " {}: {}".format(entry["family"], entry["fileName"]) ) print(" {} drivers found on disk".format(found)) if missing: print(" WARNING: {} driver zips NOT found on disk:".format(len(missing))) for m in missing[:15]: print(m) if len(missing) > 15: print(" ... and {} more".format(len(missing) - 15)) print(" (Entries still included - PESetup may download them)") # Step 3: Build unified model selection from all driver entries models = [] seen = set() for entry in unified: friendly_names = [ n.strip() for n in entry["modelsfriendlyname"].split(",") if n.strip() ] family = entry["family"] for name in friendly_names: key = (name, family) if key not in seen: seen.add(key) models.append({"Model": name, "Id": family}) models.sort(key=lambda x: x["Model"]) print() print("Unified model selection: {} models".format(len(models))) # Step 4: Update each image print() print("Updating images...") for img_dir in image_dirs: hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json" us_file = img_dir / "Tools" / "user_selections.json" if not hw_file.exists() or not us_file.exists(): continue # Write unified HardwareDriver.json with open(hw_file, "w") as f: json.dump(unified, f, indent=2) f.write("\n") # Update user_selections.json (preserve OperatingSystemSelection etc.) with open(us_file) as f: user_sel = json.load(f) old_count = len(user_sel[0].get("HardwareModelSelection", [])) user_sel[0]["HardwareModelSelection"] = models with open(us_file, "w") as f: json.dump(user_sel, f, indent=2) f.write("\n") print( " {}: {} -> {} models, {} driver entries".format( img_dir.name, old_count, len(models), len(unified) ) ) print() print("Done!") if __name__ == "__main__": main()