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