From bb08392b84f89ba7471929f9732c80c9c41559ce Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 6 Aug 2026 14:38:54 -0400 Subject: [PATCH] download-drivers: reuse an existing directory that differs only in case Dell and GE manifests spell the same folder inconsistently - OptiPlex vs Optiplex. Windows does not care, but the Samba share sits on a case-sensitive filesystem, so a blind mkdir -p created a SECOND tree and the drivers split between them. That is exactly how the OptiPlex Micro 7020 pack went missing: the manifest asked for OptiPlex/D13MLK while the 3.2 GB zip sat in Optiplex/D13MLK. PESetup found no pack, logged a warning rather than an error, and the bay imaged with no network drivers - so DNS failed at first boot and bulk enrollment could not reach the CDN. Symptoms three steps from the cause. mkdir_ci walks the path one component at a time and reuses whatever is already there whatever its case, creating only genuinely new components. Callers must use the RETURNED path, since it may differ in case from the requested one. Falls back to plain mkdir -p rather than skipping a download if the resolve fails. Both spellings still exist in the live catalogues; scripts/lint-driver-catalogue.py reports a case mismatch as an error so the pair cannot silently drift again. --- scripts/download-drivers.py | 51 ++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/scripts/download-drivers.py b/scripts/download-drivers.py index 5b97978..999bb01 100755 --- a/scripts/download-drivers.py +++ b/scripts/download-drivers.py @@ -68,6 +68,53 @@ def ssh_cmd(host, cmd): +# Remote path resolver that is case-insensitive about EXISTING directories. +# +# Dell/GE manifests spell the same folder inconsistently ("OptiPlex" vs +# "Optiplex"). Windows does not care; the Samba share is backed by a +# case-sensitive Linux filesystem, so a blind "mkdir -p" creates a SECOND tree +# and the drivers split between them. That is exactly how the OptiPlex Micro +# 7020 package went missing: the manifest asked for OptiPlex/D13MLK while the +# 3.2 GB zip sat in Optiplex/D13MLK, so imaging installed no drivers at all - +# no NIC, no WiFi, and bulk enrollment could not reach the CDN. +# +# Walk the path one component at a time and reuse whatever is already there, +# whatever its case. Only genuinely new components get created. +CI_RESOLVE_SH = r""" +set -e +p="$1"; shift +for c in "$@"; do + [ -z "$c" ] && continue + if [ -d "$p/$c" ]; then + p="$p/$c" + else + m=$(ls -1 "$p" 2>/dev/null | awk -v c="$c" 'tolower($0)==tolower(c){print; exit}') + if [ -n "$m" ]; then p="$p/$m"; else mkdir -p "$p/$c"; p="$p/$c"; fi + fi +done +printf '%s' "$p" +""" + + +def mkdir_ci(host, target_dir): + """mkdir -p that reuses existing dirs differing only in case. + + Returns the path that actually exists on the server, which may differ in + case from target_dir. Callers must use the RETURNED path.""" + parts = [c for c in target_dir.split("/") if c] + if not parts: + return target_dir + base = "/" if target_dir.startswith("/") else "." + r = ssh_cmd(host, "bash -s -- '%s' %s <<'CIEOF'\n%s\nCIEOF" % ( + base, " ".join("'%s'" % c.replace("'", "'\''") for c in parts), CI_RESOLVE_SH)) + out = (r.stdout or "").strip() + if r.returncode != 0 or not out: + # Fall back to the old behaviour rather than skipping the download. + ssh_cmd(host, "mkdir -p '%s'" % target_dir) + return target_dir + return out + + def verify_sha256(filepath, expected): sha = hashlib.sha256() with open(filepath, "rb") as f: @@ -324,7 +371,9 @@ def process_download(args, url, filename, sha256, size, target_dir, label, tmpdi # Push zip to PXE server print(f" [{label}] Pushing to {target_dir}/{zip_name}...") - ssh_cmd(args.server, f"mkdir -p '{target_dir}'") + # Reuse an existing directory that differs only in case, or the drivers + # split across two trees and imaging silently installs none of them. + target_dir = mkdir_ci(args.server, target_dir) r = subprocess.run([ "rsync", "-a", "-e", f"sshpass -p {PXE_PASS} ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR",