Shopfloor PC type system, webapp enhancements, slim Blancco GRUB

- Shopfloor PC type menu (CMM, WaxAndTrace, Keyence, Genspect, Display, Standard)
- Baseline scripts: OpenText CSF, Start Menu shortcuts, network/WinRM, power/display
- Standard type: eDNC + MarkZebra with 64-bit path mirroring
- CMM type: Hexagon CLM Tools, PC-DMIS 2016/2019 R2
- Display sub-type: Lobby vs Dashboard
- Webapp: enrollment management, image config editor, UI refresh
- Upload-Image.ps1: robocopy MCL cache to PXE server
- Download-Drivers.ps1: Dell driver download pipeline
- Slim Blancco GRUB EFI (10MB -> 660KB) for old hardware compat
- Shopfloor display imaging guide docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-03-26 11:25:07 -04:00
parent 6d0e6ee284
commit 76165495ff
49 changed files with 4304 additions and 147 deletions

177
sync_hardware_models.py Normal file
View File

@@ -0,0 +1,177 @@
#!/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()