feat: a bmc driver that takes the best of Redfish and IPMI (0.4.0)

bmc (napalm_redfish.bmc:BmcDriver) reads a BMC over Redfish where it
answers. A BMC that refuses Redfish (no license) is read and switched over
IPMI. A getter that fails over Redfish is asked over IPMI where IPMI has it;
IPMI is opened only then. Power control goes over Redfish when it answers,
and a refusal is final. The BMC is reset over Redfish only. Same account
for both; ipmi_port in optional_args.

Discovery suggests bmc for a BMC it finds: the fingerprint rules moved from
RedfishDriver to BmcDriver.
This commit is contained in:
Christian Manivong committed 2026-10-17 10:34:20 +02:00
1 parent adf152ee12
commit e9ad7951cd
9 files changed
+412 -23

No files matched your search

+18
View File
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.4.0] - 2026-10-17
### Added
- `bmc` NAPALM driver (`napalm_redfish.bmc:BmcDriver`): the best of Redfish and IPMI for
one BMC, chosen by itself. It reads over Redfish where the BMC answers. A BMC that refuses
Redfish altogether (no license) is read and switched over IPMI, and a getter that fails
over Redfish is asked over IPMI where IPMI has it; IPMI is opened only then. Power control
goes over Redfish when it answers. A refusal by the BMC is not retried over IPMI. The BMC
itself is reset over Redfish only. `get_device_warnings` adds `ipmi_fallback` to
`bmc_license_required` while it reads over IPMI. `get_management_protocols` over IPMI
reports IPMI over LAN on, the one thing it knows. Same account for both; `ipmi_port`
(623) in `optional_args`.
### Changed
- Discovery suggests `bmc` for a BMC it finds: the fingerprint rules and the probed
`/redfish/v1/` path moved from `RedfishDriver` to `BmcDriver`
(`BMC_PORT_SPECS`, `BMC_HTTP_FINGERPRINT`). `redfish` stays for choosing by hand.
## [0.3.1] - 2026-10-17 ## [0.3.1] - 2026-10-17
First run against real hardware over Redfish: a Supermicro X10DRL-i (BMC 3.91) with the First run against real hardware over Redfish: a Supermicro X10DRL-i (BMC 3.91) with the
+7
View File
@@ -36,6 +36,13 @@ not depend on it. Finding IPMI over LAN enabled is itself worth reporting, see
|---|---|---| |---|---|---|
| `redfish` | HTTPS, Redfish REST (session token or basic auth) | Read, plus power control and BMC reset | | `redfish` | HTTPS, Redfish REST (session token or basic auth) | Read, plus power control and BMC reset |
| `ipmi` | IPMI 2.0 over LAN (UDP 623) via `pyghmi` | Read, plus power control (no graceful restart, no power cycle); no BMC reset | | `ipmi` | IPMI 2.0 over LAN (UDP 623) via `pyghmi` | Read, plus power control (no graceful restart, no power cycle); no BMC reset |
| `bmc` | Redfish first, IPMI where Redfish does not answer | Everything `redfish` does; a BMC that refuses Redfish (no license) is read and switched over IPMI, and a getter that fails over Redfish is asked over IPMI |
`bmc` is the one to choose when unsure: it follows the BMC's license as it changes. A
Supermicro X10 without a license is read over IPMI, with SFT-OOB-LIC over Redfish (which then
reads more than IPMI: CPUs, DIMMs, NICs, power draw, the BIOS version, the SMBIOS UUID), and
with DCMS also its drives and firmware inventory. `optional_args`: those of `redfish`, plus
`ipmi_port` (623).
Vendor quirks (Supermicro, Dell, HPE, Lenovo) live in one module per vendor, selected from 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 the service root's `Vendor`/`Oem` data and `Manager.Model`. No separate entry point per
+14
View File
@@ -45,3 +45,17 @@ Via `pyghmi`: device ID and FRU data for `get_facts`, sensor data repository for
via ACPI, `ForceOff` → power down, `ForceRestart` → hard reset). IPMI has no graceful via ACPI, `ForceOff` → power down, `ForceRestart` → hard reset). IPMI has no graceful
restart, and pyghmi offers no power cycle, so `get_power_state` never offers those two. restart, and pyghmi offers no power cycle, so `get_power_state` never offers those two.
No BMC reset over IPMI. No BMC reset over IPMI.
## Redfish and IPMI together (`bmc` entry point)
`BmcDriver` holds a `RedfishDriver` and an `IpmiDriver` for the same BMC and account:
| Situation | Used |
|---|---|
| Redfish answers | Redfish for every getter; IPMI is not opened |
| A getter fails over Redfish (`RedfishError`, `ConnectionException`) | that getter over IPMI, if IPMI has it (Redfish-only getters re-raise) |
| The service root refuses Redfish for want of a license, or Redfish does not answer | IPMI for everything; Redfish-only getters raise `NotImplementedError` |
| `set_power_state` | Redfish when it answers; a refusal (`RuntimeError`) is final |
| `reboot_host` | Redfish only (`Manager.Reset`) |
| `get_management_protocols` over IPMI | `{"ipmi": {"enabled": True, "port": 623}}` |
+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 from napalm_redfish.driver import RedfishDriver
__version__ = "0.3.1" __version__ = "0.4.0"
__all__ = ["RedfishDriver"] __all__ = ["RedfishDriver"]
+176
View File
@@ -0,0 +1,176 @@
"""NAPALM driver for a BMC that takes the best of Redfish and IPMI, chosen by itself.
``redfish`` and ``ipmi`` make the operator choose, and the right choice changes with the
BMC's license: a Supermicro X10 refuses Redfish without one, answers most of it with
SFT-OOB-LIC, and all of it with DCMS. This driver asks Redfish first and IPMI where Redfish
does not answer:
* A BMC that refuses Redfish altogether (no license) is read over IPMI entirely.
* A getter that fails over Redfish (an error, a resource the BMC withholds) is asked over
IPMI, where IPMI has it. IPMI is opened only then.
* Power control goes over Redfish when it answers, else over IPMI. A refusal by the BMC is
an answer, not a failure, and is not retried over IPMI.
* The BMC itself is reset over Redfish only (``Manager.Reset``); IPMI's cold reset is not
offered, as in the ``ipmi`` driver.
Both protocols use the same account. ``optional_args``: everything ``RedfishDriver``
takes, plus ``ipmi_port`` (623).
"""
from __future__ import annotations
import logging
from typing import Any
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HealthMetricsMixin, ManagementControllerDriver
from napalm_redfish.driver import BMC_HTTP_FINGERPRINT, BMC_PORT_SPECS, RedfishDriver
from napalm_redfish.ipmi import IpmiDriver
from napalm_redfish.transport import RedfishError
LOG = logging.getLogger(__name__)
_JsonDict = dict[str, Any]
_IPMI_PORT = 623
class BmcDriver(HealthMetricsMixin, ManagementControllerDriver):
"""One BMC over Redfish and IPMI: Redfish first, IPMI where Redfish does not answer."""
platform = "bmc"
DRIVER_NAME = "bmc"
default_port = 443
#: Discovery suggests this driver for a BMC it finds, not ``redfish``.
PORT_SPECS = BMC_PORT_SPECS
HTTP_FINGERPRINT = BMC_HTTP_FINGERPRINT
#: A BMC restart takes a few minutes before Redfish answers again.
REBOOT_SETTLE_SECONDS = RedfishDriver.REBOOT_SETTLE_SECONDS
def __init__(
self,
hostname: str,
username: str,
password: str,
timeout: int = 60,
optional_args: _JsonDict | None = None,
) -> None:
self.hostname = hostname
self.username = username
self.password = password
self.timeout = timeout
self.optional_args: _JsonDict = dict(optional_args or {})
ipmi_port = int(self.optional_args.pop("ipmi_port", None) or _IPMI_PORT)
self.ipmi_port = ipmi_port
self._redfish = RedfishDriver(hostname, username, password, timeout, self.optional_args)
self._ipmi = IpmiDriver(hostname, username, password, timeout, {"port": ipmi_port})
self._redfish_ok = False
self._ipmi_open = False
# --- connection ----------------------------------------------------------------------
def open(self) -> None:
"""Connect over Redfish; a BMC that refuses it or does not answer it is opened over
IPMI instead. Fails only when neither answers."""
try:
self._redfish.open()
except ConnectionException as exc:
LOG.info("%s: no Redfish (%s); reading it over IPMI", self.hostname, exc)
self._redfish_ok = False
else:
self._redfish_ok = self._redfish._license_refused is None
if not self._redfish_ok:
self._ipmi_conn()
def close(self) -> None:
self._redfish.close()
if self._ipmi_open:
self._ipmi.close()
self._ipmi_open = False
def is_alive(self) -> _JsonDict:
if self._redfish_ok:
return self._redfish.is_alive()
return self._ipmi.is_alive()
def _ipmi_conn(self) -> IpmiDriver:
if not self._ipmi_open:
self._ipmi.open()
self._ipmi_open = True
return self._ipmi
def _call(self, name: str, *args: Any) -> Any:
"""``name`` over Redfish; over IPMI when Redfish is out or fails and IPMI has it."""
has_ipmi = callable(getattr(IpmiDriver, name, None))
if self._redfish_ok:
try:
return getattr(self._redfish, name)(*args)
except (RedfishError, ConnectionException) as exc:
if not has_ipmi:
raise
LOG.info("%s: %s failed over Redfish (%s); asking IPMI", self.hostname, name,
exc)
if not has_ipmi:
raise NotImplementedError(f"{self.hostname}: {name} needs Redfish")
return getattr(self._ipmi_conn(), name)(*args)
# --- NAPALM getters ------------------------------------------------------------------
def get_facts(self) -> _JsonDict:
return self._call("get_facts")
def get_interfaces(self) -> _JsonDict:
return self._call("get_interfaces")
def get_interfaces_ip(self) -> _JsonDict:
return self._call("get_interfaces_ip")
def get_environment(self) -> _JsonDict:
return self._call("get_environment")
def get_users(self) -> _JsonDict:
return self._call("get_users")
def get_ntp_servers(self) -> _JsonDict:
return self._call("get_ntp_servers")
def get_snmp_information(self) -> _JsonDict:
return self._call("get_snmp_information")
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."""
warnings = self._redfish.get_device_warnings()
if self._redfish_ok:
return warnings
return [{**w, "meta": {**w.get("meta", {}), "ipmi_fallback": True}} for w in warnings]
# --- role methods --------------------------------------------------------------------
def get_power_state(self) -> _JsonDict:
return self._call("get_power_state")
def set_power_state(self, action: str) -> None:
self._call("set_power_state", action)
def reboot_host(self) -> None:
"""Restart the BMC (``Manager.Reset``), never the server. Needs Redfish."""
if not self._redfish_ok:
raise RuntimeError(f"{self.hostname}: resetting the BMC needs Redfish")
self._redfish.reboot_host()
def get_hardware_inventory(self) -> _JsonDict:
return self._call("get_hardware_inventory")
def get_event_log(self, limit: int = 500) -> list[_JsonDict]:
return self._call("get_event_log", limit)
def get_firmware_inventory(self) -> list[_JsonDict]:
return self._call("get_firmware_inventory")
def get_management_protocols(self) -> _JsonDict:
"""Over Redfish, what the BMC reports. Over IPMI only one thing is known: IPMI over
LAN is on, since this driver is talking it."""
if self._redfish_ok:
return self._redfish.get_management_protocols()
return {"ipmi": {"enabled": True, "port": self.ipmi_port}}
+21 -16
View File
@@ -94,6 +94,25 @@ _WITHHELD_ORDER = ("drives", "firmware inventory")
#: system "Super Server"; the board is the system's PartNumber and the chassis model. #: system "Super Server"; the board is the system's PartNumber and the chassis model.
_PLACEHOLDER_MODELS = frozenset({"super server", "default string", "to be filled by o.e.m."}) _PLACEHOLDER_MODELS = frozenset({"super server", "default string", "to be filled by o.e.m."})
#: What discovery recognises a BMC by; the ``bmc`` driver carries them (0.4.0), so a BMC
#: found is suggested with the driver that works with and without a license.
#: Every Redfish service answers ``GET /redfish/v1/`` without credentials. The port
#: itself is worth nothing: every switch with a web UI answers on 443.
BMC_PORT_SPECS = [PortSpec("https", 443, ("/redfish/v1/",), weight=0.0)]
#: Rules in one group are alternatives and count once towards the maximum
#: (napalm-device-types' ``rules_max_weight``).
BMC_HTTP_FINGERPRINT = [
# The service root, or the error body a service refuses it with (an unlicensed
# Supermicro X10 answers 403 with a Redfish error).
FingerprintRule("redfishversion", 10.0, group="redfish_answer"),
FingerprintRule("@message.extendedinfo", 8.0, group="redfish_answer"),
FingerprintRule("#serviceroot.", 5.0),
# Landing pages of the BMC web UIs: Supermicro X9-X11 (ATEN firmware), iDRAC, iLO.
FingerprintRule("aten international", 6.0, group="landing_page"),
FingerprintRule("idrac", 6.0, group="landing_page"),
FingerprintRule("integrated lights-out", 6.0, group="landing_page"),
]
def _timestamp(created: str | None) -> float | None: def _timestamp(created: str | None) -> float | None:
"""Unix time of a Redfish ``Created`` value; a time without offset counts as UTC.""" """Unix time of a Redfish ``Created`` value; a time without offset counts as UTC."""
@@ -124,22 +143,8 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
platform = "redfish" platform = "redfish"
DRIVER_NAME = "redfish" DRIVER_NAME = "redfish"
#: Every Redfish service answers ``GET /redfish/v1/`` without credentials. The port # No fingerprint of its own: discovery suggests ``bmc`` (BMC_PORT_SPECS and
#: itself is worth nothing: every switch with a web UI answers on 443. # BMC_HTTP_FINGERPRINT below), which also reads a BMC that refuses Redfish.
PORT_SPECS = [PortSpec("https", 443, ("/redfish/v1/",), weight=0.0)]
#: Rules in one group are alternatives and count once towards the maximum
#: (napalm-device-types' ``rules_max_weight``).
HTTP_FINGERPRINT = [
# The service root, or the error body a service refuses it with (an unlicensed
# Supermicro X10 answers 403 with a Redfish error).
FingerprintRule("redfishversion", 10.0, group="redfish_answer"),
FingerprintRule("@message.extendedinfo", 8.0, group="redfish_answer"),
FingerprintRule("#serviceroot.", 5.0),
# Landing pages of the BMC web UIs: Supermicro X9-X11 (ATEN firmware), iDRAC, iLO.
FingerprintRule("aten international", 6.0, group="landing_page"),
FingerprintRule("idrac", 6.0, group="landing_page"),
FingerprintRule("integrated lights-out", 6.0, group="landing_page"),
]
#: A BMC restart takes a few minutes before Redfish answers again. #: A BMC restart takes a few minutes before Redfish answers again.
REBOOT_SETTLE_SECONDS = 180 REBOOT_SETTLE_SECONDS = 180
+2 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "napalm-redfish" name = "napalm-redfish"
version = "0.3.1" version = "0.4.0"
description = "NAPALM driver for server baseboard management controllers over DMTF Redfish" description = "NAPALM driver for server baseboard management controllers over DMTF Redfish"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
@@ -32,6 +32,7 @@ test = [
[project.entry-points."napalm.drivers"] [project.entry-points."napalm.drivers"]
redfish = "napalm_redfish:RedfishDriver" redfish = "napalm_redfish:RedfishDriver"
ipmi = "napalm_redfish.ipmi:IpmiDriver" ipmi = "napalm_redfish.ipmi:IpmiDriver"
bmc = "napalm_redfish.bmc:BmcDriver"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["napalm_redfish*"] include = ["napalm_redfish*"]
+158
View File
@@ -0,0 +1,158 @@
"""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()
+15 -5
View File
@@ -8,23 +8,33 @@ import json
from pathlib import Path from pathlib import Path
from napalm_redfish import RedfishDriver from napalm_redfish import RedfishDriver
from napalm_redfish.bmc import BmcDriver
from napalm_redfish.ipmi import IpmiDriver
X10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91" X10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91"
def _score(text: str) -> float: def _score(text: str) -> float:
text = " ".join(text.lower().split()) text = " ".join(text.lower().split())
return sum(rule.weight for rule in RedfishDriver.HTTP_FINGERPRINT if rule.pattern in text) return sum(rule.weight for rule in BmcDriver.HTTP_FINGERPRINT if rule.pattern in text)
def test_discovery_suggests_the_bmc_driver_only():
# bmc works with and without a license; two drivers on the same rules would tie.
assert BmcDriver.HTTP_FINGERPRINT
assert RedfishDriver.HTTP_FINGERPRINT == []
assert not RedfishDriver.PORT_SPECS
assert IpmiDriver.HTTP_FINGERPRINT == []
def test_an_open_https_port_proves_nothing(): def test_an_open_https_port_proves_nothing():
# Every switch with a web UI answers on 443; the port alone made six of # Every switch with a web UI answers on 443; the port alone made six of
# them look like BMCs. # them look like BMCs.
assert [spec.weight for spec in RedfishDriver.PORT_SPECS if spec.port == 443] == [0.0] assert [spec.weight for spec in BmcDriver.PORT_SPECS if spec.port == 443] == [0.0]
def test_service_root_is_still_probed(): def test_service_root_is_still_probed():
assert any("/redfish/v1/" in spec.paths for spec in RedfishDriver.PORT_SPECS) assert any("/redfish/v1/" in spec.paths for spec in BmcDriver.PORT_SPECS)
def test_supermicro_landing_page_is_recognised(): def test_supermicro_landing_page_is_recognised():
@@ -53,11 +63,11 @@ def test_alternatives_count_once_towards_the_maximum():
# BMC's SNMP sysDescr (5 of 18) beat the Supermicro landing page (6 of 41). # BMC's SNMP sysDescr (5 of 18) beat the Supermicro landing page (6 of 41).
from napalm_device_types import rules_max_weight from napalm_device_types import rules_max_weight
assert rules_max_weight(RedfishDriver.HTTP_FINGERPRINT) == 21.0 assert rules_max_weight(BmcDriver.HTTP_FINGERPRINT) == 21.0
def test_supermicro_landing_page_alone_outranks_a_linux_sysdescr(): def test_supermicro_landing_page_alone_outranks_a_linux_sysdescr():
from napalm_device_types import rules_max_weight from napalm_device_types import rules_max_weight
page = _score((X10 / "landing_page.html").read_text()) page = _score((X10 / "landing_page.html").read_text())
assert page / rules_max_weight(RedfishDriver.HTTP_FINGERPRINT) > 5.0 / 18.0 assert page / rules_max_weight(BmcDriver.HTTP_FINGERPRINT) > 5.0 / 18.0