Files
napalm-redfish/tests/parsers/test_environment.py
T
Christian Manivong 209537a793 Extend parsers for the driver
parse_power_supply also reads items of the legacy Power.PowerSupplies array, parse_ethernet_interface exposes the raw LinkStatus, and parse_network_protocol reports protocols that carry a Port without a ProtocolEnabled flag.
2026-10-13 05:02:58 +00:00

298 lines
11 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