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"]