From 68cef5896de367e4ec3bdbe35a721c27a35a260c Mon Sep 17 00:00:00 2001 From: Andreas Doehler Date: Tue, 3 Feb 2026 16:04:17 +0100 Subject: [PATCH 1/2] All bugfixes and new checks for Redfish integration --- .../agent_based/inv_redfish_firmware.py | 2 +- .../agent_based/inventory_redfish_data.py | 121 +++++++++++++++ .../redfish/agent_based/redfish_pdus.py | 68 +++++++++ .../agent_based/redfish_physicaldrives.py | 4 +- .../agent_based/redfish_power_consumption.py | 81 ++++++++++ .../agent_based/redfish_power_redundancy.py | 56 +++++++ .../redfish/agent_based/redfish_storage.py | 53 ++++++- .../redfish/agent_based/redfish_system.py | 31 ++-- .../cmk/plugins/redfish/graphing/power.py | 139 +++++++++++++++++- .../cmk-plugins/cmk/plugins/redfish/lib.py | 2 + .../redfish/rulesets/redfish_storage.py | 54 +++++++ .../redfish/special_agents/agent_redfish.py | 12 +- .../special_agents/agent_redfish_power.py | 14 ++ 13 files changed, 617 insertions(+), 20 deletions(-) create mode 100644 packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py create mode 100644 packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py create mode 100644 packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_consumption.py create mode 100644 packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_redundancy.py create mode 100644 packages/cmk-plugins/cmk/plugins/redfish/rulesets/redfish_storage.py diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inv_redfish_firmware.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inv_redfish_firmware.py index a58f82e9033..843356dbe27 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inv_redfish_firmware.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inv_redfish_firmware.py @@ -53,7 +53,7 @@ def _item_name(item_data, padding): def inventorize_redfish_firmware(section: RedfishAPIData) -> InventoryResult: """create inventory table for firmware""" - path = ["hardware", "firmware", "redfish"] + path = ["hardware", "firmware"] if section.get("FirmwareInventory", {}).get("Current"): data = section.get("FirmwareInventory", {}).get("Current") padding = len(str(len(data))) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py new file mode 100644 index 00000000000..b9aefcf5527 --- /dev/null +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Redfish HW/SW inventory plugin""" + +# (c) Andreas Doehler +# License: GNU General Public License v2 + +from collections.abc import Mapping, Sequence + +from cmk.agent_based.v2 import InventoryPlugin, InventoryResult, TableRow +from cmk.plugins.redfish.lib import RedfishAPIData + + +def _extract_odata_ids(data, ids_set): + if isinstance(data, Mapping): + for key, value in data.items(): + # if key '@odata.id' and value is a string, extract the OData ID value + if key == "@odata.id" and isinstance(value, str): + key_name = data.get("Name") + serial_number = data.get("SerialNumber", "nothing set") + part_number = data.get("PartNumber", "nothing set") + manufacturer = data.get("Manufacturer", "nothing set") + model = data.get("Model", "nothing set") + # if 'Oem' in data, skip this entry and go deeper + if "Oem" in value: + continue + if key_name: + # add name is not None add to set + ids_set.add( + ( + value, + key_name, + serial_number, + part_number, + manufacturer, + model, + ) + ) + # else recursively call for nested mappings or sequences + else: + ids_set = _extract_odata_ids(value, ids_set) + elif isinstance(data, Sequence) and not isinstance(data, (str, bytes)): + for item in data: + # Rekursiv für jedes Element in der Liste/Tupel aufrufen + ids_set = _extract_odata_ids(item, ids_set) + + return ids_set + + +def inventory_redfish_data( + section_redfish_storage: None | RedfishAPIData, + section_redfish_processors: None | RedfishAPIData, + section_redfish_drives: None | RedfishAPIData, + section_redfish_psu: None | RedfishAPIData, + section_redfish_memory: None | RedfishAPIData, + section_redfish_power: None | RedfishAPIData, + section_redfish_thermal: None | RedfishAPIData, + section_redfish_networkadapters: None | RedfishAPIData, +) -> InventoryResult: + result_path = ["redfish"] + + odata_ids_set = set() # Set to remove duplicates + odata_ids_set = _extract_odata_ids(section_redfish_processors, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_storage, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_drives, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_psu, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_memory, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_power, odata_ids_set) + odata_ids_set = _extract_odata_ids(section_redfish_thermal, odata_ids_set) + odata_ids_set = _extract_odata_ids( + section_redfish_networkadapters, odata_ids_set + ) + + for path, name, serial, part_number, manufacturer, model in odata_ids_set: + if serial == "nothing set": + continue + if path.startswith("/redfish/"): + segments = ( + path.replace("#", "") + .replace(":", "-") + .replace(".", "_") + .replace("'", "") + .replace("(", "_") + .replace(")", "_") + .replace("%", "_") + .strip("/") + .split("/") + ) + result_path = [element for element in segments if element != ""] + item_id = result_path.pop() + if result_path[0] == "redfish": + result_path = result_path[1:] + if result_path[0] == "v1": + result_path = result_path[1:] + final_path = ["hardware"] + result_path + yield TableRow( + path=final_path, + key_columns={"name": name}, + inventory_columns={ + "id": item_id, + "serial": serial, + "part_number": part_number, + "manufacturer": manufacturer, + "model": model, + }, + ) + + +inventory_plugin_redfish_data = InventoryPlugin( + name="redfish_data", + sections=[ + "redfish_storage", + "redfish_processors", + "redfish_drives", + "redfish_psu", + "redfish_memory", + "redfish_power", + "redfish_thermal", + "redfish_networkadapters", + ], + inventory_function=inventory_redfish_data, +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py new file mode 100644 index 00000000000..bfd4a9892e8 --- /dev/null +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""check single redfish pdu state""" + +# (c) Andreas Doehler +# License: GNU General Public License v2 + +from cmk.agent_based.v2 import ( + AgentSection, + CheckPlugin, + CheckResult, + DiscoveryResult, + Result, + Service, + State, +) +from cmk.plugins.redfish.lib import ( + parse_redfish_multiple, + redfish_health_state, + RedfishAPIData, +) + +agent_section_redfish_pdus = AgentSection( + name="redfish_rackpdus", + parse_function=parse_redfish_multiple, + parsed_section_name="redfish_pdus", +) + + +def discovery_redfish_pdus(section: RedfishAPIData) -> DiscoveryResult: + """Discover single pdus""" + for key in section.keys(): + if section[key].get("Status", {}).get("State") == "Absent": + continue + item = key.split("/")[-1] + yield Service(item=item) + + +def check_redfish_pdus(item: str, section: RedfishAPIData) -> CheckResult: + """Check single pdu state""" + + for key in section.keys(): + if key.endswith(f"/{item}"): + item = key + break + data = section.get(item, None) + if data is None: + return + print(data) + firmware = data.get("FirmwareVersion") + serial = data.get("SerialNumber") + model = data.get("Model") + manufacturer = data.get("Manufacturer") + + yield Result( + state=State.OK, summary=f"PDU {manufacturer} {model} S/N {serial} FW {firmware}" + ) + + dev_state, dev_msg = redfish_health_state(data.get("Status", {})) + yield Result(state=State(dev_state), summary=dev_msg) + + +check_plugin_redfish_pdus = CheckPlugin( + name="redfish_pdus", + service_name="PDU %s", + sections=["redfish_pdus"], + discovery_function=discovery_redfish_pdus, + check_function=check_redfish_pdus, +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py index 963eeed73c5..3daf4ac29ff 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py @@ -85,9 +85,9 @@ def check_redfish_physicaldrives(item: str, section: RedfishAPIData) -> CheckRes ) yield Metric("media_life_left", int(data.get("PredictedMediaLifeLeftPercent"))) elif data.get("SSDEnduranceUtilizationPercentage"): + media_life_left = 100 - int(data.get("SSDEnduranceUtilizationPercentage", 0)) disc_msg = ( - f"{disc_msg}, SSD Utilization: " - f"{int(data.get('SSDEnduranceUtilizationPercentage', 0))}%" + f"{disc_msg}, Media Life Left: {media_life_left}%" ) yield Metric("ssd_utilization", int(data.get("SSDEnduranceUtilizationPercentage"))) yield Result(state=State(0), summary=disc_msg) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_consumption.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_consumption.py new file mode 100644 index 00000000000..a6b5168ac2e --- /dev/null +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_consumption.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""check the power consumption of a system via redfish""" + +# (c) Andreas Doehler +# License: GNU General Public License v2 + +from cmk.agent_based.v2 import ( + CheckPlugin, + CheckResult, + DiscoveryResult, + Metric, + Result, + Service, + State, +) +from cmk.plugins.redfish.lib import ( + RedfishAPIData, +) + + +def discovery_redfish_power_consumption(section: RedfishAPIData) -> DiscoveryResult: + for key in section.keys(): + if section[key].get("PowerControl", None): + yield Service() + + +def check_redfish_power_consumption(section: RedfishAPIData) -> CheckResult: + powercontrol = [] + for key in section.keys(): + if powercontrol_element := section[key].get("PowerControl", None): + powercontrol.extend(powercontrol_element) + + if not powercontrol: + return + result_submited = False + for element in powercontrol: + summary_msg = [] + mem_id = element.get("MemberId", "0") + mem_name = element.get("Name", "PowerControl") + system_wide_values = {} + for i in ["PowerCapacityWatts", "PowerConsumedWatts"]: + if (value := element.get(i, None)) is not None: + system_wide_values[i] = value + summary_msg.append(f"{i} - {value} W") + + if summary_msg: + result_submited = True + yield Result(state=State(0), summary=f"{mem_name}: {' / '.join(summary_msg)}") + if metrics := element.get("PowerMetrics", None): + for metric_name in [ + "AverageConsumedWatts", + "MinConsumedWatts", + "MaxConsumedWatts", + ]: + if (metric_value := metrics.get(metric_name, None)) is not None: + maximum_value = system_wide_values.get("PowerCapacityWatts", None) + if maximum_value: + yield Metric( + name=f"{metric_name.lower()}_{mem_id}", + value=float(metric_value), + boundaries=(0, float(maximum_value)), + ) + else: + yield Metric( + name=f"{metric_name.lower()}_{mem_id}", + value=float(metric_value), + ) + if not result_submited: + yield Result( + state=State(0), + summary="No power consumption data available.", + ) + + +check_plugin_redfish_power_consumption = CheckPlugin( + name="redfish_power_consumption", + service_name="Power consumption", + sections=["redfish_power"], + discovery_function=discovery_redfish_power_consumption, + check_function=check_redfish_power_consumption, +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_redundancy.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_redundancy.py new file mode 100644 index 00000000000..3aba59709a6 --- /dev/null +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_power_redundancy.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""check the power redundancy state of a system via redfish""" + +# (c) Andreas Doehler +# License: GNU General Public License v2 + +from cmk.agent_based.v2 import ( + CheckPlugin, + CheckResult, + DiscoveryResult, + Result, + Service, + State, +) +from cmk.plugins.redfish.lib import ( + redfish_health_state, + RedfishAPIData, +) + + +def discovery_redfish_power_redundancy(section: RedfishAPIData) -> DiscoveryResult: + for key in section.keys(): + if section[key].get("Redundancy", None): + yield Service() + + +def check_redfish_power_redundancy(section: RedfishAPIData) -> CheckResult: + redundancy = [] + for key in section.keys(): + if redundancy_element := section[key].get("Redundancy", None): + redundancy.extend(redundancy_element) + + if not redundancy: + return + + for element in redundancy: + dev_state, dev_msg = redfish_health_state(element.get("Status", {})) + yield Result(state=State(dev_state), summary=dev_msg) + details_msg = [] + for i in ["Name", "Mode", "MinNumNeeded", "MaxNumSupported"]: + if value := element.get(i, None): + details_msg.append(f"{i}: {value}") + if (num_ps := len(element.get("RedundancySet", []))) > 0: + details_msg.append(f"Number of Power Supplies in Redundancy Set: {num_ps}") + + if details_msg: + yield Result(state=State(0), notice="\n".join(details_msg)) + + +check_plugin_redfish_power_redundancy = CheckPlugin( + name="redfish_power_redundancy", + service_name="Power redundancy", + sections=["redfish_power"], + discovery_function=discovery_redfish_power_redundancy, + check_function=check_redfish_power_redundancy, +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py index 5efe3b1e0af..f41028df4e2 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py @@ -33,12 +33,28 @@ def discovery_redfish_storage(section: RedfishAPIData) -> DiscoveryResult: yield Service(item=section[key]["Id"]) -def check_redfish_storage(item: str, section: RedfishAPIData) -> CheckResult: +def discovery_redfish_storage_battery(section: RedfishAPIData) -> DiscoveryResult: + """Discover single controller batteries""" + for key in section.keys(): + if section[key].get("Status", {}).get("State") == "UnavailableOffline": + continue + if (section[key].get("Oem") or {}).get("Dell", {}).get( + "DellControllerBattery", None + ) is not None: + yield Service(item=section[key]["Id"]) + + +def check_redfish_storage(item: str, params: dict, section: RedfishAPIData) -> CheckResult: """Check single Controller state""" data = section.get(item, None) if data is None: return + if params.get("check_type", "full") == "rollup": + dev_state, dev_msg = redfish_health_state(data.get("Status", {})) + yield Result(state=State(dev_state), summary=dev_msg) + return + controller_list = data.get("StorageControllers", []) if len(controller_list) == 1: for ctrl_data in controller_list: @@ -77,4 +93,39 @@ def check_redfish_storage(item: str, section: RedfishAPIData) -> CheckResult: sections=["redfish_storage"], discovery_function=discovery_redfish_storage, check_function=check_redfish_storage, + check_default_parameters={}, + check_ruleset_name="redfish_storage", +) + + +def check_redfish_storage_battery(item: str, section: RedfishAPIData) -> CheckResult: + """Check single Controller battery state""" + data = section.get(item, None) + if data is None: + return + + battery_data = ( + data.get("Oem", {}).get("Dell", {}).get("DellControllerBattery", None) + ) + if not battery_data: + return + + batt_name = battery_data.get("Name", "Controller Battery") + batt_state = battery_data.get("PrimaryStatus", "Unknown") + batt_raid_state = battery_data.get("RAIDState", "Unknown") + state = 0 + if batt_state != "OK": + state = 2 + yield Result( + state=State(state), + summary=f"{batt_name} state: {batt_state}, RAID Status: {batt_raid_state}", + ) + + +check_plugin_redfish_storate_battery = CheckPlugin( + name="redfish_storage_battery", + service_name="Storage controller %s battery", + sections=["redfish_storage"], + discovery_function=discovery_redfish_storage_battery, + check_function=check_redfish_storage_battery, ) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_system.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_system.py index c40bfc87aba..0cdc1aa90c6 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_system.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_system.py @@ -41,17 +41,26 @@ def check_redfish_system(item: str, section: SectionSystem) -> CheckResult: if not (data := section.get(item)): return - state = data.get("Status", {"Health": "Unknown"}) - result_state, state_text = redfish_health_state(state) - message = f"System with SerialNr: {data.get('SerialNumber')}, has State: {state_text}" - details = None - - if service_tag := ( - data.get("Oem", {}).get("Dell", {}).get("DellSystem", {}).get("ChassisServiceTag") - ): - details = f"Service Tag: {service_tag}" - - yield Result(state=State(result_state), summary=message, details=details) + if serial := data.get("SerialNumber"): + yield Result(state=State(0), summary=f"Serial Number: {serial}") + + details = [] + if sku_data := data.get("SKU"): + details.append(f"SKU: {sku_data}") + + if dell_data := (data.get("Oem") or {}).get("Dell", {}).get("DellSystem", {}): + for key in dell_data.keys(): + if "Rollup" in key or "Tag" in key or "Code" in key: + key_name = key.replace("Rollup", " Rollup").replace("Status", " Status").strip() + details.append(f"{key_name}: {dell_data[key]}") + + if details: + details_msg = "\n".join(details) + else: + details_msg = "No additional details available." + + dev_state, dev_msg = redfish_health_state(data.get("Status", {})) + yield Result(state=State(dev_state), summary=dev_msg, details=details_msg) check_plugin_redfish_system = CheckPlugin( diff --git a/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py b/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py index 5bb8b7f443e..e5fb5733023 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py @@ -4,7 +4,7 @@ # conditions defined in the file COPYING, which is part of this source code package. """Metric definition for PSUs""" -from cmk.graphing.v1 import metrics, perfometers, Title +from cmk.graphing.v1 import graphs, metrics, perfometers, Title metric_input_power = metrics.Metric( name="input_power", @@ -33,3 +33,140 @@ segments=[metric_output_power.name], ), ) + +metric_averageconsumedwatts_0 = metrics.Metric( + name="averageconsumedwatts_0", + title=Title("Average Consumed Watts System 0"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.BLUE, +) + +metric_minconsumedwatts_0 = metrics.Metric( + name="minconsumedwatts_0", + title=Title("Minimum Consumed Watts System 0"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.LIGHT_BLUE, +) + +metric_maxconsumedwatts_0 = metrics.Metric( + name="maxconsumedwatts_0", + title=Title("Maximum Consumed Watts System 0"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.DARK_BLUE, +) + +metric_averageconsumedwatts_2 = metrics.Metric( + name="averageconsumedwatts_2", + title=Title("Average Consumed Watts System 2"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.BLUE, +) + +metric_minconsumedwatts_2 = metrics.Metric( + name="minconsumedwatts_2", + title=Title("Minimum Consumed Watts System 2"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.LIGHT_BLUE, +) + +metric_maxconsumedwatts_2 = metrics.Metric( + name="maxconsumedwatts_2", + title=Title("Maximum Consumed Watts System 2"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.DARK_BLUE, +) + +metric_averageconsumedwatts_1 = metrics.Metric( + name="averageconsumedwatts_1", + title=Title("Average Consumed Watts System 1"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.BLUE, +) + +metric_minconsumedwatts_1 = metrics.Metric( + name="minconsumedwatts_1", + title=Title("Minimum Consumed Watts System 1"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.LIGHT_BLUE, +) + +metric_maxconsumedwatts_1 = metrics.Metric( + name="maxconsumedwatts_1", + title=Title("Maximum Consumed Watts System 1"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.DARK_BLUE, +) + +metric_averageconsumedwatts_3 = metrics.Metric( + name="averageconsumedwatts_3", + title=Title("Average Consumed Watts System 3"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.BLUE, +) + +metric_minconsumedwatts_3 = metrics.Metric( + name="minconsumedwatts_3", + title=Title("Minimum Consumed Watts System 3"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.LIGHT_BLUE, +) + +metric_maxconsumedwatts_3 = metrics.Metric( + name="maxconsumedwatts_3", + title=Title("Maximum Consumed Watts System 3"), + unit=metrics.Unit(metrics.DecimalNotation("W")), + color=metrics.Color.DARK_BLUE, +) + +perfometer_averageconsumedwatts_0 = perfometers.Perfometer( + name="averageconsumedwatts_0", + focus_range=perfometers.FocusRange( + perfometers.Closed(0), + perfometers.Open( + metrics.MaximumOf("averageconsumedwatts_0", color=metrics.Color.BLUE) + ), + ), + segments=[ + "averageconsumedwatts_0", + ], +) + +graph_power_metrics_system_0 = graphs.Graph( + name="redfish_power_metrics_system_0", + title=Title("Redfish Power Metrics System 0"), + simple_lines=[ + "averageconsumedwatts_0", + "minconsumedwatts_0", + "maxconsumedwatts_0", + ], +) + +graph_power_metrics_system_1 = graphs.Graph( + name="redfish_power_metrics_system_1", + title=Title("Redfish Power Metrics System 1"), + simple_lines=[ + "averageconsumedwatts_1", + "minconsumedwatts_1", + "maxconsumedwatts_1", + ], +) + +graph_power_metrics_system_2 = graphs.Graph( + name="redfish_power_metrics_system_2", + title=Title("Redfish Power Metrics System 2"), + simple_lines=[ + "averageconsumedwatts_2", + "minconsumedwatts_2", + "maxconsumedwatts_2", + ], +) + +graph_power_metrics_system_3 = graphs.Graph( + name="redfish_power_metrics_system_3", + title=Title("Redfish Power Metrics System 3"), + simple_lines=[ + "averageconsumedwatts_3", + "minconsumedwatts_3", + "maxconsumedwatts_3", + ], +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/lib.py b/packages/cmk-plugins/cmk/plugins/redfish/lib.py index c99d8268b04..19a46e41759 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/lib.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/lib.py @@ -259,6 +259,8 @@ def process_redfish_perfdata(entry: Mapping[str, Any]) -> None | Perfdata: def optional_tuple(warn: float | None, crit: float | None) -> Levels | None: assert (warn is None) == (crit is None) + if warn == 0 and crit == 0: + return None if warn is not None and crit is not None: return ("fixed", (warn, crit)) return None diff --git a/packages/cmk-plugins/cmk/plugins/redfish/rulesets/redfish_storage.py b/packages/cmk-plugins/cmk/plugins/redfish/rulesets/redfish_storage.py new file mode 100644 index 00000000000..606e8115c55 --- /dev/null +++ b/packages/cmk-plugins/cmk/plugins/redfish/rulesets/redfish_storage.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Redfish Storage Controller Ruleset""" + +# (c) Andreas Doehler +# License: GNU General Public License v2 + +from cmk.rulesets.v1 import Title +from cmk.rulesets.v1.form_specs import ( + DictElement, + Dictionary, + SingleChoice, + SingleChoiceElement, + String, +) +from cmk.rulesets.v1.rule_specs import ( + CheckParameters, + Topic, + HostAndItemCondition, + LengthInRange, +) + + +def _parameter_valuespec_redfish_storage() -> Dictionary: + return Dictionary( + elements={ + "check_type": DictElement( + parameter_form=SingleChoice( + title=Title("Check Type for Storage Controller"), + elements=[ + SingleChoiceElement( + name="full", + title=Title("Full controller status with sub systems"), + ), + SingleChoiceElement( + name="rollup", + title=Title("Rollup check only"), + ), + ], + ) + ), + }, + ) + + +rule_spec_redfish_storage = CheckParameters( + name="redfish_storage", + title=Title("Redfish Storage Controller"), + topic=Topic.SERVER_HARDWARE, + condition=HostAndItemCondition( + item_title=Title("Controller ID"), + item_form=String(custom_validate=(LengthInRange(min_value=1),)), + ), + parameter_form=_parameter_valuespec_redfish_storage, +) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py index 159af133f68..638516f6c69 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py @@ -343,9 +343,12 @@ def fetch_sections( continue if section not in data.keys(): continue - section_data = fetch_data( - redfishobj.redfish_connection, data[section]["@odata.id"], section - ) + if data.get(section, {}): + section_data = fetch_data( + redfishobj.redfish_connection, data.get(section, {}).get("@odata.id"), section + ) + else: + continue if section_data.get("Members@odata.count") == 0: continue if "Collection" in section_data.get("@odata.type", {}): @@ -499,7 +502,8 @@ def get_information(storage: Storage, redfishobj: RedfishData) -> Literal[0]: manager_url = redfishobj.base_data.get("Managers", {}).get("@odata.id") chassis_url = redfishobj.base_data.get("Chassis", {}).get("@odata.id") systems_url = redfishobj.base_data.get("Systems", {}).get("@odata.id") - + if not systems_url or not chassis_url: + raise CannotRecover("ERROR: probably not a Redfish computer system - missing Systems or Chassis information") data_model = "" manager_data: Sequence[Mapping[str, Any]] = [] diff --git a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish_power.py b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish_power.py index 97ce983a259..acda3bbb69b 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish_power.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish_power.py @@ -283,6 +283,7 @@ def get_information(redfishobj): manager_url = base_data.get("Managers", {}).get("@odata.id") systems_url = base_data.get("PowerEquipment", {}).get("@odata.id") + chasis_url = base_data.get("Chassis", {}).get("@odata.id") manager_data = [] fw_version = vendor_data.firmware_version @@ -295,6 +296,19 @@ def get_information(redfishobj): for element in manager_data: fw_version = fw_version or element.get("FirmwareVersion") + if chasis_url: + chasis_col = fetch_data(redfishobj, chasis_url, "Chassis") + chasis_data = fetch_collection(redfishobj, chasis_col, "Chassis") + sys.stdout.write("<<>>\n") + sys.stdout.write(f"{json.dumps(chasis_data, sort_keys=True)}\n") + chassis_sections = [ + "Sensors", + ] + + for chassis in chasis_data: + result = fetch_sections(redfishobj, chassis_sections, chassis) + process_result(result) + labels: Mapping[str, str] = { "cmk/os_family": "redfish", **({"cmk/os_name": v} if (v := vendor_data.version) else {}), From 572a343698312a296c15853eeb492cec5ea21eb8 Mon Sep 17 00:00:00 2001 From: Andreas Doehler Date: Tue, 3 Feb 2026 20:15:27 +0100 Subject: [PATCH 2/2] make formatter happy --- .../cmk/plugins/redfish/agent_based/inventory_redfish_data.py | 4 +--- .../cmk/plugins/redfish/agent_based/redfish_pdus.py | 4 +--- .../cmk/plugins/redfish/agent_based/redfish_physicaldrives.py | 4 +--- .../cmk/plugins/redfish/agent_based/redfish_storage.py | 4 +--- packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py | 4 +--- .../cmk/plugins/redfish/special_agents/agent_redfish.py | 4 +++- 6 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py index b9aefcf5527..1099f213fab 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/inventory_redfish_data.py @@ -66,9 +66,7 @@ def inventory_redfish_data( odata_ids_set = _extract_odata_ids(section_redfish_memory, odata_ids_set) odata_ids_set = _extract_odata_ids(section_redfish_power, odata_ids_set) odata_ids_set = _extract_odata_ids(section_redfish_thermal, odata_ids_set) - odata_ids_set = _extract_odata_ids( - section_redfish_networkadapters, odata_ids_set - ) + odata_ids_set = _extract_odata_ids(section_redfish_networkadapters, odata_ids_set) for path, name, serial, part_number, manufacturer, model in odata_ids_set: if serial == "nothing set": diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py index bfd4a9892e8..21c4aba8bc8 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_pdus.py @@ -51,9 +51,7 @@ def check_redfish_pdus(item: str, section: RedfishAPIData) -> CheckResult: model = data.get("Model") manufacturer = data.get("Manufacturer") - yield Result( - state=State.OK, summary=f"PDU {manufacturer} {model} S/N {serial} FW {firmware}" - ) + yield Result(state=State.OK, summary=f"PDU {manufacturer} {model} S/N {serial} FW {firmware}") dev_state, dev_msg = redfish_health_state(data.get("Status", {})) yield Result(state=State(dev_state), summary=dev_msg) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py index 3daf4ac29ff..d278f200c94 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_physicaldrives.py @@ -86,9 +86,7 @@ def check_redfish_physicaldrives(item: str, section: RedfishAPIData) -> CheckRes yield Metric("media_life_left", int(data.get("PredictedMediaLifeLeftPercent"))) elif data.get("SSDEnduranceUtilizationPercentage"): media_life_left = 100 - int(data.get("SSDEnduranceUtilizationPercentage", 0)) - disc_msg = ( - f"{disc_msg}, Media Life Left: {media_life_left}%" - ) + disc_msg = f"{disc_msg}, Media Life Left: {media_life_left}%" yield Metric("ssd_utilization", int(data.get("SSDEnduranceUtilizationPercentage"))) yield Result(state=State(0), summary=disc_msg) diff --git a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py index f41028df4e2..ab89fe03be5 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/agent_based/redfish_storage.py @@ -104,9 +104,7 @@ def check_redfish_storage_battery(item: str, section: RedfishAPIData) -> CheckRe if data is None: return - battery_data = ( - data.get("Oem", {}).get("Dell", {}).get("DellControllerBattery", None) - ) + battery_data = data.get("Oem", {}).get("Dell", {}).get("DellControllerBattery", None) if not battery_data: return diff --git a/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py b/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py index e5fb5733023..dea992bf746 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/graphing/power.py @@ -122,9 +122,7 @@ name="averageconsumedwatts_0", focus_range=perfometers.FocusRange( perfometers.Closed(0), - perfometers.Open( - metrics.MaximumOf("averageconsumedwatts_0", color=metrics.Color.BLUE) - ), + perfometers.Open(metrics.MaximumOf("averageconsumedwatts_0", color=metrics.Color.BLUE)), ), segments=[ "averageconsumedwatts_0", diff --git a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py index 638516f6c69..c11eb8576fd 100644 --- a/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py +++ b/packages/cmk-plugins/cmk/plugins/redfish/special_agents/agent_redfish.py @@ -503,7 +503,9 @@ def get_information(storage: Storage, redfishobj: RedfishData) -> Literal[0]: chassis_url = redfishobj.base_data.get("Chassis", {}).get("@odata.id") systems_url = redfishobj.base_data.get("Systems", {}).get("@odata.id") if not systems_url or not chassis_url: - raise CannotRecover("ERROR: probably not a Redfish computer system - missing Systems or Chassis information") + raise CannotRecover( + "ERROR: probably not a Redfish computer system - missing Systems or Chassis information" + ) data_model = "" manager_data: Sequence[Mapping[str, Any]] = []