From 52074620efb5940c42e5d01278318ad0dc9f4dca Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 5 Oct 2026 06:17:30 +0200 Subject: [PATCH] feat: report the node kernel's modules and build configuration A Proxmox node runs its own kernel under every guest, which makes it the host where a kernel CVE's preconditions matter most. ProxmoxDriver mixes in KernelFactsMixin from napalm-device-types and supplies only the transport, the existing exec path. Requires napalm-device-types 2.1.0. --- napalm_proxmox/driver.py | 3 ++- napalm_proxmox/system_mixin.py | 8 ++++++ pyproject.toml | 2 +- tests/test_kernel_facts.py | 46 ++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/test_kernel_facts.py diff --git a/napalm_proxmox/driver.py b/napalm_proxmox/driver.py index 2e3ce5e..7c3b279 100644 --- a/napalm_proxmox/driver.py +++ b/napalm_proxmox/driver.py @@ -34,7 +34,7 @@ from typing import Any logger = logging.getLogger(__name__) -from napalm_device_types import FingerprintRule, HypervisorDriver, PortSpec +from napalm_device_types import FingerprintRule, HypervisorDriver, KernelFactsMixin, PortSpec from napalm.base.exceptions import ConnectionException try: @@ -76,6 +76,7 @@ class ProxmoxDriver( ProxmoxVMProvisionMixin, ProxmoxRoutingMixin, ProxmoxSystemMixin, + KernelFactsMixin, HypervisorDriver, ): """NAPALM driver for Proxmox VE nodes.""" diff --git a/napalm_proxmox/system_mixin.py b/napalm_proxmox/system_mixin.py index 779cb73..3c78524 100644 --- a/napalm_proxmox/system_mixin.py +++ b/napalm_proxmox/system_mixin.py @@ -221,6 +221,14 @@ class ProxmoxSystemMixin: return result + # ------------------------------------------------------------------ # + # Kernel facts (KernelFactsMixin supplies get_kernel_facts) + # ------------------------------------------------------------------ # + + def _run_kernel_facts_command(self, command: str) -> str: + """The transport for ``KernelFactsMixin.get_kernel_facts``: the exec path.""" + return self._exec_ssh_command(command) + # ------------------------------------------------------------------ # # Packages (Debian APT) # ------------------------------------------------------------------ # diff --git a/pyproject.toml b/pyproject.toml index 859ae9e..fdd5f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "napalm>=5.0.0", - "napalm_device_types>=2.0.0", + "napalm_device_types>=2.1.0", "paramiko>=5.0.0", # CVE-2026-44405; imported directly for SSH fallback (driver.py) "proxmoxer>=2.0.0", "netaddr>=0.9.0", diff --git a/tests/test_kernel_facts.py b/tests/test_kernel_facts.py new file mode 100644 index 0000000..645bbd7 --- /dev/null +++ b/tests/test_kernel_facts.py @@ -0,0 +1,46 @@ +"""`get_kernel_facts`: what the node's kernel has built and loaded. + +A Proxmox node runs its own kernel under every guest, which makes it the host +where a kernel CVE's preconditions matter most. The command and its parse are +napalm-device-types'; the driver only carries the command over its exec path. +""" + +from __future__ import annotations + +import base64 +import gzip +from unittest.mock import patch + +import pytest + +from napalm_device_types import KernelFactsMixin +from napalm_device_types.kernel import KERNEL_FACTS_COMMAND +from napalm_proxmox.driver import ProxmoxDriver + +REPORT = ( + "[release]\n6.8.12-4-pve\n[loaded]\nkvm_intel\n[builtin]\nkernel/net/ipv4/tcp_cubic.ko\n" + "[available]\nkernel/net/tipc/tipc.ko\n[config]\nCONFIG_TIPC=m\n" +) +WIRE = "KFACTS_BEGIN\n" + base64.encodebytes(gzip.compress(REPORT.encode())).decode() + "KFACTS_END" + + +def test_the_driver_declares_the_contract(): + assert issubclass(ProxmoxDriver, KernelFactsMixin) + + +def test_it_runs_the_shared_command(driver): + with patch.object(driver, "_exec_ssh_command", return_value=WIRE) as exec_: + facts = driver.get_kernel_facts() + + exec_.assert_called_once_with(KERNEL_FACTS_COMMAND) + assert facts["release"] == "6.8.12-4-pve" + assert facts["loaded"] == ["kvm_intel"] + assert facts["builtin"] == ["tcp_cubic"] + assert facts["available"] == ["tipc"] + assert facts["config"] == {"CONFIG_TIPC": "m"} + + +def test_output_without_a_report_raises(driver): + with patch.object(driver, "_exec_ssh_command", return_value=""): + with pytest.raises(ValueError): + driver.get_kernel_facts()