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.
This commit is contained in:
1 parent
5b24675a12
commit
09029ca0a4
29 files changed
+985
-456
No files matched your search
@@ -87,10 +87,11 @@ These follow the other drivers in the `NAPALM` organisation:
|
|||||||
import napalm
|
import napalm
|
||||||
|
|
||||||
driver = napalm.get_network_driver("redfish")
|
driver = napalm.get_network_driver("redfish")
|
||||||
with driver("bmc01.example.net", "netork", "secret",
|
with driver(
|
||||||
optional_args={"verify_ssl": "/etc/netork/bmc-ca.pem"}) as bmc:
|
"bmc01.example.net", "netork", "secret", optional_args={"verify_ssl": "/etc/netork/bmc-ca.pem"}
|
||||||
bmc.get_facts() # the BMC: firmware as os_version, its ports
|
) as bmc:
|
||||||
bmc.get_power_state() # the server: {"state": "On", "actions": [...]}
|
bmc.get_facts() # the BMC: firmware as os_version, its ports
|
||||||
|
bmc.get_power_state() # the server: {"state": "On", "actions": [...]}
|
||||||
bmc.get_hardware_inventory()
|
bmc.get_hardware_inventory()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -18,24 +18,30 @@ package updates and service control for a BMC.
|
|||||||
```python
|
```python
|
||||||
class ManagementControllerDriver(DeviceTypeDriver):
|
class ManagementControllerDriver(DeviceTypeDriver):
|
||||||
"""Contract, not code."""
|
"""Contract, not code."""
|
||||||
ROLE: str = "management_controller" # key exposed as device_class
|
|
||||||
|
ROLE: str = "management_controller" # key exposed as device_class
|
||||||
TYPE_LABEL: str = "Management Controller"
|
TYPE_LABEL: str = "Management Controller"
|
||||||
USES_SSH: bool = False # Redfish is HTTPS; IPMI is UDP
|
USES_SSH: bool = False # Redfish is HTTPS; IPMI is UDP
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# The managed machine
|
# The managed machine
|
||||||
def get_power_state(self) -> PowerStateDict: ...
|
def get_power_state(self) -> PowerStateDict: ...
|
||||||
def set_power_state(self, action: PowerAction) -> None: ... # On, GracefulShutdown,
|
def set_power_state(self, action: PowerAction) -> None:
|
||||||
# ForceOff, GracefulRestart,
|
... # On, GracefulShutdown,
|
||||||
# ForceRestart, PowerCycle
|
# ForceOff, GracefulRestart,
|
||||||
def get_hardware_inventory(self) -> HardwareInventoryDict: ... # CPUs, DIMMs, drives,
|
# ForceRestart, PowerCycle
|
||||||
# PSUs, NICs, serials
|
|
||||||
|
def get_hardware_inventory(self) -> HardwareInventoryDict:
|
||||||
|
... # CPUs, DIMMs, drives,
|
||||||
|
# PSUs, NICs, serials
|
||||||
|
|
||||||
def get_event_log(self, limit: int = 500) -> List[EventLogEntryDict]: ... # SEL
|
def get_event_log(self, limit: int = 500) -> List[EventLogEntryDict]: ... # SEL
|
||||||
# The controller itself
|
# The controller itself
|
||||||
def get_firmware_inventory(self) -> List[FirmwareDict]: ... # BMC, BIOS, others
|
def get_firmware_inventory(self) -> List[FirmwareDict]: ... # BMC, BIOS, others
|
||||||
def get_management_protocols(self) -> ManagementProtocolsDict: ... # IPMI, SSH,
|
def get_management_protocols(self) -> ManagementProtocolsDict:
|
||||||
# HTTP(S), SNMP,
|
... # IPMI, SSH,
|
||||||
# KVM, virtual media
|
# HTTP(S), SNMP,
|
||||||
|
# KVM, virtual media
|
||||||
```
|
```
|
||||||
|
|
||||||
Points to settle in napalm-device-types:
|
Points to settle in napalm-device-types:
|
||||||
|
|||||||
@@ -108,8 +108,7 @@ class BmcDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver):
|
|||||||
except (RedfishError, ConnectionException) as exc:
|
except (RedfishError, ConnectionException) as exc:
|
||||||
if not has_ipmi:
|
if not has_ipmi:
|
||||||
raise
|
raise
|
||||||
LOG.info("%s: %s failed over Redfish (%s); asking IPMI", self.hostname, name,
|
LOG.info("%s: %s failed over Redfish (%s); asking IPMI", self.hostname, name, exc)
|
||||||
exc)
|
|
||||||
if not has_ipmi:
|
if not has_ipmi:
|
||||||
raise NotImplementedError(f"{self.hostname}: {name} needs Redfish")
|
raise NotImplementedError(f"{self.hostname}: {name} needs Redfish")
|
||||||
return getattr(self._ipmi_conn(), name)(*args)
|
return getattr(self._ipmi_conn(), name)(*args)
|
||||||
|
|||||||
+144
-87
@@ -190,8 +190,9 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
except RedfishLicenseError as exc:
|
except RedfishLicenseError as exc:
|
||||||
self._license_refused = exc
|
self._license_refused = exc
|
||||||
except RedfishError as exc:
|
except RedfishError as exc:
|
||||||
raise ConnectionException(f"Cannot connect to Redfish at {self.hostname}: {exc}") \
|
raise ConnectionException(
|
||||||
from exc
|
f"Cannot connect to Redfish at {self.hostname}: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
@@ -271,26 +272,37 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
return parse_network_protocol(raw or {})
|
return parse_network_protocol(raw or {})
|
||||||
|
|
||||||
def _manager_interfaces(self) -> list[_JsonDict]:
|
def _manager_interfaces(self) -> list[_JsonDict]:
|
||||||
return [parse_ethernet_interface(raw)
|
return [
|
||||||
for raw in self._members_of(self._manager_raw(), "EthernetInterfaces")]
|
parse_ethernet_interface(raw)
|
||||||
|
for raw in self._members_of(self._manager_raw(), "EthernetInterfaces")
|
||||||
|
]
|
||||||
|
|
||||||
# --- NAPALM getters ----------------------------------------------------------------
|
# --- NAPALM getters ----------------------------------------------------------------
|
||||||
|
|
||||||
def get_facts(self) -> _JsonDict:
|
def get_facts(self) -> _JsonDict:
|
||||||
if self._license_refused is not None:
|
if self._license_refused is not None:
|
||||||
return {"uptime": -1.0, "vendor": "", "model": "", "hostname": self.hostname,
|
return {
|
||||||
"fqdn": self.hostname, "os_version": "", "serial_number": "",
|
"uptime": -1.0,
|
||||||
"interface_list": []}
|
"vendor": "",
|
||||||
|
"model": "",
|
||||||
|
"hostname": self.hostname,
|
||||||
|
"fqdn": self.hostname,
|
||||||
|
"os_version": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"interface_list": [],
|
||||||
|
}
|
||||||
system = parse_system(self._system_raw())
|
system = parse_system(self._system_raw())
|
||||||
manager = parse_manager(self._manager_raw())
|
manager = parse_manager(self._manager_raw())
|
||||||
interfaces = self._manager_interfaces()
|
interfaces = self._manager_interfaces()
|
||||||
protocol = self._network_protocol()
|
protocol = self._network_protocol()
|
||||||
hostname = (protocol["hostname"]
|
hostname = (
|
||||||
or next((i["hostname"] for i in interfaces if i["hostname"]), None)
|
protocol["hostname"]
|
||||||
or self.hostname)
|
or next((i["hostname"] for i in interfaces if i["hostname"]), None)
|
||||||
fqdn = (protocol["fqdn"]
|
or self.hostname
|
||||||
or next((i["fqdn"] for i in interfaces if i["fqdn"]), None)
|
)
|
||||||
or hostname)
|
fqdn = (
|
||||||
|
protocol["fqdn"] or next((i["fqdn"] for i in interfaces if i["fqdn"]), None) or hostname
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"uptime": -1.0, # Redfish has no uptime
|
"uptime": -1.0, # Redfish has no uptime
|
||||||
"vendor": system["manufacturer"] or "",
|
"vendor": system["manufacturer"] or "",
|
||||||
@@ -327,11 +339,13 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
fans, temperature = legacy["fans"], legacy["temperature"]
|
fans, temperature = legacy["fans"], legacy["temperature"]
|
||||||
elif thermal:
|
elif thermal:
|
||||||
subsystem = parse_thermal_subsystem(self.transport.get(thermal["path"]))
|
subsystem = parse_thermal_subsystem(self.transport.get(thermal["path"]))
|
||||||
parsed_fans = [parse_fan(raw) for raw in self.transport.members(
|
parsed_fans = (
|
||||||
subsystem["fans_path"])] if subsystem["fans_path"] else []
|
[parse_fan(raw) for raw in self.transport.members(subsystem["fans_path"])]
|
||||||
|
if subsystem["fans_path"]
|
||||||
|
else []
|
||||||
|
)
|
||||||
if sources["sensors"]:
|
if sources["sensors"]:
|
||||||
sensors = [parse_sensor(raw)
|
sensors = [parse_sensor(raw) for raw in self.transport.members(sources["sensors"])]
|
||||||
for raw in self.transport.members(sources["sensors"])]
|
|
||||||
temperature = build_environment(sensors=sensors)["temperature"]
|
temperature = build_environment(sensors=sensors)["temperature"]
|
||||||
elif subsystem["thermal_metrics_path"]:
|
elif subsystem["thermal_metrics_path"]:
|
||||||
metrics = self.transport.get_optional(subsystem["thermal_metrics_path"])
|
metrics = self.transport.get_optional(subsystem["thermal_metrics_path"])
|
||||||
@@ -342,12 +356,13 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
if supply and supply["schema"] == "Power":
|
if supply and supply["schema"] == "Power":
|
||||||
power = parse_power(self.transport.get(supply["path"]))["power"]
|
power = parse_power(self.transport.get(supply["path"]))["power"]
|
||||||
elif supply:
|
elif supply:
|
||||||
psus = [parse_power_supply(raw, self.transport.follow(raw, "Metrics"))
|
psus = [
|
||||||
for raw in self._power_supplies_raw(supply["path"])]
|
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"]
|
power = build_environment(power_supplies=psus)["power"]
|
||||||
|
|
||||||
return {"fans": fans, "temperature": temperature, "power": power,
|
return {"fans": fans, "temperature": temperature, "power": power, "cpu": {}, "memory": {}}
|
||||||
"cpu": {}, "memory": {}}
|
|
||||||
|
|
||||||
def _power_supplies_raw(self, subsystem_path: str) -> list[_JsonDict]:
|
def _power_supplies_raw(self, subsystem_path: str) -> list[_JsonDict]:
|
||||||
subsystem = parse_power_subsystem(self.transport.get(subsystem_path))
|
subsystem = parse_power_subsystem(self.transport.get(subsystem_path))
|
||||||
@@ -358,8 +373,7 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
service = self._service("AccountService")
|
service = self._service("AccountService")
|
||||||
if service is None:
|
if service is None:
|
||||||
return {}
|
return {}
|
||||||
return to_napalm_users(parse_account(raw) for raw in self._members_of(service,
|
return to_napalm_users(parse_account(raw) for raw in self._members_of(service, "Accounts"))
|
||||||
"Accounts"))
|
|
||||||
|
|
||||||
def get_ntp_servers(self) -> _JsonDict:
|
def get_ntp_servers(self) -> _JsonDict:
|
||||||
return {server: {} for server in self._network_protocol()["ntp"]["servers"]}
|
return {server: {} for server in self._network_protocol()["ntp"]["servers"]}
|
||||||
@@ -387,15 +401,23 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
"""
|
"""
|
||||||
refused = self._license_refused
|
refused = self._license_refused
|
||||||
if refused is not None:
|
if refused is not None:
|
||||||
return [{"code": "bmc_license_required",
|
return [
|
||||||
"meta": {"licenses": refused.licenses, "message": refused.detail}}]
|
{
|
||||||
|
"code": "bmc_license_required",
|
||||||
|
"meta": {"licenses": refused.licenses, "message": refused.detail},
|
||||||
|
}
|
||||||
|
]
|
||||||
self._probe_withheld()
|
self._probe_withheld()
|
||||||
if not self._withheld:
|
if not self._withheld:
|
||||||
return []
|
return []
|
||||||
licenses = sorted({name for names in self._withheld.values() for name in names.split()})
|
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]
|
parts = [part for part in _WITHHELD_ORDER if part in self._withheld]
|
||||||
return [{"code": "bmc_license_partial",
|
return [
|
||||||
"meta": {"licenses": " ".join(licenses), "parts": parts}}]
|
{
|
||||||
|
"code": "bmc_license_partial",
|
||||||
|
"meta": {"licenses": " ".join(licenses), "parts": parts},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def get_licenses(self) -> list[_JsonDict]:
|
def get_licenses(self) -> list[_JsonDict]:
|
||||||
"""The licenses the BMC asks for and does not have (``status`` "missing").
|
"""The licenses the BMC asks for and does not have (``status`` "missing").
|
||||||
@@ -407,8 +429,10 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
"""
|
"""
|
||||||
refused = self._license_refused
|
refused = self._license_refused
|
||||||
if refused is not None:
|
if refused is not None:
|
||||||
return [{"name": name, "status": "missing", "detail": refused.detail}
|
return [
|
||||||
for name in refused.licenses.split() or [""]]
|
{"name": name, "status": "missing", "detail": refused.detail}
|
||||||
|
for name in refused.licenses.split() or [""]
|
||||||
|
]
|
||||||
self._probe_withheld()
|
self._probe_withheld()
|
||||||
withheld: dict[str, list[str]] = {}
|
withheld: dict[str, list[str]] = {}
|
||||||
for part in _WITHHELD_ORDER:
|
for part in _WITHHELD_ORDER:
|
||||||
@@ -416,8 +440,10 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
continue
|
continue
|
||||||
for name in self._withheld[part].split() or [""]:
|
for name in self._withheld[part].split() or [""]:
|
||||||
withheld.setdefault(name, []).append(part)
|
withheld.setdefault(name, []).append(part)
|
||||||
return [{"name": name, "status": "missing", "withholds": parts}
|
return [
|
||||||
for name, parts in withheld.items()]
|
{"name": name, "status": "missing", "withholds": parts}
|
||||||
|
for name, parts in withheld.items()
|
||||||
|
]
|
||||||
|
|
||||||
def _withhold(self, part: str, exc: RedfishFeatureLicenseError) -> None:
|
def _withhold(self, part: str, exc: RedfishFeatureLicenseError) -> None:
|
||||||
LOG.info("%s: %s withheld by the BMC: %s", self.hostname, part, exc)
|
LOG.info("%s: %s withheld by the BMC: %s", self.hostname, part, exc)
|
||||||
@@ -460,8 +486,7 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
|
|
||||||
def get_power_state(self) -> _JsonDict:
|
def get_power_state(self) -> _JsonDict:
|
||||||
system = self.transport.get(self._system_path())
|
system = self.transport.get(self._system_path())
|
||||||
return {"state": parse_power_state(system) or "",
|
return {"state": parse_power_state(system) or "", "actions": self._accepted_actions(system)}
|
||||||
"actions": self._accepted_actions(system)}
|
|
||||||
|
|
||||||
def set_power_state(self, action: str) -> None:
|
def set_power_state(self, action: str) -> None:
|
||||||
if action not in POWER_ACTIONS:
|
if action not in POWER_ACTIONS:
|
||||||
@@ -475,7 +500,8 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{self.hostname}: {action} not accepted while the server is "
|
f"{self.hostname}: {action} not accepted while the server is "
|
||||||
f"{parse_power_state(system) or 'in an unknown state'} "
|
f"{parse_power_state(system) or 'in an unknown state'} "
|
||||||
f"(accepted: {', '.join(accepted) or 'none'})")
|
f"(accepted: {', '.join(accepted) or 'none'})"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.transport.post(target, {"ResetType": action})
|
self.transport.post(target, {"ResetType": action})
|
||||||
except RedfishError as exc:
|
except RedfishError as exc:
|
||||||
@@ -490,11 +516,13 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
raise RuntimeError(f"{self.hostname}: the BMC offers no reset action")
|
raise RuntimeError(f"{self.hostname}: the BMC offers no reset action")
|
||||||
body: _JsonDict = {}
|
body: _JsonDict = {}
|
||||||
if reset["allowed"] is not None:
|
if reset["allowed"] is not None:
|
||||||
reset_type = next((t for t in ("GracefulRestart", "ForceRestart")
|
reset_type = next(
|
||||||
if t in reset["allowed"]), None)
|
(t for t in ("GracefulRestart", "ForceRestart") if t in reset["allowed"]), None
|
||||||
|
)
|
||||||
if reset_type is None:
|
if reset_type is None:
|
||||||
raise RuntimeError(f"{self.hostname}: BMC reset types "
|
raise RuntimeError(
|
||||||
f"{reset['allowed']} include no restart")
|
f"{self.hostname}: BMC reset types {reset['allowed']} include no restart"
|
||||||
|
)
|
||||||
body["ResetType"] = reset_type
|
body["ResetType"] = reset_type
|
||||||
try:
|
try:
|
||||||
self.transport.post(reset["target"], body)
|
self.transport.post(reset["target"], body)
|
||||||
@@ -524,9 +552,15 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
cpu = parse_processor(raw)
|
cpu = parse_processor(raw)
|
||||||
if cpu["processor_type"] not in (None, "CPU"):
|
if cpu["processor_type"] not in (None, "CPU"):
|
||||||
continue # GPUs, FPGAs, accelerators
|
continue # GPUs, FPGAs, accelerators
|
||||||
cpus.append({"socket": cpu["socket"] or cpu["id"] or "", "model": cpu["model"] or "",
|
cpus.append(
|
||||||
"cores": cpu["cores"], "threads": cpu["threads"],
|
{
|
||||||
"status": _status(cpu)})
|
"socket": cpu["socket"] or cpu["id"] or "",
|
||||||
|
"model": cpu["model"] or "",
|
||||||
|
"cores": cpu["cores"],
|
||||||
|
"threads": cpu["threads"],
|
||||||
|
"status": _status(cpu),
|
||||||
|
}
|
||||||
|
)
|
||||||
return cpus
|
return cpus
|
||||||
|
|
||||||
def _dimms(self, system: _JsonDict) -> list[_JsonDict]:
|
def _dimms(self, system: _JsonDict) -> list[_JsonDict]:
|
||||||
@@ -534,15 +568,17 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
for raw in self._members_of(system, "Memory"):
|
for raw in self._members_of(system, "Memory"):
|
||||||
dimm = parse_memory(raw)
|
dimm = parse_memory(raw)
|
||||||
size = dimm["capacity_mib"]
|
size = dimm["capacity_mib"]
|
||||||
dimms.append({
|
dimms.append(
|
||||||
"slot": dimm["location"] or dimm["id"] or "",
|
{
|
||||||
"size_bytes": size * 1024 * 1024 if size else None,
|
"slot": dimm["location"] or dimm["id"] or "",
|
||||||
"type": dimm["memory_device_type"] or "",
|
"size_bytes": size * 1024 * 1024 if size else None,
|
||||||
"speed_mts": dimm["speed_mhz"], # Redfish: MHz or MT/s "as appropriate"
|
"type": dimm["memory_device_type"] or "",
|
||||||
"part_number": dimm["part_number"] or "",
|
"speed_mts": dimm["speed_mhz"], # Redfish: MHz or MT/s "as appropriate"
|
||||||
"serial_number": dimm["serial_number"] or "",
|
"part_number": dimm["part_number"] or "",
|
||||||
"status": _status(dimm),
|
"serial_number": dimm["serial_number"] or "",
|
||||||
})
|
"status": _status(dimm),
|
||||||
|
}
|
||||||
|
)
|
||||||
return dimms
|
return dimms
|
||||||
|
|
||||||
def _storage_drives(self, system: _JsonDict) -> list[_JsonDict] | None:
|
def _storage_drives(self, system: _JsonDict) -> list[_JsonDict] | None:
|
||||||
@@ -572,15 +608,18 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
parsed = []
|
parsed = []
|
||||||
for storage in self._members_of(system, "SimpleStorage"):
|
for storage in self._members_of(system, "SimpleStorage"):
|
||||||
parsed.extend(parse_simple_storage(storage)["drives"])
|
parsed.extend(parse_simple_storage(storage)["drives"])
|
||||||
return [{
|
return [
|
||||||
"slot": d["name"] or d["id"] or "",
|
{
|
||||||
"model": d["model"] or "",
|
"slot": d["name"] or d["id"] or "",
|
||||||
"serial_number": d["serial_number"] or "",
|
"model": d["model"] or "",
|
||||||
"size_bytes": d["capacity_bytes"],
|
"serial_number": d["serial_number"] or "",
|
||||||
"media": d["media_type"] or "",
|
"size_bytes": d["capacity_bytes"],
|
||||||
"protocol": d["protocol"] or "",
|
"media": d["media_type"] or "",
|
||||||
"status": _status(d),
|
"protocol": d["protocol"] or "",
|
||||||
} for d in parsed]
|
"status": _status(d),
|
||||||
|
}
|
||||||
|
for d in parsed
|
||||||
|
]
|
||||||
|
|
||||||
def _psus(self) -> list[_JsonDict]:
|
def _psus(self) -> list[_JsonDict]:
|
||||||
source = environment_sources(self._chassis_raw())["power"]
|
source = environment_sources(self._chassis_raw())["power"]
|
||||||
@@ -595,13 +634,15 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
continue
|
continue
|
||||||
psu = parse_power_supply(raw)
|
psu = parse_power_supply(raw)
|
||||||
psus.append({
|
psus.append(
|
||||||
"slot": psu["name"],
|
{
|
||||||
"model": psu["model"] or "",
|
"slot": psu["name"],
|
||||||
"serial_number": psu["serial_number"] or "",
|
"model": psu["model"] or "",
|
||||||
"capacity_watts": int(psu["capacity"]) if psu["capacity"] >= 0 else None,
|
"serial_number": psu["serial_number"] or "",
|
||||||
"status": _status(psu),
|
"capacity_watts": int(psu["capacity"]) if psu["capacity"] >= 0 else None,
|
||||||
})
|
"status": _status(psu),
|
||||||
|
}
|
||||||
|
)
|
||||||
return psus
|
return psus
|
||||||
|
|
||||||
def _nics(self, system: _JsonDict) -> list[_JsonDict]:
|
def _nics(self, system: _JsonDict) -> list[_JsonDict]:
|
||||||
@@ -610,13 +651,16 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
nic = parse_ethernet_interface(raw)
|
nic = parse_ethernet_interface(raw)
|
||||||
speed = nic["interface"]["speed"]
|
speed = nic["interface"]["speed"]
|
||||||
link_up = {"LinkUp": True, "LinkDown": False, "NoLink": False}.get(
|
link_up = {"LinkUp": True, "LinkDown": False, "NoLink": False}.get(
|
||||||
nic["link_status"] or "")
|
nic["link_status"] or ""
|
||||||
nics.append({
|
)
|
||||||
"name": nic["name"] or "",
|
nics.append(
|
||||||
"mac_address": nic["interface"]["mac_address"].lower(),
|
{
|
||||||
"speed_mbps": int(speed) if speed >= 0 else None,
|
"name": nic["name"] or "",
|
||||||
"link_up": link_up,
|
"mac_address": nic["interface"]["mac_address"].lower(),
|
||||||
})
|
"speed_mbps": int(speed) if speed >= 0 else None,
|
||||||
|
"link_up": link_up,
|
||||||
|
}
|
||||||
|
)
|
||||||
return nics
|
return nics
|
||||||
|
|
||||||
# --- role: event log, firmware, protocols --------------------------------------------
|
# --- role: event log, firmware, protocols --------------------------------------------
|
||||||
@@ -640,20 +684,24 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
try:
|
try:
|
||||||
raw_entries = self.transport.members(service["entries_path"])
|
raw_entries = self.transport.members(service["entries_path"])
|
||||||
except RedfishNotFound:
|
except RedfishNotFound:
|
||||||
LOG.warning("%s: log %s has no entries collection", self.hostname,
|
LOG.warning(
|
||||||
service["id"])
|
"%s: log %s has no entries collection", self.hostname, service["id"]
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
for raw in raw_entries:
|
for raw in raw_entries:
|
||||||
entry = parse_log_entry(raw)
|
entry = parse_log_entry(raw)
|
||||||
severity = entry["severity"]
|
severity = entry["severity"]
|
||||||
entries.append({
|
entries.append(
|
||||||
"id": odata_id(raw) or f"{service['id']}/{entry['id']}",
|
{
|
||||||
"timestamp": _timestamp(entry["created"]),
|
"id": odata_id(raw) or f"{service['id']}/{entry['id']}",
|
||||||
"severity": severity if severity in _SEVERITIES else "OK",
|
"timestamp": _timestamp(entry["created"]),
|
||||||
"message": entry["message"] or "",
|
"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)
|
)
|
||||||
|
entries.sort(
|
||||||
|
key=lambda e: (e["timestamp"] is not None, e["timestamp"] or 0.0), reverse=True
|
||||||
|
)
|
||||||
return entries[:limit]
|
return entries[:limit]
|
||||||
|
|
||||||
def _update_service(self) -> _JsonDict | None:
|
def _update_service(self) -> _JsonDict | None:
|
||||||
@@ -673,11 +721,20 @@ class RedfishDriver(HealthMetricsMixin, LicenseMixin, ManagementControllerDriver
|
|||||||
return []
|
return []
|
||||||
bmc = parse_manager(self._manager_raw())["firmware_version"]
|
bmc = parse_manager(self._manager_raw())["firmware_version"]
|
||||||
bios = self._system_raw().get("BiosVersion")
|
bios = self._system_raw().get("BiosVersion")
|
||||||
return [{"name": name, "version": version, "updateable": None}
|
return [
|
||||||
for name, version in (("BMC", bmc), ("BIOS", bios)) if version]
|
{"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")]
|
items = [parse_firmware(raw) for raw in self._members_of(service, "FirmwareInventory")]
|
||||||
return [{"name": fw["name"] or fw["id"] or "", "version": fw["version"] or "",
|
return [
|
||||||
"updateable": fw["updateable"]} for fw in items]
|
{
|
||||||
|
"name": fw["name"] or fw["id"] or "",
|
||||||
|
"version": fw["version"] or "",
|
||||||
|
"updateable": fw["updateable"],
|
||||||
|
}
|
||||||
|
for fw in items
|
||||||
|
]
|
||||||
|
|
||||||
def get_management_protocols(self) -> _JsonDict:
|
def get_management_protocols(self) -> _JsonDict:
|
||||||
"""What the BMC listens on. A protocol the BMC does not report counts as off."""
|
"""What the BMC listens on. A protocol the BMC does not report counts as off."""
|
||||||
|
|||||||
+16
-12
@@ -85,8 +85,7 @@ class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
|||||||
port=self.port,
|
port=self.port,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ConnectionException(f"Cannot connect to IPMI at {self.hostname}: {exc}") \
|
raise ConnectionException(f"Cannot connect to IPMI at {self.hostname}: {exc}") from exc
|
||||||
from exc
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
ipmi, self._ipmi = self._ipmi, None
|
ipmi, self._ipmi = self._ipmi, None
|
||||||
@@ -219,7 +218,8 @@ class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
|||||||
if action not in accepted:
|
if action not in accepted:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{self.hostname}: {action} not accepted while the server is "
|
f"{self.hostname}: {action} not accepted while the server is "
|
||||||
f"{state or 'in an unknown state'} (accepted: {', '.join(accepted) or 'none'})")
|
f"{state or 'in an unknown state'} (accepted: {', '.join(accepted) or 'none'})"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self._cmd().set_power(_CHASSIS_CONTROL[action], wait=False)
|
self._cmd().set_power(_CHASSIS_CONTROL[action], wait=False)
|
||||||
except IpmiException as exc:
|
except IpmiException as exc:
|
||||||
@@ -235,7 +235,8 @@ class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
|||||||
"serial_number": "",
|
"serial_number": "",
|
||||||
"capacity_watts": None,
|
"capacity_watts": None,
|
||||||
"status": _severity(int(reading.health or 0))
|
"status": _severity(int(reading.health or 0))
|
||||||
if _supply_present(reading) else "Absent",
|
if _supply_present(reading)
|
||||||
|
else "Absent",
|
||||||
}
|
}
|
||||||
for reading in self._sensors()
|
for reading in self._sensors()
|
||||||
if reading.type == "Power Supply"
|
if reading.type == "Power Supply"
|
||||||
@@ -259,14 +260,17 @@ class IpmiDriver(HealthMetricsMixin, ManagementControllerDriver):
|
|||||||
component = event.get("component") or ""
|
component = event.get("component") or ""
|
||||||
text = event.get("event") or ""
|
text = event.get("event") or ""
|
||||||
timecode = event.get("timecode")
|
timecode = event.get("timecode")
|
||||||
entries.append({
|
entries.append(
|
||||||
"id": str(event.get("record_id", "")),
|
{
|
||||||
"timestamp": float(timecode) if timecode else None,
|
"id": str(event.get("record_id", "")),
|
||||||
"severity": _severity(int(event.get("severity") or 0)),
|
"timestamp": float(timecode) if timecode else None,
|
||||||
"message": f"{component}: {text}" if component and text else component or text,
|
"severity": _severity(int(event.get("severity") or 0)),
|
||||||
})
|
"message": f"{component}: {text}" if component and text else component or text,
|
||||||
entries.sort(key=lambda e: (e["timestamp"] is not None, e["timestamp"] or 0.0),
|
}
|
||||||
reverse=True)
|
)
|
||||||
|
entries.sort(
|
||||||
|
key=lambda e: (e["timestamp"] is not None, e["timestamp"] or 0.0), reverse=True
|
||||||
|
)
|
||||||
return entries[:limit]
|
return entries[:limit]
|
||||||
|
|
||||||
def get_firmware_inventory(self) -> list[_JsonDict]:
|
def get_firmware_inventory(self) -> list[_JsonDict]:
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import boolean, text
|
from .common import boolean, text
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,24 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# DMI defaults that board vendors leave in place (Supermicro boards ship most of them).
|
# DMI defaults that board vendors leave in place (Supermicro boards ship most of them).
|
||||||
PLACEHOLDERS = frozenset({
|
PLACEHOLDERS = frozenset(
|
||||||
"to be filled by o.e.m.",
|
{
|
||||||
"default string",
|
"to be filled by o.e.m.",
|
||||||
"not specified",
|
"default string",
|
||||||
"not applicable",
|
"not specified",
|
||||||
"n/a",
|
"not applicable",
|
||||||
"none",
|
"n/a",
|
||||||
"0123456789",
|
"none",
|
||||||
"system serial number",
|
"0123456789",
|
||||||
"system product name",
|
"system serial number",
|
||||||
"system manufacturer",
|
"system product name",
|
||||||
})
|
"system manufacturer",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def text(value: Any) -> str | None:
|
def text(value: Any) -> str | None:
|
||||||
|
|||||||
@@ -16,12 +16,20 @@ left out.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import link, number, present, status, text, unique_name
|
from .common import link, number, present, status, text, unique_name
|
||||||
|
|
||||||
_SENSOR_TYPES_BY_UNITS = {"Cel": "Temperature", "V": "Voltage", "W": "Power", "A": "Current",
|
_SENSOR_TYPES_BY_UNITS = {
|
||||||
"RPM": "Rotational", "%": "Percent", "Hz": "Frequency"}
|
"Cel": "Temperature",
|
||||||
|
"V": "Voltage",
|
||||||
|
"W": "Power",
|
||||||
|
"A": "Current",
|
||||||
|
"RPM": "Rotational",
|
||||||
|
"%": "Percent",
|
||||||
|
"Hz": "Frequency",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def environment_sources(chassis: Mapping[str, Any]) -> dict[str, Any]:
|
def environment_sources(chassis: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
@@ -138,8 +146,11 @@ def parse_power(power: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
if not psu["present"]:
|
if not psu["present"]:
|
||||||
continue
|
continue
|
||||||
name = unique_name(supplies, psu["name"], item.get("MemberId", index))
|
name = unique_name(supplies, psu["name"], item.get("MemberId", index))
|
||||||
supplies[name] = {"status": psu["status"], "capacity": psu["capacity"],
|
supplies[name] = {
|
||||||
"output": psu["output"]}
|
"status": psu["status"],
|
||||||
|
"capacity": psu["capacity"],
|
||||||
|
"output": psu["output"],
|
||||||
|
}
|
||||||
|
|
||||||
voltages: dict[str, Any] = {}
|
voltages: dict[str, Any] = {}
|
||||||
for index, item in _members(power, "Voltages"):
|
for index, item in _members(power, "Voltages"):
|
||||||
@@ -204,8 +215,9 @@ def parse_fan(fan: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_power_supply(supply: Mapping[str, Any],
|
def parse_power_supply(
|
||||||
metrics: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
supply: Mapping[str, Any], metrics: Mapping[str, Any] | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""One member of ``PowerSubsystem/PowerSupplies``, with its ``Metrics`` if read.
|
"""One member of ``PowerSubsystem/PowerSupplies``, with its ``Metrics`` if read.
|
||||||
|
|
||||||
An item of the legacy ``Power.PowerSupplies`` array parses too (``MemberId`` as id,
|
An item of the legacy ``Power.PowerSupplies`` array parses too (``MemberId`` as id,
|
||||||
@@ -273,16 +285,24 @@ def parse_thermal_metrics(metrics: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
return {"temperature": temperature}
|
return {"temperature": temperature}
|
||||||
|
|
||||||
|
|
||||||
def build_environment(*, fans: Iterable[Mapping[str, Any]] = (),
|
def build_environment(
|
||||||
power_supplies: Iterable[Mapping[str, Any]] = (),
|
*,
|
||||||
sensors: Iterable[Mapping[str, Any]] = ()) -> dict[str, Any]:
|
fans: Iterable[Mapping[str, Any]] = (),
|
||||||
|
power_supplies: Iterable[Mapping[str, Any]] = (),
|
||||||
|
sensors: Iterable[Mapping[str, Any]] = (),
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Combine parsed fans, power supplies and sensors into the shape of the legacy parsers.
|
"""Combine parsed fans, power supplies and sensors into the shape of the legacy parsers.
|
||||||
|
|
||||||
``consumed_watts`` comes from the chassis total power sensor
|
``consumed_watts`` comes from the chassis total power sensor
|
||||||
(``ReadingType`` Power, ``ElectricalContext`` Total, ``PhysicalContext`` Chassis).
|
(``ReadingType`` Power, ``ElectricalContext`` Total, ``PhysicalContext`` Chassis).
|
||||||
"""
|
"""
|
||||||
env: dict[str, Any] = {"fans": {}, "temperature": {}, "power": {}, "voltages": {},
|
env: dict[str, Any] = {
|
||||||
"consumed_watts": None}
|
"fans": {},
|
||||||
|
"temperature": {},
|
||||||
|
"power": {},
|
||||||
|
"voltages": {},
|
||||||
|
"consumed_watts": None,
|
||||||
|
}
|
||||||
for fan in fans:
|
for fan in fans:
|
||||||
if fan["present"]:
|
if fan["present"]:
|
||||||
name = unique_name(env["fans"], fan["name"], fan["id"])
|
name = unique_name(env["fans"], fan["name"], fan["id"])
|
||||||
@@ -290,8 +310,11 @@ def build_environment(*, fans: Iterable[Mapping[str, Any]] = (),
|
|||||||
for psu in power_supplies:
|
for psu in power_supplies:
|
||||||
if psu["present"]:
|
if psu["present"]:
|
||||||
name = unique_name(env["power"], psu["name"], psu["id"])
|
name = unique_name(env["power"], psu["name"], psu["id"])
|
||||||
env["power"][name] = {"status": psu["status"], "capacity": psu["capacity"],
|
env["power"][name] = {
|
||||||
"output": psu["output"]}
|
"status": psu["status"],
|
||||||
|
"capacity": psu["capacity"],
|
||||||
|
"output": psu["output"],
|
||||||
|
}
|
||||||
for sensor in sensors:
|
for sensor in sensors:
|
||||||
reading = sensor["reading"]
|
reading = sensor["reading"]
|
||||||
if reading is None:
|
if reading is None:
|
||||||
@@ -303,7 +326,11 @@ def build_environment(*, fans: Iterable[Mapping[str, Any]] = (),
|
|||||||
elif sensor["reading_type"] == "Voltage":
|
elif sensor["reading_type"] == "Voltage":
|
||||||
name = unique_name(env["voltages"], sensor["name"], sensor["id"])
|
name = unique_name(env["voltages"], sensor["name"], sensor["id"])
|
||||||
env["voltages"][name] = {"voltage": reading, **flags}
|
env["voltages"][name] = {"voltage": reading, **flags}
|
||||||
elif (sensor["reading_type"] == "Power" and sensor["electrical_context"] == "Total"
|
elif (
|
||||||
and sensor["physical_context"] == "Chassis" and env["consumed_watts"] is None):
|
sensor["reading_type"] == "Power"
|
||||||
|
and sensor["electrical_context"] == "Total"
|
||||||
|
and sensor["physical_context"] == "Chassis"
|
||||||
|
and env["consumed_watts"] is None
|
||||||
|
):
|
||||||
env["consumed_watts"] = reading
|
env["consumed_watts"] = reading
|
||||||
return env
|
return env
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import boolean, link_list, status, text
|
from .common import boolean, link_list, status, text
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import boolean, ident, integer, link, link_list, location_label, present, status, text
|
from .common import boolean, ident, integer, link, link_list, location_label, present, status, text
|
||||||
|
|
||||||
@@ -39,8 +40,9 @@ def parse_memory(dimm: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"id": text(dimm.get("Id")),
|
"id": text(dimm.get("Id")),
|
||||||
"name": text(dimm.get("Name")),
|
"name": text(dimm.get("Name")),
|
||||||
"location": (location_label(dimm) or text(dimm.get("DeviceLocator"))
|
"location": (
|
||||||
or text(dimm.get("Name"))),
|
location_label(dimm) or text(dimm.get("DeviceLocator")) or text(dimm.get("Name"))
|
||||||
|
),
|
||||||
"capacity_mib": capacity,
|
"capacity_mib": capacity,
|
||||||
"memory_device_type": text(dimm.get("MemoryDeviceType")),
|
"memory_device_type": text(dimm.get("MemoryDeviceType")),
|
||||||
"base_module_type": text(dimm.get("BaseModuleType")),
|
"base_module_type": text(dimm.get("BaseModuleType")),
|
||||||
@@ -62,7 +64,8 @@ def _controller(ctrl: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
"model": ident(ctrl.get("Model")),
|
"model": ident(ctrl.get("Model")),
|
||||||
"serial_number": ident(ctrl.get("SerialNumber")),
|
"serial_number": ident(ctrl.get("SerialNumber")),
|
||||||
"firmware_version": text(ctrl.get("FirmwareVersion")),
|
"firmware_version": text(ctrl.get("FirmwareVersion")),
|
||||||
"speed_gbps": ctrl.get("SpeedGbps") if isinstance(ctrl.get("SpeedGbps"), (int, float))
|
"speed_gbps": ctrl.get("SpeedGbps")
|
||||||
|
if isinstance(ctrl.get("SpeedGbps"), (int, float))
|
||||||
else None,
|
else None,
|
||||||
"state": state,
|
"state": state,
|
||||||
"health": health,
|
"health": health,
|
||||||
@@ -83,7 +86,8 @@ def parse_storage(storage: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
"state": state,
|
"state": state,
|
||||||
"health": health,
|
"health": health,
|
||||||
"controllers": [_controller(c) for c in controllers if isinstance(c, Mapping)]
|
"controllers": [_controller(c) for c in controllers if isinstance(c, Mapping)]
|
||||||
if isinstance(controllers, list) else [],
|
if isinstance(controllers, list)
|
||||||
|
else [],
|
||||||
"controllers_path": link(storage, "Controllers"),
|
"controllers_path": link(storage, "Controllers"),
|
||||||
"drive_paths": link_list(storage, "Drives"),
|
"drive_paths": link_list(storage, "Drives"),
|
||||||
}
|
}
|
||||||
@@ -117,8 +121,11 @@ def parse_simple_storage(storage: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
state, health = status(storage)
|
state, health = status(storage)
|
||||||
devices = storage.get("Devices")
|
devices = storage.get("Devices")
|
||||||
drives = [parse_drive(dev) for dev in devices if isinstance(dev, Mapping)] \
|
drives = (
|
||||||
if isinstance(devices, list) else []
|
[parse_drive(dev) for dev in devices if isinstance(dev, Mapping)]
|
||||||
|
if isinstance(devices, list)
|
||||||
|
else []
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"id": text(storage.get("Id")),
|
"id": text(storage.get("Id")),
|
||||||
"name": text(storage.get("Name")),
|
"name": text(storage.get("Name")),
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import boolean, integer, link, status, text
|
from .common import boolean, integer, link, status, text
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import boolean, integer, link_list, links, number, status, text
|
from .common import boolean, integer, link_list, links, number, status, text
|
||||||
|
|
||||||
@@ -132,8 +133,9 @@ def parse_network_protocol(proto: Mapping[str, Any]) -> dict[str, Any]:
|
|||||||
for community in snmp.get("CommunityStrings") or []:
|
for community in snmp.get("CommunityStrings") or []:
|
||||||
if isinstance(community, Mapping):
|
if isinstance(community, Mapping):
|
||||||
# The community string itself is never kept, even if a BMC returns it.
|
# The community string itself is never kept, even if a BMC returns it.
|
||||||
communities.append({"name": text(community.get("Name")),
|
communities.append(
|
||||||
"access": text(community.get("AccessMode"))})
|
{"name": text(community.get("Name")), "access": text(community.get("AccessMode"))}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"hostname": text(proto.get("HostName")),
|
"hostname": text(proto.get("HostName")),
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import link, links, text
|
from .common import link, links, text
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .common import ident, integer, link_list, links, number, status, text
|
from .common import ident, integer, link_list, links, number, status, text
|
||||||
|
|
||||||
@@ -10,10 +11,12 @@ from .common import ident, integer, link_list, links, number, status, text
|
|||||||
def parse_system(system: Mapping[str, Any]) -> dict[str, Any]:
|
def parse_system(system: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
"""Identity, summaries and links of the managed server."""
|
"""Identity, summaries and links of the managed server."""
|
||||||
state, health = status(system)
|
state, health = status(system)
|
||||||
cpus = system.get("ProcessorSummary") if isinstance(system.get("ProcessorSummary"),
|
cpus = (
|
||||||
Mapping) else {}
|
system.get("ProcessorSummary")
|
||||||
memory = system.get("MemorySummary") if isinstance(system.get("MemorySummary"),
|
if isinstance(system.get("ProcessorSummary"), Mapping)
|
||||||
Mapping) else {}
|
else {}
|
||||||
|
)
|
||||||
|
memory = system.get("MemorySummary") if isinstance(system.get("MemorySummary"), Mapping) else {}
|
||||||
return {
|
return {
|
||||||
"id": text(system.get("Id")),
|
"id": text(system.get("Id")),
|
||||||
"name": text(system.get("Name")),
|
"name": text(system.get("Name")),
|
||||||
@@ -44,8 +47,9 @@ def parse_power_state(system: Mapping[str, Any]) -> str | None:
|
|||||||
return text(system.get("PowerState"))
|
return text(system.get("PowerState"))
|
||||||
|
|
||||||
|
|
||||||
def parse_reset_action(resource: Mapping[str, Any],
|
def parse_reset_action(
|
||||||
action: str = "#ComputerSystem.Reset") -> dict[str, Any]:
|
resource: Mapping[str, Any], action: str = "#ComputerSystem.Reset"
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Target and allowed ``ResetType`` values of a reset action.
|
"""Target and allowed ``ResetType`` values of a reset action.
|
||||||
|
|
||||||
``allowed`` is ``None`` when the service does not list the values inline. Then
|
``allowed`` is ``None`` when the service does not list the values inline. Then
|
||||||
|
|||||||
+28
-14
@@ -20,7 +20,8 @@ import ipaddress
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Mapping
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -80,8 +81,14 @@ class RedfishFeatureLicenseError(RedfishAuthError):
|
|||||||
refuses ``Systems/1/Storage`` and ``UpdateService`` for want of DCMS.
|
refuses ``Systems/1/Storage`` and ``UpdateService`` for want of DCMS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message: str, *, licenses: str = "", status: int | None = None,
|
def __init__(
|
||||||
path: str | None = None):
|
self,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
licenses: str = "",
|
||||||
|
status: int | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
):
|
||||||
super().__init__(message, status=status, path=path)
|
super().__init__(message, status=status, path=path)
|
||||||
#: The licenses the service names, e.g. "DCMS"; empty when it names none.
|
#: The licenses the service names, e.g. "DCMS"; empty when it names none.
|
||||||
self.licenses = licenses
|
self.licenses = licenses
|
||||||
@@ -270,8 +277,9 @@ class RedfishTransport:
|
|||||||
return "session"
|
return "session"
|
||||||
if self.auth == "session":
|
if self.auth == "session":
|
||||||
raise RedfishAuthError(f"{self.base_url}: session login returned no X-Auth-Token")
|
raise RedfishAuthError(f"{self.base_url}: session login returned no X-Auth-Token")
|
||||||
LOG.info("%s: session login returned no token, falling back to basic auth",
|
LOG.info(
|
||||||
self.base_url)
|
"%s: session login returned no token, falling back to basic auth", self.base_url
|
||||||
|
)
|
||||||
self._call(client.login, auth=AuthMethod.BASIC)
|
self._call(client.login, auth=AuthMethod.BASIC)
|
||||||
return "basic"
|
return "basic"
|
||||||
|
|
||||||
@@ -281,8 +289,9 @@ class RedfishTransport:
|
|||||||
except InvalidCredentialsError as exc:
|
except InvalidCredentialsError as exc:
|
||||||
raise RedfishAuthError(f"{self.base_url}: invalid credentials", status=401) from exc
|
raise RedfishAuthError(f"{self.base_url}: invalid credentials", status=401) from exc
|
||||||
except RedfishPasswordChangeRequiredError as exc:
|
except RedfishPasswordChangeRequiredError as exc:
|
||||||
raise RedfishAuthError(f"{self.base_url}: the account must change its password "
|
raise RedfishAuthError(
|
||||||
"before it can be used") from exc
|
f"{self.base_url}: the account must change its password before it can be used"
|
||||||
|
) from exc
|
||||||
except SessionCreationError as exc:
|
except SessionCreationError as exc:
|
||||||
raise _SessionUnsupported(str(exc).strip()) from exc
|
raise _SessionUnsupported(str(exc).strip()) from exc
|
||||||
except (RetriesExhaustedError, ServerDownOrUnreachableError) as exc:
|
except (RetriesExhaustedError, ServerDownOrUnreachableError) as exc:
|
||||||
@@ -300,7 +309,7 @@ class RedfishTransport:
|
|||||||
LOG.warning("%s: could not delete the Redfish session: %s", self.base_url, exc)
|
LOG.warning("%s: could not delete the Redfish session: %s", self.base_url, exc)
|
||||||
client._session.close()
|
client._session.close()
|
||||||
|
|
||||||
def __enter__(self) -> "RedfishTransport":
|
def __enter__(self) -> RedfishTransport:
|
||||||
self.open()
|
self.open()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -340,20 +349,25 @@ class RedfishTransport:
|
|||||||
data = resp.dict
|
data = resp.dict
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if status < 400:
|
if status < 400:
|
||||||
raise RedfishError(f"{method} {path}: response is not JSON",
|
raise RedfishError(
|
||||||
status=status, path=path) from exc
|
f"{method} {path}: response is not JSON", status=status, path=path
|
||||||
|
) from exc
|
||||||
data = {}
|
data = {}
|
||||||
if status < 400:
|
if status < 400:
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise RedfishError(f"{method} {path}: response is not a JSON object",
|
raise RedfishError(
|
||||||
status=status, path=path)
|
f"{method} {path}: response is not a JSON object", status=status, path=path
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
detail = _error_message(data)
|
detail = _error_message(data)
|
||||||
message = f"{method} {path}: HTTP {status}" + (f": {detail}" if detail else "")
|
message = f"{method} {path}: HTTP {status}" + (f": {detail}" if detail else "")
|
||||||
if status == 403 and any(i.endswith(LICENSE_REFUSED) for i in _message_ids(data)):
|
if status == 403 and any(i.endswith(LICENSE_REFUSED) for i in _message_ids(data)):
|
||||||
raise RedfishFeatureLicenseError(
|
raise RedfishFeatureLicenseError(
|
||||||
message, licenses=" ".join(" ".join(_message_args(data)).split()),
|
message,
|
||||||
status=status, path=path)
|
licenses=" ".join(" ".join(_message_args(data)).split()),
|
||||||
|
status=status,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
if status in (401, 403):
|
if status in (401, 403):
|
||||||
raise RedfishAuthError(message, status=status, path=path)
|
raise RedfishAuthError(message, status=status, path=path)
|
||||||
if status == 404:
|
if status == 404:
|
||||||
|
|||||||
@@ -39,3 +39,14 @@ include = ["napalm_redfish*"]
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# Recorded device output keeps its lines.
|
||||||
|
"tests/**" = ["E501"]
|
||||||
+29
-10
@@ -17,7 +17,7 @@ def fixture_file(mockup: str, path: str) -> Path:
|
|||||||
path = path.split("#", 1)[0].split("?", 1)[0].rstrip("/")
|
path = path.split("#", 1)[0].split("?", 1)[0].rstrip("/")
|
||||||
if not path.startswith(PREFIX):
|
if not path.startswith(PREFIX):
|
||||||
raise ValueError(f"not a Redfish path: {path}")
|
raise ValueError(f"not a Redfish path: {path}")
|
||||||
relative = path[len(PREFIX):].lstrip("/")
|
relative = path[len(PREFIX) :].lstrip("/")
|
||||||
return FIXTURES / mockup / relative / "index.json"
|
return FIXTURES / mockup / relative / "index.json"
|
||||||
|
|
||||||
|
|
||||||
@@ -64,8 +64,14 @@ class MockupServer:
|
|||||||
for method in (responses.GET, responses.POST, responses.DELETE):
|
for method in (responses.GET, responses.POST, responses.DELETE):
|
||||||
rsps.add_callback(method, anything, callback=self._handle)
|
rsps.add_callback(method, anything, callback=self._handle)
|
||||||
|
|
||||||
def override(self, method: str, path: str, status: int, body: dict | str | None = None,
|
def override(
|
||||||
headers: dict | None = None):
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
status: int,
|
||||||
|
body: dict | str | None = None,
|
||||||
|
headers: dict | None = None,
|
||||||
|
):
|
||||||
self.overrides[(method, path)] = (status, body if body is not None else {}, headers or {})
|
self.overrides[(method, path)] = (status, body if body is not None else {}, headers or {})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -82,8 +88,11 @@ class MockupServer:
|
|||||||
if key in self.overrides:
|
if key in self.overrides:
|
||||||
return self._reply(*self.overrides[key])
|
return self._reply(*self.overrides[key])
|
||||||
if request.method == "POST" and parts.path == SESSIONS:
|
if request.method == "POST" and parts.path == SESSIONS:
|
||||||
return self._reply(201, {"@odata.id": SESSION_PATH, "Id": "0123456789abcdef"},
|
return self._reply(
|
||||||
{"X-Auth-Token": TOKEN, "Location": BASE_URL + SESSION_PATH})
|
201,
|
||||||
|
{"@odata.id": SESSION_PATH, "Id": "0123456789abcdef"},
|
||||||
|
{"X-Auth-Token": TOKEN, "Location": BASE_URL + SESSION_PATH},
|
||||||
|
)
|
||||||
if request.method == "DELETE" and parts.path == SESSION_PATH:
|
if request.method == "DELETE" and parts.path == SESSION_PATH:
|
||||||
return self._reply(204, {})
|
return self._reply(204, {})
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
@@ -91,13 +100,23 @@ class MockupServer:
|
|||||||
return self._reply(200, load(self.mockup, parts.path))
|
return self._reply(200, load(self.mockup, parts.path))
|
||||||
except (FileNotFoundError, ValueError):
|
except (FileNotFoundError, ValueError):
|
||||||
pass
|
pass
|
||||||
return self._reply(404, {"error": {"code": "Base.1.0.ResourceMissingAtURI",
|
return self._reply(
|
||||||
"message": f"{parts.path} not found"}})
|
404,
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "Base.1.0.ResourceMissingAtURI",
|
||||||
|
"message": f"{parts.path} not found",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def calls(self, method: str | None = None, path: str | None = None):
|
def calls(self, method: str | None = None, path: str | None = None):
|
||||||
return [c for c in self.rsps.calls
|
return [
|
||||||
if (method is None or c.request.method == method)
|
c
|
||||||
and (path is None or urlsplit(c.request.url).path == path)]
|
for c in self.rsps.calls
|
||||||
|
if (method is None or c.request.method == method)
|
||||||
|
and (path is None or urlsplit(c.request.url).path == path)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -34,14 +34,39 @@ def test_account_defaults():
|
|||||||
|
|
||||||
|
|
||||||
def test_to_napalm_users(rackmount):
|
def test_to_napalm_users(rackmount):
|
||||||
accounts = [parse_account(rackmount(f"/redfish/v1/AccountService/Accounts/{i}"))
|
accounts = [
|
||||||
for i in (1, 2)]
|
parse_account(rackmount(f"/redfish/v1/AccountService/Accounts/{i}")) for i in (1, 2)
|
||||||
accounts.append({"id": "3", "username": "netork", "role_id": "ReadOnly", "enabled": True,
|
]
|
||||||
"locked": False, "account_types": []})
|
accounts.append(
|
||||||
accounts.append({"id": "4", "username": "ops", "role_id": "Operator", "enabled": False,
|
{
|
||||||
"locked": False, "account_types": []})
|
"id": "3",
|
||||||
accounts.append({"id": "5", "username": "custom", "role_id": "OemSpecial", "enabled": True,
|
"username": "netork",
|
||||||
"locked": False, "account_types": []})
|
"role_id": "ReadOnly",
|
||||||
|
"enabled": True,
|
||||||
|
"locked": False,
|
||||||
|
"account_types": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
accounts.append(
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"username": "ops",
|
||||||
|
"role_id": "Operator",
|
||||||
|
"enabled": False,
|
||||||
|
"locked": False,
|
||||||
|
"account_types": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
accounts.append(
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"username": "custom",
|
||||||
|
"role_id": "OemSpecial",
|
||||||
|
"enabled": True,
|
||||||
|
"locked": False,
|
||||||
|
"account_types": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
accounts.append(None)
|
accounts.append(None)
|
||||||
assert to_napalm_users(accounts) == {
|
assert to_napalm_users(accounts) == {
|
||||||
"Administrator": {"level": 15, "password": "", "sshkeys": []},
|
"Administrator": {"level": 15, "password": "", "sshkeys": []},
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ def test_sources_legacy_only(localstorage):
|
|||||||
|
|
||||||
|
|
||||||
def test_sources_mixed_and_missing():
|
def test_sources_mixed_and_missing():
|
||||||
src = environment_sources({
|
src = environment_sources(
|
||||||
"ThermalSubsystem": {"@odata.id": "/redfish/v1/Chassis/X/ThermalSubsystem"},
|
{
|
||||||
"Power": {"@odata.id": "/redfish/v1/Chassis/X/Power"},
|
"ThermalSubsystem": {"@odata.id": "/redfish/v1/Chassis/X/ThermalSubsystem"},
|
||||||
})
|
"Power": {"@odata.id": "/redfish/v1/Chassis/X/Power"},
|
||||||
|
}
|
||||||
|
)
|
||||||
assert src["thermal"]["schema"] == "ThermalSubsystem"
|
assert src["thermal"]["schema"] == "ThermalSubsystem"
|
||||||
assert src["power"]["schema"] == "Power"
|
assert src["power"]["schema"] == "Power"
|
||||||
assert environment_sources({}) == {"thermal": None, "power": None, "sensors": None}
|
assert environment_sources({}) == {"thermal": None, "power": None, "sensors": None}
|
||||||
@@ -68,42 +70,70 @@ def test_thermal(rackmount):
|
|||||||
"reading, health, alert, critical",
|
"reading, health, alert, critical",
|
||||||
[
|
[
|
||||||
(41, "OK", False, False),
|
(41, "OK", False, False),
|
||||||
(42, "OK", True, False), # at UpperThresholdNonCritical
|
(42, "OK", True, False), # at UpperThresholdNonCritical
|
||||||
(45, "OK", True, True), # at UpperThresholdCritical
|
(45, "OK", True, True), # at UpperThresholdCritical
|
||||||
(49, "OK", True, True), # above UpperThresholdFatal
|
(49, "OK", True, True), # above UpperThresholdFatal
|
||||||
(4, "OK", True, True), # below LowerThresholdCritical
|
(4, "OK", True, True), # below LowerThresholdCritical
|
||||||
(9, "OK", True, False), # below LowerThresholdNonCritical
|
(9, "OK", True, False), # below LowerThresholdNonCritical
|
||||||
(30, "Warning", True, False),
|
(30, "Warning", True, False),
|
||||||
(30, "Critical", True, True),
|
(30, "Critical", True, True),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_thermal_thresholds(reading, health, alert, critical):
|
def test_thermal_thresholds(reading, health, alert, critical):
|
||||||
env = parse_thermal({"Temperatures": [{
|
env = parse_thermal(
|
||||||
"Name": "T", "ReadingCelsius": reading, "Status": {"State": "Enabled", "Health": health},
|
{
|
||||||
"UpperThresholdNonCritical": 42, "UpperThresholdCritical": 45, "UpperThresholdFatal": 48,
|
"Temperatures": [
|
||||||
"LowerThresholdNonCritical": 10, "LowerThresholdCritical": 5,
|
{
|
||||||
}]})
|
"Name": "T",
|
||||||
|
"ReadingCelsius": reading,
|
||||||
|
"Status": {"State": "Enabled", "Health": health},
|
||||||
|
"UpperThresholdNonCritical": 42,
|
||||||
|
"UpperThresholdCritical": 45,
|
||||||
|
"UpperThresholdFatal": 48,
|
||||||
|
"LowerThresholdNonCritical": 10,
|
||||||
|
"LowerThresholdCritical": 5,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
assert env["temperature"]["T"] == {
|
assert env["temperature"]["T"] == {
|
||||||
"temperature": float(reading), "is_alert": alert, "is_critical": critical}
|
"temperature": float(reading),
|
||||||
|
"is_alert": alert,
|
||||||
|
"is_critical": critical,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_thermal_old_schema_names_and_duplicates():
|
def test_thermal_old_schema_names_and_duplicates():
|
||||||
# Thermal 1.0 uses FanName; Supermicro repeats names across members.
|
# Thermal 1.0 uses FanName; Supermicro repeats names across members.
|
||||||
env = parse_thermal({
|
env = parse_thermal(
|
||||||
"Fans": [
|
{
|
||||||
{"MemberId": "0", "FanName": "FAN1", "Reading": 3000, "Status": {"State": "Enabled",
|
"Fans": [
|
||||||
"Health": "OK"}},
|
{
|
||||||
{"MemberId": "1", "FanName": "FAN2", "Status": {"State": "Absent"}},
|
"MemberId": "0",
|
||||||
{"MemberId": "2", "FanName": "FAN3", "Reading": 0,
|
"FanName": "FAN1",
|
||||||
"Status": {"State": "Enabled", "Health": "Critical"}},
|
"Reading": 3000,
|
||||||
{"MemberId": "3", "FanName": "FAN1", "Reading": 2900,
|
"Status": {"State": "Enabled", "Health": "OK"},
|
||||||
"Status": {"State": "Enabled", "Health": "OK"}},
|
},
|
||||||
],
|
{"MemberId": "1", "FanName": "FAN2", "Status": {"State": "Absent"}},
|
||||||
"Temperatures": [
|
{
|
||||||
{"MemberId": "0", "Name": "System Temp", "ReadingCelsius": 30},
|
"MemberId": "2",
|
||||||
{"MemberId": "1", "Name": "System Temp", "ReadingCelsius": 31},
|
"FanName": "FAN3",
|
||||||
],
|
"Reading": 0,
|
||||||
})
|
"Status": {"State": "Enabled", "Health": "Critical"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"MemberId": "3",
|
||||||
|
"FanName": "FAN1",
|
||||||
|
"Reading": 2900,
|
||||||
|
"Status": {"State": "Enabled", "Health": "OK"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Temperatures": [
|
||||||
|
{"MemberId": "0", "Name": "System Temp", "ReadingCelsius": 30},
|
||||||
|
{"MemberId": "1", "Name": "System Temp", "ReadingCelsius": 31},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
assert env["fans"] == {
|
assert env["fans"] == {
|
||||||
"FAN1": {"status": True},
|
"FAN1": {"status": True},
|
||||||
"FAN3": {"status": False},
|
"FAN3": {"status": False},
|
||||||
@@ -126,10 +156,14 @@ def test_power(rackmount):
|
|||||||
|
|
||||||
|
|
||||||
def test_power_unknown_values_are_minus_one():
|
def test_power_unknown_values_are_minus_one():
|
||||||
env = parse_power({"PowerSupplies": [
|
env = parse_power(
|
||||||
{"MemberId": "0", "Name": "PS1", "Status": {"State": "Enabled", "Health": "OK"}},
|
{
|
||||||
{"MemberId": "1", "Name": "PS2", "Status": {"State": "Absent"}},
|
"PowerSupplies": [
|
||||||
]})
|
{"MemberId": "0", "Name": "PS1", "Status": {"State": "Enabled", "Health": "OK"}},
|
||||||
|
{"MemberId": "1", "Name": "PS2", "Status": {"State": "Absent"}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
assert env["power"] == {"PS1": {"status": True, "capacity": -1.0, "output": -1.0}}
|
assert env["power"] == {"PS1": {"status": True, "capacity": -1.0, "output": -1.0}}
|
||||||
assert env["consumed_watts"] is None
|
assert env["consumed_watts"] is None
|
||||||
assert env["voltages"] == {}
|
assert env["voltages"] == {}
|
||||||
@@ -211,8 +245,8 @@ def test_sensor_temperature_by_units(rackmount):
|
|||||||
assert s["units"] == "Cel"
|
assert s["units"] == "Cel"
|
||||||
assert s["physical_context"] == "CPU"
|
assert s["physical_context"] == "CPU"
|
||||||
assert s["health"] == "Warning"
|
assert s["health"] == "Warning"
|
||||||
assert s["is_alert"] is True # 44 >= UpperCaution 42
|
assert s["is_alert"] is True # 44 >= UpperCaution 42
|
||||||
assert s["is_critical"] is False # 44 < UpperCritical 45
|
assert s["is_critical"] is False # 44 < UpperCritical 45
|
||||||
|
|
||||||
|
|
||||||
def test_sensor_power(rackmount):
|
def test_sensor_power(rackmount):
|
||||||
@@ -234,28 +268,37 @@ def test_sensor_voltage_and_fan(rackmount):
|
|||||||
|
|
||||||
|
|
||||||
def test_sensor_without_reading():
|
def test_sensor_without_reading():
|
||||||
s = parse_sensor({"Id": "X", "Name": "X", "ReadingUnits": "Cel",
|
s = parse_sensor(
|
||||||
"Status": {"State": "Disabled"}})
|
{"Id": "X", "Name": "X", "ReadingUnits": "Cel", "Status": {"State": "Disabled"}}
|
||||||
|
)
|
||||||
assert s["reading"] is None
|
assert s["reading"] is None
|
||||||
assert s["is_alert"] is False
|
assert s["is_alert"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_thermal_metrics(rackmount):
|
def test_thermal_metrics(rackmount):
|
||||||
env = parse_thermal_metrics(rackmount(CHASSIS + "/ThermalSubsystem/ThermalMetrics"))
|
env = parse_thermal_metrics(rackmount(CHASSIS + "/ThermalSubsystem/ThermalMetrics"))
|
||||||
assert env == {"temperature": {
|
assert env == {
|
||||||
"Intake": {"temperature": 24.8, "is_alert": False, "is_critical": False},
|
"temperature": {
|
||||||
"Exhaust": {"temperature": 40.5, "is_alert": False, "is_critical": False},
|
"Intake": {"temperature": 24.8, "is_alert": False, "is_critical": False},
|
||||||
}}
|
"Exhaust": {"temperature": 40.5, "is_alert": False, "is_critical": False},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_build_environment_from_new_resources(rackmount):
|
def test_build_environment_from_new_resources(rackmount):
|
||||||
fans = [parse_fan(rackmount(f"{CHASSIS}/ThermalSubsystem/Fans/{f}"))
|
fans = [
|
||||||
for f in ("Bay1", "Bay2", "CPU1", "CPU2")]
|
parse_fan(rackmount(f"{CHASSIS}/ThermalSubsystem/Fans/{f}"))
|
||||||
|
for f in ("Bay1", "Bay2", "CPU1", "CPU2")
|
||||||
|
]
|
||||||
bay1 = f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay1"
|
bay1 = f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay1"
|
||||||
psus = [parse_power_supply(rackmount(bay1), rackmount(bay1 + "/Metrics")),
|
psus = [
|
||||||
parse_power_supply(rackmount(f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay2"))]
|
parse_power_supply(rackmount(bay1), rackmount(bay1 + "/Metrics")),
|
||||||
sensors = [parse_sensor(rackmount(f"{CHASSIS}/Sensors/{s}"))
|
parse_power_supply(rackmount(f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay2")),
|
||||||
for s in ("CPU1Temp", "IntakeTemp", "CPUFan1", "TotalPower", "PS1InputVoltage")]
|
]
|
||||||
|
sensors = [
|
||||||
|
parse_sensor(rackmount(f"{CHASSIS}/Sensors/{s}"))
|
||||||
|
for s in ("CPU1Temp", "IntakeTemp", "CPUFan1", "TotalPower", "PS1InputVoltage")
|
||||||
|
]
|
||||||
env = build_environment(fans=fans, power_supplies=psus, sensors=sensors)
|
env = build_environment(fans=fans, power_supplies=psus, sensors=sensors)
|
||||||
assert env["fans"] == {
|
assert env["fans"] == {
|
||||||
"Fan Bay 1": {"status": True},
|
"Fan Bay 1": {"status": True},
|
||||||
@@ -269,18 +312,29 @@ def test_build_environment_from_new_resources(rackmount):
|
|||||||
assert env["temperature"] == {
|
assert env["temperature"] == {
|
||||||
"CPU #1 Temperature": {"temperature": 44.0, "is_alert": True, "is_critical": False},
|
"CPU #1 Temperature": {"temperature": 44.0, "is_alert": True, "is_critical": False},
|
||||||
"Front Panel Intake Temperature": {
|
"Front Panel Intake Temperature": {
|
||||||
"temperature": 24.8, "is_alert": False, "is_critical": False},
|
"temperature": 24.8,
|
||||||
|
"is_alert": False,
|
||||||
|
"is_critical": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
assert env["consumed_watts"] == 374.0
|
assert env["consumed_watts"] == 374.0
|
||||||
assert env["voltages"] == {
|
assert env["voltages"] == {
|
||||||
"Power Supply #1 Input Voltage": {
|
"Power Supply #1 Input Voltage": {
|
||||||
"voltage": 119.27, "is_alert": False, "is_critical": False},
|
"voltage": 119.27,
|
||||||
|
"is_alert": False,
|
||||||
|
"is_critical": False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_build_environment_empty():
|
def test_build_environment_empty():
|
||||||
assert build_environment() == {
|
assert build_environment() == {
|
||||||
"fans": {}, "temperature": {}, "power": {}, "voltages": {}, "consumed_watts": None}
|
"fans": {},
|
||||||
|
"temperature": {},
|
||||||
|
"power": {},
|
||||||
|
"voltages": {},
|
||||||
|
"consumed_watts": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_power_supply_legacy_item(rackmount):
|
def test_power_supply_legacy_item(rackmount):
|
||||||
|
|||||||
@@ -83,12 +83,20 @@ def test_memory_absent(rackmount):
|
|||||||
|
|
||||||
def test_memory_older_schema():
|
def test_memory_older_schema():
|
||||||
# Memory 1.0-1.x: DeviceLocator instead of Location, speed in OperatingSpeedMhz.
|
# Memory 1.0-1.x: DeviceLocator instead of Location, speed in OperatingSpeedMhz.
|
||||||
dimm = parse_memory({
|
dimm = parse_memory(
|
||||||
"Id": "1", "Name": "Memory 1", "DeviceLocator": "P1-DIMMA1", "CapacityMiB": 16384,
|
{
|
||||||
"OperatingSpeedMhz": 2133, "Manufacturer": "Samsung ", "PartNumber": "M393A2G40DB0-CPB",
|
"Id": "1",
|
||||||
"SerialNumber": "12345678", "MemoryDeviceType": "DDR4",
|
"Name": "Memory 1",
|
||||||
"Status": {"State": "Enabled", "Health": "OK"},
|
"DeviceLocator": "P1-DIMMA1",
|
||||||
})
|
"CapacityMiB": 16384,
|
||||||
|
"OperatingSpeedMhz": 2133,
|
||||||
|
"Manufacturer": "Samsung ",
|
||||||
|
"PartNumber": "M393A2G40DB0-CPB",
|
||||||
|
"SerialNumber": "12345678",
|
||||||
|
"MemoryDeviceType": "DDR4",
|
||||||
|
"Status": {"State": "Enabled", "Health": "OK"},
|
||||||
|
}
|
||||||
|
)
|
||||||
assert dimm["location"] == "P1-DIMMA1"
|
assert dimm["location"] == "P1-DIMMA1"
|
||||||
assert dimm["speed_mhz"] == 2133
|
assert dimm["speed_mhz"] == 2133
|
||||||
assert dimm["manufacturer"] == "Samsung"
|
assert dimm["manufacturer"] == "Samsung"
|
||||||
@@ -110,16 +118,18 @@ def test_storage(localstorage):
|
|||||||
assert st["name"] == "Local Storage Controller"
|
assert st["name"] == "Local Storage Controller"
|
||||||
assert st["state"] == "Enabled"
|
assert st["state"] == "Enabled"
|
||||||
assert st["health"] == "OK"
|
assert st["health"] == "OK"
|
||||||
assert st["controllers"] == [{
|
assert st["controllers"] == [
|
||||||
"name": "Contoso Integrated RAID",
|
{
|
||||||
"manufacturer": "Contoso",
|
"name": "Contoso Integrated RAID",
|
||||||
"model": "12Gbs Integrated RAID",
|
"manufacturer": "Contoso",
|
||||||
"serial_number": "2M220100SL",
|
"model": "12Gbs Integrated RAID",
|
||||||
"firmware_version": "1.0.0.7",
|
"serial_number": "2M220100SL",
|
||||||
"speed_gbps": 12,
|
"firmware_version": "1.0.0.7",
|
||||||
"state": "Enabled",
|
"speed_gbps": 12,
|
||||||
"health": "OK",
|
"state": "Enabled",
|
||||||
}]
|
"health": "OK",
|
||||||
|
}
|
||||||
|
]
|
||||||
assert st["drive_paths"] == [
|
assert st["drive_paths"] == [
|
||||||
"/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3",
|
"/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3",
|
||||||
"/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233",
|
"/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233",
|
||||||
@@ -163,7 +173,11 @@ def test_simple_storage(rackmount):
|
|||||||
assert st["name"] == "Simple Storage Controller"
|
assert st["name"] == "Simple Storage Controller"
|
||||||
assert st["health"] == "OK"
|
assert st["health"] == "OK"
|
||||||
assert [d["name"] for d in st["drives"]] == [
|
assert [d["name"] for d in st["drives"]] == [
|
||||||
"SATA Bay 1", "SATA Bay 2", "SATA Bay 3", "SATA Bay 4"]
|
"SATA Bay 1",
|
||||||
|
"SATA Bay 2",
|
||||||
|
"SATA Bay 3",
|
||||||
|
"SATA Bay 4",
|
||||||
|
]
|
||||||
bay1, bay2, bay3, _ = st["drives"]
|
bay1, bay2, bay3, _ = st["drives"]
|
||||||
assert bay1["model"] == "3000GT8"
|
assert bay1["model"] == "3000GT8"
|
||||||
assert bay1["capacity_bytes"] == 8000000000000
|
assert bay1["capacity_bytes"] == 8000000000000
|
||||||
|
|||||||
@@ -88,29 +88,33 @@ def test_ethernet_link_state(props, is_up, is_enabled):
|
|||||||
|
|
||||||
def test_ethernet_unset_addresses_and_formats():
|
def test_ethernet_unset_addresses_and_formats():
|
||||||
# Supermicro and others report unset addresses as 0.0.0.0 or "::" and MACs in lower case.
|
# Supermicro and others report unset addresses as 0.0.0.0 or "::" and MACs in lower case.
|
||||||
eth = parse_ethernet_interface({
|
eth = parse_ethernet_interface(
|
||||||
"Id": "1",
|
{
|
||||||
"MACAddress": "0c:c4:7a:aa:bb:cc",
|
"Id": "1",
|
||||||
"SpeedMbps": None,
|
"MACAddress": "0c:c4:7a:aa:bb:cc",
|
||||||
"IPv4Addresses": [
|
"SpeedMbps": None,
|
||||||
{"Address": "0.0.0.0", "SubnetMask": "0.0.0.0"},
|
"IPv4Addresses": [
|
||||||
{"Address": "10.1.2.3", "SubnetMask": "255.255.255.0"},
|
{"Address": "0.0.0.0", "SubnetMask": "0.0.0.0"},
|
||||||
{"Address": "10.1.2.4", "SubnetMask": "garbage"},
|
{"Address": "10.1.2.3", "SubnetMask": "255.255.255.0"},
|
||||||
{"Address": None},
|
{"Address": "10.1.2.4", "SubnetMask": "garbage"},
|
||||||
],
|
{"Address": None},
|
||||||
"IPv6Addresses": [
|
],
|
||||||
{"Address": "::", "PrefixLength": 0},
|
"IPv6Addresses": [
|
||||||
{"Address": "2001:DB8::5", "PrefixLength": 64},
|
{"Address": "::", "PrefixLength": 0},
|
||||||
{"Address": "2001:db8::6"},
|
{"Address": "2001:DB8::5", "PrefixLength": 64},
|
||||||
],
|
{"Address": "2001:db8::6"},
|
||||||
})
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
assert eth["interface"]["mac_address"] == "0C:C4:7A:AA:BB:CC"
|
assert eth["interface"]["mac_address"] == "0C:C4:7A:AA:BB:CC"
|
||||||
assert eth["interface"]["speed"] == -1.0
|
assert eth["interface"]["speed"] == -1.0
|
||||||
assert eth["interface"]["mtu"] == 0
|
assert eth["interface"]["mtu"] == 0
|
||||||
assert eth["interface"]["description"] == ""
|
assert eth["interface"]["description"] == ""
|
||||||
assert eth["ipv4"] == {"10.1.2.3": {"prefix_length": 24}}
|
assert eth["ipv4"] == {"10.1.2.3": {"prefix_length": 24}}
|
||||||
assert eth["ipv6"] == {"2001:db8::5": {"prefix_length": 64},
|
assert eth["ipv6"] == {
|
||||||
"2001:db8::6": {"prefix_length": 128}}
|
"2001:db8::5": {"prefix_length": 64},
|
||||||
|
"2001:db8::6": {"prefix_length": 128},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# --- NetworkProtocol -----------------------------------------------------------------------
|
# --- NetworkProtocol -----------------------------------------------------------------------
|
||||||
@@ -133,7 +137,11 @@ def test_network_protocol(rackmount):
|
|||||||
}
|
}
|
||||||
assert np["ntp"] == {"enabled": None, "servers": []}
|
assert np["ntp"] == {"enabled": None, "servers": []}
|
||||||
assert np["snmp"] == {
|
assert np["snmp"] == {
|
||||||
"enabled": True, "port": 161, "v1": True, "v2c": False, "v3": True,
|
"enabled": True,
|
||||||
|
"port": 161,
|
||||||
|
"v1": True,
|
||||||
|
"v2c": False,
|
||||||
|
"v3": True,
|
||||||
"communities": [
|
"communities": [
|
||||||
{"name": "Read-only community", "access": "Limited"},
|
{"name": "Read-only community", "access": "Limited"},
|
||||||
{"name": "Full access community", "access": "Full"},
|
{"name": "Full access community", "access": "Full"},
|
||||||
@@ -149,8 +157,14 @@ def test_network_protocol_ntp(telemetry):
|
|||||||
}
|
}
|
||||||
assert np["protocols"]["NTP"] == {"enabled": True, "port": None}
|
assert np["protocols"]["NTP"] == {"enabled": True, "port": None}
|
||||||
# Older SNMP schema: only ProtocolEnabled and Port.
|
# Older SNMP schema: only ProtocolEnabled and Port.
|
||||||
assert np["snmp"] == {"enabled": True, "port": 161, "v1": None, "v2c": None, "v3": None,
|
assert np["snmp"] == {
|
||||||
"communities": []}
|
"enabled": True,
|
||||||
|
"port": 161,
|
||||||
|
"v1": None,
|
||||||
|
"v2c": None,
|
||||||
|
"v3": None,
|
||||||
|
"communities": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_network_protocol_port_without_flag():
|
def test_network_protocol_port_without_flag():
|
||||||
@@ -162,5 +176,11 @@ def test_network_protocol_ntp_blank_entries():
|
|||||||
servers = ["", None, " pool.ntp.org ", "pool.ntp.org"]
|
servers = ["", None, " pool.ntp.org ", "pool.ntp.org"]
|
||||||
np = parse_network_protocol({"NTP": {"ProtocolEnabled": True, "NTPServers": servers}})
|
np = parse_network_protocol({"NTP": {"ProtocolEnabled": True, "NTPServers": servers}})
|
||||||
assert np["ntp"]["servers"] == ["pool.ntp.org"]
|
assert np["ntp"]["servers"] == ["pool.ntp.org"]
|
||||||
assert np["snmp"] == {"enabled": None, "port": None, "v1": None, "v2c": None, "v3": None,
|
assert np["snmp"] == {
|
||||||
"communities": []}
|
"enabled": None,
|
||||||
|
"port": None,
|
||||||
|
"v1": None,
|
||||||
|
"v2c": None,
|
||||||
|
"v3": None,
|
||||||
|
"communities": [],
|
||||||
|
}
|
||||||
@@ -26,13 +26,15 @@ def test_service_root_links(rackmount):
|
|||||||
|
|
||||||
|
|
||||||
def test_service_root_vendor_and_oem():
|
def test_service_root_vendor_and_oem():
|
||||||
root = parse_service_root({
|
root = parse_service_root(
|
||||||
"RedfishVersion": "1.0.1",
|
{
|
||||||
"Vendor": "Supermicro",
|
"RedfishVersion": "1.0.1",
|
||||||
"Product": "X10DRL-i",
|
"Vendor": "Supermicro",
|
||||||
"Oem": {"Supermicro": {"@odata.type": "#SmcServiceRoot"}},
|
"Product": "X10DRL-i",
|
||||||
"Systems": {"@odata.id": "/redfish/v1/Systems"},
|
"Oem": {"Supermicro": {"@odata.type": "#SmcServiceRoot"}},
|
||||||
})
|
"Systems": {"@odata.id": "/redfish/v1/Systems"},
|
||||||
|
}
|
||||||
|
)
|
||||||
assert root["vendor"] == "Supermicro"
|
assert root["vendor"] == "Supermicro"
|
||||||
assert root["product"] == "X10DRL-i"
|
assert root["product"] == "X10DRL-i"
|
||||||
assert root["oem_vendors"] == ["Supermicro"]
|
assert root["oem_vendors"] == ["Supermicro"]
|
||||||
|
|||||||
@@ -47,16 +47,18 @@ def test_system_links(rackmount):
|
|||||||
|
|
||||||
def test_system_placeholders_and_blanks_are_none():
|
def test_system_placeholders_and_blanks_are_none():
|
||||||
# Supermicro boards report the DMI defaults the board vendor never filled in.
|
# Supermicro boards report the DMI defaults the board vendor never filled in.
|
||||||
s = parse_system({
|
s = parse_system(
|
||||||
"Id": "1",
|
{
|
||||||
"Manufacturer": "Supermicro",
|
"Id": "1",
|
||||||
"Model": "X10DRL-i",
|
"Manufacturer": "Supermicro",
|
||||||
"SerialNumber": "0123456789",
|
"Model": "X10DRL-i",
|
||||||
"SKU": "To Be Filled By O.E.M.",
|
"SerialNumber": "0123456789",
|
||||||
"HostName": "",
|
"SKU": "To Be Filled By O.E.M.",
|
||||||
"UUID": " ",
|
"HostName": "",
|
||||||
"PartNumber": "Default string",
|
"UUID": " ",
|
||||||
})
|
"PartNumber": "Default string",
|
||||||
|
}
|
||||||
|
)
|
||||||
assert s["manufacturer"] == "Supermicro"
|
assert s["manufacturer"] == "Supermicro"
|
||||||
assert s["model"] == "X10DRL-i"
|
assert s["model"] == "X10DRL-i"
|
||||||
assert s["serial_number"] is None
|
assert s["serial_number"] is None
|
||||||
@@ -88,8 +90,15 @@ def test_reset_action_inline_allowable_values(rackmount):
|
|||||||
reset = parse_reset_action(rackmount(SYSTEM))
|
reset = parse_reset_action(rackmount(SYSTEM))
|
||||||
assert reset["target"] == SYSTEM + "/Actions/ComputerSystem.Reset"
|
assert reset["target"] == SYSTEM + "/Actions/ComputerSystem.Reset"
|
||||||
assert reset["allowed"] == [
|
assert reset["allowed"] == [
|
||||||
"On", "ForceOff", "GracefulShutdown", "GracefulRestart", "ForceRestart",
|
"On",
|
||||||
"Nmi", "ForceOn", "PushPowerButton", "FullPowerCycle",
|
"ForceOff",
|
||||||
|
"GracefulShutdown",
|
||||||
|
"GracefulRestart",
|
||||||
|
"ForceRestart",
|
||||||
|
"Nmi",
|
||||||
|
"ForceOn",
|
||||||
|
"PushPowerButton",
|
||||||
|
"FullPowerCycle",
|
||||||
]
|
]
|
||||||
assert reset["action_info"] is None
|
assert reset["action_info"] is None
|
||||||
|
|
||||||
@@ -104,13 +113,25 @@ def test_reset_action_with_action_info(telemetry):
|
|||||||
|
|
||||||
def test_action_info(telemetry):
|
def test_action_info(telemetry):
|
||||||
allowed = parse_action_info(telemetry("/redfish/v1/Systems/1/ResetActionInfo"))
|
allowed = parse_action_info(telemetry("/redfish/v1/Systems/1/ResetActionInfo"))
|
||||||
assert allowed == ["On", "ForceOff", "GracefulShutdown", "GracefulRestart",
|
assert allowed == [
|
||||||
"ForceRestart", "Nmi", "ForceOn", "PushPowerButton"]
|
"On",
|
||||||
|
"ForceOff",
|
||||||
|
"GracefulShutdown",
|
||||||
|
"GracefulRestart",
|
||||||
|
"ForceRestart",
|
||||||
|
"Nmi",
|
||||||
|
"ForceOn",
|
||||||
|
"PushPowerButton",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_action_info_other_parameter():
|
def test_action_info_other_parameter():
|
||||||
info = {"Parameters": [{"Name": "ResetType", "AllowableValues": ["On"]},
|
info = {
|
||||||
{"Name": "Delay", "AllowableValues": ["0", "5"]}]}
|
"Parameters": [
|
||||||
|
{"Name": "ResetType", "AllowableValues": ["On"]},
|
||||||
|
{"Name": "Delay", "AllowableValues": ["0", "5"]},
|
||||||
|
]
|
||||||
|
}
|
||||||
assert parse_action_info(info, "Delay") == ["0", "5"]
|
assert parse_action_info(info, "Delay") == ["0", "5"]
|
||||||
assert parse_action_info(info, "Missing") is None
|
assert parse_action_info(info, "Missing") is None
|
||||||
assert parse_action_info({}) is None
|
assert parse_action_info({}) is None
|
||||||
|
|||||||
+20
-10
@@ -35,8 +35,9 @@ def fake_pyghmi(monkeypatch):
|
|||||||
def x10(http):
|
def x10(http):
|
||||||
server = MockupServer(http, MOCKUP)
|
server = MockupServer(http, MOCKUP)
|
||||||
for path, status in json.loads((FIXTURES / MOCKUP / "STATUS.json").read_text()).items():
|
for path, status in json.loads((FIXTURES / MOCKUP / "STATUS.json").read_text()).items():
|
||||||
body = json.loads((FIXTURES / MOCKUP / path[len("/redfish/v1/"):] / "index.json")
|
body = json.loads(
|
||||||
.read_text())
|
(FIXTURES / MOCKUP / path[len("/redfish/v1/") :] / "index.json").read_text()
|
||||||
|
)
|
||||||
server.override("GET", path, status, body)
|
server.override("GET", path, status, body)
|
||||||
return server
|
return server
|
||||||
|
|
||||||
@@ -49,8 +50,9 @@ def _open(**optional):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def unlicensed(x10):
|
def unlicensed(x10):
|
||||||
x10.override("GET", "/redfish/v1/", 403,
|
x10.override(
|
||||||
json.loads((X10 / "service_root_403.json").read_text()))
|
"GET", "/redfish/v1/", 403, json.loads((X10 / "service_root_403.json").read_text())
|
||||||
|
)
|
||||||
d = _open()
|
d = _open()
|
||||||
yield d
|
yield d
|
||||||
d.close()
|
d.close()
|
||||||
@@ -84,7 +86,11 @@ def test_redfish_is_used_and_ipmi_is_not_even_opened(x10):
|
|||||||
assert d.get_facts()["model"] == "X10DRL-i"
|
assert d.get_facts()["model"] == "X10DRL-i"
|
||||||
assert len(d.get_hardware_inventory()["cpus"]) == 2
|
assert len(d.get_hardware_inventory()["cpus"]) == 2
|
||||||
assert d.get_power_state()["actions"] == [
|
assert d.get_power_state()["actions"] == [
|
||||||
"GracefulShutdown", "ForceOff", "GracefulRestart", "ForceRestart"]
|
"GracefulShutdown",
|
||||||
|
"ForceOff",
|
||||||
|
"GracefulRestart",
|
||||||
|
"ForceRestart",
|
||||||
|
]
|
||||||
finally:
|
finally:
|
||||||
d.close()
|
d.close()
|
||||||
assert FakeCommand.instances == []
|
assert FakeCommand.instances == []
|
||||||
@@ -117,10 +123,12 @@ def test_the_ipmi_port_is_its_own(x10):
|
|||||||
def test_the_partial_license_warning_passes_through(x10):
|
def test_the_partial_license_warning_passes_through(x10):
|
||||||
d = _open()
|
d = _open()
|
||||||
try:
|
try:
|
||||||
assert d.get_device_warnings() == [{
|
assert d.get_device_warnings() == [
|
||||||
"code": "bmc_license_partial",
|
{
|
||||||
"meta": {"licenses": "DCMS", "parts": ["drives", "firmware inventory"]},
|
"code": "bmc_license_partial",
|
||||||
}]
|
"meta": {"licenses": "DCMS", "parts": ["drives", "firmware inventory"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
finally:
|
finally:
|
||||||
d.close()
|
d.close()
|
||||||
|
|
||||||
@@ -134,7 +142,9 @@ def test_an_unlicensed_bmc_is_read_over_ipmi(unlicensed):
|
|||||||
assert facts["model"] == "X10DRL-i"
|
assert facts["model"] == "X10DRL-i"
|
||||||
assert facts["os_version"] == "3.91"
|
assert facts["os_version"] == "3.91"
|
||||||
assert unlicensed.get_power_state() == {
|
assert unlicensed.get_power_state() == {
|
||||||
"state": "On", "actions": ["GracefulShutdown", "ForceOff", "ForceRestart"]}
|
"state": "On",
|
||||||
|
"actions": ["GracefulShutdown", "ForceOff", "ForceRestart"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_an_unlicensed_bmc_switches_power_over_ipmi(unlicensed):
|
def test_an_unlicensed_bmc_switches_power_over_ipmi(unlicensed):
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def test_idrac_and_ilo_landing_pages_are_recognised():
|
|||||||
|
|
||||||
|
|
||||||
def test_a_switch_page_scores_nothing():
|
def test_a_switch_page_scores_nothing():
|
||||||
page = '<html><head><title>Loading...</title></head><body>Web Switch</body></html>'
|
page = "<html><head><title>Loading...</title></head><body>Web Switch</body></html>"
|
||||||
assert _score(page) == 0.0
|
assert _score(page) == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+212
-73
@@ -34,10 +34,22 @@ def ts(iso: str) -> float:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def driver(server):
|
def driver(server):
|
||||||
# The mockup's system log advertises a nextLink page it does not contain.
|
# The mockup's system log advertises a nextLink page it does not contain.
|
||||||
server.override("GET", SYSTEM_LOG + "?$skiptoken=2", 200, {
|
server.override(
|
||||||
"Members": [{"@odata.id": SYSTEM_LOG + "/3", "Id": "3", "Severity": "Warning",
|
"GET",
|
||||||
"Created": "2012-03-07T14:46:00Z", "Message": "Fan redundancy lost"}],
|
SYSTEM_LOG + "?$skiptoken=2",
|
||||||
})
|
200,
|
||||||
|
{
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"@odata.id": SYSTEM_LOG + "/3",
|
||||||
|
"Id": "3",
|
||||||
|
"Severity": "Warning",
|
||||||
|
"Created": "2012-03-07T14:46:00Z",
|
||||||
|
"Message": "Fan redundancy lost",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
|
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
|
||||||
d.open()
|
d.open()
|
||||||
yield d
|
yield d
|
||||||
@@ -59,9 +71,15 @@ def test_role():
|
|||||||
assert primary_role_of(RedfishDriver) == "management_controller"
|
assert primary_role_of(RedfishDriver) == "management_controller"
|
||||||
assert RedfishDriver.USES_SSH is False
|
assert RedfishDriver.USES_SSH is False
|
||||||
assert RedfishDriver.DRIVER_NAME == "redfish"
|
assert RedfishDriver.DRIVER_NAME == "redfish"
|
||||||
for method in ("get_power_state", "set_power_state", "get_hardware_inventory",
|
for method in (
|
||||||
"get_event_log", "get_firmware_inventory", "get_management_protocols",
|
"get_power_state",
|
||||||
"reboot_host"):
|
"set_power_state",
|
||||||
|
"get_hardware_inventory",
|
||||||
|
"get_event_log",
|
||||||
|
"get_firmware_inventory",
|
||||||
|
"get_management_protocols",
|
||||||
|
"reboot_host",
|
||||||
|
):
|
||||||
assert hasattr(RedfishDriver, method), method
|
assert hasattr(RedfishDriver, method), method
|
||||||
|
|
||||||
|
|
||||||
@@ -92,8 +110,13 @@ def test_open_failure_is_connection_exception(server):
|
|||||||
|
|
||||||
|
|
||||||
def test_optional_args(server):
|
def test_optional_args(server):
|
||||||
d = RedfishDriver("bmc.example.net", "u", "p", timeout=12,
|
d = RedfishDriver(
|
||||||
optional_args={"port": 8443, "verify_ssl": False, "auth": "basic"})
|
"bmc.example.net",
|
||||||
|
"u",
|
||||||
|
"p",
|
||||||
|
timeout=12,
|
||||||
|
optional_args={"port": 8443, "verify_ssl": False, "auth": "basic"},
|
||||||
|
)
|
||||||
assert d.transport.base_url == "https://bmc.example.net:8443"
|
assert d.transport.base_url == "https://bmc.example.net:8443"
|
||||||
assert d.transport.verify is False
|
assert d.transport.verify is False
|
||||||
assert d.transport.auth == "basic"
|
assert d.transport.auth == "basic"
|
||||||
@@ -141,10 +164,14 @@ def test_get_interfaces(driver):
|
|||||||
|
|
||||||
def test_get_interfaces_ip(driver):
|
def test_get_interfaces_ip(driver):
|
||||||
assert driver.get_interfaces_ip() == {
|
assert driver.get_interfaces_ip() == {
|
||||||
"eth0": {"ipv4": {"192.168.0.10": {"prefix_length": 22}},
|
"eth0": {
|
||||||
"ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}}},
|
"ipv4": {"192.168.0.10": {"prefix_length": 22}},
|
||||||
"ToHost": {"ipv4": {"192.168.20.55": {"prefix_length": 24}},
|
"ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}},
|
||||||
"ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}}},
|
},
|
||||||
|
"ToHost": {
|
||||||
|
"ipv4": {"192.168.20.55": {"prefix_length": 24}},
|
||||||
|
"ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -164,23 +191,33 @@ def test_get_environment_new_resources(driver, server):
|
|||||||
}
|
}
|
||||||
assert len(env["temperature"]) == 8
|
assert len(env["temperature"]) == 8
|
||||||
assert env["temperature"]["CPU #1 Temperature"] == {
|
assert env["temperature"]["CPU #1 Temperature"] == {
|
||||||
"temperature": 44.0, "is_alert": True, "is_critical": False}
|
"temperature": 44.0,
|
||||||
|
"is_alert": True,
|
||||||
|
"is_critical": False,
|
||||||
|
}
|
||||||
# The legacy resources were not read.
|
# The legacy resources were not read.
|
||||||
assert not server.calls("GET", CHASSIS + "/Thermal")
|
assert not server.calls("GET", CHASSIS + "/Thermal")
|
||||||
assert not server.calls("GET", CHASSIS + "/Power")
|
assert not server.calls("GET", CHASSIS + "/Power")
|
||||||
|
|
||||||
|
|
||||||
def test_get_environment_legacy(driver, server):
|
def test_get_environment_legacy(driver, server):
|
||||||
server.override("GET", CHASSIS, 200,
|
server.override(
|
||||||
without(CHASSIS, "ThermalSubsystem", "PowerSubsystem", "Sensors"))
|
"GET", CHASSIS, 200, without(CHASSIS, "ThermalSubsystem", "PowerSubsystem", "Sensors")
|
||||||
|
)
|
||||||
env = driver.get_environment()
|
env = driver.get_environment()
|
||||||
assert_shape(env, napalm_models.EnvironmentDict)
|
assert_shape(env, napalm_models.EnvironmentDict)
|
||||||
assert env["fans"] == {"BaseBoard System Fan": {"status": True},
|
assert env["fans"] == {
|
||||||
"BaseBoard System Fan Backup": {"status": True}}
|
"BaseBoard System Fan": {"status": True},
|
||||||
|
"BaseBoard System Fan Backup": {"status": True},
|
||||||
|
}
|
||||||
assert env["temperature"]["CPU1 Temp"] == {
|
assert env["temperature"]["CPU1 Temp"] == {
|
||||||
"temperature": 41.0, "is_alert": False, "is_critical": False}
|
"temperature": 41.0,
|
||||||
assert env["power"] == {"Power Supply Bay": {"status": False, "capacity": 800.0,
|
"is_alert": False,
|
||||||
"output": 325.0}}
|
"is_critical": False,
|
||||||
|
}
|
||||||
|
assert env["power"] == {
|
||||||
|
"Power Supply Bay": {"status": False, "capacity": 800.0, "output": 325.0}
|
||||||
|
}
|
||||||
assert not server.calls("GET", CHASSIS + "/Sensors")
|
assert not server.calls("GET", CHASSIS + "/Sensors")
|
||||||
|
|
||||||
|
|
||||||
@@ -202,10 +239,17 @@ def test_get_users(driver):
|
|||||||
|
|
||||||
def test_get_ntp_servers(driver, server):
|
def test_get_ntp_servers(driver, server):
|
||||||
assert driver.get_ntp_servers() == {}
|
assert driver.get_ntp_servers() == {}
|
||||||
server.override("GET", MANAGER + "/NetworkProtocol", 200,
|
server.override(
|
||||||
load("dmtf-public-telemetry", "/redfish/v1/Managers/1/NetworkProtocol"))
|
"GET",
|
||||||
|
MANAGER + "/NetworkProtocol",
|
||||||
|
200,
|
||||||
|
load("dmtf-public-telemetry", "/redfish/v1/Managers/1/NetworkProtocol"),
|
||||||
|
)
|
||||||
assert driver.get_ntp_servers() == {
|
assert driver.get_ntp_servers() == {
|
||||||
"0.pool.ntp.org": {}, "1.pool.ntp.org": {}, "time-a.nist.gov": {}}
|
"0.pool.ntp.org": {},
|
||||||
|
"1.pool.ntp.org": {},
|
||||||
|
"time-a.nist.gov": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_snmp_information(driver):
|
def test_get_snmp_information(driver):
|
||||||
@@ -234,9 +278,10 @@ def test_get_power_state(driver):
|
|||||||
state = driver.get_power_state()
|
state = driver.get_power_state()
|
||||||
assert_shape(state, models.PowerStateDict)
|
assert_shape(state, models.PowerStateDict)
|
||||||
# On: "On" is not offered; Redfish-only values (Nmi, ForceOn, FullPowerCycle) are dropped.
|
# On: "On" is not offered; Redfish-only values (Nmi, ForceOn, FullPowerCycle) are dropped.
|
||||||
assert state == {"state": "On",
|
assert state == {
|
||||||
"actions": ["GracefulShutdown", "ForceOff", "GracefulRestart",
|
"state": "On",
|
||||||
"ForceRestart"]}
|
"actions": ["GracefulShutdown", "ForceOff", "GracefulRestart", "ForceRestart"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_power_state_off(driver, server):
|
def test_get_power_state_off(driver, server):
|
||||||
@@ -252,8 +297,16 @@ def test_get_power_state_from_action_info(driver, server):
|
|||||||
del reset["ResetType@Redfish.AllowableValues"]
|
del reset["ResetType@Redfish.AllowableValues"]
|
||||||
reset["@Redfish.ActionInfo"] = SYSTEM + "/ResetActionInfo"
|
reset["@Redfish.ActionInfo"] = SYSTEM + "/ResetActionInfo"
|
||||||
server.override("GET", SYSTEM, 200, system)
|
server.override("GET", SYSTEM, 200, system)
|
||||||
server.override("GET", SYSTEM + "/ResetActionInfo", 200, {"Parameters": [
|
server.override(
|
||||||
{"Name": "ResetType", "AllowableValues": ["On", "ForceOff", "PowerCycle"]}]})
|
"GET",
|
||||||
|
SYSTEM + "/ResetActionInfo",
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Parameters": [
|
||||||
|
{"Name": "ResetType", "AllowableValues": ["On", "ForceOff", "PowerCycle"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
assert driver.get_power_state()["actions"] == ["ForceOff", "PowerCycle"]
|
assert driver.get_power_state()["actions"] == ["ForceOff", "PowerCycle"]
|
||||||
|
|
||||||
|
|
||||||
@@ -263,7 +316,12 @@ def test_get_power_state_unlisted_values(driver, server):
|
|||||||
del system["Actions"]["#ComputerSystem.Reset"]["ResetType@Redfish.AllowableValues"]
|
del system["Actions"]["#ComputerSystem.Reset"]["ResetType@Redfish.AllowableValues"]
|
||||||
server.override("GET", SYSTEM, 200, system)
|
server.override("GET", SYSTEM, 200, system)
|
||||||
assert driver.get_power_state()["actions"] == [
|
assert driver.get_power_state()["actions"] == [
|
||||||
"GracefulShutdown", "ForceOff", "GracefulRestart", "ForceRestart", "PowerCycle"]
|
"GracefulShutdown",
|
||||||
|
"ForceOff",
|
||||||
|
"GracefulRestart",
|
||||||
|
"ForceRestart",
|
||||||
|
"PowerCycle",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_get_power_state_without_reset_action(driver, server):
|
def test_get_power_state_without_reset_action(driver, server):
|
||||||
@@ -351,42 +409,73 @@ def test_get_hardware_inventory(driver):
|
|||||||
assert_shape(cpu, models.BmcCpuDict)
|
assert_shape(cpu, models.BmcCpuDict)
|
||||||
# The FPGA is a processor but not a CPU.
|
# The FPGA is a processor but not a CPU.
|
||||||
assert inv["cpus"] == [
|
assert inv["cpus"] == [
|
||||||
{"socket": "CPU 1", "model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series",
|
{
|
||||||
"cores": 8, "threads": 16, "status": "OK"},
|
"socket": "CPU 1",
|
||||||
|
"model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series",
|
||||||
|
"cores": 8,
|
||||||
|
"threads": 16,
|
||||||
|
"status": "OK",
|
||||||
|
},
|
||||||
{"socket": "CPU 2", "model": "", "cores": None, "threads": None, "status": "Absent"},
|
{"socket": "CPU 2", "model": "", "cores": None, "threads": None, "status": "Absent"},
|
||||||
]
|
]
|
||||||
|
|
||||||
for dimm in inv["memory"]:
|
for dimm in inv["memory"]:
|
||||||
assert_shape(dimm, models.BmcDimmDict)
|
assert_shape(dimm, models.BmcDimmDict)
|
||||||
assert [d["slot"] for d in inv["memory"]] == ["DIMM 1", "DIMM 2", "DIMM 3", "DIMM 4"]
|
assert [d["slot"] for d in inv["memory"]] == ["DIMM 1", "DIMM 2", "DIMM 3", "DIMM 4"]
|
||||||
assert inv["memory"][0] == {"slot": "DIMM 1", "size_bytes": 32768 * 1024 * 1024,
|
assert inv["memory"][0] == {
|
||||||
"type": "DDR4", "speed_mts": None, "part_number": "",
|
"slot": "DIMM 1",
|
||||||
"serial_number": "", "status": "OK"}
|
"size_bytes": 32768 * 1024 * 1024,
|
||||||
|
"type": "DDR4",
|
||||||
|
"speed_mts": None,
|
||||||
|
"part_number": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"status": "OK",
|
||||||
|
}
|
||||||
assert inv["memory"][3]["status"] == "Absent"
|
assert inv["memory"][3]["status"] == "Absent"
|
||||||
|
|
||||||
for drive in inv["drives"]:
|
for drive in inv["drives"]:
|
||||||
assert_shape(drive, models.BmcDriveDict)
|
assert_shape(drive, models.BmcDriveDict)
|
||||||
# This system has SimpleStorage only.
|
# This system has SimpleStorage only.
|
||||||
assert inv["drives"][0] == {"slot": "SATA Bay 1", "model": "3000GT8", "serial_number": "",
|
assert inv["drives"][0] == {
|
||||||
"size_bytes": 8000000000000, "media": "", "protocol": "",
|
"slot": "SATA Bay 1",
|
||||||
"status": "OK"}
|
"model": "3000GT8",
|
||||||
|
"serial_number": "",
|
||||||
|
"size_bytes": 8000000000000,
|
||||||
|
"media": "",
|
||||||
|
"protocol": "",
|
||||||
|
"status": "OK",
|
||||||
|
}
|
||||||
assert [d["status"] for d in inv["drives"]] == ["OK", "Warning", "Absent", "Absent"]
|
assert [d["status"] for d in inv["drives"]] == ["OK", "Warning", "Absent", "Absent"]
|
||||||
|
|
||||||
for psu in inv["power_supplies"]:
|
for psu in inv["power_supplies"]:
|
||||||
assert_shape(psu, models.BmcPsuDict)
|
assert_shape(psu, models.BmcPsuDict)
|
||||||
assert inv["power_supplies"] == [
|
assert inv["power_supplies"] == [
|
||||||
{"slot": "Power Supply Bay 1", "model": "RKS-440DC", "serial_number": "3488247",
|
{
|
||||||
"capacity_watts": 400, "status": "Warning"},
|
"slot": "Power Supply Bay 1",
|
||||||
{"slot": "Power Supply Bay 2", "model": "", "serial_number": "",
|
"model": "RKS-440DC",
|
||||||
"capacity_watts": None, "status": "Absent"},
|
"serial_number": "3488247",
|
||||||
|
"capacity_watts": 400,
|
||||||
|
"status": "Warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slot": "Power Supply Bay 2",
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"capacity_watts": None,
|
||||||
|
"status": "Absent",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for nic in inv["nics"]:
|
for nic in inv["nics"]:
|
||||||
assert_shape(nic, models.BmcNicDict)
|
assert_shape(nic, models.BmcNicDict)
|
||||||
nics = {n["name"]: n for n in inv["nics"]}
|
nics = {n["name"]: n for n in inv["nics"]}
|
||||||
assert set(nics) == {"12446A3B0411", "12446A3B8890", "ToManager", "VLAN1"}
|
assert set(nics) == {"12446A3B0411", "12446A3B8890", "ToManager", "VLAN1"}
|
||||||
assert nics["12446A3B0411"] == {"name": "12446A3B0411", "mac_address": "12:44:6a:3b:04:11",
|
assert nics["12446A3B0411"] == {
|
||||||
"speed_mbps": 1000, "link_up": True}
|
"name": "12446A3B0411",
|
||||||
|
"mac_address": "12:44:6a:3b:04:11",
|
||||||
|
"speed_mbps": 1000,
|
||||||
|
"link_up": True,
|
||||||
|
}
|
||||||
assert nics["ToManager"]["link_up"] is None
|
assert nics["ToManager"]["link_up"] is None
|
||||||
|
|
||||||
|
|
||||||
@@ -396,25 +485,38 @@ def test_get_hardware_inventory_storage_drives(driver, server):
|
|||||||
del system["SimpleStorage"]
|
del system["SimpleStorage"]
|
||||||
system["Storage"] = {"@odata.id": SYSTEM + "/Storage"}
|
system["Storage"] = {"@odata.id": SYSTEM + "/Storage"}
|
||||||
server.override("GET", SYSTEM, 200, system)
|
server.override("GET", SYSTEM, 200, system)
|
||||||
server.override("GET", SYSTEM + "/Storage", 200,
|
server.override(
|
||||||
load("dmtf-public-localstorage", SYSTEM + "/Storage"))
|
"GET", SYSTEM + "/Storage", 200, load("dmtf-public-localstorage", SYSTEM + "/Storage")
|
||||||
server.override("GET", SYSTEM + "/Storage/1", 200,
|
)
|
||||||
load("dmtf-public-localstorage", SYSTEM + "/Storage/1"))
|
server.override(
|
||||||
|
"GET", SYSTEM + "/Storage/1", 200, load("dmtf-public-localstorage", SYSTEM + "/Storage/1")
|
||||||
|
)
|
||||||
for drive in ("35D38F11ACEF7BD3", "3F5A8C54207B7233", "32ADF365C6C1B7BD", "3D58ECBC375FD9F2"):
|
for drive in ("35D38F11ACEF7BD3", "3F5A8C54207B7233", "32ADF365C6C1B7BD", "3D58ECBC375FD9F2"):
|
||||||
path = f"{CHASSIS}/Drives/{drive}"
|
path = f"{CHASSIS}/Drives/{drive}"
|
||||||
server.override("GET", path, 200, load("dmtf-public-localstorage", path))
|
server.override("GET", path, 200, load("dmtf-public-localstorage", path))
|
||||||
drives = driver.get_hardware_inventory()["drives"]
|
drives = driver.get_hardware_inventory()["drives"]
|
||||||
assert len(drives) == 4
|
assert len(drives) == 4
|
||||||
assert drives[0] == {"slot": "Drive Sample", "model": "C123", "serial_number": "1234567",
|
assert drives[0] == {
|
||||||
"size_bytes": 899527000064, "media": "HDD", "protocol": "SAS",
|
"slot": "Drive Sample",
|
||||||
"status": "OK"}
|
"model": "C123",
|
||||||
|
"serial_number": "1234567",
|
||||||
|
"size_bytes": 899527000064,
|
||||||
|
"media": "HDD",
|
||||||
|
"protocol": "SAS",
|
||||||
|
"status": "OK",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_hardware_inventory_legacy_power(driver, server):
|
def test_get_hardware_inventory_legacy_power(driver, server):
|
||||||
server.override("GET", CHASSIS, 200, without(CHASSIS, "PowerSubsystem"))
|
server.override("GET", CHASSIS, 200, without(CHASSIS, "PowerSubsystem"))
|
||||||
assert driver.get_hardware_inventory()["power_supplies"] == [
|
assert driver.get_hardware_inventory()["power_supplies"] == [
|
||||||
{"slot": "Power Supply Bay", "model": "499253-B21", "serial_number": "1Z0000001",
|
{
|
||||||
"capacity_watts": 800, "status": "Warning"},
|
"slot": "Power Supply Bay",
|
||||||
|
"model": "499253-B21",
|
||||||
|
"serial_number": "1Z0000001",
|
||||||
|
"capacity_watts": 800,
|
||||||
|
"status": "Warning",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -423,14 +525,30 @@ def test_get_event_log(driver):
|
|||||||
for entry in log:
|
for entry in log:
|
||||||
assert_shape(entry, models.EventLogEntryDict)
|
assert_shape(entry, models.EventLogEntryDict)
|
||||||
assert log == [
|
assert log == [
|
||||||
{"id": SYSTEM_LOG + "/3", "timestamp": ts("2012-03-07T14:46:00Z"),
|
{
|
||||||
"severity": "Warning", "message": "Fan redundancy lost"},
|
"id": SYSTEM_LOG + "/3",
|
||||||
{"id": SYSTEM_LOG + "/2", "timestamp": ts("2012-03-07T14:45:00Z"),
|
"timestamp": ts("2012-03-07T14:46:00Z"),
|
||||||
"severity": "Critical", "message": "Temperature threshold exceeded"},
|
"severity": "Warning",
|
||||||
{"id": SYSTEM_LOG + "/1", "timestamp": ts("2012-03-07T14:44:00Z"),
|
"message": "Fan redundancy lost",
|
||||||
"severity": "Critical", "message": "Temperature threshold exceeded"},
|
},
|
||||||
{"id": MANAGER + "/LogServices/Log/Entries/1", "timestamp": ts("2012-03-07T14:44:00Z"),
|
{
|
||||||
"severity": "Critical", "message": "System May be Melting"},
|
"id": SYSTEM_LOG + "/2",
|
||||||
|
"timestamp": ts("2012-03-07T14:45:00Z"),
|
||||||
|
"severity": "Critical",
|
||||||
|
"message": "Temperature threshold exceeded",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": SYSTEM_LOG + "/1",
|
||||||
|
"timestamp": ts("2012-03-07T14:44:00Z"),
|
||||||
|
"severity": "Critical",
|
||||||
|
"message": "Temperature threshold exceeded",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": MANAGER + "/LogServices/Log/Entries/1",
|
||||||
|
"timestamp": ts("2012-03-07T14:44:00Z"),
|
||||||
|
"severity": "Critical",
|
||||||
|
"message": "System May be Melting",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -441,11 +559,18 @@ def test_get_event_log_limit(driver):
|
|||||||
|
|
||||||
def test_get_event_log_timestamps(driver, server):
|
def test_get_event_log_timestamps(driver, server):
|
||||||
entries = SYSTEM_LOG + "?$skiptoken=2"
|
entries = SYSTEM_LOG + "?$skiptoken=2"
|
||||||
server.override("GET", entries, 200, {"Members": [
|
server.override(
|
||||||
{"Id": "3", "Created": "2026-10-13T08:00:00+02:00", "Message": "offset"},
|
"GET",
|
||||||
{"Id": "4", "Message": "no time", "Severity": "Informational"},
|
entries,
|
||||||
{"Id": "5", "Created": "garbage", "Message": "bad time"},
|
200,
|
||||||
]})
|
{
|
||||||
|
"Members": [
|
||||||
|
{"Id": "3", "Created": "2026-10-13T08:00:00+02:00", "Message": "offset"},
|
||||||
|
{"Id": "4", "Message": "no time", "Severity": "Informational"},
|
||||||
|
{"Id": "5", "Created": "garbage", "Message": "bad time"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
log = {e["message"]: e for e in driver.get_event_log()}
|
log = {e["message"]: e for e in driver.get_event_log()}
|
||||||
assert log["offset"]["timestamp"] == ts("2026-10-13T06:00:00Z")
|
assert log["offset"]["timestamp"] == ts("2026-10-13T06:00:00Z")
|
||||||
assert log["no time"]["timestamp"] is None
|
assert log["no time"]["timestamp"] is None
|
||||||
@@ -482,8 +607,12 @@ def test_get_management_protocols(driver):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_management_protocols_missing(driver, server):
|
def test_get_management_protocols_missing(driver, server):
|
||||||
server.override("GET", MANAGER + "/NetworkProtocol", 200,
|
server.override(
|
||||||
{"HTTPS": {"ProtocolEnabled": True, "Port": 443}, "IPMI": {"Port": 623}})
|
"GET",
|
||||||
|
MANAGER + "/NetworkProtocol",
|
||||||
|
200,
|
||||||
|
{"HTTPS": {"ProtocolEnabled": True, "Port": 443}, "IPMI": {"Port": 623}},
|
||||||
|
)
|
||||||
protocols = driver.get_management_protocols()
|
protocols = driver.get_management_protocols()
|
||||||
assert protocols["https"] == {"enabled": True, "port": 443}
|
assert protocols["https"] == {"enabled": True, "port": 443}
|
||||||
assert protocols["ipmi"] == {"enabled": False, "port": 623}
|
assert protocols["ipmi"] == {"enabled": False, "port": 623}
|
||||||
@@ -493,8 +622,9 @@ def test_get_management_protocols_missing(driver, server):
|
|||||||
def _unlicensed(server):
|
def _unlicensed(server):
|
||||||
# A Supermicro X10 refuses Redfish entirely until a license is activated.
|
# A Supermicro X10 refuses Redfish entirely until a license is activated.
|
||||||
x10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91"
|
x10 = Path(__file__).parent / "fixtures" / "supermicro-x10drl-i-3.91"
|
||||||
server.override("GET", "/redfish/v1/", 403,
|
server.override(
|
||||||
json.loads((x10 / "service_root_403.json").read_text()))
|
"GET", "/redfish/v1/", 403, json.loads((x10 / "service_root_403.json").read_text())
|
||||||
|
)
|
||||||
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
|
d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False})
|
||||||
d.open()
|
d.open()
|
||||||
return d
|
return d
|
||||||
@@ -506,15 +636,24 @@ def test_unlicensed_bmc_still_opens(server):
|
|||||||
d = _unlicensed(server)
|
d = _unlicensed(server)
|
||||||
assert d.is_alive() == {"is_alive": True}
|
assert d.is_alive() == {"is_alive": True}
|
||||||
assert d.get_facts() == {
|
assert d.get_facts() == {
|
||||||
"uptime": -1.0, "vendor": "", "model": "", "hostname": "bmc.example.net",
|
"uptime": -1.0,
|
||||||
"fqdn": "bmc.example.net", "os_version": "", "serial_number": "", "interface_list": [],
|
"vendor": "",
|
||||||
|
"model": "",
|
||||||
|
"hostname": "bmc.example.net",
|
||||||
|
"fqdn": "bmc.example.net",
|
||||||
|
"os_version": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"interface_list": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_unlicensed_bmc_names_every_license_it_asks_for_as_missing(server):
|
def test_unlicensed_bmc_names_every_license_it_asks_for_as_missing(server):
|
||||||
licenses = _unlicensed(server).get_licenses()
|
licenses = _unlicensed(server).get_licenses()
|
||||||
assert [(lic["name"], lic["status"]) for lic in licenses] == [
|
assert [(lic["name"], lic["status"]) for lic in licenses] == [
|
||||||
("SUM", "missing"), ("DCMS", "missing"), ("OOB", "missing")]
|
("SUM", "missing"),
|
||||||
|
("DCMS", "missing"),
|
||||||
|
("OOB", "missing"),
|
||||||
|
]
|
||||||
# Refused altogether: nothing is withheld in part.
|
# Refused altogether: nothing is withheld in part.
|
||||||
assert all("withholds" not in lic and "Not licensed" in lic["detail"] for lic in licenses)
|
assert all("withholds" not in lic and "Not licensed" in lic["detail"] for lic in licenses)
|
||||||
|
|
||||||
|
|||||||
+90
-34
@@ -105,8 +105,12 @@ def test_open_logs_in_on_the_ipmi_port_and_close_logs_out():
|
|||||||
d = IpmiDriver("192.0.2.150", "netork", "s3cret", optional_args={"port": 6230})
|
d = IpmiDriver("192.0.2.150", "netork", "s3cret", optional_args={"port": 6230})
|
||||||
d.open()
|
d.open()
|
||||||
(cmd,) = FakeCommand.instances
|
(cmd,) = FakeCommand.instances
|
||||||
assert cmd.args == {"bmc": "192.0.2.150", "userid": "netork", "password": "s3cret",
|
assert cmd.args == {
|
||||||
"port": 6230}
|
"bmc": "192.0.2.150",
|
||||||
|
"userid": "netork",
|
||||||
|
"password": "s3cret",
|
||||||
|
"port": 6230,
|
||||||
|
}
|
||||||
assert d.is_alive() == {"is_alive": True}
|
assert d.is_alive() == {"is_alive": True}
|
||||||
d.close()
|
d.close()
|
||||||
assert cmd.logged_out
|
assert cmd.logged_out
|
||||||
@@ -167,8 +171,11 @@ def test_get_environment(driver):
|
|||||||
assert set(env) == {"fans", "temperature", "power", "cpu", "memory"}
|
assert set(env) == {"fans", "temperature", "power", "cpu", "memory"}
|
||||||
assert env["cpu"] == {} and env["memory"] == {}
|
assert env["cpu"] == {} and env["memory"] == {}
|
||||||
assert len(env["temperature"]) == 19
|
assert len(env["temperature"]) == 19
|
||||||
assert env["temperature"]["CPU1 Temp"] == {"temperature": 34.0, "is_alert": False,
|
assert env["temperature"]["CPU1 Temp"] == {
|
||||||
"is_critical": False}
|
"temperature": 34.0,
|
||||||
|
"is_alert": False,
|
||||||
|
"is_critical": False,
|
||||||
|
}
|
||||||
assert env["fans"] == {name: {"status": True} for name in ("FAN2", "FAN3", "FAN4", "FANB")}
|
assert env["fans"] == {name: {"status": True} for name in ("FAN2", "FAN3", "FAN4", "FANB")}
|
||||||
# Only supplies that report themselves present.
|
# Only supplies that report themselves present.
|
||||||
assert env["power"] == {"PS1 Status": {"status": True, "capacity": -1.0, "output": -1.0}}
|
assert env["power"] == {"PS1 Status": {"status": True, "capacity": -1.0, "output": -1.0}}
|
||||||
@@ -176,17 +183,46 @@ def test_get_environment(driver):
|
|||||||
|
|
||||||
def test_sensor_health_flags(driver, monkeypatch):
|
def test_sensor_health_flags(driver, monkeypatch):
|
||||||
readings = [
|
readings = [
|
||||||
{"name": "Hot", "type": "Temperature", "value": 91.0, "units": "°C", "health": 1,
|
{
|
||||||
"states": [], "unavailable": 0},
|
"name": "Hot",
|
||||||
{"name": "Melting", "type": "Temperature", "value": 105.0, "units": "°C", "health": 2,
|
"type": "Temperature",
|
||||||
"states": [], "unavailable": 0},
|
"value": 91.0,
|
||||||
{"name": "Gone", "type": "Temperature", "value": None, "units": "°C", "health": 0,
|
"units": "°C",
|
||||||
"states": [], "unavailable": 1},
|
"health": 1,
|
||||||
{"name": "FAN1", "type": "Fan", "value": 0.0, "units": "RPM", "health": 2,
|
"states": [],
|
||||||
"states": [], "unavailable": 0},
|
"unavailable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Melting",
|
||||||
|
"type": "Temperature",
|
||||||
|
"value": 105.0,
|
||||||
|
"units": "°C",
|
||||||
|
"health": 2,
|
||||||
|
"states": [],
|
||||||
|
"unavailable": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gone",
|
||||||
|
"type": "Temperature",
|
||||||
|
"value": None,
|
||||||
|
"units": "°C",
|
||||||
|
"health": 0,
|
||||||
|
"states": [],
|
||||||
|
"unavailable": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FAN1",
|
||||||
|
"type": "Fan",
|
||||||
|
"value": 0.0,
|
||||||
|
"units": "RPM",
|
||||||
|
"health": 2,
|
||||||
|
"states": [],
|
||||||
|
"unavailable": 0,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
monkeypatch.setattr(FakeCommand, "get_sensor_data",
|
monkeypatch.setattr(
|
||||||
lambda self: iter(SimpleNamespace(**r) for r in readings))
|
FakeCommand, "get_sensor_data", lambda self: iter(SimpleNamespace(**r) for r in readings)
|
||||||
|
)
|
||||||
env = driver.get_environment()
|
env = driver.get_environment()
|
||||||
assert env["temperature"] == {
|
assert env["temperature"] == {
|
||||||
"Hot": {"temperature": 91.0, "is_alert": True, "is_critical": False},
|
"Hot": {"temperature": 91.0, "is_alert": True, "is_critical": False},
|
||||||
@@ -201,8 +237,7 @@ def test_sensor_health_flags(driver, monkeypatch):
|
|||||||
def test_get_power_state(driver):
|
def test_get_power_state(driver):
|
||||||
state = driver.get_power_state()
|
state = driver.get_power_state()
|
||||||
assert_shape(state, models.PowerStateDict)
|
assert_shape(state, models.PowerStateDict)
|
||||||
assert state == {"state": "On",
|
assert state == {"state": "On", "actions": ["GracefulShutdown", "ForceOff", "ForceRestart"]}
|
||||||
"actions": ["GracefulShutdown", "ForceOff", "ForceRestart"]}
|
|
||||||
|
|
||||||
|
|
||||||
def test_a_server_that_is_off_can_only_be_switched_on(driver):
|
def test_a_server_that_is_off_can_only_be_switched_on(driver):
|
||||||
@@ -216,12 +251,15 @@ def test_every_offered_action_is_a_role_power_action(driver):
|
|||||||
assert set(driver.get_power_state()["actions"]) <= set(models.POWER_ACTIONS)
|
assert set(driver.get_power_state()["actions"]) <= set(models.POWER_ACTIONS)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("action, powerstate, expected", [
|
@pytest.mark.parametrize(
|
||||||
("GracefulShutdown", "on", "shutdown"),
|
"action, powerstate, expected",
|
||||||
("ForceOff", "on", "off"),
|
[
|
||||||
("ForceRestart", "on", "reset"),
|
("GracefulShutdown", "on", "shutdown"),
|
||||||
("On", "off", "on"),
|
("ForceOff", "on", "off"),
|
||||||
])
|
("ForceRestart", "on", "reset"),
|
||||||
|
("On", "off", "on"),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_set_power_state_sends_the_chassis_control(driver, action, powerstate, expected):
|
def test_set_power_state_sends_the_chassis_control(driver, action, powerstate, expected):
|
||||||
cmd = FakeCommand.instances[0]
|
cmd = FakeCommand.instances[0]
|
||||||
cmd.powerstate = powerstate
|
cmd.powerstate = powerstate
|
||||||
@@ -236,12 +274,15 @@ def test_set_power_state_rejects_an_unknown_action(driver):
|
|||||||
assert FakeCommand.instances[0].power_requests == []
|
assert FakeCommand.instances[0].power_requests == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("action, powerstate", [
|
@pytest.mark.parametrize(
|
||||||
("On", "on"),
|
"action, powerstate",
|
||||||
("ForceOff", "off"),
|
[
|
||||||
("GracefulRestart", "on"), # IPMI has no graceful restart
|
("On", "on"),
|
||||||
("PowerCycle", "on"),
|
("ForceOff", "off"),
|
||||||
])
|
("GracefulRestart", "on"), # IPMI has no graceful restart
|
||||||
|
("PowerCycle", "on"),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_set_power_state_refuses_what_is_not_offered_now(driver, action, powerstate):
|
def test_set_power_state_refuses_what_is_not_offered_now(driver, action, powerstate):
|
||||||
cmd = FakeCommand.instances[0]
|
cmd = FakeCommand.instances[0]
|
||||||
cmd.powerstate = powerstate
|
cmd.powerstate = powerstate
|
||||||
@@ -268,8 +309,13 @@ def test_get_hardware_inventory(driver):
|
|||||||
assert inv["system_uuid"] == "00000000-534d-c40c-0000-000000000001"
|
assert inv["system_uuid"] == "00000000-534d-c40c-0000-000000000001"
|
||||||
assert inv["power_supplies"] == [
|
assert inv["power_supplies"] == [
|
||||||
{"slot": "PS1", "model": "", "serial_number": "", "capacity_watts": None, "status": "OK"},
|
{"slot": "PS1", "model": "", "serial_number": "", "capacity_watts": None, "status": "OK"},
|
||||||
{"slot": "PS2", "model": "", "serial_number": "", "capacity_watts": None,
|
{
|
||||||
"status": "Absent"},
|
"slot": "PS2",
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"capacity_watts": None,
|
||||||
|
"status": "Absent",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
assert inv["cpus"] == [] and inv["memory"] == [] and inv["drives"] == [] and inv["nics"] == []
|
assert inv["cpus"] == [] and inv["memory"] == [] and inv["drives"] == [] and inv["nics"] == []
|
||||||
|
|
||||||
@@ -277,14 +323,24 @@ def test_get_hardware_inventory(driver):
|
|||||||
def test_get_event_log(driver):
|
def test_get_event_log(driver):
|
||||||
(entry,) = driver.get_event_log()
|
(entry,) = driver.get_event_log()
|
||||||
assert_shape(entry, models.EventLogEntryDict)
|
assert_shape(entry, models.EventLogEntryDict)
|
||||||
assert entry == {"id": "1", "timestamp": 1790788051.0, "severity": "OK",
|
assert entry == {
|
||||||
"message": "PS2 Status: Recovered"}
|
"id": "1",
|
||||||
|
"timestamp": 1790788051.0,
|
||||||
|
"severity": "OK",
|
||||||
|
"message": "PS2 Status: Recovered",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_event_log_newest_first_and_limited(driver, monkeypatch):
|
def test_event_log_newest_first_and_limited(driver, monkeypatch):
|
||||||
sel = [
|
sel = [
|
||||||
{"record_id": i, "timecode": 1000 + i, "component": "FAN1", "event": "Lower critical",
|
{
|
||||||
"severity": 2, "deassertion": False}
|
"record_id": i,
|
||||||
|
"timecode": 1000 + i,
|
||||||
|
"component": "FAN1",
|
||||||
|
"event": "Lower critical",
|
||||||
|
"severity": 2,
|
||||||
|
"deassertion": False,
|
||||||
|
}
|
||||||
for i in range(1, 6)
|
for i in range(1, 6)
|
||||||
]
|
]
|
||||||
monkeypatch.setattr(FakeCommand, "get_event_log", lambda self: iter(sel))
|
monkeypatch.setattr(FakeCommand, "get_event_log", lambda self: iter(sel))
|
||||||
|
|||||||
+37
-13
@@ -189,8 +189,13 @@ def test_login_url_from_service_root(server):
|
|||||||
custom = dict(_root(), Links={"Sessions": {"@odata.id": "/redfish/v1/Custom/Sessions"}})
|
custom = dict(_root(), Links={"Sessions": {"@odata.id": "/redfish/v1/Custom/Sessions"}})
|
||||||
server.override("GET", "/redfish/v1/", 200, custom)
|
server.override("GET", "/redfish/v1/", 200, custom)
|
||||||
server.override("GET", "/redfish/v1", 200, custom)
|
server.override("GET", "/redfish/v1", 200, custom)
|
||||||
server.override("POST", "/redfish/v1/Custom/Sessions", 201, {},
|
server.override(
|
||||||
{"X-Auth-Token": TOKEN, "Location": SESSION_PATH})
|
"POST",
|
||||||
|
"/redfish/v1/Custom/Sessions",
|
||||||
|
201,
|
||||||
|
{},
|
||||||
|
{"X-Auth-Token": TOKEN, "Location": SESSION_PATH},
|
||||||
|
)
|
||||||
t = make()
|
t = make()
|
||||||
t.open()
|
t.open()
|
||||||
assert server.calls("POST", "/redfish/v1/Custom/Sessions")
|
assert server.calls("POST", "/redfish/v1/Custom/Sessions")
|
||||||
@@ -314,9 +319,18 @@ def test_get_optional_returns_none_on_404(transport):
|
|||||||
|
|
||||||
|
|
||||||
def test_get_error_carries_redfish_message(transport, server):
|
def test_get_error_carries_redfish_message(transport, server):
|
||||||
server.override("GET", "/redfish/v1/Managers", 500, {
|
server.override(
|
||||||
"error": {"code": "Base.1.0.InternalError", "message": "General error",
|
"GET",
|
||||||
"@Message.ExtendedInfo": [{"Message": "The BMC is busy."}]}})
|
"/redfish/v1/Managers",
|
||||||
|
500,
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "Base.1.0.InternalError",
|
||||||
|
"message": "General error",
|
||||||
|
"@Message.ExtendedInfo": [{"Message": "The BMC is busy."}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
with pytest.raises(RedfishHTTPError) as exc:
|
with pytest.raises(RedfishHTTPError) as exc:
|
||||||
transport.get("/redfish/v1/Managers")
|
transport.get("/redfish/v1/Managers")
|
||||||
assert exc.value.status == 500
|
assert exc.value.status == 500
|
||||||
@@ -366,20 +380,30 @@ def test_members_uses_expanded_members_without_extra_requests(transport, server)
|
|||||||
|
|
||||||
def test_members_follows_next_link(transport, server):
|
def test_members_follows_next_link(transport, server):
|
||||||
path = "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries"
|
path = "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries"
|
||||||
server.override("GET", path + "?$skiptoken=2", 200, {
|
server.override(
|
||||||
"Members": [{"@odata.id": path + "/3", "Id": "3", "Message": "third"}],
|
"GET",
|
||||||
"@odata.id": path,
|
path + "?$skiptoken=2",
|
||||||
})
|
200,
|
||||||
|
{
|
||||||
|
"Members": [{"@odata.id": path + "/3", "Id": "3", "Message": "third"}],
|
||||||
|
"@odata.id": path,
|
||||||
|
},
|
||||||
|
)
|
||||||
entries = transport.members(path)
|
entries = transport.members(path)
|
||||||
assert [e["Id"] for e in entries] == ["1", "2", "3"]
|
assert [e["Id"] for e in entries] == ["1", "2", "3"]
|
||||||
|
|
||||||
|
|
||||||
def test_members_next_link_loop_terminates(transport, server):
|
def test_members_next_link_loop_terminates(transport, server):
|
||||||
path = "/redfish/v1/Managers"
|
path = "/redfish/v1/Managers"
|
||||||
server.override("GET", path, 200, {
|
server.override(
|
||||||
"Members": [{"@odata.id": "/redfish/v1/Managers/BMC"}],
|
"GET",
|
||||||
"Members@odata.nextLink": path,
|
path,
|
||||||
})
|
200,
|
||||||
|
{
|
||||||
|
"Members": [{"@odata.id": "/redfish/v1/Managers/BMC"}],
|
||||||
|
"Members@odata.nextLink": path,
|
||||||
|
},
|
||||||
|
)
|
||||||
assert [m["Id"] for m in transport.members(path)] == ["BMC"]
|
assert [m["Id"] for m in transport.members(path)] == ["BMC"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+19
-13
@@ -23,16 +23,18 @@ REFUSED = json.loads((FIXTURES / MOCKUP / "STATUS.json").read_text())
|
|||||||
def x10(http):
|
def x10(http):
|
||||||
server = MockupServer(http, MOCKUP)
|
server = MockupServer(http, MOCKUP)
|
||||||
for path, status in REFUSED.items():
|
for path, status in REFUSED.items():
|
||||||
body = json.loads((FIXTURES / MOCKUP / path[len("/redfish/v1/"):] / "index.json")
|
body = json.loads(
|
||||||
.read_text())
|
(FIXTURES / MOCKUP / path[len("/redfish/v1/") :] / "index.json").read_text()
|
||||||
|
)
|
||||||
server.override("GET", path, status, body)
|
server.override("GET", path, status, body)
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def driver(x10):
|
def driver(x10):
|
||||||
d = RedfishDriver(BASE_URL.removeprefix("https://"), "netork", "secret",
|
d = RedfishDriver(
|
||||||
optional_args={"verify_ssl": False})
|
BASE_URL.removeprefix("https://"), "netork", "secret", optional_args={"verify_ssl": False}
|
||||||
|
)
|
||||||
d.open()
|
d.open()
|
||||||
yield d
|
yield d
|
||||||
d.close()
|
d.close()
|
||||||
@@ -65,10 +67,12 @@ def test_the_firmware_comes_from_the_manager_and_the_system_without_updateservic
|
|||||||
|
|
||||||
|
|
||||||
def test_the_missing_license_is_named_with_what_it_withholds(driver):
|
def test_the_missing_license_is_named_with_what_it_withholds(driver):
|
||||||
assert driver.get_device_warnings() == [{
|
assert driver.get_device_warnings() == [
|
||||||
"code": "bmc_license_partial",
|
{
|
||||||
"meta": {"licenses": "DCMS", "parts": ["drives", "firmware inventory"]},
|
"code": "bmc_license_partial",
|
||||||
}]
|
"meta": {"licenses": "DCMS", "parts": ["drives", "firmware inventory"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_the_power_actions_are_the_ones_the_bmc_advertises(driver):
|
def test_the_power_actions_are_the_ones_the_bmc_advertises(driver):
|
||||||
@@ -95,8 +99,10 @@ def test_the_sensors_and_the_event_log_read(driver):
|
|||||||
|
|
||||||
|
|
||||||
def test_the_missing_license_is_a_fact_with_what_it_withholds(driver):
|
def test_the_missing_license_is_a_fact_with_what_it_withholds(driver):
|
||||||
assert driver.get_licenses() == [{
|
assert driver.get_licenses() == [
|
||||||
"name": "DCMS",
|
{
|
||||||
"status": "missing",
|
"name": "DCMS",
|
||||||
"withholds": ["drives", "firmware inventory"],
|
"status": "missing",
|
||||||
}]
|
"withholds": ["drives", "firmware inventory"],
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in new issue
Block a user