feat: ARM hardware detection via device tree and /proc/cpuinfo
On ARM boards (Raspberry Pi, ODROID, etc.) /sys/class/dmi/id/ does not
exist. _collect_platform_info() now falls back to:
- /sys/firmware/devicetree/base/model (preferred)
- /proc/cpuinfo Model: / Serial: (fallback)
Three bugs fixed in the process:
1. systemd-detect-virt exits 1 on bare metal, so the old
"|| echo none" pattern produced d="none\nnone" (two lines),
shifting all subsequent fields by one. Fixed with ${d:-none}.
2. _send() calls .strip() on output, silently eating the five leading
blank lines that represent empty DMI fields on ARM. Fixed by
prefixing the printf output with a DMIBEGIN sentinel so the parser
can locate field 0 regardless of leading whitespace.
3. Vendor was always empty for ARM, falling back to the generic "Linux"
constant. Added _ARM_VENDOR_PREFIXES lookup table and
_arm_vendor_from_model() to derive the canonical vendor name from
the model string (e.g. "Raspberry Pi Foundation" for any RPi board).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
450aa193ba
commit
1bfc8dd245
+68
-10
@@ -85,6 +85,36 @@ _VM_DMI_VENDORS: frozenset[str] = frozenset({
|
||||
"parallels software international inc.",
|
||||
})
|
||||
|
||||
# Known ARM board model prefixes → canonical vendor name
|
||||
_ARM_VENDOR_PREFIXES: list[tuple[str, str]] = [
|
||||
("Raspberry Pi", "Raspberry Pi Foundation"),
|
||||
("NVIDIA Jetson", "NVIDIA"),
|
||||
("ODROID", "Hardkernel"),
|
||||
("Hardkernel", "Hardkernel"),
|
||||
("Rock Pi", "Radxa"),
|
||||
("ROCK Pi", "Radxa"),
|
||||
("Radxa", "Radxa"),
|
||||
("Orange Pi", "Xunlong Software"),
|
||||
("Banana Pi", "SinoVoip"),
|
||||
("NanoPi", "FriendlyElec"),
|
||||
("PINE64", "Pine64"),
|
||||
("BeagleBone", "BeagleBoard.org"),
|
||||
]
|
||||
|
||||
|
||||
def _arm_vendor_from_model(model: str) -> str:
|
||||
"""Extract vendor from an ARM device tree / cpuinfo model string."""
|
||||
for prefix, vendor in _ARM_VENDOR_PREFIXES:
|
||||
if model.startswith(prefix):
|
||||
return vendor
|
||||
# Generic fallback: words before the first numeric token
|
||||
brand = []
|
||||
for word in model.split():
|
||||
if word[0].isdigit():
|
||||
break
|
||||
brand.append(word)
|
||||
return " ".join(brand)
|
||||
|
||||
|
||||
class LinuxDriver(OSDriver):
|
||||
"""NAPALM driver for generic Linux systems.
|
||||
@@ -235,11 +265,23 @@ class LinuxDriver(OSDriver):
|
||||
"r=$(cat /sys/class/dmi/id/product_version 2>/dev/null); "
|
||||
"s=$(cat /sys/class/dmi/id/product_serial 2>/dev/null); "
|
||||
"u=$(cat /sys/class/dmi/id/product_uuid 2>/dev/null); "
|
||||
"d=$(systemd-detect-virt 2>/dev/null || echo none); "
|
||||
"printf '%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' \"$v\" \"$n\" \"$r\" \"$s\" \"$u\" \"$d\""
|
||||
"d=$(systemd-detect-virt 2>/dev/null); d=${d:-none}; "
|
||||
"dt=$(tr -d '\\0' </sys/firmware/devicetree/base/model 2>/dev/null); "
|
||||
"cs=$(grep '^Serial' /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs 2>/dev/null); "
|
||||
"cm=$(grep '^Model' /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs 2>/dev/null); "
|
||||
# DMIBEGIN sentinel: _send() strips leading blank lines (ARM has no DMI
|
||||
# files, so fields 0-4 are empty). The sentinel anchors the output so
|
||||
# splitlines()[start+N] always maps to the correct field index.
|
||||
"printf 'DMIBEGIN\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' "
|
||||
"\"$v\" \"$n\" \"$r\" \"$s\" \"$u\" \"$d\" \"$dt\" \"$cs\" \"$cm\""
|
||||
)
|
||||
try:
|
||||
lines = self._send(dmi_cmd).splitlines()
|
||||
raw_lines = self._send(dmi_cmd).splitlines()
|
||||
try:
|
||||
start = raw_lines.index("DMIBEGIN") + 1
|
||||
except ValueError:
|
||||
start = 0
|
||||
lines = raw_lines[start:]
|
||||
except Exception:
|
||||
return {"vendor": "", "model": "", "serial": "", "is_vm": False}
|
||||
|
||||
@@ -253,6 +295,9 @@ class LinuxDriver(OSDriver):
|
||||
product_ser = _clean(3)
|
||||
product_uuid = _clean(4)
|
||||
detect_virt = lines[5].strip().lower() if len(lines) > 5 else "none"
|
||||
dt_model = _clean(6)
|
||||
cpuinfo_ser = _clean(7)
|
||||
cpuinfo_mdl = _clean(8)
|
||||
|
||||
is_container = detect_virt in _CONTAINER_VIRT
|
||||
is_vm = (
|
||||
@@ -292,13 +337,26 @@ class LinuxDriver(OSDriver):
|
||||
}
|
||||
|
||||
# Bare-metal: prefer product_version when it reads like a marketing name
|
||||
pv_usable = product_ver and product_ver != product_name and " " in product_ver
|
||||
return {
|
||||
"vendor": sys_vendor,
|
||||
"model": product_ver if pv_usable else product_name,
|
||||
"serial": product_ser,
|
||||
"is_vm": False,
|
||||
}
|
||||
if sys_vendor or product_name:
|
||||
pv_usable = product_ver and product_ver != product_name and " " in product_ver
|
||||
return {
|
||||
"vendor": sys_vendor,
|
||||
"model": product_ver if pv_usable else product_name,
|
||||
"serial": product_ser,
|
||||
"is_vm": False,
|
||||
}
|
||||
|
||||
# ARM/embedded fallback: no DMI, try device tree and /proc/cpuinfo
|
||||
arm_model = dt_model or cpuinfo_mdl
|
||||
if arm_model:
|
||||
return {
|
||||
"vendor": _arm_vendor_from_model(arm_model),
|
||||
"model": arm_model,
|
||||
"serial": cpuinfo_ser,
|
||||
"is_vm": False,
|
||||
}
|
||||
|
||||
return {"vendor": "", "model": "", "serial": "", "is_vm": False}
|
||||
|
||||
def get_facts(self) -> dict[str, Any]:
|
||||
"""Return basic system facts."""
|
||||
|
||||
Reference in New Issue
Block a user