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

+37 -8
View File
@@ -490,14 +490,43 @@ def test_get_management_protocols_missing(driver, server):
assert protocols["ssh"] == {"enabled": False, "port": None}
def test_unlicensed_bmc_says_so(server):
def _unlicensed(server):
# A Supermicro X10 refuses Redfish entirely until a license is activated.
x10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91"
body = json.loads((x10 / "service_root_403.json").read_text())
server.override("GET", "/redfish/v1/", 403, body)
server.override("GET", "/redfish/v1/", 403,
json.loads((x10 / "service_root_403.json").read_text()))
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
with pytest.raises(ConnectionException) as exc:
d.open()
message = str(exc.value)
assert "license" in message.lower()
assert "SUM DCMS OOB" in message
d.open()
return d
def test_unlicensed_bmc_still_opens(server):
# Reachable, just refusing: the poll goes through and says why instead of
# reporting the BMC offline.
d = _unlicensed(server)
assert d.is_alive() == {"is_alive": True}
assert d.get_facts() == {
"uptime": -1.0, "vendor": "", "model": "", "hostname": "bmc.example.net",
"fqdn": "bmc.example.net", "os_version": "", "serial_number": "", "interface_list": [],
}
def test_unlicensed_bmc_reports_the_missing_license(server):
(warning,) = _unlicensed(server).get_device_warnings()
assert warning["code"] == "bmc_license_required"
assert warning["meta"]["licenses"] == "SUM DCMS OOB"
assert "Not licensed" in warning["meta"]["message"]
# Raw signal only: presentation is netOrk's.
assert set(warning) == {"code", "meta"}
def test_unlicensed_bmc_refuses_everything_else(server):
d = _unlicensed(server)
with pytest.raises(ConnectionException, match="license"):
d.get_interfaces()
with pytest.raises(ConnectionException, match="license"):
d.get_hardware_inventory()
def test_licensed_bmc_has_no_warnings(driver):
assert driver.get_device_warnings() == []