Initial commit: CMP 170HX per-card check tooling

Add check_cards.py CLI and cxlib.py host helpers for preparing,
unlocking, and PCIe-checking NVIDIA CMP 170HX cards, plus operator
README, agent docs, and bundled nvflash tooling.
This commit is contained in:
2026-09-27 21:02:27 +02:00
commit b30be648d7
13 changed files with 1562 additions and 0 deletions
Executable
+884
View File
@@ -0,0 +1,884 @@
#!/usr/bin/env python3
"""CMP 170HX setup, unlock, PCIe checks, and optional diagnostics."""
import datetime
import hashlib
import io
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import threading
import time
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
HARNESS_DIR = SCRIPT_DIR / "harness"
CARDS_DIR = SCRIPT_DIR / "cards"
UNIDENTIFIED_DIR = SCRIPT_DIR / "unidentified"
UNLOCKER_COMMIT = "f51da030ef4260656288edf07e95ae45f339aea8" # upstream v0.3
UNLOCKER_ZIP_SHA256 = "a10b226ed4667fb2ae4c9c5ad1575938a85ceb707f8c40ea146887778930ce21"
REPOS = {"cmpunlocker": ("commits", UNLOCKER_COMMIT)}
VERIFY_SH = HARNESS_DIR / "cmpunlocker" / "verify.sh"
INSTALL_SH = HARNESS_DIR / "cmpunlocker" / "install.sh"
PENDING_FILE = HARNESS_DIR / "unlock_pending.json"
GPU_IDS = ("20c2", "2082")
SPEED_TO_GEN = {"2.5GT/s": 1, "5GT/s": 2, "8GT/s": 3, "16GT/s": 4, "32GT/s": 5, "64GT/s": 6}
USE_COLOR = sys.stdout.isatty()
class C:
RESET = "\033[0m" if USE_COLOR else ""
BOLD = "\033[1m" if USE_COLOR else ""
DIM = "\033[2m" if USE_COLOR else ""
GREEN = "\033[32m" if USE_COLOR else ""
YELLOW = "\033[33m" if USE_COLOR else ""
RED = "\033[31m" if USE_COLOR else ""
CYAN = "\033[36m" if USE_COLOR else ""
def info(msg):
print(f"{C.CYAN}[*]{C.RESET} {msg}", flush=True)
def ok(msg):
print(f"{C.GREEN}[+]{C.RESET} {msg}", flush=True)
def warn(msg):
print(f"{C.YELLOW}[!]{C.RESET} {msg}", flush=True)
def err(msg):
print(f"{C.RED}[ERROR] {msg}{C.RESET}", flush=True)
def step(msg):
print(f"\n{C.BOLD}== {msg} =={C.RESET}", flush=True)
def ask(prompt, default=""):
suffix = f" [{default}]" if default else ""
try:
val = input(f"{C.BOLD}?{C.RESET} {prompt}{suffix}: ").strip()
except EOFError:
return default
return val if val else default
def now_iso():
return datetime.datetime.now().isoformat(timespec="seconds")
def sanitize(name):
return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("._-") or "UNKNOWN"
# ---------------------------------------------------------------- subprocess
def run(cmd, timeout=60, env=None, cwd=None):
try:
p = subprocess.run(cmd, capture_output=True, text=True, errors="replace",
timeout=timeout, env=env, cwd=cwd)
return {"rc": p.returncode, "out": p.stdout, "err": p.stderr, "cmd": cmd}
except FileNotFoundError:
return {"rc": 127, "out": "", "err": f"not found: {cmd[0]}", "cmd": cmd}
except subprocess.TimeoutExpired as e:
out = e.stdout.decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")
return {"rc": 124, "out": out, "err": "timeout", "cmd": cmd}
def as_root(cmd, timeout=60, cwd=None):
if os.geteuid() == 0:
return run(cmd, timeout=timeout, cwd=cwd)
if shutil.which("sudo"):
r = run(["sudo", "-n"] + cmd, timeout=timeout, cwd=cwd)
if r["rc"] != 0 and "password is required" in (r["err"] + r["out"]).lower():
r["err"] = "sudo requires a password (sudo -n failed)"
return r
return {"rc": 1, "out": "", "err": "no root: neither uid 0 nor sudo available", "cmd": cmd}
def run_logged_as_root(cmd, log_path, timeout, cwd=None):
"""Show a long-running command live while keeping its full output on disk."""
actual = cmd if os.geteuid() == 0 else ["sudo", "-n"] + cmd
log_path.parent.mkdir(parents=True, exist_ok=True)
tail = bytearray()
try:
with log_path.open("wb") as log:
process = subprocess.Popen(actual, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, cwd=cwd,
start_new_session=True)
def copy_output():
show_console = True
while True:
try:
chunk = os.read(process.stdout.fileno(), 4096)
except OSError:
break
if not chunk:
break
log.write(chunk)
log.flush()
if show_console:
try:
if hasattr(sys.stdout, "buffer"):
sys.stdout.buffer.write(chunk)
else:
sys.stdout.write(chunk.decode(errors="replace"))
sys.stdout.flush()
except BrokenPipeError:
show_console = False
tail.extend(chunk)
if len(tail) > 4096:
del tail[:-4096]
def stop_group(sig):
try:
os.killpg(process.pid, sig)
except ProcessLookupError:
pass
reader = threading.Thread(target=copy_output, daemon=True)
reader.start()
try:
rc = process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
stop_group(signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
stop_group(signal.SIGKILL)
process.wait()
rc = 124
except KeyboardInterrupt:
stop_group(signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
stop_group(signal.SIGKILL)
process.wait()
raise
finally:
reader.join(timeout=10)
if reader.is_alive():
stop_group(signal.SIGTERM)
reader.join(timeout=5)
if reader.is_alive():
stop_group(signal.SIGKILL)
reader.join(timeout=5)
process.stdout.close()
reader.join(timeout=2)
except OSError as exc:
return {"rc": 127, "out": "", "err": str(exc), "cmd": actual,
"log": str(log_path)}
return {"rc": rc, "out": tail.decode(errors="replace"), "err": "",
"cmd": actual, "log": str(log_path)}
def which(name):
return shutil.which(name)
def find_nvflash():
"""nvflash binary: repo-bundled copy first (no system install needed), then PATH."""
import platform
arch_map = {"x86_64": "x64", "amd64": "x64", "aarch64": "aarch64",
"ppc64le": "ppc64", "ppc64": "ppc64", "i686": "x86", "i386": "x86"}
arch = arch_map.get(platform.machine(), "x64")
candidates = [
*sorted(SCRIPT_DIR.glob(f"nvflash_*_linux/{arch}/nvflash")),
*sorted(SCRIPT_DIR.glob("nvflash_*_linux/x64/nvflash")),
SCRIPT_DIR / "nvflash",
]
for p in candidates:
if p.is_file() and os.access(p, os.X_OK):
return str(p)
return which("nvflash")
def have_root():
return os.geteuid() == 0 or bool(shutil.which("sudo"))
# ---------------------------------------------------------------- preflight
def ensure_repos():
"""Fetch upstream source archives without changing any Git remotes."""
HARNESS_DIR.mkdir(exist_ok=True)
complete = True
for name, (kind, ref) in REPOS.items():
dest = HARNESS_DIR / name
if (dest / ("install.sh" if name == "cmpunlocker" else "README.md")).exists():
continue
suffix = f"archive/{ref}.zip" if kind == "commits" else f"archive/refs/{kind}/{ref}.zip"
url = f"https://github.com/amoghmunikote/{name}/{suffix}"
info(f"downloading {name} ({ref}) ...")
try:
with urllib.request.urlopen(url, timeout=60) as response:
archive = response.read()
if name == "cmpunlocker" and hashlib.sha256(archive).hexdigest() != UNLOCKER_ZIP_SHA256:
raise ValueError("cmpunlocker archive checksum differs from pinned v0.3 source")
with tempfile.TemporaryDirectory(dir=HARNESS_DIR) as temp:
staging = Path(temp) / name
staging.mkdir()
archive_root = f"{name}-{ref[1:] if kind == 'tags' and ref.startswith('v') else ref}"
with zipfile.ZipFile(io.BytesIO(archive)) as zf:
for member in zf.infolist():
parts = Path(member.filename).parts
if parts == (archive_root,):
continue
if len(parts) < 2 or ".." in parts or parts[0] != archive_root:
raise ValueError(f"unsafe archive path: {member.filename}")
if (member.external_attr >> 16) & 0o170000 == 0o120000:
raise ValueError(f"archive symlink: {member.filename}")
path = staging.joinpath(*parts[1:])
if member.is_dir():
path.mkdir(parents=True, exist_ok=True)
else:
path.parent.mkdir(parents=True, exist_ok=True)
with zf.open(member) as source, path.open("wb") as output:
shutil.copyfileobj(source, output)
path.chmod(((member.external_attr >> 16) & 0o777) or 0o644)
if dest.exists():
raise ValueError(f"{dest} exists but lacks required source files")
staging.rename(dest)
ok(f"downloaded {name}")
except (OSError, ValueError, zipfile.BadZipFile, urllib.error.URLError) as exc:
err(f"could not download {name}: {exc}")
complete = False
return complete
TOOL_CHECKS = {
"lspci": ("critical", "PCIe checks (package: pciutils)"),
"setpci": ("critical", "link generation probe (package: pciutils)"),
"nvidia-smi": ("critical", "GPU state / cmpunlocker verify"),
"nvflash": ("optional", "VBIOS backup + serial (bundled in repo, or on PATH)"),
"dmesg": ("optional", "kernel log scrape"),
"journalctl": ("optional", "kernel log scrape fallback"),
"vulkaninfo": ("optional", "Vulkan capability probe (package: vulkan-tools)"),
}
def os_release():
data = read_file("/etc/os-release") or ""
out = {}
for line in data.splitlines():
if "=" in line:
k, v = line.split("=", 1)
out[k] = v.strip().strip('"')
return out
def sudo_precondition(interactive=True):
"""Check root or working sudo for host setup and privileged inspection.
Tries passwordless sudo first; falls back to an interactive `sudo -v` so the
credentials get cached, after which plain sudo -n calls succeed.
"""
if os.geteuid() == 0:
return True, "running as root"
if not shutil.which("sudo"):
return False, "sudo is not installed"
r = run(["sudo", "-n", "true"], timeout=15)
if r["rc"] == 0:
return True, "sudo available (credentials cached)"
if interactive and sys.stdin.isatty():
info("sudo authentication required - enter your password")
try:
p = subprocess.run(["sudo", "-v"])
except KeyboardInterrupt:
return False, "interrupted"
if p.returncode == 0:
return True, "sudo authenticated just now"
return False, "sudo authentication failed"
return False, "sudo requires a password (non-interactive session)"
def boot_id():
return (read_file("/proc/sys/kernel/random/boot_id") or "").strip()
def pending_state():
try:
return json.loads(PENDING_FILE.read_text())
except (OSError, ValueError):
return None
def mark_pending(stage, card=None):
HARNESS_DIR.mkdir(exist_ok=True)
PENDING_FILE.write_text(json.dumps({"stage": stage, "boot_id": boot_id(),
"card": card}, indent=2))
def card_identity(bdf, device_id, serial=None):
"""Identify a card across boots without treating its GPU UUID as a serial."""
query = run(["nvidia-smi", "-i", bdf, "--query-gpu=uuid",
"--format=csv,noheader"], timeout=30)
uuid = query["out"].strip().splitlines()[0] if query["rc"] == 0 and query["out"].strip() else None
if uuid and uuid.upper() in ("N/A", "UNKNOWN", "NONE"):
uuid = None
return {"serial": serial, "gpu_uuid": uuid, "device_id": device_id, "bdf": bdf}
def same_card(previous, current):
"""Return None when no stable identity is available for comparison."""
if not isinstance(previous, dict):
return None
for key in ("serial", "gpu_uuid"):
if previous.get(key) and current.get(key):
return previous[key] == current[key]
if previous.get("device_id") and previous["device_id"] != current["device_id"]:
return False
return None
def supported_versions():
text = read_file(HARNESS_DIR / "cmpunlocker" / "driver" / "VERSION") or ""
return [line.strip() for line in text.splitlines() if re.fullmatch(r"\d+\.\d+\.\d+", line.strip())]
def current_driver_version():
r = run(["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"], timeout=30)
if r["rc"] == 0:
return r["out"].splitlines()[0].strip() if r["out"].strip() else None
return None
def open_driver_active():
version_text = read_file("/proc/driver/nvidia/version") or ""
return "Open Kernel Module" in version_text
def preflight(auto_install=True, do_download=True):
step("Preflight")
report = {"tools": {}, "root": have_root()}
rel = os_release()
report["os"] = {"id": rel.get("ID", ""), "version_id": rel.get("VERSION_ID", ""),
"pretty": rel.get("PRETTY_NAME", "unknown")}
info(f"OS: {report['os']['pretty']}")
if do_download:
report["repos_ok"] = ensure_repos()
versions = supported_versions()
report["supported_driver_versions"] = versions
if not INSTALL_SH.exists() or not VERIFY_SH.exists() or not versions:
report["blocking_missing"] = ["cmpunlocker v0.3 source"]
report["preflight_ok"] = False
err("preflight blocked: cmpunlocker v0.3 source is missing or incomplete")
return report
pending = pending_state()
if (pending and pending.get("stage") == "nvidia-driver-610-open"
and pending.get("boot_id") == boot_id()):
report["reboot_required"] = True
warn(f"{pending['stage']} installed during this boot; cold shut down and power on, then rerun")
return report
driver_rebooted = bool(pending and pending.get("boot_id") != boot_id()
and pending.get("stage") == "nvidia-driver-610-open")
version = current_driver_version()
report["driver_version"] = version
need_driver = not version or version not in versions or not open_driver_active()
if driver_rebooted and need_driver:
report["blocking_missing"] = ["nvidia-driver-610-open installed but inactive after reboot; inspect Secure Boot and driver logs"]
report["preflight_ok"] = False
err(report["blocking_missing"][0])
return report
if driver_rebooted:
PENDING_FILE.unlink(missing_ok=True)
pkgs = ["pciutils", "build-essential", "patch", "dkms", "curl",
f"linux-headers-{run(['uname', '-r'])['out'].strip()}"]
build_tools = ("lspci", "setpci", "gcc", "make", "patch", "curl")
need_tools = not all(which(t) for t in build_tools)
need_headers = not Path("/lib/modules", run(["uname", "-r"])["out"].strip(), "build").exists()
if auto_install and (need_tools or need_headers or need_driver) and rel.get("ID") == "ubuntu":
allowed, reason = sudo_precondition()
report["sudo"] = {"ok": allowed, "why": reason}
if allowed:
update_log = HARNESS_DIR / "logs" / f"apt_update_{datetime.datetime.now().strftime('%Y%m%dT%H%M%S%f')}.log"
info(f"apt-get update log: {update_log}")
update = run_logged_as_root(["apt-get", "update"], update_log, timeout=300)
if update["rc"] != 0:
err(f"apt-get update failed: {(update['err'] or update['out'])[-300:]}")
else:
to_install = pkgs + (["nvidia-driver-610-open"] if need_driver else [])
info("installing: " + " ".join(to_install))
install_log = HARNESS_DIR / "logs" / f"apt_install_{datetime.datetime.now().strftime('%Y%m%dT%H%M%S%f')}.log"
info(f"apt-get install log: {install_log}")
install = run_logged_as_root(["apt-get", "install", "-y"] + to_install,
install_log, timeout=1800)
report["install"] = {"rc": install["rc"], "log": str(install_log),
"tail": (install["out"] + install["err"])[-2000:]}
if install["rc"] != 0:
err("package installation failed: " + report["install"]["tail"][-300:])
elif need_driver:
mark_pending("nvidia-driver-610-open")
report["reboot_required"] = True
warn("driver installed; cold shut down and power on, then rerun")
else:
err(f"sudo unavailable: {reason}")
for tool, (level, why) in TOOL_CHECKS.items():
path = find_nvflash() if tool == "nvflash" else which(tool)
report["tools"][tool] = {"found": bool(path), "path": path, "level": level, "why": why}
info(f"{tool}: {path or 'missing'}")
blocking = [t for t in build_tools if not which(t)]
if not report.get("reboot_required") and (current_driver_version() not in versions or not open_driver_active()):
blocking.append("supported active nvidia-open 610 driver")
if need_headers and not Path("/lib/modules", run(["uname", "-r"])["out"].strip(), "build").exists():
blocking.append("matching kernel headers")
report["blocking_missing"] = blocking
report["preflight_ok"] = not blocking and not report.get("reboot_required")
if blocking and not report.get("reboot_required"):
err("preflight blocked: " + ", ".join(blocking))
elif report["preflight_ok"]:
ok("preflight complete")
return report
# ---------------------------------------------------------------- GPU detect
def detect_gpus():
bdfs = []
for dev_id in GPU_IDS:
r = run(["lspci", "-D", "-d", f"10de:{dev_id}"])
for line in r["out"].splitlines():
parts = line.split()
if not parts:
continue
bdf = parts[0]
if re.match(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-f]$", bdf):
bdfs.append(bdf.lower())
return sorted(set(bdfs))
def rescan_bus():
r = as_root(["sh", "-c", "echo 1 > /sys/bus/pci/rescan"], timeout=15)
if r["rc"] == 0:
time.sleep(2)
return r["rc"] == 0
def sysfs(bdf):
return Path("/sys/bus/pci/devices") / bdf
def read_file(path, binary=False):
try:
with open(path, "rb" if binary else "r", errors="replace") as f:
return f.read()
except (PermissionError, FileNotFoundError, OSError):
return None
# ---------------------------------------------------------------- PCIe checks
def parse_link(text):
out = {"lnksta_speed": None, "lnksta_width": None, "lnksta_gen": None,
"lnkcap_speed": None, "lnkcap_width": None, "lnksta_raw": None}
for line in text.splitlines():
s = line.strip()
if s.startswith("LnkSta:") and not s.startswith("LnkSta2"):
out["lnksta_raw"] = s
m = re.search(r"Speed\s+([^,\s]+)", s)
if m:
out["lnksta_speed"] = m.group(1)
out["lnksta_gen"] = SPEED_TO_GEN.get(m.group(1))
m = re.search(r"Width\s+(x\d+)", s)
if m:
out["lnksta_width"] = m.group(1)
elif s.startswith("LnkCap:"):
m = re.search(r"Speed\s+([^,\s]+)", s)
if m:
out["lnkcap_speed"] = m.group(1)
m = re.search(r"Width\s+(x\d+)", s)
if m:
out["lnkcap_width"] = m.group(1)
return out
def parse_aer_flags(text):
res = {"CESta": [], "UESta": []}
for line in text.splitlines():
s = line.strip()
for key in ("CESta", "UESta"):
if s.startswith(key + ":"):
body = s.split(":", 1)[1]
res[key].extend(re.findall(r"([A-Za-z0-9_]+)\+", body))
return res
def parent_bdf(bdf):
path = Path(os.path.realpath(sysfs(bdf)))
while True:
path = path.parent
name = path.name
if re.match(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-f]$", name):
return name.lower()
if name.startswith("pci") or path.parent == path:
return None
def aer_snapshot(bdf):
snap = {"aer_sysfs_correctable": None, "aer_sysfs_fatal": None,
"aer_correctable_sum": 0, "aer_fatal_sum": 0,
"ce_active": [], "ue_active": [],
"parent_bdf": None, "parent_ce_active": []}
base = sysfs(bdf)
data = read_file(base / "aer_dev_correctable")
if data is not None:
snap["aer_sysfs_correctable"] = data.strip()
snap["aer_correctable_sum"] = sum(int(x) for x in re.findall(r"\d+", data))
fatal = read_file(base / "aer_dev_fatal")
if fatal is not None:
snap["aer_sysfs_fatal"] = fatal.strip()
snap["aer_fatal_sum"] = sum(int(x) for x in re.findall(r"\d+", fatal))
r = run(["lspci", "-vvv", "-D", "-s", bdf], timeout=30)
if r["rc"] == 0:
aer = parse_aer_flags(r["out"])
snap["ce_active"] = aer["CESta"]
snap["ue_active"] = aer["UESta"]
parent = parent_bdf(bdf)
if parent:
snap["parent_bdf"] = parent
rp = run(["lspci", "-vvv", "-D", "-s", parent], timeout=30)
if rp["rc"] == 0:
snap["parent_ce_active"] = parse_aer_flags(rp["out"])["CESta"]
return snap
def link_gen_setpci(bdf):
r = as_root(["setpci", "-s", bdf, "CAP_EXP+12.w"], timeout=15)
val = r["out"].strip()
if re.match(r"^[0-9a-fA-F]{4}$", val):
return int(val, 16) & 0x0F
return None
def check_link(bdf):
cmd = ["lspci", "-vvv", "-D", "-s", bdf]
r = run(cmd, timeout=30)
link = parse_link(r["out"]) if r["rc"] == 0 else {}
if (not link.get("lnksta_width") or not link.get("lnksta_gen")) and os.geteuid() != 0:
allowed, _ = sudo_precondition()
if allowed:
privileged = as_root(cmd, timeout=30)
privileged_link = parse_link(privileged["out"]) if privileged["rc"] == 0 else {}
fields = ("lnksta_width", "lnksta_gen")
if sum(bool(privileged_link.get(field)) for field in fields) > sum(
bool(link.get(field)) for field in fields):
r, link = privileged, privileged_link
link["setpci_current_gen"] = link_gen_setpci(bdf)
link["lspci_rc"] = r["rc"]
return link, r["out"]
def dump_config(bdf):
r = run(["lspci", "-nnvvxxxx", "-D", "-s", bdf], timeout=30)
return r["out"] if r["rc"] == 0 else f"lspci failed rc={r['rc']}: {r['err']}"
def retrain_test(bdf):
result = {"attempted": False, "ok": None, "note": "", "width_before": None, "width_after": None}
link_before, _ = check_link(bdf)
result["width_before"] = link_before.get("lnksta_width")
node = sysfs(bdf) / "link" / "retrain"
if not node.exists():
result["note"] = "sysfs link/retrain not available on this kernel"
return result
result["attempted"] = True
r = as_root(["sh", "-c", f"echo 1 > {node}"], timeout=15)
if r["rc"] != 0:
result["note"] = f"retrain write failed: {r['err'].strip()}"
return result
time.sleep(3)
if bdf not in detect_gpus():
result.update(ok=False, note="device vanished after retrain")
return result
link_after, _ = check_link(bdf)
result["width_after"] = link_after.get("lnksta_width")
result["ok"] = result["width_after"] == result["width_before"] and result["width_after"] is not None
result["note"] = f"{result['width_before']} -> {result['width_after']}"
return result
def rescan_test(bdf):
result = {"attempted": True, "ok": None, "note": ""}
if not rescan_bus():
result["note"] = "bus rescan not permitted (no root)"
return result
time.sleep(1)
present = bdf in detect_gpus()
result["ok"] = present
result["note"] = "re-enumerated at same BDF" if present else "device missing after rescan"
return result
def dmesg_capture(bdf, since):
"""Warn only for recent lines naming this endpoint; keep others separately."""
since = since.replace("T", " ")
text, source = "", None
for command, label in (
(["journalctl", "-k", "-b", "--since", since, "--no-pager", "-o", "short-precise"], "journalctl"),
(["dmesg", "--ctime", "--since", since], "dmesg")):
r = as_root(command, timeout=25)
if r["rc"] == 0:
text, source = r["out"], label
break
if source is None:
return {"source": None, "related": [], "errors": [], "other_pcie": [],
"note": "recent kernel log unavailable (root or journal access needed)"}
tokens = (bdf.lower(), bdf[:-2].lower())
related, errors, other = [], [], []
for line in text.splitlines():
low = line.lower()
if any(token in low for token in tokens):
related.append(line)
if any(k in low for k in ("xid", "error", "aer:", "fail", "corrupted", "fallen off")):
errors.append(line)
elif any(k in low for k in ("aer:", "pcieport", "nvrm", "xid")):
other.append(line)
return {"source": source, "related": related[-200:], "errors": errors[-50:],
"other_pcie": other[-200:], "note": f"{len(related)} card lines, {len(other)} other PCIe/GPU lines since {since}"}
# ---------------------------------------------------------------- BIOS / serial
def nvflash_bin(args):
if getattr(args, "nvflash", None):
return args.nvflash
return find_nvflash()
def usable_serial(value):
return bool(value and sanitize(value) == value and
value.upper() not in ("N/A", "UNKNOWN", "NONE") and
not re.fullmatch(r"0+", value))
def get_serial(nf, bdf):
"""Read the physical board serial; never substitute a GPU UUID or PCI address."""
smi = run(["nvidia-smi", "-i", bdf, "--query-gpu=serial",
"--format=csv,noheader"], timeout=30)
if smi["rc"] == 0:
serial = smi["out"].strip().splitlines()[0] if smi["out"].strip() else ""
if usable_serial(serial):
info(f"serial auto-read from nvidia-smi: {serial}")
return serial, "nvidia-smi"
detail = run(["nvidia-smi", "-q", "-i", bdf], timeout=30)
if detail["rc"] == 0:
match = re.search(r"^\s*Serial Number\s*:\s*(\S+)", detail["out"], re.I | re.M)
if match and usable_serial(match.group(1)):
info(f"serial auto-read from nvidia-smi details: {match.group(1)}")
return match.group(1), "nvidia-smi-detail"
vpd_cmd = ["lspci", "-vvv", "-D", "-s", bdf]
for vpd in (run(vpd_cmd, timeout=30), as_root(vpd_cmd, timeout=30)):
if vpd["rc"] == 0:
match = re.search(r"\[SN\]\s+Serial number:\s*(\S+)", vpd["out"], re.I)
if match and usable_serial(match.group(1)):
info(f"serial auto-read from PCI VPD: {match.group(1)}")
return match.group(1), "pci-vpd"
if nf:
q = as_root([nf, "--pcisegbus", bdf, "--query"], timeout=40)
serials = {m.group(1) for m in re.finditer(
r"Serial(?:\s*Number)?\s*[:=]\s*(\S+)", q["out"], re.I)
if q["rc"] == 0 and usable_serial(m.group(1))}
if q["rc"] == 0 and len(serials) == 1:
serial = serials.pop()
info(f"serial auto-read from card-scoped nvflash query: {serial}")
return serial, "nvflash-pcisegbus-query"
return None, "unavailable"
def normalize_bdf(value):
match = re.search(r"([0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7])$", value)
return match.group(1).lower() if match else None
def memory_state(bdf, device_id):
"""Identify unlock from this card's reported memory, independent of Gen2 status."""
r = run(["nvidia-smi", "--query-gpu=pci.bus_id,memory.total",
"--format=csv,noheader,nounits"], timeout=30)
if r["rc"] != 0:
return {"status": "unavailable", "note": (r["err"] or r["out"])[-300:]}
for line in r["out"].splitlines():
fields = [field.strip() for field in line.split(",")]
if len(fields) != 2 or normalize_bdf(fields[0]) != bdf:
continue
try:
mib = int(fields[1])
except ValueError:
return {"status": "unavailable", "note": f"invalid memory total: {fields[1]}"}
if device_id == "10de:20c2":
status = "unlocked" if mib >= 60000 else "stock" if 7680 <= mib <= 8704 else "unexpected"
elif device_id == "10de:2082":
status = "unlocked" if 35000 <= mib < 60000 else "stock" if 9728 <= mib <= 10752 else "unexpected"
else:
status = "unsupported"
return {"status": status, "memory_mib": mib}
return {"status": "unavailable", "note": f"{bdf} absent from nvidia-smi"}
def ensure_unlocked(bdf, device_id, folder, card=None, allow_install=True):
before = memory_state(bdf, device_id)
result = {"before": before, "status": before["status"], "install_rc": None}
pending = pending_state()
if pending and pending.get("stage") == "cmpunlocker":
if pending.get("boot_id") == boot_id():
result.update(status="reboot-required", note="cold shut down and power on after cmpunlocker install")
return result
match = same_card(pending.get("card"), card or card_identity(bdf, device_id))
if match is False:
warn("previous unlock marker belongs to a different card; checking this card anew")
PENDING_FILE.unlink(missing_ok=True)
elif match is None and before["status"] != "unlocked":
result.update(status="failed", note="cannot tie the previous unlock to this card; inspect the pending marker before retrying")
return result
elif before["status"] != "unlocked":
result.update(status="failed", note="cmpunlocker installed but card is not unlocked after reboot")
return result
else:
PENDING_FILE.unlink(missing_ok=True)
if before["status"] == "unlocked":
result.update(status="unlocked", note="unlocked memory is active")
return result
if before["status"] != "stock":
result.update(status="failed", note="card memory/PCI ID is unavailable or unexpected; refusing to select an unlock profile")
return result
if not allow_install:
result.update(status="failed", note="unlock inactive and --no-install was requested")
return result
if not INSTALL_SH.exists():
result.update(status="failed", note="cmpunlocker install.sh is missing")
return result
allowed, reason = sudo_precondition()
if not allowed:
result.update(status="failed", note=f"root access required: {reason}")
return result
info("installing cmpunlocker for the detected card (this can take several minutes)")
log_path = (folder / "extras" /
f"cmpunlocker_install_{datetime.datetime.now().strftime('%Y%m%dT%H%M%S%f')}.log")
info(f"cmpunlocker install log: {log_path}")
install = run_logged_as_root(["bash", str(INSTALL_SH)], log_path, timeout=3600,
cwd=INSTALL_SH.parent)
result["install_rc"] = install["rc"]
result["install_log"] = str(log_path)
if install["rc"] != 0:
result.update(status="failed", note=f"cmpunlocker installer failed (rc={install['rc']}); see {log_path}")
return result
mark_pending("cmpunlocker", card or card_identity(bdf, device_id))
result.update(status="reboot-required", note="cmpunlocker installed; cold shut down and power on, then rerun")
return result
def save_bios(nf, out_path, bdf):
result = {"method": None, "path": None, "sha256": None, "size": 0, "ok": False, "note": ""}
if out_path.exists():
data = out_path.read_bytes()
if len(data) > 512 and data.strip(b"\x00"):
result.update(method="existing", path=str(out_path), size=len(data),
sha256=hashlib.sha256(data).hexdigest(), ok=True,
note="existing backup preserved")
else:
result["note"] = "existing backup is empty or invalid; move it before retrying"
return result
if nf:
r = as_root([nf, "--pcisegbus", bdf, "--save", str(out_path)], timeout=90)
if r["rc"] == 0 and out_path.exists() and out_path.stat().st_size > 512:
data = out_path.read_bytes()
if data.strip(b"\x00") == b"":
result["note"] = "nvflash wrote an all-zero image"
else:
result.update(method="nvflash", path=str(out_path), size=len(data),
sha256=hashlib.sha256(data).hexdigest(), ok=True)
return result
else:
result["note"] = f"card-scoped nvflash --save failed or image too small (rc={r['rc']}): {(r['err'] or r['out'])[-200:]}"
out_path.unlink(missing_ok=True)
else:
result["note"] = "nvflash unavailable"
rom = sysfs(bdf) / "rom"
if as_root(["sh", "-c", f"echo 1 > {rom}"], timeout=15)["rc"] == 0:
command = ["cat", str(rom)] if os.geteuid() == 0 else ["sudo", "-n", "cat", str(rom)]
try:
read = subprocess.run(command, capture_output=True, timeout=30)
data = read.stdout if read.returncode == 0 else None
except (OSError, subprocess.TimeoutExpired):
data = None
finally:
as_root(["sh", "-c", f"echo 0 > {rom}"], timeout=15)
if data and len(data) > 512 and data.strip(b"\x00") != b"":
out_path.write_bytes(data)
result.update(method="sysfs-rom", path=str(out_path), size=len(data),
sha256=hashlib.sha256(data).hexdigest(), ok=True)
result["note"] = (result["note"] + " | saved via sysfs rom").strip(" |")
return result
result["note"] += " | sysfs rom empty/all-zero"
else:
result["note"] += " | sysfs rom enable failed"
return result
# ---------------------------------------------------------------- nvidia / verify
def nvidia_smi_info():
if not which("nvidia-smi"):
return {"available": False}
q = run(["nvidia-smi", "--query-gpu=name,uuid,memory.total,pcie.link.gen.current,"
"pcie.link.gen.max,pcie.link.width.current,temperature.gpu,driver_version",
"--format=csv,noheader"], timeout=30)
if q["rc"] != 0:
return {"available": False, "error": (q["err"] or q["out"])[-300:]}
rows = [[f.strip() for f in line.split(",")] for line in q["out"].strip().splitlines()]
return {"available": True, "gpus": rows}
def run_verify_unlocker(bdf, device_id):
if not VERIFY_SH.exists():
return {"ran": False, "note": "harness/cmpunlocker source missing"}
if not which("nvidia-smi"):
return {"ran": False, "note": "nvidia-smi missing - unlock verify skipped"}
kernel = run(["uname", "-r"])["out"].strip()
inventory = Path("/lib/modules") / kernel / "updates" / "cmpunlocker" / "gpu_inventory"
contents = read_file(inventory)
if contents:
entries = [line.split() for line in contents.splitlines()
if line.strip() and not line.lstrip().startswith("#")]
if len(entries) != 1 or len(entries[0]) < 2 or normalize_bdf(entries[0][0]) != bdf or not entries[0][1].lower().endswith(device_id[-4:]):
return {"ran": False, "status": "skipped-stale-inventory",
"note": f"installed gpu_inventory does not match {bdf} ({device_id}); live per-card memory and PCIe checks remain authoritative"}
r = as_root(["bash", str(VERIFY_SH)], timeout=180)
lines = [l for l in (r["out"] + r["err"]).splitlines() if l.strip()]
status = "unknown"
if r["rc"] == 0 and any("unlocked memory" in l.lower() for l in lines):
status = "unlocked-ok"
elif "STOCK" in r["out"]:
status = "stock-memory"
elif "MISSING" in r["out"]:
status = "gpu-missing-in-smi"
elif r["rc"] != 0:
status = f"verify-failed-rc{r['rc']}"
return {"ran": True, "rc": r["rc"], "status": status, "tail": lines[-30:]}
# ---------------------------------------------------------------- optional Vulkan capability probe
def run_vulkan_probe():
if not which("vulkaninfo"):
return {"requested": True, "ran": False, "note": "vulkaninfo is missing"}
r = run(["vulkaninfo", "--summary"], timeout=30)
return {"requested": True, "ran": True, "rc": r["rc"],
"note": "generic Vulkan capability listing; does not validate CMP VRAM",
"log_tail": (r["out"] + r["err"]).splitlines()[-40:]}