Files
napalm-redfish/tests/parsers/test_environment.py
T
Christian Manivong 09029ca0a4 style: ruff format and the lint rules of the driver standard
Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM
driver uses (E, F, I, UP, B at line length 100, Python 3.10). Mapping and
Iterable come from collections.abc, imports are sorted. What ruff cannot
fix comes in the next commit.
2026-10-23 08:39:45 +02:00

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