fix: stop scoring every HTTPS port as a BMC; name the missing license
netOrk's discovery took six switches for BMCs: the PortSpec for 443 added 5 points to every device with a web UI, and nothing else scored them. The port now carries no weight. The real BMC, a Supermicro X10DRL-i, was not recognised at all. Its web UI (ATEN firmware) gets a rule, as do iDRAC and iLO; discovery fetches those landing pages today. A rule for a Redfish error body covers the service root once discovery reads it. That board refuses Redfish without a Supermicro license, even the service root (403 OemLicenseNotPassed, "SUM DCMS OOB"). The library reports only the status, so open() reads the error body once more and raises RedfishLicenseError naming the licenses. Release 0.1.1.
This commit is contained in:
1 parent
c7d947a870
commit
93bbc63f3d
10 files changed
+183
-8
No files matched your search
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -39,6 +40,11 @@ SERVICE_ROOT = "/redfish/v1/"
|
||||
DEFAULT_SESSIONS = "/redfish/v1/SessionService/Sessions"
|
||||
AUTH_METHODS = ("auto", "session", "basic")
|
||||
MAX_PAGES = 1000
|
||||
#: The MessageId with which a service refuses every request until a vendor license is
|
||||
#: activated (Supermicro X10 without one of SUM, DCMS or OOB).
|
||||
LICENSE_REFUSED = "OemLicenseNotPassed"
|
||||
#: How the library reports a service root that answered with an HTTP error.
|
||||
_ROOT_STATUS = re.compile(r"return code:\s*(\d{3})")
|
||||
|
||||
Reference = str | Mapping[str, Any]
|
||||
|
||||
@@ -52,6 +58,10 @@ class RedfishError(Exception):
|
||||
self.path = path
|
||||
|
||||
|
||||
class RedfishLicenseError(RedfishError):
|
||||
"""The service refuses Redfish until a vendor license is activated."""
|
||||
|
||||
|
||||
class RedfishAuthError(RedfishError):
|
||||
"""Login failed, or the account may not read the resource (HTTP 401/403)."""
|
||||
|
||||
@@ -64,6 +74,18 @@ class RedfishNotFound(RedfishHTTPError):
|
||||
"""The resource does not exist (HTTP 404)."""
|
||||
|
||||
|
||||
def _message_ids(body: Any) -> list[str]:
|
||||
"""The MessageIds of a Redfish error body's extended info."""
|
||||
error = body.get("error") if isinstance(body, Mapping) else None
|
||||
if not isinstance(error, Mapping):
|
||||
return []
|
||||
return [
|
||||
str(info["MessageId"])
|
||||
for info in error.get("@Message.ExtendedInfo") or []
|
||||
if isinstance(info, Mapping) and info.get("MessageId")
|
||||
]
|
||||
|
||||
|
||||
def _error_message(body: Any) -> str:
|
||||
"""The human-readable part of a Redfish error body, if any."""
|
||||
if not isinstance(body, Mapping):
|
||||
@@ -157,9 +179,9 @@ class RedfishTransport:
|
||||
max_retry=self.retries,
|
||||
)
|
||||
except (RetriesExhaustedError, ServerDownOrUnreachableError) as exc:
|
||||
raise RedfishError(f"{self.base_url}: service root not reachable: {exc}") from exc
|
||||
raise self._root_failure(f"service root not reachable: {exc}", exc) from exc
|
||||
except Exception as exc:
|
||||
raise RedfishError(f"{self.base_url}: cannot read the service root: {exc}") from exc
|
||||
raise self._root_failure(f"cannot read the service root: {exc}", exc) from exc
|
||||
self._client = client
|
||||
self.root = dict(getattr(client, "root", None) or {})
|
||||
try:
|
||||
@@ -169,6 +191,34 @@ class RedfishTransport:
|
||||
client._session.close()
|
||||
raise
|
||||
|
||||
def _root_failure(self, reason: str, exc: BaseException) -> RedfishError:
|
||||
"""The error for an unreadable service root, with the service's own reason if it gave one.
|
||||
|
||||
The library keeps only the status code. When the service did answer, its error body is
|
||||
read once more: an unlicensed Supermicro X10 refuses even the service root, and only
|
||||
the body says that a license is what is missing.
|
||||
"""
|
||||
body = self._root_error_body() if _ROOT_STATUS.search(str(exc)) else None
|
||||
detail = _error_message(body)
|
||||
if any(message_id.endswith(LICENSE_REFUSED) for message_id in _message_ids(body)):
|
||||
return RedfishLicenseError(
|
||||
f"{self.base_url}: Redfish needs a vendor license on this BMC: {detail}"
|
||||
)
|
||||
return RedfishError(f"{self.base_url}: {reason}" + (f": {detail}" if detail else ""))
|
||||
|
||||
def _root_error_body(self) -> Any:
|
||||
"""The service root's error body, or None when there is none to read."""
|
||||
try:
|
||||
resp = requests.get(
|
||||
self.base_url + SERVICE_ROOT,
|
||||
verify=self._cafile or False,
|
||||
timeout=self.timeout,
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
return resp.json() if resp.status_code >= 400 else None
|
||||
except Exception: # noqa: BLE001 - the original error is what gets reported
|
||||
return None
|
||||
|
||||
def _login(self, client) -> str:
|
||||
if self.auth == "basic":
|
||||
self._call(client.login, auth=AuthMethod.BASIC)
|
||||
|
||||
Reference in new issue
Block a user