From 0e38e18e264a083c117a9e7defd8087ff1cd0c94 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 11:24:29 +0200 Subject: [PATCH] fix: IPMI polls after the first failed; BMC drivers collect SNMP health (0.2.1) The second IPMI poll of a BMC in the same worker process failed with "'<' not supported between instances of 'float' and 'NoneType'". pyghmi files a session in Session.initting_sessions under the credentials as str and deletes it only under bytes, so the entry stays and the next Command got the old, logged-out session back. The driver drops that entry itself before open() and after close(). Reproduced and verified live against the Supermicro X10DRL-i: three polls in one process now succeed. IpmiDriver and RedfishDriver mix in HealthMetricsMixin, so netOrk's SNMP health poll reads the BMC's own load, memory and interfaces where the BMC runs net-snmp with the UCD-MIB, as the X10's does. --- CHANGELOG.md | 15 +++++++++++++++ napalm_redfish/__init__.py | 2 +- napalm_redfish/driver.py | 10 +++++++--- napalm_redfish/ipmi.py | 26 ++++++++++++++++++++++---- pyproject.toml | 2 +- tests/test_ipmi.py | 36 +++++++++++++++++++++++++++++++++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b403454..7bbdc0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2026-10-13 + +### Added +- `IpmiDriver` and `RedfishDriver` mix in `HealthMetricsMixin`: netOrk's SNMP health + poll reads the BMC's own load, memory and interfaces (UCD-MIB) where the BMC runs + net-snmp, as the Supermicro X10's does. The role base stays without it. + +### Fixed +- `IpmiDriver`: the second poll of a BMC in the same process failed with + `'<' not supported between instances of 'float' and 'NoneType'`. pyghmi files a + session in `Session.initting_sessions` under the credentials as str but deletes it + only under bytes, so the next `Command` got the old, logged-out session back. The + driver drops that entry itself before `open()` and after `close()`. Reproduced and + verified live against a Supermicro X10DRL-i (three polls in one process). + ## [0.2.0] - 2026-10-13 ### Added diff --git a/napalm_redfish/__init__.py b/napalm_redfish/__init__.py index 5695283..4648fa6 100644 --- a/napalm_redfish/__init__.py +++ b/napalm_redfish/__init__.py @@ -7,5 +7,5 @@ takes the first driver class of this package. ``IpmiDriver`` lives in from napalm_redfish.driver import RedfishDriver -__version__ = "0.2.0" +__version__ = "0.2.1" __all__ = ["RedfishDriver"] diff --git a/napalm_redfish/driver.py b/napalm_redfish/driver.py index e25bf33..2835f2e 100644 --- a/napalm_redfish/driver.py +++ b/napalm_redfish/driver.py @@ -30,7 +30,7 @@ from datetime import datetime, timezone from typing import Any from napalm.base.exceptions import ConnectionException -from napalm_device_types import ManagementControllerDriver +from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver from napalm_device_types.base import FingerprintRule, PortSpec from napalm_device_types.models import POWER_ACTIONS @@ -109,8 +109,12 @@ def _status(item: _JsonDict) -> str: return item.get("health") or "" -class RedfishDriver(ManagementControllerDriver): - """NAPALM driver for Redfish BMCs (Supermicro, iDRAC, iLO, XCC and others).""" +class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver): + """NAPALM driver for Redfish BMCs (Supermicro, iDRAC, iLO, XCC and others). + + ``HealthMetricsMixin``: the BMC's own load, memory and interfaces over SNMP (UCD-MIB), + where the BMC runs net-snmp. The managed server's sensors come from ``get_environment``. + """ platform = "redfish" DRIVER_NAME = "redfish" diff --git a/napalm_redfish/ipmi.py b/napalm_redfish/ipmi.py index 2d52dbb..e3542a6 100644 --- a/napalm_redfish/ipmi.py +++ b/napalm_redfish/ipmi.py @@ -11,8 +11,9 @@ from __future__ import annotations from typing import Any from napalm.base.exceptions import ConnectionException -from napalm_device_types import ManagementControllerDriver +from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver from pyghmi.ipmi.command import Command +from pyghmi.ipmi.private.session import Session _JsonDict = dict[str, Any] @@ -34,8 +35,12 @@ def _supply_present(reading: Any) -> bool: return "Present" in (getattr(reading, "states", None) or []) -class IpmiDriver(ManagementControllerDriver): - """NAPALM driver for BMCs over IPMI 2.0 (lanplus), read-only.""" +class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver): + """NAPALM driver for BMCs over IPMI 2.0 (lanplus), read-only. + + ``HealthMetricsMixin``: the BMC's own load, memory and interfaces over SNMP (UCD-MIB), + where the BMC runs net-snmp. The managed server's sensors come from ``get_environment``. + """ platform = "ipmi" DRIVER_NAME = "ipmi" @@ -61,13 +66,13 @@ class IpmiDriver(ManagementControllerDriver): # --- connection ---------------------------------------------------------------------- def open(self) -> None: + self._forget_session() try: self._ipmi = Command( bmc=self.hostname, userid=self.username, password=self.password, port=self.port, - keepalive=False, ) except Exception as exc: raise ConnectionException(f"Cannot connect to IPMI at {self.hostname}: {exc}") \ @@ -80,6 +85,19 @@ class IpmiDriver(ManagementControllerDriver): ipmi.ipmi_session.logout() except Exception: # a session left open times out on the BMC pass + self._forget_session() + + def _forget_session(self) -> None: + """Drop this BMC's session from pyghmi's cache of sessions being set up. + + pyghmi files a new session there under ``(bmc, userid, password, port, kg)`` with + the credentials as str, and only ever deletes it under the credentials as bytes, so + the entry stays. The next ``Command`` for the same BMC in this worker process then + got the old, logged-out session back and failed with ``'<' not supported between + instances of 'float' and 'NoneType'``. + """ + key = (self.hostname, self.username, self.password, self.port, None) + Session.initting_sessions.pop(key, None) def is_alive(self) -> _JsonDict: return {"is_alive": self._ipmi is not None} diff --git a/pyproject.toml b/pyproject.toml index 782a01b..06cb2e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "napalm-redfish" -version = "0.2.0" +version = "0.2.1" description = "NAPALM driver for server baseboard management controllers over DMTF Redfish" readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_ipmi.py b/tests/test_ipmi.py index 0500367..692c191 100644 --- a/tests/test_ipmi.py +++ b/tests/test_ipmi.py @@ -28,7 +28,7 @@ class FakeCommand: instances: list = [] - def __init__(self, bmc=None, userid=None, password=None, port=623, keepalive=True, **kw): + def __init__(self, bmc=None, userid=None, password=None, port=623, **kw): self.args = {"bmc": bmc, "userid": userid, "password": password, "port": port} self.logged_out = False self.ipmi_session = SimpleNamespace(logout=self._logout) @@ -246,3 +246,37 @@ def test_napalm_still_finds_redfish_by_name(): from napalm_redfish import RedfishDriver assert napalm.get_network_driver("redfish") is RedfishDriver + + +def test_each_open_and_close_forgets_pyghmis_cached_session(monkeypatch): + # pyghmi files a session in Session.initting_sessions under the credentials as str + # and deletes it only under bytes, so it stays; the next Command for the BMC in the + # same process got the old, logged-out one back and failed with "'<' not supported + # between float and NoneType". Reproduced live against the X10, fixed by this. + cache = {} + monkeypatch.setattr(ipmi, "Session", SimpleNamespace(initting_sessions=cache)) + key = ("192.0.2.150", "netork", "s3cret", 623, None) + + cache[key] = "stale session of an earlier poll" + d = IpmiDriver("192.0.2.150", "netork", "s3cret") + d.open() + assert key not in cache + + cache[key] = "this poll's session" + d.close() + assert key not in cache + assert FakeCommand.instances[0].logged_out + + +def test_both_drivers_collect_the_bmcs_own_snmp_health(): + # Many BMCs run net-snmp with the UCD-MIB (the X10's answers load, memory, CPU idle and + # five interfaces), so netOrk's SNMP health poll covers the BMC itself. The managed + # server's sensors stay with get_environment. + from napalm_device_types import HealthMetricsMixin + from napalm_device_types.roles import role_keys_of + + from napalm_redfish import RedfishDriver + + for cls in (IpmiDriver, RedfishDriver): + assert issubclass(cls, HealthMetricsMixin), cls.__name__ + assert role_keys_of(cls) == ["management_controller"]