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.
298 lines
11 KiB
Python
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
|