feat: report the licenses a BMC asks for as facts (0.5.0)

get_licenses (napalm-device-types LicenseMixin, 4.15) on redfish and bmc,
for NetOrk/netork#1111.

- A BMC that refuses Redfish altogether reports each license it names as
  missing, with its message.
- One that answers but withholds parts reports the license with withholds,
  e.g. DCMS withholding drives and the firmware inventory on an X10 with
  only SFT-OOB-LIC.
- ipmi knows no licenses.

get_device_warnings keeps its two codes until netOrk derives them.
This commit is contained in:
Christian Manivong committed 2026-10-17 11:32:59 +02:00
1 parent e9ad7951cd
commit 5b24675a12
9 files changed
+86 -7

No files matched your search

+15
View File
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.0] - 2026-10-17
### Added
- `get_licenses` (napalm-device-types `LicenseMixin`, 4.15) on `redfish` and `bmc`: the
licenses the BMC asks for, as facts (NetOrk/netork#1111). A BMC that refuses Redfish
altogether reports each license it names as `missing`, with its message. One that answers
but withholds parts reports the license with `withholds`, e.g. DCMS withholding
`drives` and `firmware inventory` on a Supermicro X10 with only SFT-OOB-LIC. `ipmi`
knows no licenses and has no `get_licenses`.
### Changed
- Requires napalm-device-types 4.15 (`LicenseDict.withholds`).
- `get_device_warnings` still reports `bmc_license_required` / `bmc_license_partial` for
callers that do not read `get_licenses` yet; it goes once netOrk derives both.
## [0.4.0] - 2026-10-17
### Added
+4
View File
@@ -44,6 +44,10 @@ reads more than IPMI: CPUs, DIMMs, NICs, power draw, the BIOS version, the SMBIO
with DCMS also its drives and firmware inventory. `optional_args`: those of `redfish`, plus
`ipmi_port` (623).
`get_licenses()` (`redfish`, `bmc`) reports the licenses the BMC asks for as facts:
`{"name": "DCMS", "status": "missing", "withholds": ["drives", "firmware inventory"]}`.
Whether that is worth a warning is the caller's to decide.
Vendor quirks (Supermicro, Dell, HPE, Lenovo) live in one module per vendor, selected from
the service root's `Vendor`/`Oem` data and `Manager.Model`. No separate entry point per
vendor.
+1 -1
View File
@@ -7,5 +7,5 @@ takes the first driver class of this package. ``IpmiDriver`` lives in
from napalm_redfish.driver import RedfishDriver
__version__ = "0.4.0"
__version__ = "0.5.0"
__all__ = ["RedfishDriver"]
+6 -2
View File
@@ -23,7 +23,7 @@ import logging
from typing import Any
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver
from napalm_device_types import HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
from napalm_redfish.driver import BMC_HTTP_FINGERPRINT, BMC_PORT_SPECS, RedfishDriver
from napalm_redfish.ipmi import IpmiDriver
@@ -35,7 +35,7 @@ _JsonDict = dict[str, Any]
_IPMI_PORT = 623
class BmcDriver(HealthMetricsMixin, ManagementControllerDriver):
class BmcDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver):
"""One BMC over Redfish and IPMI: Redfish first, IPMI where Redfish does not answer."""
platform = "bmc"
@@ -137,6 +137,10 @@ class BmcDriver(HealthMetricsMixin, ManagementControllerDriver):
def get_snmp_information(self) -> _JsonDict:
return self._call("get_snmp_information")
def get_licenses(self) -> list[_JsonDict]:
"""Redfish's license facts, also while IPMI reads the BMC: IPMI knows none."""
return self._redfish.get_licenses()
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."""
+24 -2
View File
@@ -30,7 +30,7 @@ from datetime import datetime, timezone
from typing import Any
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver
from napalm_device_types import HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
from napalm_device_types.base import FingerprintRule, PortSpec
from napalm_device_types.models import POWER_ACTIONS
@@ -134,7 +134,7 @@ def _status(item: _JsonDict) -> str:
return item.get("health") or ""
class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
class RedfishDriver(HealthMetricsMixin, LicenseMixin, 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),
@@ -397,6 +397,28 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
return [{"code": "bmc_license_partial",
"meta": {"licenses": " ".join(licenses), "parts": parts}}]
def get_licenses(self) -> list[_JsonDict]:
"""The licenses the BMC asks for and does not have (``status`` "missing").
Refused altogether: one entry per license the service names, with its message.
Answering but withholding parts: one entry per license, with ``withholds``
naming the parts. A BMC that names nothing missing reports nothing: Redfish has
no standard way to list the licenses a BMC does have.
"""
refused = self._license_refused
if refused is not None:
return [{"name": name, "status": "missing", "detail": refused.detail}
for name in refused.licenses.split() or [""]]
self._probe_withheld()
withheld: dict[str, list[str]] = {}
for part in _WITHHELD_ORDER:
if part not in self._withheld:
continue
for name in self._withheld[part].split() or [""]:
withheld.setdefault(name, []).append(part)
return [{"name": name, "status": "missing", "withholds": parts}
for name, parts in withheld.items()]
def _withhold(self, part: str, exc: RedfishFeatureLicenseError) -> None:
LOG.info("%s: %s withheld by the BMC: %s", self.hostname, part, exc)
self._withheld[part] = exc.licenses
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "napalm-redfish"
version = "0.4.0"
version = "0.5.0"
description = "NAPALM driver for server baseboard management controllers over DMTF Redfish"
readme = "README.md"
license = { file = "LICENSE" }
@@ -12,7 +12,7 @@ requires-python = ">=3.10"
dependencies = [
"napalm>=5.0",
# Not on PyPI: install from git before this package (see README).
"napalm-device-types>=4.8",
"napalm-device-types>=4.15",
"redfish>=3.2",
"pyghmi>=1.5",
]
+9
View File
@@ -156,3 +156,12 @@ def test_over_ipmi_the_protocol_netork_talks_is_known_to_be_on(unlicensed):
def test_the_bmc_is_not_reset_over_ipmi(unlicensed):
with pytest.raises(RuntimeError, match="needs Redfish"):
unlicensed.reboot_host()
def test_the_license_facts_are_redfish_s_even_while_ipmi_reads(unlicensed):
assert [lic["name"] for lic in unlicensed.get_licenses()] == ["SUM", "DCMS", "OOB"]
def test_ipmi_knows_no_licenses():
assert not hasattr(ipmi.IpmiDriver, "get_licenses")
assert callable(getattr(BmcDriver, "get_licenses", None))
+14
View File
@@ -511,6 +511,20 @@ def test_unlicensed_bmc_still_opens(server):
}
def test_unlicensed_bmc_names_every_license_it_asks_for_as_missing(server):
licenses = _unlicensed(server).get_licenses()
assert [(lic["name"], lic["status"]) for lic in licenses] == [
("SUM", "missing"), ("DCMS", "missing"), ("OOB", "missing")]
# Refused altogether: nothing is withheld in part.
assert all("withholds" not in lic and "Not licensed" in lic["detail"] for lic in licenses)
def test_a_licensed_bmc_reports_no_license_facts(server):
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
d.open()
assert d.get_licenses() == []
def test_unlicensed_bmc_reports_the_missing_license(server):
(warning,) = _unlicensed(server).get_device_warnings()
assert warning["code"] == "bmc_license_required"
+11
View File
@@ -89,3 +89,14 @@ def test_the_sensors_and_the_event_log_read(driver):
assert len(env["temperature"]) == 19
assert len(env["fans"]) == 4
assert isinstance(driver.get_event_log(limit=5), list)
# ── licenses as facts (#1111) ───────────────────────────────────────────
def test_the_missing_license_is_a_fact_with_what_it_withholds(driver):
assert driver.get_licenses() == [{
"name": "DCMS",
"status": "missing",
"withholds": ["drives", "firmware inventory"],
}]