Files
napalm-redfish/napalm_redfish/driver.py
T
Christian Manivong 09029ca0a4 style: ruff format and the lint rules of the driver standard
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.
2026-10-23 08:39:45 +02:00

747 lines
30 KiB
Python

"""NAPALM driver for server BMCs over DMTF Redfish.
The device is the BMC. NAPALM's getters describe the BMC (its ports, users, services);
the ``ManagementControllerDriver`` methods describe and control the server it manages.
``reboot_host`` restarts the BMC (``Manager.Reset``), never the server.
Optional args
-------------
port : int
HTTPS port (default 443).
verify_ssl : bool or str
``True`` (default) verifies the certificate against the CA bundle of ``requests``,
a path verifies against that CA file, ``False`` turns verification off (self-signed
BMC certificates are common).
auth : str
``auto`` (default: session, basic auth if the service cannot create sessions),
``session`` or ``basic``.
retries : int
Extra attempts after a connection error (default 1).
The managed server is the first member of ``Systems``; its BMC is the first manager in
its ``Links.ManagedBy`` and its chassis the first in its ``Links.Chassis`` (the first
member of ``Managers``/``Chassis`` when the links are missing).
"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from typing import Any
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
from napalm_device_types.base import FingerprintRule, PortSpec
from napalm_device_types.models import POWER_ACTIONS
from napalm_redfish.parsers import (
build_environment,
environment_sources,
parse_account,
parse_action_info,
parse_drive,
parse_ethernet_interface,
parse_fan,
parse_firmware,
parse_log_entry,
parse_log_service,
parse_manager,
parse_memory,
parse_network_protocol,
parse_power,
parse_power_state,
parse_power_subsystem,
parse_power_supply,
parse_processor,
parse_reset_action,
parse_sensor,
parse_service_root,
parse_simple_storage,
parse_storage,
parse_system,
parse_thermal,
parse_thermal_metrics,
parse_thermal_subsystem,
to_napalm_users,
)
from napalm_redfish.parsers.common import link, link_list, odata_id
from napalm_redfish.transport import (
RedfishError,
RedfishFeatureLicenseError,
RedfishLicenseError,
RedfishNotFound,
RedfishTransport,
)
LOG = logging.getLogger(__name__)
_JsonDict = dict[str, Any]
#: ``get_management_protocols`` key -> ManagerNetworkProtocol property.
_PROTOCOLS = {
"ipmi": "IPMI",
"ssh": "SSH",
"http": "HTTP",
"https": "HTTPS",
"snmp": "SNMP",
"kvm": "KVMIP",
"virtual_media": "VirtualMedia",
}
_SEVERITIES = ("OK", "Warning", "Critical")
#: The parts a BMC may withhold for want of a license, in the order they are named.
_WITHHELD_ORDER = ("drives", "firmware inventory")
#: Models a system reports when it has none of its own. Supermicro's Redfish calls every
#: 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."})
#: 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:
"""Unix time of a Redfish ``Created`` value; a time without offset counts as UTC."""
if not created:
return None
try:
when = datetime.fromisoformat(created.replace("Z", "+00:00"))
except ValueError:
return None
if when.tzinfo is None:
when = when.replace(tzinfo=timezone.utc)
return when.timestamp()
def _status(item: _JsonDict) -> str:
"""Role ``status``: ``Absent`` for an empty slot, else the Redfish health."""
if not item.get("present", True):
return "Absent"
return item.get("health") or ""
class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver):
"""NAPALM driver for Redfish BMCs (Supermicro, iDRAC, iLO, XCC and others).
``HealthMetricsMixin``: the BMC's own load, memory and interfaces over SNMP (UCD-MIB),
where the BMC runs net-snmp. The managed server's sensors come from ``get_environment``.
"""
platform = "redfish"
DRIVER_NAME = "redfish"
# No fingerprint of its own: discovery suggests ``bmc`` (BMC_PORT_SPECS and
# BMC_HTTP_FINGERPRINT below), which also reads a BMC that refuses Redfish.
#: A BMC restart takes a few minutes before Redfish answers again.
REBOOT_SETTLE_SECONDS = 180
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 = optional_args or {}
self.transport = RedfishTransport(
hostname,
username,
password,
port=self.optional_args.get("port"),
verify=self.optional_args.get("verify_ssl", True),
auth=self.optional_args.get("auth", "auto"),
timeout=timeout,
retries=self.optional_args.get("retries", 1),
)
self._cache: _JsonDict = {}
self._license_refused: RedfishLicenseError | None = None
#: Parts the BMC withholds for want of a license, with the licenses it names.
self._withheld: dict[str, str] = {}
# --- connection ----------------------------------------------------------------------
def open(self) -> None:
"""Connect. A BMC that refuses Redfish for want of a license is reachable all the
same: ``open`` succeeds, ``get_facts`` returns what is known without Redfish, and
``get_device_warnings`` says which license is missing. Every other getter raises.
"""
self._license_refused = None
self._withheld = {}
try:
self.transport.open()
except RedfishLicenseError as exc:
self._license_refused = exc
except RedfishError as exc:
raise ConnectionException(
f"Cannot connect to Redfish at {self.hostname}: {exc}"
) from exc
def close(self) -> None:
self.transport.close()
self._cache = {}
def is_alive(self) -> _JsonDict:
if self._license_refused is not None:
return {"is_alive": True}
try:
self.transport.get("/redfish/v1/")
except Exception:
return {"is_alive": False}
return {"is_alive": True}
# --- discovery (cached per connection) -------------------------------------------------
def _cached(self, key: str, load):
if key not in self._cache:
self._cache[key] = load()
return self._cache[key]
def _root(self) -> _JsonDict:
if self._license_refused is not None:
raise ConnectionException(f"{self.hostname}: {self._license_refused}")
return self._cached("root", lambda: parse_service_root(self.transport.root))
def _first(self, collection: str) -> _JsonDict:
path = self._root()["links"].get(collection)
if path is None:
raise RedfishError(f"the service root links no {collection}")
members = self.transport.members(path, limit=1)
if not members:
raise RedfishError(f"{path} has no members")
return members[0]
def _system_raw(self) -> _JsonDict:
return self._cached("system", lambda: self._first("Systems"))
def _system_path(self) -> str:
return odata_id(self._system_raw()) or ""
def _linked_or_first(self, key: str, links_key: str, collection: str) -> _JsonDict:
def load() -> _JsonDict:
linked = link_list(self._system_raw(), "Links", links_key)
if linked:
return self.transport.get(linked[0])
return self._first(collection)
return self._cached(key, load)
def _manager_raw(self) -> _JsonDict:
return self._linked_or_first("manager", "ManagedBy", "Managers")
def _chassis_raw(self) -> _JsonDict:
return self._linked_or_first("chassis", "Chassis", "Chassis")
def _server_model(self) -> str:
"""The system's model; for a placeholder, the board (PartNumber or chassis model)."""
raw = self._system_raw()
model = (parse_system(raw)["model"] or "").strip()
if model.lower() not in _PLACEHOLDER_MODELS:
return model
board = raw.get("PartNumber") or self._chassis_raw().get("Model") or ""
return str(board).strip() or model
def _members_of(self, resource: _JsonDict, key: str) -> list[_JsonDict]:
"""Members of the collection ``resource[key]`` links; [] if not linked."""
path = link(resource, key)
return self.transport.members(path) if path else []
def _service(self, name: str) -> _JsonDict | None:
path = self._root()["links"].get(name)
return self.transport.get_optional(path) if path else None
def _network_protocol(self) -> _JsonDict:
raw = self.transport.follow(self._manager_raw(), "NetworkProtocol")
return parse_network_protocol(raw or {})
def _manager_interfaces(self) -> list[_JsonDict]:
return [
parse_ethernet_interface(raw)
for raw in self._members_of(self._manager_raw(), "EthernetInterfaces")
]
# --- NAPALM getters ----------------------------------------------------------------
def get_facts(self) -> _JsonDict:
if self._license_refused is not None:
return {
"uptime": -1.0,
"vendor": "",
"model": "",
"hostname": self.hostname,
"fqdn": self.hostname,
"os_version": "",
"serial_number": "",
"interface_list": [],
}
system = parse_system(self._system_raw())
manager = parse_manager(self._manager_raw())
interfaces = self._manager_interfaces()
protocol = self._network_protocol()
hostname = (
protocol["hostname"]
or next((i["hostname"] for i in interfaces if i["hostname"]), None)
or self.hostname
)
fqdn = (
protocol["fqdn"] or next((i["fqdn"] for i in interfaces if i["fqdn"]), None) or hostname
)
return {
"uptime": -1.0, # Redfish has no uptime
"vendor": system["manufacturer"] or "",
"model": self._server_model(),
"hostname": hostname,
"fqdn": fqdn,
"os_version": manager["firmware_version"] or "",
"serial_number": system["serial_number"] or "",
"interface_list": sorted(i["name"] for i in interfaces if i["name"]),
}
def get_interfaces(self) -> _JsonDict:
return {i["name"]: i["interface"] for i in self._manager_interfaces() if i["name"]}
def get_interfaces_ip(self) -> _JsonDict:
result = {}
for iface in self._manager_interfaces():
families = {f: iface[f] for f in ("ipv4", "ipv6") if iface[f]}
if iface["name"] and families:
result[iface["name"]] = families
return result
def get_environment(self) -> _JsonDict:
"""Fans, temperatures and power supplies of the chassis; ``cpu`` and ``memory`` are
empty, since a BMC does not know the host OS's load."""
sources = environment_sources(self._chassis_raw())
fans: _JsonDict = {}
temperature: _JsonDict = {}
power: _JsonDict = {}
thermal = sources["thermal"]
if thermal and thermal["schema"] == "Thermal":
legacy = parse_thermal(self.transport.get(thermal["path"]))
fans, temperature = legacy["fans"], legacy["temperature"]
elif thermal:
subsystem = parse_thermal_subsystem(self.transport.get(thermal["path"]))
parsed_fans = (
[parse_fan(raw) for raw in self.transport.members(subsystem["fans_path"])]
if subsystem["fans_path"]
else []
)
if sources["sensors"]:
sensors = [parse_sensor(raw) for raw in self.transport.members(sources["sensors"])]
temperature = build_environment(sensors=sensors)["temperature"]
elif subsystem["thermal_metrics_path"]:
metrics = self.transport.get_optional(subsystem["thermal_metrics_path"])
temperature = parse_thermal_metrics(metrics or {})["temperature"]
fans = build_environment(fans=parsed_fans)["fans"]
supply = sources["power"]
if supply and supply["schema"] == "Power":
power = parse_power(self.transport.get(supply["path"]))["power"]
elif supply:
psus = [
parse_power_supply(raw, self.transport.follow(raw, "Metrics"))
for raw in self._power_supplies_raw(supply["path"])
]
power = build_environment(power_supplies=psus)["power"]
return {"fans": fans, "temperature": temperature, "power": power, "cpu": {}, "memory": {}}
def _power_supplies_raw(self, subsystem_path: str) -> list[_JsonDict]:
subsystem = parse_power_subsystem(self.transport.get(subsystem_path))
path = subsystem["power_supplies_path"]
return self.transport.members(path) if path else []
def get_users(self) -> _JsonDict:
service = self._service("AccountService")
if service is None:
return {}
return to_napalm_users(parse_account(raw) for raw in self._members_of(service, "Accounts"))
def get_ntp_servers(self) -> _JsonDict:
return {server: {} for server in self._network_protocol()["ntp"]["servers"]}
def get_snmp_information(self) -> _JsonDict:
"""SNMP state from ManagerNetworkProtocol. Redfish hides community strings, so the
``community`` keys are the communities' names; contact and location are not in
Redfish."""
snmp = self._network_protocol()["snmp"]
communities = {}
if snmp["enabled"]:
for community in snmp["communities"]:
if community["name"]:
mode = "rw" if community["access"] == "Full" else "ro"
communities[community["name"]] = {"acl": "", "mode": mode}
return {"chassis_id": "", "community": communities, "contact": "", "location": ""}
def get_device_warnings(self) -> list[_JsonDict]:
"""Raw signals for netOrk's warning catalog; presentation is netOrk's.
``bmc_license_required`` when the BMC refuses Redfish altogether;
``bmc_license_partial`` when it answers but withholds parts (drives, the firmware
inventory) for want of a license. The parts are probed here, so the warning does
not depend on which getters ran before.
"""
refused = self._license_refused
if refused is not None:
return [
{
"code": "bmc_license_required",
"meta": {"licenses": refused.licenses, "message": refused.detail},
}
]
self._probe_withheld()
if not self._withheld:
return []
licenses = sorted({name for names in self._withheld.values() for name in names.split()})
parts = [part for part in _WITHHELD_ORDER if part in self._withheld]
return [
{
"code": "bmc_license_partial",
"meta": {"licenses": " ".join(licenses), "parts": parts},
}
]
def get_licenses(self) -> list[_JsonDict]:
"""The licenses the BMC asks for and does not have (``status`` "missing").
Refused altogether: one entry per license the service names, with its message.
Answering but withholding parts: one entry per license, with ``withholds``
naming the parts. A BMC that names nothing missing reports nothing: Redfish has
no standard way to list the licenses a BMC does have.
"""
refused = self._license_refused
if refused is not None:
return [
{"name": name, "status": "missing", "detail": refused.detail}
for name in refused.licenses.split() or [""]
]
self._probe_withheld()
withheld: dict[str, list[str]] = {}
for part in _WITHHELD_ORDER:
if part not in self._withheld:
continue
for name in self._withheld[part].split() or [""]:
withheld.setdefault(name, []).append(part)
return [
{"name": name, "status": "missing", "withholds": parts}
for name, parts in withheld.items()
]
def _withhold(self, part: str, exc: RedfishFeatureLicenseError) -> None:
LOG.info("%s: %s withheld by the BMC: %s", self.hostname, part, exc)
self._withheld[part] = exc.licenses
def _probe_withheld(self) -> None:
"""Ask for the license-gated resources once, so ``_withheld`` is complete."""
storage = link(self._system_raw(), "Storage")
if storage and "drives" not in self._withheld:
try:
self.transport.get(storage)
except RedfishFeatureLicenseError as exc:
self._withhold("drives", exc)
except RedfishError:
pass
if "firmware inventory" not in self._withheld:
self._update_service()
# --- role: power -------------------------------------------------------------------
def _accepted_actions(self, system: _JsonDict) -> list[str]:
"""The PowerAction values the BMC accepts for ``system`` in its current state."""
reset = parse_reset_action(system)
if reset["target"] is None:
return []
allowed = reset["allowed"]
if allowed is None and reset["action_info"]:
info = self.transport.get_optional(reset["action_info"])
allowed = parse_action_info(info) if info else None
if allowed is None:
# Not advertised: assume the standard values; the BMC rejects what it lacks.
allowed = list(POWER_ACTIONS)
actions = [a for a in POWER_ACTIONS if a in allowed]
state = parse_power_state(system)
if state == "Off":
return [a for a in actions if a == "On"]
if state == "On":
return [a for a in actions if a != "On"]
return actions
def get_power_state(self) -> _JsonDict:
system = self.transport.get(self._system_path())
return {"state": parse_power_state(system) or "", "actions": self._accepted_actions(system)}
def set_power_state(self, action: str) -> None:
if action not in POWER_ACTIONS:
raise ValueError(f"{action!r} is not one of {', '.join(POWER_ACTIONS)}")
system = self.transport.get(self._system_path())
target = parse_reset_action(system)["target"]
if target is None:
raise RuntimeError(f"{self.hostname}: the system offers no reset action")
accepted = self._accepted_actions(system)
if action not in accepted:
raise RuntimeError(
f"{self.hostname}: {action} not accepted while the server is "
f"{parse_power_state(system) or 'in an unknown state'} "
f"(accepted: {', '.join(accepted) or 'none'})"
)
try:
self.transport.post(target, {"ResetType": action})
except RedfishError as exc:
raise RuntimeError(f"{self.hostname}: {action} refused: {exc}") from exc
def reboot_host(self) -> None:
"""Restart the BMC (``Manager.Reset``, graceful when offered). The server keeps
running; the Redfish session ends with the restart."""
manager = self.transport.get(odata_id(self._manager_raw()) or "")
reset = parse_reset_action(manager, "#Manager.Reset")
if reset["target"] is None:
raise RuntimeError(f"{self.hostname}: the BMC offers no reset action")
body: _JsonDict = {}
if reset["allowed"] is not None:
reset_type = next(
(t for t in ("GracefulRestart", "ForceRestart") if t in reset["allowed"]), None
)
if reset_type is None:
raise RuntimeError(
f"{self.hostname}: BMC reset types {reset['allowed']} include no restart"
)
body["ResetType"] = reset_type
try:
self.transport.post(reset["target"], body)
except RedfishError as exc:
raise RuntimeError(f"{self.hostname}: BMC reset refused: {exc}") from exc
# --- role: inventory -----------------------------------------------------------------
def get_hardware_inventory(self) -> _JsonDict:
raw = self._system_raw()
system = parse_system(raw)
return {
"manufacturer": system["manufacturer"] or "",
"model": self._server_model(),
"serial_number": system["serial_number"] or "",
"system_uuid": (system["uuid"] or "").lower(),
"cpus": self._cpus(raw),
"memory": self._dimms(raw),
"drives": self._drives(raw),
"power_supplies": self._psus(),
"nics": self._nics(raw),
}
def _cpus(self, system: _JsonDict) -> list[_JsonDict]:
cpus = []
for raw in self._members_of(system, "Processors"):
cpu = parse_processor(raw)
if cpu["processor_type"] not in (None, "CPU"):
continue # GPUs, FPGAs, accelerators
cpus.append(
{
"socket": cpu["socket"] or cpu["id"] or "",
"model": cpu["model"] or "",
"cores": cpu["cores"],
"threads": cpu["threads"],
"status": _status(cpu),
}
)
return cpus
def _dimms(self, system: _JsonDict) -> list[_JsonDict]:
dimms = []
for raw in self._members_of(system, "Memory"):
dimm = parse_memory(raw)
size = dimm["capacity_mib"]
dimms.append(
{
"slot": dimm["location"] or dimm["id"] or "",
"size_bytes": size * 1024 * 1024 if size else None,
"type": dimm["memory_device_type"] or "",
"speed_mts": dimm["speed_mhz"], # Redfish: MHz or MT/s "as appropriate"
"part_number": dimm["part_number"] or "",
"serial_number": dimm["serial_number"] or "",
"status": _status(dimm),
}
)
return dimms
def _storage_drives(self, system: _JsonDict) -> list[_JsonDict] | None:
"""Drives from ``Storage``; ``None`` when it is not linked or withheld."""
if not link(system, "Storage"):
return None
try:
storages = self._members_of(system, "Storage")
except RedfishFeatureLicenseError as exc:
self._withhold("drives", exc)
return None
parsed: list[_JsonDict] = []
seen: set[str] = set()
for storage in storages:
for path in parse_storage(storage)["drive_paths"]:
if path not in seen:
seen.add(path)
drive = self.transport.get_optional(path)
if drive is not None:
parsed.append(parse_drive(drive))
return parsed
def _drives(self, system: _JsonDict) -> list[_JsonDict]:
parsed = self._storage_drives(system)
if parsed is None:
# Older services, and a Supermicro X10 that withholds Storage without DCMS.
parsed = []
for storage in self._members_of(system, "SimpleStorage"):
parsed.extend(parse_simple_storage(storage)["drives"])
return [
{
"slot": d["name"] or d["id"] or "",
"model": d["model"] or "",
"serial_number": d["serial_number"] or "",
"size_bytes": d["capacity_bytes"],
"media": d["media_type"] or "",
"protocol": d["protocol"] or "",
"status": _status(d),
}
for d in parsed
]
def _psus(self) -> list[_JsonDict]:
source = environment_sources(self._chassis_raw())["power"]
if source is None:
return []
if source["schema"] == "Power":
items = self.transport.get(source["path"]).get("PowerSupplies") or []
else:
items = self._power_supplies_raw(source["path"])
psus = []
for raw in items:
if not isinstance(raw, dict):
continue
psu = parse_power_supply(raw)
psus.append(
{
"slot": psu["name"],
"model": psu["model"] or "",
"serial_number": psu["serial_number"] or "",
"capacity_watts": int(psu["capacity"]) if psu["capacity"] >= 0 else None,
"status": _status(psu),
}
)
return psus
def _nics(self, system: _JsonDict) -> list[_JsonDict]:
nics = []
for raw in self._members_of(system, "EthernetInterfaces"):
nic = parse_ethernet_interface(raw)
speed = nic["interface"]["speed"]
link_up = {"LinkUp": True, "LinkDown": False, "NoLink": False}.get(
nic["link_status"] or ""
)
nics.append(
{
"name": nic["name"] or "",
"mac_address": nic["interface"]["mac_address"].lower(),
"speed_mbps": int(speed) if speed >= 0 else None,
"link_up": link_up,
}
)
return nics
# --- role: event log, firmware, protocols --------------------------------------------
def get_event_log(self, limit: int = 500) -> list[_JsonDict]:
"""Entries of every log service of the system and of the BMC, newest first.
Where the SEL lives varies by vendor, so all of them are read; entries without a
time sort last.
"""
entries: list[_JsonDict] = []
for owner in (self._system_raw(), self._manager_raw()):
try:
services = self._members_of(owner, "LogServices")
except RedfishNotFound:
continue
for raw_service in services:
service = parse_log_service(raw_service)
if not service["entries_path"]:
continue
try:
raw_entries = self.transport.members(service["entries_path"])
except RedfishNotFound:
LOG.warning(
"%s: log %s has no entries collection", self.hostname, service["id"]
)
continue
for raw in raw_entries:
entry = parse_log_entry(raw)
severity = entry["severity"]
entries.append(
{
"id": odata_id(raw) or f"{service['id']}/{entry['id']}",
"timestamp": _timestamp(entry["created"]),
"severity": severity if severity in _SEVERITIES else "OK",
"message": entry["message"] or "",
}
)
entries.sort(
key=lambda e: (e["timestamp"] is not None, e["timestamp"] or 0.0), reverse=True
)
return entries[:limit]
def _update_service(self) -> _JsonDict | None:
try:
return self._service("UpdateService")
except RedfishFeatureLicenseError as exc:
self._withhold("firmware inventory", exc)
return None
def get_firmware_inventory(self) -> list[_JsonDict]:
"""``UpdateService/FirmwareInventory``. Where the BMC withholds it for want of a
license, the BMC's own firmware and the BIOS, which the manager and the system
state themselves."""
service = self._update_service()
if service is None:
if "firmware inventory" not in self._withheld:
return []
bmc = parse_manager(self._manager_raw())["firmware_version"]
bios = self._system_raw().get("BiosVersion")
return [
{"name": name, "version": version, "updateable": None}
for name, version in (("BMC", bmc), ("BIOS", bios))
if version
]
items = [parse_firmware(raw) for raw in self._members_of(service, "FirmwareInventory")]
return [
{
"name": fw["name"] or fw["id"] or "",
"version": fw["version"] or "",
"updateable": fw["updateable"],
}
for fw in items
]
def get_management_protocols(self) -> _JsonDict:
"""What the BMC listens on. A protocol the BMC does not report counts as off."""
protocols = self._network_protocol()["protocols"]
result = {}
for key, name in _PROTOCOLS.items():
entry = protocols.get(name) or {}
result[key] = {"enabled": bool(entry.get("enabled")), "port": entry.get("port")}
return result