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.
352 lines
12 KiB
Python
352 lines
12 KiB
Python
import pytest
|
|
|
|
from napalm_redfish.parsers import (
|
|
build_environment,
|
|
environment_sources,
|
|
parse_fan,
|
|
parse_power,
|
|
parse_power_subsystem,
|
|
parse_power_supply,
|
|
parse_sensor,
|
|
parse_thermal,
|
|
parse_thermal_metrics,
|
|
parse_thermal_subsystem,
|
|
)
|
|
|
|
CHASSIS = "/redfish/v1/Chassis/1U"
|
|
|
|
|
|
# --- choosing legacy or new resources ------------------------------------------------------
|
|
|
|
|
|
def test_sources_prefer_new_resources(rackmount):
|
|
# This chassis links both; the new ones are current, Thermal/Power are deprecated.
|
|
src = environment_sources(rackmount(CHASSIS))
|
|
assert src == {
|
|
"thermal": {"schema": "ThermalSubsystem", "path": CHASSIS + "/ThermalSubsystem"},
|
|
"power": {"schema": "PowerSubsystem", "path": CHASSIS + "/PowerSubsystem"},
|
|
"sensors": CHASSIS + "/Sensors",
|
|
}
|
|
|
|
|
|
def test_sources_legacy_only(localstorage):
|
|
src = environment_sources(localstorage(CHASSIS))
|
|
assert src == {
|
|
"thermal": {"schema": "Thermal", "path": CHASSIS + "/Thermal"},
|
|
"power": {"schema": "Power", "path": CHASSIS + "/Power"},
|
|
"sensors": None,
|
|
}
|
|
|
|
|
|
def test_sources_mixed_and_missing():
|
|
src = environment_sources(
|
|
{
|
|
"ThermalSubsystem": {"@odata.id": "/redfish/v1/Chassis/X/ThermalSubsystem"},
|
|
"Power": {"@odata.id": "/redfish/v1/Chassis/X/Power"},
|
|
}
|
|
)
|
|
assert src["thermal"]["schema"] == "ThermalSubsystem"
|
|
assert src["power"]["schema"] == "Power"
|
|
assert environment_sources({}) == {"thermal": None, "power": None, "sensors": None}
|
|
|
|
|
|
# --- legacy Thermal and Power --------------------------------------------------------------
|
|
|
|
|
|
def test_thermal(rackmount):
|
|
env = parse_thermal(rackmount(CHASSIS + "/Thermal"))
|
|
assert env["temperature"] == {
|
|
"CPU1 Temp": {"temperature": 41.0, "is_alert": False, "is_critical": False},
|
|
# "CPU2 Temp" is Disabled and has no reading: left out.
|
|
"Chassis Intake Temp": {"temperature": 25.0, "is_alert": False, "is_critical": False},
|
|
}
|
|
assert env["fans"] == {
|
|
"BaseBoard System Fan": {"status": True},
|
|
"BaseBoard System Fan Backup": {"status": True},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"reading, health, alert, critical",
|
|
[
|
|
(41, "OK", False, False),
|
|
(42, "OK", True, False), # at UpperThresholdNonCritical
|
|
(45, "OK", True, True), # at UpperThresholdCritical
|
|
(49, "OK", True, True), # above UpperThresholdFatal
|
|
(4, "OK", True, True), # below LowerThresholdCritical
|
|
(9, "OK", True, False), # below LowerThresholdNonCritical
|
|
(30, "Warning", True, False),
|
|
(30, "Critical", True, True),
|
|
],
|
|
)
|
|
def test_thermal_thresholds(reading, health, alert, critical):
|
|
env = parse_thermal(
|
|
{
|
|
"Temperatures": [
|
|
{
|
|
"Name": "T",
|
|
"ReadingCelsius": reading,
|
|
"Status": {"State": "Enabled", "Health": health},
|
|
"UpperThresholdNonCritical": 42,
|
|
"UpperThresholdCritical": 45,
|
|
"UpperThresholdFatal": 48,
|
|
"LowerThresholdNonCritical": 10,
|
|
"LowerThresholdCritical": 5,
|
|
}
|
|
]
|
|
}
|
|
)
|
|
assert env["temperature"]["T"] == {
|
|
"temperature": float(reading),
|
|
"is_alert": alert,
|
|
"is_critical": critical,
|
|
}
|
|
|
|
|
|
def test_thermal_old_schema_names_and_duplicates():
|
|
# Thermal 1.0 uses FanName; Supermicro repeats names across members.
|
|
env = parse_thermal(
|
|
{
|
|
"Fans": [
|
|
{
|
|
"MemberId": "0",
|
|
"FanName": "FAN1",
|
|
"Reading": 3000,
|
|
"Status": {"State": "Enabled", "Health": "OK"},
|
|
},
|
|
{"MemberId": "1", "FanName": "FAN2", "Status": {"State": "Absent"}},
|
|
{
|
|
"MemberId": "2",
|
|
"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"] == {
|
|
"FAN1": {"status": True},
|
|
"FAN3": {"status": False},
|
|
"FAN1 (3)": {"status": True},
|
|
}
|
|
assert list(env["temperature"]) == ["System Temp", "System Temp (1)"]
|
|
|
|
|
|
def test_power(rackmount):
|
|
env = parse_power(rackmount(CHASSIS + "/Power"))
|
|
assert env["power"] == {
|
|
# Health Warning: the supply works but reports a problem.
|
|
"Power Supply Bay": {"status": False, "capacity": 800.0, "output": 325.0},
|
|
}
|
|
assert env["consumed_watts"] == 344.0
|
|
assert env["voltages"] == {
|
|
"VRM1 Voltage": {"voltage": 12.0, "is_alert": False, "is_critical": False},
|
|
"VRM2 Voltage": {"voltage": 5.0, "is_alert": False, "is_critical": False},
|
|
}
|
|
|
|
|
|
def test_power_unknown_values_are_minus_one():
|
|
env = parse_power(
|
|
{
|
|
"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["consumed_watts"] is None
|
|
assert env["voltages"] == {}
|
|
|
|
|
|
# --- ThermalSubsystem, PowerSubsystem, Sensors ---------------------------------------------
|
|
|
|
|
|
def test_thermal_subsystem(rackmount):
|
|
ts = parse_thermal_subsystem(rackmount(CHASSIS + "/ThermalSubsystem"))
|
|
assert ts == {
|
|
"fans_path": CHASSIS + "/ThermalSubsystem/Fans",
|
|
"thermal_metrics_path": CHASSIS + "/ThermalSubsystem/ThermalMetrics",
|
|
"state": "Enabled",
|
|
"health": "OK",
|
|
}
|
|
|
|
|
|
def test_power_subsystem(rackmount):
|
|
ps = parse_power_subsystem(rackmount(CHASSIS + "/PowerSubsystem"))
|
|
assert ps == {
|
|
"power_supplies_path": CHASSIS + "/PowerSubsystem/PowerSupplies",
|
|
"capacity_watts": 2000.0,
|
|
"state": "Enabled",
|
|
"health": "OK",
|
|
}
|
|
|
|
|
|
def test_fan(rackmount):
|
|
fan = parse_fan(rackmount(CHASSIS + "/ThermalSubsystem/Fans/CPU1"))
|
|
assert fan == {
|
|
"id": "CPU1",
|
|
"name": "Fan for CPU 1",
|
|
"present": True,
|
|
"status": True,
|
|
"speed_rpm": 1490,
|
|
"speed_percent": 45.0,
|
|
"state": "Enabled",
|
|
"health": "OK",
|
|
}
|
|
|
|
|
|
def test_power_supply_with_metrics(rackmount):
|
|
base = CHASSIS + "/PowerSubsystem/PowerSupplies/Bay1"
|
|
psu = parse_power_supply(rackmount(base), rackmount(base + "/Metrics"))
|
|
assert psu == {
|
|
"id": "Bay1",
|
|
"name": "Power Supply Bay 1",
|
|
"present": True,
|
|
"status": False, # predicted failure, Health Warning
|
|
"capacity": 400.0,
|
|
"output": 937.4,
|
|
"manufacturer": "Contoso Power",
|
|
"model": "RKS-440DC",
|
|
"serial_number": "3488247",
|
|
"part_number": "23456-133",
|
|
"firmware_version": "1.00",
|
|
"state": "Enabled",
|
|
"health": "Warning",
|
|
}
|
|
assert psu["output"] == 937.4
|
|
|
|
|
|
def test_power_supply_without_metrics(rackmount):
|
|
base = CHASSIS + "/PowerSubsystem/PowerSupplies/Bay1"
|
|
assert parse_power_supply(rackmount(base))["output"] == -1.0
|
|
absent = parse_power_supply(rackmount(CHASSIS + "/PowerSubsystem/PowerSupplies/Bay2"))
|
|
assert absent["present"] is False
|
|
assert absent["capacity"] == -1.0
|
|
|
|
|
|
def test_sensor_temperature_by_units(rackmount):
|
|
# No ReadingType on this sensor: "Cel" makes it a temperature.
|
|
s = parse_sensor(rackmount(CHASSIS + "/Sensors/CPU1Temp"))
|
|
assert s["id"] == "CPU1Temp"
|
|
assert s["name"] == "CPU #1 Temperature"
|
|
assert s["reading_type"] == "Temperature"
|
|
assert s["reading"] == 44.0
|
|
assert s["units"] == "Cel"
|
|
assert s["physical_context"] == "CPU"
|
|
assert s["health"] == "Warning"
|
|
assert s["is_alert"] is True # 44 >= UpperCaution 42
|
|
assert s["is_critical"] is False # 44 < UpperCritical 45
|
|
|
|
|
|
def test_sensor_power(rackmount):
|
|
s = parse_sensor(rackmount(CHASSIS + "/Sensors/TotalPower"))
|
|
assert s["reading_type"] == "Power"
|
|
assert s["reading"] == 374.0
|
|
assert s["electrical_context"] == "Total"
|
|
assert s["is_alert"] is False
|
|
|
|
|
|
def test_sensor_voltage_and_fan(rackmount):
|
|
v = parse_sensor(rackmount(CHASSIS + "/Sensors/PS1InputVoltage"))
|
|
assert v["reading_type"] == "Voltage"
|
|
assert v["reading"] == 119.27
|
|
assert v["is_alert"] is False
|
|
fan = parse_sensor(rackmount(CHASSIS + "/Sensors/CPUFan1"))
|
|
assert fan["reading_type"] == "Percent"
|
|
assert fan["reading"] == 45.0
|
|
|
|
|
|
def test_sensor_without_reading():
|
|
s = parse_sensor(
|
|
{"Id": "X", "Name": "X", "ReadingUnits": "Cel", "Status": {"State": "Disabled"}}
|
|
)
|
|
assert s["reading"] is None
|
|
assert s["is_alert"] is False
|
|
|
|
|
|
def test_thermal_metrics(rackmount):
|
|
env = parse_thermal_metrics(rackmount(CHASSIS + "/ThermalSubsystem/ThermalMetrics"))
|
|
assert env == {
|
|
"temperature": {
|
|
"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):
|
|
fans = [
|
|
parse_fan(rackmount(f"{CHASSIS}/ThermalSubsystem/Fans/{f}"))
|
|
for f in ("Bay1", "Bay2", "CPU1", "CPU2")
|
|
]
|
|
bay1 = f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay1"
|
|
psus = [
|
|
parse_power_supply(rackmount(bay1), rackmount(bay1 + "/Metrics")),
|
|
parse_power_supply(rackmount(f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay2")),
|
|
]
|
|
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)
|
|
assert env["fans"] == {
|
|
"Fan Bay 1": {"status": True},
|
|
"Fan Bay 2": {"status": True},
|
|
"Fan for CPU 1": {"status": True},
|
|
"Fan for CPU 2": {"status": True},
|
|
}
|
|
assert env["power"] == {
|
|
"Power Supply Bay 1": {"status": False, "capacity": 400.0, "output": 937.4},
|
|
}
|
|
assert env["temperature"] == {
|
|
"CPU #1 Temperature": {"temperature": 44.0, "is_alert": True, "is_critical": False},
|
|
"Front Panel Intake Temperature": {
|
|
"temperature": 24.8,
|
|
"is_alert": False,
|
|
"is_critical": False,
|
|
},
|
|
}
|
|
assert env["consumed_watts"] == 374.0
|
|
assert env["voltages"] == {
|
|
"Power Supply #1 Input Voltage": {
|
|
"voltage": 119.27,
|
|
"is_alert": False,
|
|
"is_critical": False,
|
|
},
|
|
}
|
|
|
|
|
|
def test_build_environment_empty():
|
|
assert build_environment() == {
|
|
"fans": {},
|
|
"temperature": {},
|
|
"power": {},
|
|
"voltages": {},
|
|
"consumed_watts": None,
|
|
}
|
|
|
|
|
|
def test_power_supply_legacy_item(rackmount):
|
|
# Items of the legacy Power.PowerSupplies array parse the same way: MemberId as id,
|
|
# LastPowerOutputWatts as output.
|
|
(item,) = rackmount(CHASSIS + "/Power")["PowerSupplies"]
|
|
psu = parse_power_supply(item)
|
|
assert psu["id"] == "0"
|
|
assert psu["name"] == "Power Supply Bay"
|
|
assert psu["capacity"] == 800.0
|
|
assert psu["output"] == 325.0
|
|
assert psu["model"] == "499253-B21"
|
|
assert psu["serial_number"] == "1Z0000001"
|
|
assert psu["status"] is False
|