Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM driver uses (E, F, I, UP, B at line length 100, Python 3.10). Mapping and Iterable come from collections.abc, imports are sorted. What ruff cannot fix comes in the next commit.
178 lines
5.8 KiB
Python
178 lines
5.8 KiB
Python
"""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))
|