feat: read-only IPMI driver; report a missing Redfish license (0.2.0)

A Supermicro X10DRL-i refuses Redfish without a license (SUM, DCMS or
OOB), even the service root. IPMI answers without one.

- New entry point `ipmi` (napalm_redfish.ipmi.IpmiDriver) on
  ManagementControllerDriver, over pyghmi: facts and inventory from the
  FRU (serial number, system UUID), the BMC's LAN interface, sensors
  (temperatures, fans, power supplies), the power state, the event log
  and the BMC firmware. Read-only: no power control, no BMC reset.
  Tested against recordings of the X10 with serials, the UUID, MACs and
  IPs replaced.
- RedfishDriver.open() no longer fails on a license refusal: get_facts
  returns what is known without Redfish, get_device_warnings reports
  bmc_license_required with the licenses the BMC names, every other
  getter raises. netOrk then shows the reason instead of an offline BMC.
- IpmiDriver is not exported from the package: NAPALM's own
  get_network_driver("redfish") takes the first driver class of
  napalm_redfish.
This commit is contained in:
christianmanivong committed 2026-10-13 10:53:51 +02:00
1 parent bfbeb19255
commit 332e6090a7
17 files changed
+1194 -19

No files matched your search

+30 -1
View File
@@ -65,7 +65,12 @@ from napalm_redfish.parsers import (
to_napalm_users,
)
from napalm_redfish.parsers.common import link, link_list, odata_id
from napalm_redfish.transport import RedfishError, RedfishNotFound, RedfishTransport
from napalm_redfish.transport import (
RedfishError,
RedfishLicenseError,
RedfishNotFound,
RedfishTransport,
)
LOG = logging.getLogger(__name__)
@@ -152,12 +157,20 @@ class RedfishDriver(ManagementControllerDriver):
retries=self.optional_args.get("retries", 1),
)
self._cache: _JsonDict = {}
self._license_refused: RedfishLicenseError | None = None
# --- connection ----------------------------------------------------------------------
def open(self) -> None:
"""Connect. A BMC that refuses Redfish for want of a license is reachable all the
same: ``open`` succeeds, ``get_facts`` returns what is known without Redfish, and
``get_device_warnings`` says which license is missing. Every other getter raises.
"""
self._license_refused = None
try:
self.transport.open()
except RedfishLicenseError as exc:
self._license_refused = exc
except RedfishError as exc:
raise ConnectionException(f"Cannot connect to Redfish at {self.hostname}: {exc}") \
from exc
@@ -167,6 +180,8 @@ class RedfishDriver(ManagementControllerDriver):
self._cache = {}
def is_alive(self) -> _JsonDict:
if self._license_refused is not None:
return {"is_alive": True}
try:
self.transport.get("/redfish/v1/")
except Exception:
@@ -181,6 +196,8 @@ class RedfishDriver(ManagementControllerDriver):
return self._cache[key]
def _root(self) -> _JsonDict:
if self._license_refused is not None:
raise ConnectionException(f"{self.hostname}: {self._license_refused}")
return self._cached("root", lambda: parse_service_root(self.transport.root))
def _first(self, collection: str) -> _JsonDict:
@@ -233,6 +250,10 @@ class RedfishDriver(ManagementControllerDriver):
# --- NAPALM getters ----------------------------------------------------------------
def get_facts(self) -> _JsonDict:
if self._license_refused is not None:
return {"uptime": -1.0, "vendor": "", "model": "", "hostname": self.hostname,
"fqdn": self.hostname, "os_version": "", "serial_number": "",
"interface_list": []}
system = parse_system(self._system_raw())
manager = parse_manager(self._manager_raw())
interfaces = self._manager_interfaces()
@@ -329,6 +350,14 @@ class RedfishDriver(ManagementControllerDriver):
communities[community["name"]] = {"acl": "", "mode": mode}
return {"chassis_id": "", "community": communities, "contact": "", "location": ""}
def get_device_warnings(self) -> list[_JsonDict]:
"""Raw signals for netOrk's warning catalog; presentation is netOrk's."""
refused = self._license_refused
if refused is None:
return []
return [{"code": "bmc_license_required",
"meta": {"licenses": refused.licenses, "message": refused.detail}}]
# --- role: power -------------------------------------------------------------------
def _accepted_actions(self, system: _JsonDict) -> list[str]: