"""BmcDriver: the best of Redfish and IPMI for one BMC, chosen by itself. Redfish where the BMC answers it; IPMI where it does not. A BMC that refuses Redfish altogether (no license) is read over IPMI entirely; a getter that fails over Redfish is asked over IPMI, where IPMI has it. Both use the same account. Against the recordings of the Supermicro X10DRL-i: its Redfish tree with the OOB license, its unlicensed service root, and its IPMI answers. """ import json from pathlib import Path import pytest from napalm_device_types import ManagementControllerDriver from napalm_device_types.roles import primary_role_of import napalm_redfish.ipmi as ipmi from napalm_redfish.bmc import BmcDriver from tests.conftest import BASE_URL, FIXTURES, MockupServer from tests.test_ipmi import FakeCommand X10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91" MOCKUP = "supermicro-x10drl-i-3.91/redfish" HOST = BASE_URL.removeprefix("https://") @pytest.fixture(autouse=True) def fake_pyghmi(monkeypatch): FakeCommand.instances = [] monkeypatch.setattr(ipmi, "Command", FakeCommand) @pytest.fixture def x10(http): server = MockupServer(http, MOCKUP) for path, status in json.loads((FIXTURES / MOCKUP / "STATUS.json").read_text()).items(): body = json.loads( (FIXTURES / MOCKUP / path[len("/redfish/v1/") :] / "index.json").read_text() ) server.override("GET", path, status, body) return server def _open(**optional): d = BmcDriver(HOST, "netork", "s3cret", optional_args={"verify_ssl": False, **optional}) d.open() return d @pytest.fixture def unlicensed(x10): x10.override( "GET", "/redfish/v1/", 403, json.loads((X10 / "service_root_403.json").read_text()) ) d = _open() yield d d.close() # ── the class ─────────────────────────────────────────────────────────── def test_it_is_one_bmc_driver_with_power_and_bmc_reset(): assert issubclass(BmcDriver, ManagementControllerDriver) assert primary_role_of(BmcDriver) == "management_controller" assert BmcDriver.DRIVER_NAME == "bmc" assert BmcDriver.default_port == 443 assert callable(getattr(BmcDriver, "set_power_state", None)) assert callable(getattr(BmcDriver, "reboot_host", None)) def test_entry_point(): from importlib.metadata import entry_points (ep,) = [e for e in entry_points(group="napalm.drivers") if e.name == "bmc"] assert ep.load() is BmcDriver # ── a BMC that answers Redfish ────────────────────────────────────────── def test_redfish_is_used_and_ipmi_is_not_even_opened(x10): d = _open() try: assert d.get_facts()["model"] == "X10DRL-i" assert len(d.get_hardware_inventory()["cpus"]) == 2 assert d.get_power_state()["actions"] == [ "GracefulShutdown", "ForceOff", "GracefulRestart", "ForceRestart", ] finally: d.close() assert FakeCommand.instances == [] def test_a_getter_that_fails_over_redfish_is_asked_over_ipmi(x10): x10.override("GET", "/redfish/v1/Chassis/1/Thermal", 500, {"error": {"message": "busy"}}) d = _open() try: env = d.get_environment() finally: d.close() # From the IPMI sensor records. assert len(env["temperature"]) == 19 assert len(FakeCommand.instances) == 1 def test_the_ipmi_port_is_its_own(x10): x10.override("GET", "/redfish/v1/Chassis/1/Thermal", 500, {"error": {"message": "busy"}}) d = _open(ipmi_port=6230) try: d.get_environment() finally: d.close() (cmd,) = FakeCommand.instances assert cmd.args == {"bmc": HOST, "userid": "netork", "password": "s3cret", "port": 6230} assert cmd.logged_out def test_the_partial_license_warning_passes_through(x10): d = _open() try: assert d.get_device_warnings() == [ { "code": "bmc_license_partial", "meta": {"licenses": "DCMS", "parts": ["drives", "firmware inventory"]}, } ] finally: d.close() # ── a BMC that refuses Redfish ────────────────────────────────────────── def test_an_unlicensed_bmc_is_read_over_ipmi(unlicensed): facts = unlicensed.get_facts() assert facts["vendor"] == "Supermicro" assert facts["model"] == "X10DRL-i" assert facts["os_version"] == "3.91" assert unlicensed.get_power_state() == { "state": "On", "actions": ["GracefulShutdown", "ForceOff", "ForceRestart"], } def test_an_unlicensed_bmc_switches_power_over_ipmi(unlicensed): unlicensed.set_power_state("GracefulShutdown") assert FakeCommand.instances[0].power_requests == [("shutdown", False)] def test_an_unlicensed_bmc_says_ipmi_reads_it_meanwhile(unlicensed): (warning,) = unlicensed.get_device_warnings() assert warning["code"] == "bmc_license_required" assert warning["meta"]["licenses"] == "SUM DCMS OOB" assert warning["meta"]["ipmi_fallback"] is True def test_over_ipmi_the_protocol_netork_talks_is_known_to_be_on(unlicensed): assert unlicensed.get_management_protocols() == {"ipmi": {"enabled": True, "port": 623}} 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))