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

+6
View File
@@ -4,3 +4,9 @@ without a Supermicro license. Requests were made without credentials.
service_root_403.json GET /redfish/v1/ -> HTTP 403, Content-Type application/json
landing_page.html GET / -> HTTP 200, the BMC web login page (trimmed to
the head and the login form action)
ipmi/ Read over IPMI 2.0 (pyghmi 1.6, lanplus) with a BMC account, 2026-10-13. One file per
pyghmi call: power (get_power), net_configuration (get_net_configuration),
firmware (get_firmware), inventory (get_inventory), sensors (get_sensor_data,
each SensorReading's attributes), health (get_health), event_log (get_event_log).
Serial numbers, the system UUID, MAC addresses and IPs are replaced by placeholders.
@@ -0,0 +1,16 @@
[
{
"record_id": 1,
"timecode": 1790788051,
"component_id": "32.201.0",
"component": "PS2 Status",
"deassertion": true,
"component_type_id": 8,
"event_id": "111.1",
"component_type": "Power Supply",
"event": "Recovered",
"severity": 0,
"oem_handler": null,
"timestamp": "2026-09-30T15:07:31"
}
]
@@ -0,0 +1,8 @@
[
[
"BMC Version",
{
"version": "3.91"
}
]
]
@@ -0,0 +1,4 @@
{
"badreadings": [],
"health": 0
}
@@ -0,0 +1,50 @@
[
[
"System",
{
"Board manufacture date": null,
"Board manufacturer": "Supermicro",
"Board product name": "X10DRL-i",
"Board serial number": "ZM000S000001",
"Board model": "X10DRL-i",
"Board FRU Id": null,
"board_extra": [],
"Manufacturer": "Supermicro",
"Product name": "X10DRL-i",
"Model": "X10DRL-i",
"Hardware Version": null,
"Serial Number": "ZM000S000001",
"Asset Number": null,
"FRU ID": null,
"product_extra": [],
"firmware_version": "3.91",
"Device ID": 32,
"Device Revision": 1,
"Manufacturer ID": 10876,
"Product ID": 2097,
"oem_parser": null,
"UUID": "00000000-534D-C40C-0000-000000000001"
}
],
[
"BMC FRU",
{
"Board manufacture date": null,
"Board manufacturer": "Supermicro",
"Board product name": "X10DRL-i",
"Board serial number": "ZM000S000001",
"Board model": "X10DRL-i",
"Board FRU Id": null,
"board_extra": [],
"Manufacturer": "Supermicro",
"Product name": "X10DRL-i",
"Model": "X10DRL-i",
"Hardware Version": null,
"Serial Number": "ZM000S000001",
"Asset Number": null,
"FRU ID": null,
"product_extra": [],
"oem_parser": null
}
]
]
@@ -0,0 +1,10 @@
{
"ipv4_address": "192.0.2.150/24",
"ipv4_configuration": "DHCP",
"mac_address": "0c:c4:7a:00:00:01",
"ipv4_gateway": "192.0.2.1",
"ipv4_backup_gateway": null,
"ipv4_gateway_mac": "02:00:00:00:00:01",
"ipv4_backup_gateway_mac": null,
"vlan_id": "off"
}
@@ -0,0 +1,3 @@
{
"powerstate": "on"
}
@@ -0,0 +1,498 @@
[
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 34.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "CPU1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 37.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "CPU2 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 28.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "PCH Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 18.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "System Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 38.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "Peripheral Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 29.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "Vcpu1VRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 33.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "Vcpu2VRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 24.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "VmemABVRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 22.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "VmemCDVRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 35.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "VmemEFVRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 30.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "VmemGHVRM Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 25.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P1-DIMMA1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 24.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P1-DIMMB1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 25.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P1-DIMMC1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 22.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P1-DIMMD1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 26.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P2-DIMME1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 26.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P2-DIMMF1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 27.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P2-DIMMG1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Temperature",
"value": 27.0,
"imprecision": 0.5,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "°C",
"name": "P2-DIMMH1 Temp"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Fan",
"value": 6300.0,
"imprecision": 50.0,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "RPM",
"name": "FAN2"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Fan",
"value": 6900.0,
"imprecision": 50.0,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "RPM",
"name": "FAN3"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Fan",
"value": 6400.0,
"imprecision": 50.0,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "RPM",
"name": "FAN4"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Fan",
"value": 5100.0,
"imprecision": 50.0,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "RPM",
"name": "FANB"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 12.0,
"imprecision": 0.03149999999999942,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "12V"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 4.922000000000001,
"imprecision": 0.013000000000000789,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "5VCC"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 3.35,
"imprecision": 0.008500000000000174,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "3.3VCC"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 2.69,
"imprecision": 0.0129999999999999,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "VBAT"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.818,
"imprecision": 0.0044999999999999485,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "Vcpu1"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.818,
"imprecision": 0.0044999999999999485,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "Vcpu2"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.2000000000000002,
"imprecision": 0.0045000000000001705,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "VDIMMAB"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.2000000000000002,
"imprecision": 0.0045000000000001705,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "VDIMMCD"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.2000000000000002,
"imprecision": 0.0045000000000001705,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "VDIMMEF"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.2000000000000002,
"imprecision": 0.0045000000000001705,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "VDIMMGH"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 4.896,
"imprecision": 0.0129999999999999,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "5VSB"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 3.197,
"imprecision": 0.008500000000000174,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "3.3VSB"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.5,
"imprecision": 0.0044999999999999485,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "1.5V PCH"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.209,
"imprecision": 0.0044999999999999485,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "1.2V BMC"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Voltage",
"value": 1.05,
"imprecision": 0.0044999999999999485,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "V",
"name": "1.05V PCH"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Physical Security",
"value": null,
"imprecision": null,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "",
"name": "Chassis Intru"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Power Supply",
"value": null,
"imprecision": null,
"states": [
"Present"
],
"state_ids": [
552704
],
"unavailable": 0,
"units": "",
"name": "PS1 Status"
},
{
"broken_sensor_ids": {},
"health": 0,
"type": "Power Supply",
"value": null,
"imprecision": null,
"states": [],
"state_ids": [],
"unavailable": 0,
"units": "",
"name": "PS2 Status"
}
]
+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() == []
+248
View File
@@ -0,0 +1,248 @@
"""IpmiDriver against recorded IPMI answers of a Supermicro X10DRL-i (BMC 3.91).
The board refuses Redfish without a license; IPMI answers without one. The driver
is read-only: it neither switches the server's power nor resets the BMC.
"""
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from napalm.base.exceptions import ConnectionException
from napalm_device_types import ManagementControllerDriver, models
from napalm_device_types.roles import primary_role_of
import napalm_redfish.ipmi as ipmi
from napalm_redfish.ipmi import IpmiDriver
X10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91" / "ipmi"
def fixture(name):
return json.loads((X10 / f"{name}.json").read_text())
class FakeCommand:
"""pyghmi's Command, answering from the recorded files."""
instances: list = []
def __init__(self, bmc=None, userid=None, password=None, port=623, keepalive=True, **kw):
self.args = {"bmc": bmc, "userid": userid, "password": password, "port": port}
self.logged_out = False
self.ipmi_session = SimpleNamespace(logout=self._logout)
FakeCommand.instances.append(self)
def _logout(self):
self.logged_out = True
def get_power(self):
return fixture("power")
def get_net_configuration(self):
return fixture("net_configuration")
def get_firmware(self):
return iter(tuple(item) for item in fixture("firmware"))
def get_inventory(self):
return iter(tuple(item) for item in fixture("inventory"))
def get_sensor_data(self):
return iter(SimpleNamespace(**s) for s in fixture("sensors"))
def get_event_log(self):
return iter(fixture("event_log"))
@pytest.fixture(autouse=True)
def fake_pyghmi(monkeypatch):
FakeCommand.instances = []
monkeypatch.setattr(ipmi, "Command", FakeCommand)
@pytest.fixture
def driver():
d = IpmiDriver("192.0.2.150", "netork", "s3cret")
d.open()
yield d
d.close()
def assert_shape(value, typed_dict):
assert isinstance(value, dict)
assert set(value) == set(typed_dict.__annotations__), typed_dict.__name__
# --- class and connection ------------------------------------------------------------------
def test_role_and_read_only():
assert issubclass(IpmiDriver, ManagementControllerDriver)
assert primary_role_of(IpmiDriver) == "management_controller"
assert IpmiDriver.USES_SSH is False
assert IpmiDriver.DRIVER_NAME == "ipmi"
# Read-only: no power control, no BMC reset over IPMI.
assert not hasattr(IpmiDriver, "set_power_state")
assert not hasattr(IpmiDriver, "reboot_host")
def test_entry_point():
from importlib.metadata import entry_points
(ep,) = [e for e in entry_points(group="napalm.drivers") if e.name == "ipmi"]
assert ep.load() is IpmiDriver
def test_open_logs_in_on_the_ipmi_port_and_close_logs_out():
d = IpmiDriver("192.0.2.150", "netork", "s3cret", optional_args={"port": 6230})
d.open()
(cmd,) = FakeCommand.instances
assert cmd.args == {"bmc": "192.0.2.150", "userid": "netork", "password": "s3cret",
"port": 6230}
assert d.is_alive() == {"is_alive": True}
d.close()
assert cmd.logged_out
assert d.is_alive() == {"is_alive": False}
def test_default_port_is_623():
IpmiDriver("192.0.2.150", "u", "p").open()
assert FakeCommand.instances[0].args["port"] == 623
def test_open_failure_is_connection_exception(monkeypatch):
def refuse(**kwargs):
raise RuntimeError("Incorrect password provided")
monkeypatch.setattr(ipmi, "Command", refuse)
with pytest.raises(ConnectionException, match="Incorrect password"):
IpmiDriver("192.0.2.150", "netork", "wrong").open()
# --- NAPALM getters ------------------------------------------------------------------------
def test_get_facts(driver):
facts = driver.get_facts()
assert facts == {
"uptime": -1.0,
"vendor": "Supermicro",
"model": "X10DRL-i",
"hostname": "192.0.2.150",
"fqdn": "192.0.2.150",
"os_version": "3.91",
"serial_number": "ZM000S000001",
"interface_list": ["bmc"],
}
def test_get_interfaces(driver):
assert driver.get_interfaces() == {
"bmc": {
"is_up": True,
"is_enabled": True,
"description": "BMC LAN (DHCP)",
"last_flapped": -1.0,
"speed": -1.0,
"mtu": -1,
"mac_address": "0c:c4:7a:00:00:01",
}
}
def test_get_interfaces_ip(driver):
assert driver.get_interfaces_ip() == {"bmc": {"ipv4": {"192.0.2.150": {"prefix_length": 24}}}}
def test_get_environment(driver):
env = driver.get_environment()
assert set(env) == {"fans", "temperature", "power", "cpu", "memory"}
assert env["cpu"] == {} and env["memory"] == {}
assert len(env["temperature"]) == 19
assert env["temperature"]["CPU1 Temp"] == {"temperature": 34.0, "is_alert": False,
"is_critical": False}
assert env["fans"] == {name: {"status": True} for name in ("FAN2", "FAN3", "FAN4", "FANB")}
# Only supplies that report themselves present.
assert env["power"] == {"PS1 Status": {"status": True, "capacity": -1.0, "output": -1.0}}
def test_sensor_health_flags(driver, monkeypatch):
readings = [
{"name": "Hot", "type": "Temperature", "value": 91.0, "units": "°C", "health": 1,
"states": [], "unavailable": 0},
{"name": "Melting", "type": "Temperature", "value": 105.0, "units": "°C", "health": 2,
"states": [], "unavailable": 0},
{"name": "Gone", "type": "Temperature", "value": None, "units": "°C", "health": 0,
"states": [], "unavailable": 1},
{"name": "FAN1", "type": "Fan", "value": 0.0, "units": "RPM", "health": 2,
"states": [], "unavailable": 0},
]
monkeypatch.setattr(FakeCommand, "get_sensor_data",
lambda self: iter(SimpleNamespace(**r) for r in readings))
env = driver.get_environment()
assert env["temperature"] == {
"Hot": {"temperature": 91.0, "is_alert": True, "is_critical": False},
"Melting": {"temperature": 105.0, "is_alert": True, "is_critical": True},
}
assert env["fans"] == {"FAN1": {"status": False}}
# --- role methods --------------------------------------------------------------------------
def test_get_power_state(driver):
state = driver.get_power_state()
assert_shape(state, models.PowerStateDict)
assert state == {"state": "On", "actions": []}
def test_get_hardware_inventory(driver):
inv = driver.get_hardware_inventory()
assert_shape(inv, models.HardwareInventoryDict)
assert inv["manufacturer"] == "Supermicro"
assert inv["model"] == "X10DRL-i"
assert inv["serial_number"] == "ZM000S000001"
assert inv["system_uuid"] == "00000000-534d-c40c-0000-000000000001"
assert inv["power_supplies"] == [
{"slot": "PS1", "model": "", "serial_number": "", "capacity_watts": None, "status": "OK"},
{"slot": "PS2", "model": "", "serial_number": "", "capacity_watts": None,
"status": "Absent"},
]
assert inv["cpus"] == [] and inv["memory"] == [] and inv["drives"] == [] and inv["nics"] == []
def test_get_event_log(driver):
(entry,) = driver.get_event_log()
assert_shape(entry, models.EventLogEntryDict)
assert entry == {"id": "1", "timestamp": 1790788051.0, "severity": "OK",
"message": "PS2 Status: Recovered"}
def test_event_log_newest_first_and_limited(driver, monkeypatch):
sel = [
{"record_id": i, "timecode": 1000 + i, "component": "FAN1", "event": "Lower critical",
"severity": 2, "deassertion": False}
for i in range(1, 6)
]
monkeypatch.setattr(FakeCommand, "get_event_log", lambda self: iter(sel))
entries = driver.get_event_log(limit=2)
assert [e["id"] for e in entries] == ["5", "4"]
assert entries[0]["severity"] == "Critical"
assert entries[0]["message"] == "FAN1: Lower critical"
def test_get_firmware_inventory(driver):
(fw,) = driver.get_firmware_inventory()
assert_shape(fw, models.FirmwareDict)
assert fw == {"name": "BMC", "version": "3.91", "updateable": None}
def test_napalm_still_finds_redfish_by_name():
# NAPALM takes the first driver class of the package napalm_<name>.
import napalm
from napalm_redfish import RedfishDriver
assert napalm.get_network_driver("redfish") is RedfishDriver