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:
Christian Manivong
2026-06-24 12:32:01 +02:00
co-authored by Claude Sonnet 4.6
parent 450aa193ba
commit 1bfc8dd245
2 changed files with 169 additions and 12 deletions
+101 -2
View File
@@ -2,7 +2,7 @@
import pytest
from unittest.mock import MagicMock, patch
from napalm_linux.linux import LinuxDriver
from napalm_linux.linux import LinuxDriver, _arm_vendor_from_model
# ---------------------------------------------------------------------------
@@ -368,8 +368,18 @@ def _dmi_output(
product_serial: str,
product_uuid: str,
detect_virt: str,
dt_model: str = "",
cpuinfo_serial: str = "",
cpuinfo_model: str = "",
) -> str:
return "\n".join([sys_vendor, product_name, product_version, product_serial, product_uuid, detect_virt])
# Mirrors the real shell output: DMIBEGIN sentinel followed by 9 fields.
# The sentinel prevents _send()'s .strip() from eating leading blank lines
# (which occur on ARM where all DMI files are absent).
return "\n".join([
"DMIBEGIN",
sys_vendor, product_name, product_version, product_serial,
product_uuid, detect_virt, dt_model, cpuinfo_serial, cpuinfo_model,
])
class TestCollectPlatformInfo:
@@ -482,6 +492,95 @@ class TestCollectPlatformInfo:
assert info["model"] == ""
assert info["is_vm"] is False
def test_arm_device_tree_fallback(self, driver):
raw = _dmi_output(
"", "", "", "", "", "none",
dt_model="Raspberry Pi 4 Model B Rev 1.4",
cpuinfo_serial="100000002a6d96dc",
cpuinfo_model="Raspberry Pi 4 Model B Rev 1.4",
)
with patch.object(driver, "_send", return_value=raw):
info = driver._collect_platform_info()
assert info["vendor"] == "Raspberry Pi Foundation"
assert info["model"] == "Raspberry Pi 4 Model B Rev 1.4"
assert info["serial"] == "100000002a6d96dc"
assert info["is_vm"] is False
def test_arm_cpuinfo_model_when_no_dt(self, driver):
raw = _dmi_output(
"", "", "", "", "", "none",
dt_model="",
cpuinfo_serial="0000000012345678",
cpuinfo_model="Raspberry Pi 3 Model B Plus Rev 1.3",
)
with patch.object(driver, "_send", return_value=raw):
info = driver._collect_platform_info()
assert info["vendor"] == "Raspberry Pi Foundation"
assert info["model"] == "Raspberry Pi 3 Model B Plus Rev 1.3"
assert info["serial"] == "0000000012345678"
assert info["is_vm"] is False
def test_arm_no_fallback_data_returns_empty(self, driver):
raw = _dmi_output("", "", "", "", "", "none")
with patch.object(driver, "_send", return_value=raw):
info = driver._collect_platform_info()
assert info["vendor"] == ""
assert info["model"] == ""
assert info["is_vm"] is False
def test_bare_metal_detect_virt_exit1_does_not_shift_arm_fields(self, driver):
# systemd-detect-virt exits 1 on bare metal, old "|| echo none" caused
# d="none\nnone" which shifted subsequent lines off by one.
# With the ${d:-none} fix this no longer happens.
raw = _dmi_output(
"", "", "", "", "", "none",
dt_model="",
cpuinfo_serial="0000000012345678",
cpuinfo_model="Raspberry Pi 3 Model B Rev 1.2",
)
with patch.object(driver, "_send", return_value=raw):
info = driver._collect_platform_info()
assert info["vendor"] == "Raspberry Pi Foundation"
assert info["model"] == "Raspberry Pi 3 Model B Rev 1.2"
assert info["serial"] == "0000000012345678"
def test_arm_fallback_ignored_when_dmi_present(self, driver):
raw = _dmi_output(
"Dell Inc.", "PowerEdge R720", "Not Specified", "XYZ999",
"8a2e3f00-dead-beef-0000-123456789abc", "none",
dt_model="some-dt-model",
cpuinfo_serial="deadbeef",
cpuinfo_model="some cpuinfo model",
)
with patch.object(driver, "_send", return_value=raw):
info = driver._collect_platform_info()
assert info["vendor"] == "Dell Inc."
assert info["model"] == "PowerEdge R720"
assert info["serial"] == "XYZ999"
# ---------------------------------------------------------------------------
# _arm_vendor_from_model
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("model,expected", [
("Raspberry Pi 4 Model B Rev 1.4", "Raspberry Pi Foundation"),
("Raspberry Pi 3 Model B Rev 1.2", "Raspberry Pi Foundation"),
("ODROID-N2L", "Hardkernel"),
("NVIDIA Jetson Nano Developer Kit", "NVIDIA"),
("Rock Pi 4C", "Radxa"),
("Orange Pi 5 Plus", "Xunlong Software"),
("Banana Pi BPI-R3", "SinoVoip"),
("NanoPi R4S", "FriendlyElec"),
("PINE64 RockPro64", "Pine64"),
("BeagleBone Black", "BeagleBoard.org"),
("Unknown Board 1.0", "Unknown Board"),
("SomeSingleWordBoard", "SomeSingleWordBoard"),
])
def test_arm_vendor_from_model(model, expected):
assert _arm_vendor_from_model(model) == expected
# ---------------------------------------------------------------------------
# get_facts uses _collect_platform_info