From d9b3ac12ea6f64f20670f19b7b993ae003b83717 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 22 Jun 2026 22:02:24 +0200 Subject: [PATCH] fix: read DMI fields separately to avoid shell quoting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The combined printf approach silently produced empty values when product_name/version contained special chars or the shell split tokens incorrectly. Read each /sys/class/dmi/id/ file via a separate cat, collect lines, then apply vendor-specific model name selection: - Intel NUC: product_name='NUC6CAYH' (marketing) preferred over product_version='J26843-409' (part number) - Lenovo: product_name='10MYS03U00' (type code, all-caps+digits) → prefer product_version='ThinkCentre M910x' (marketing name) Co-Authored-By: Claude Sonnet 4.6 --- napalm_proxmox/driver.py | 49 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/napalm_proxmox/driver.py b/napalm_proxmox/driver.py index 4c5dfc2..6db3640 100644 --- a/napalm_proxmox/driver.py +++ b/napalm_proxmox/driver.py @@ -357,34 +357,43 @@ class ProxmoxDriver( os_version = f"Proxmox VE {pve_version}" if pve_version else f"Proxmox VE {release}" # Physical hardware info from Linux DMI sysfs. - # product_version often contains the marketing name (e.g. "ThinkCentre M910x") - # while product_name holds the machine-type code (e.g. "10MYS03U00"). - # We prefer product_version when it is set and not identical to product_name. + # Read each field separately to avoid shell quoting issues with printf. + # Field priority for model: + # product_name — human-readable name on most vendors (e.g. "NUC6CAYH", + # "ThinkCentre M910x") + # product_version — sometimes the marketing name on Lenovo; on Intel NUC + # it is the board part number (less useful as model name) + # We prefer product_name; fall back to product_version only when + # product_name looks like a raw type code (all uppercase + digits, no spaces). vendor = "" model = "" serial = "" try: dmi_cmd = ( - "printf '%s\\t%s\\t%s\\t%s' " - "\"$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null)\" " - "\"$(cat /sys/class/dmi/id/product_name 2>/dev/null)\" " - "\"$(cat /sys/class/dmi/id/product_version 2>/dev/null)\" " - "\"$(cat /sys/class/dmi/id/product_serial 2>/dev/null)\"" + "v=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null); " + "n=$(cat /sys/class/dmi/id/product_name 2>/dev/null); " + "r=$(cat /sys/class/dmi/id/product_version 2>/dev/null); " + "s=$(cat /sys/class/dmi/id/product_serial 2>/dev/null); " + "printf '%s\\n%s\\n%s\\n%s\\n' \"$v\" \"$n\" \"$r\" \"$s\"" ) - raw = self._exec_ssh_command(dmi_cmd).strip() - parts = raw.split("\t") - if len(parts) >= 4: - vendor = parts[0].strip() - product_name = parts[1].strip() - product_version = parts[2].strip() - serial = parts[3].strip() - # Prefer product_version (marketing name) over product_name (type code) - # unless it is empty, "None", or identical to product_name. - if ( + lines = self._exec_ssh_command(dmi_cmd).splitlines() + if len(lines) >= 4: + vendor = lines[0].strip() + product_name = lines[1].strip() + product_version = lines[2].strip() + serial = lines[3].strip() + + _bad = {"none", "n/a", "not specified", "to be filled by o.e.m."} + pv_ok = ( product_version - and product_version.lower() not in ("none", "n/a", "not specified") + and product_version.lower() not in _bad and product_version != product_name - ): + ) + # Use product_version as model when product_name looks like a + # raw type code (Lenovo style: all uppercase letters and digits) + import re as _re + pn_is_code = bool(_re.fullmatch(r"[A-Z0-9]+", product_name)) + if pn_is_code and pv_ok: model = product_version else: model = product_name