fix: read a Supermicro X10 with only the OOB license over Redfish (0.3.1)
First run against real hardware over Redfish: an X10DRL-i (BMC 3.91) with
SFT-OOB-LIC. Its Redfish tree is recorded under
tests/fixtures/supermicro-x10drl-i-3.91/redfish, scrubbed.
- A resource refused for want of a further license (403 OemLicenseNotPassed)
no longer fails the getter. Storage then falls back to SimpleStorage. The
firmware list falls back to the BMC and BIOS versions when UpdateService
is withheld.
- get_device_warnings reports bmc_license_partial with the licenses and the
withheld parts (drives, firmware inventory).
- A placeholder system model ("Super Server") is replaced by the board
(PartNumber, else the chassis model).
This commit is contained in:
1 parent
6c60f7ac60
commit
adf152ee12
78 files changed
+3237
-24
No files matched your search
+94
-19
@@ -67,6 +67,7 @@ from napalm_redfish.parsers import (
|
||||
from napalm_redfish.parsers.common import link, link_list, odata_id
|
||||
from napalm_redfish.transport import (
|
||||
RedfishError,
|
||||
RedfishFeatureLicenseError,
|
||||
RedfishLicenseError,
|
||||
RedfishNotFound,
|
||||
RedfishTransport,
|
||||
@@ -87,6 +88,11 @@ _PROTOCOLS = {
|
||||
"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."})
|
||||
|
||||
|
||||
def _timestamp(created: str | None) -> float | None:
|
||||
@@ -162,6 +168,8 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
)
|
||||
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 ----------------------------------------------------------------------
|
||||
|
||||
@@ -171,6 +179,7 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
``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:
|
||||
@@ -234,6 +243,15 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
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)
|
||||
@@ -271,7 +289,7 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
return {
|
||||
"uptime": -1.0, # Redfish has no uptime
|
||||
"vendor": system["manufacturer"] or "",
|
||||
"model": system["model"] or "",
|
||||
"model": self._server_model(),
|
||||
"hostname": hostname,
|
||||
"fqdn": fqdn,
|
||||
"os_version": manager["firmware_version"] or "",
|
||||
@@ -355,12 +373,41 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
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."""
|
||||
"""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 None:
|
||||
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 []
|
||||
return [{"code": "bmc_license_required",
|
||||
"meta": {"licenses": refused.licenses, "message": refused.detail}}]
|
||||
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 _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 -------------------------------------------------------------------
|
||||
|
||||
@@ -434,7 +481,7 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
system = parse_system(raw)
|
||||
return {
|
||||
"manufacturer": system["manufacturer"] or "",
|
||||
"model": system["model"] or "",
|
||||
"model": self._server_model(),
|
||||
"serial_number": system["serial_number"] or "",
|
||||
"system_uuid": (system["uuid"] or "").lower(),
|
||||
"cpus": self._cpus(raw),
|
||||
@@ -471,18 +518,31 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
})
|
||||
return dimms
|
||||
|
||||
def _drives(self, system: _JsonDict) -> list[_JsonDict]:
|
||||
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] = []
|
||||
if link(system, "Storage"):
|
||||
seen: set[str] = set()
|
||||
for storage in self._members_of(system, "Storage"):
|
||||
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))
|
||||
else:
|
||||
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 [{
|
||||
@@ -569,10 +629,25 @@ class RedfishDriver(HealthMetricsMixin, ManagementControllerDriver):
|
||||
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]:
|
||||
service = self._service("UpdateService")
|
||||
"""``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:
|
||||
return []
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user