Files
napalm-redfish/napalm_redfish/bmc.py
T
Christian Manivong e9ad7951cd feat: a bmc driver that takes the best of Redfish and IPMI (0.4.0)
bmc (napalm_redfish.bmc:BmcDriver) reads a BMC over Redfish where it
answers. A BMC that refuses Redfish (no license) is read and switched over
IPMI. A getter that fails over Redfish is asked over IPMI where IPMI has it;
IPMI is opened only then. Power control goes over Redfish when it answers,
and a refusal is final. The BMC is reset over Redfish only. Same account
for both; ipmi_port in optional_args.

Discovery suggests bmc for a BMC it finds: the fingerprint rules moved from
RedfishDriver to BmcDriver.
2026-10-17 10:34:20 +02:00

177 lines
6.8 KiB
Python

"""NAPALM driver for a BMC that takes the best of Redfish and IPMI, chosen by itself.
``redfish`` and ``ipmi`` make the operator choose, and the right choice changes with the
BMC's license: a Supermicro X10 refuses Redfish without one, answers most of it with
SFT-OOB-LIC, and all of it with DCMS. This driver asks Redfish first and IPMI where Redfish
does not answer:
* A BMC that refuses Redfish altogether (no license) is read over IPMI entirely.
* A getter that fails over Redfish (an error, a resource the BMC withholds) is asked over
IPMI, where IPMI has it. IPMI is opened only then.
* Power control goes over Redfish when it answers, else over IPMI. A refusal by the BMC is
an answer, not a failure, and is not retried over IPMI.
* The BMC itself is reset over Redfish only (``Manager.Reset``); IPMI's cold reset is not
offered, as in the ``ipmi`` driver.
Both protocols use the same account. ``optional_args``: everything ``RedfishDriver``
takes, plus ``ipmi_port`` (623).
"""
from __future__ import annotations
import logging
from typing import Any
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver
from napalm_redfish.driver import BMC_HTTP_FINGERPRINT, BMC_PORT_SPECS, RedfishDriver
from napalm_redfish.ipmi import IpmiDriver
from napalm_redfish.transport import RedfishError
LOG = logging.getLogger(__name__)
_JsonDict = dict[str, Any]
_IPMI_PORT = 623
class BmcDriver(HealthMetricsMixin, ManagementControllerDriver):
"""One BMC over Redfish and IPMI: Redfish first, IPMI where Redfish does not answer."""
platform = "bmc"
DRIVER_NAME = "bmc"
default_port = 443
#: Discovery suggests this driver for a BMC it finds, not ``redfish``.
PORT_SPECS = BMC_PORT_SPECS
HTTP_FINGERPRINT = BMC_HTTP_FINGERPRINT
#: A BMC restart takes a few minutes before Redfish answers again.
REBOOT_SETTLE_SECONDS = RedfishDriver.REBOOT_SETTLE_SECONDS
def __init__(
self,
hostname: str,
username: str,
password: str,
timeout: int = 60,
optional_args: _JsonDict | None = None,
) -> None:
self.hostname = hostname
self.username = username
self.password = password
self.timeout = timeout
self.optional_args: _JsonDict = dict(optional_args or {})
ipmi_port = int(self.optional_args.pop("ipmi_port", None) or _IPMI_PORT)
self.ipmi_port = ipmi_port
self._redfish = RedfishDriver(hostname, username, password, timeout, self.optional_args)
self._ipmi = IpmiDriver(hostname, username, password, timeout, {"port": ipmi_port})
self._redfish_ok = False
self._ipmi_open = False
# --- connection ----------------------------------------------------------------------
def open(self) -> None:
"""Connect over Redfish; a BMC that refuses it or does not answer it is opened over
IPMI instead. Fails only when neither answers."""
try:
self._redfish.open()
except ConnectionException as exc:
LOG.info("%s: no Redfish (%s); reading it over IPMI", self.hostname, exc)
self._redfish_ok = False
else:
self._redfish_ok = self._redfish._license_refused is None
if not self._redfish_ok:
self._ipmi_conn()
def close(self) -> None:
self._redfish.close()
if self._ipmi_open:
self._ipmi.close()
self._ipmi_open = False
def is_alive(self) -> _JsonDict:
if self._redfish_ok:
return self._redfish.is_alive()
return self._ipmi.is_alive()
def _ipmi_conn(self) -> IpmiDriver:
if not self._ipmi_open:
self._ipmi.open()
self._ipmi_open = True
return self._ipmi
def _call(self, name: str, *args: Any) -> Any:
"""``name`` over Redfish; over IPMI when Redfish is out or fails and IPMI has it."""
has_ipmi = callable(getattr(IpmiDriver, name, None))
if self._redfish_ok:
try:
return getattr(self._redfish, name)(*args)
except (RedfishError, ConnectionException) as exc:
if not has_ipmi:
raise
LOG.info("%s: %s failed over Redfish (%s); asking IPMI", self.hostname, name,
exc)
if not has_ipmi:
raise NotImplementedError(f"{self.hostname}: {name} needs Redfish")
return getattr(self._ipmi_conn(), name)(*args)
# --- NAPALM getters ------------------------------------------------------------------
def get_facts(self) -> _JsonDict:
return self._call("get_facts")
def get_interfaces(self) -> _JsonDict:
return self._call("get_interfaces")
def get_interfaces_ip(self) -> _JsonDict:
return self._call("get_interfaces_ip")
def get_environment(self) -> _JsonDict:
return self._call("get_environment")
def get_users(self) -> _JsonDict:
return self._call("get_users")
def get_ntp_servers(self) -> _JsonDict:
return self._call("get_ntp_servers")
def get_snmp_information(self) -> _JsonDict:
return self._call("get_snmp_information")
def get_device_warnings(self) -> list[_JsonDict]:
"""Redfish's signals; for a BMC that refuses Redfish, with ``ipmi_fallback`` set, as
this driver reads it over IPMI meanwhile."""
warnings = self._redfish.get_device_warnings()
if self._redfish_ok:
return warnings
return [{**w, "meta": {**w.get("meta", {}), "ipmi_fallback": True}} for w in warnings]
# --- role methods --------------------------------------------------------------------
def get_power_state(self) -> _JsonDict:
return self._call("get_power_state")
def set_power_state(self, action: str) -> None:
self._call("set_power_state", action)
def reboot_host(self) -> None:
"""Restart the BMC (``Manager.Reset``), never the server. Needs Redfish."""
if not self._redfish_ok:
raise RuntimeError(f"{self.hostname}: resetting the BMC needs Redfish")
self._redfish.reboot_host()
def get_hardware_inventory(self) -> _JsonDict:
return self._call("get_hardware_inventory")
def get_event_log(self, limit: int = 500) -> list[_JsonDict]:
return self._call("get_event_log", limit)
def get_firmware_inventory(self) -> list[_JsonDict]:
return self._call("get_firmware_inventory")
def get_management_protocols(self) -> _JsonDict:
"""Over Redfish, what the BMC reports. Over IPMI only one thing is known: IPMI over
LAN is on, since this driver is talking it."""
if self._redfish_ok:
return self._redfish.get_management_protocols()
return {"ipmi": {"enabled": True, "port": self.ipmi_port}}