From: Guillaume Abrioux Date: Thu, 12 Feb 2026 14:00:11 +0000 (+0100) Subject: node-proxy: allow multiple sources per component X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=d382bbcfd43c85dba4af6b5050603ba374d4963a;p=ceph.git node-proxy: allow multiple sources per component COMPONENT_SPECS can now be a single spec or a list of specs per component. Data from all sources is merged. Unavailable paths are skipped. Extract get_component_data() from update_component() to support the merge logic. Fixes: https://tracker.ceph.com/issues/74749 Signed-off-by: Guillaume Abrioux --- diff --git a/src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py b/src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py index 7b0ea9186e3..b5103f0d8ae 100644 --- a/src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py +++ b/src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py @@ -8,6 +8,7 @@ from ceph_node_proxy.redfish import ( ComponentUpdateSpec, Endpoint, EndpointMgr, + get_component_data, update_component, ) from ceph_node_proxy.redfish_client import RedFishClient @@ -42,22 +43,31 @@ class BaseRedfishSystem(BaseSystem): "Status", ] - COMPONENT_SPECS: Dict[str, ComponentUpdateSpec] = { - "network": ComponentUpdateSpec( - "systems", "EthernetInterfaces", NETWORK_FIELDS, None - ), - "processors": ComponentUpdateSpec( - "systems", "Processors", PROCESSORS_FIELDS, None - ), - "memory": ComponentUpdateSpec("systems", "Memory", MEMORY_FIELDS, None), + COMPONENT_SPECS: Dict[str, List[ComponentUpdateSpec]] = { + "network": [ + ComponentUpdateSpec("systems", "EthernetInterfaces", NETWORK_FIELDS, None), + ComponentUpdateSpec("systems", "NetworkInterfaces", NETWORK_FIELDS, None), + ], + "processors": [ + ComponentUpdateSpec("systems", "Processors", PROCESSORS_FIELDS, None), + ], + "memory": [ + ComponentUpdateSpec("systems", "Memory", MEMORY_FIELDS, None), + ], # Power supplies: Chassis/.../PowerSubsystem/PowerSupplies (not like other components: like Systems/.../Memory) - "power": ComponentUpdateSpec( - "chassis", "PowerSubsystem/PowerSupplies", POWER_FIELDS, None - ), - "fans": ComponentUpdateSpec("chassis", "Thermal", FANS_FIELDS, "Fans"), - "firmwares": ComponentUpdateSpec( - "update_service", "FirmwareInventory", FIRMWARES_FIELDS, None - ), + "power": [ + ComponentUpdateSpec( + "chassis", "PowerSubsystem/PowerSupplies", POWER_FIELDS, None + ), + ], + "fans": [ + ComponentUpdateSpec("chassis", "Thermal", FANS_FIELDS, "Fans"), + ], + "firmwares": [ + ComponentUpdateSpec( + "update_service", "FirmwareInventory", FIRMWARES_FIELDS, None + ), + ], } def __init__(self, **kw: Any) -> None: @@ -244,8 +254,15 @@ class BaseRedfishSystem(BaseSystem): def get_component_spec_overrides(self) -> Dict[str, Dict[str, Any]]: return {} - def get_update_spec(self, component: str) -> ComponentUpdateSpec: - spec = self.COMPONENT_SPECS[component] + def get_specs(self, component: str) -> List[ComponentUpdateSpec]: + return [ + self._apply_spec_overrides(component, spec) + for spec in self.COMPONENT_SPECS[component] + ] + + def _apply_spec_overrides( + self, component: str, spec: ComponentUpdateSpec + ) -> ComponentUpdateSpec: overrides = self.get_component_spec_overrides().get(component) if not overrides: return spec @@ -253,10 +270,37 @@ class BaseRedfishSystem(BaseSystem): def _run_update(self, component: str) -> None: self.log.debug(f"Updating {component}") - spec = self.get_update_spec(component) - self.update( - spec.collection, component, spec.path, spec.fields, attribute=spec.attribute - ) + specs = self.get_specs(component) + self._sys[component] = {} + use_single_key = len(specs) == 1 + for spec in specs: + try: + result = get_component_data( + self.endpoints, + spec.collection, + spec.path, + spec.fields, + self.log, + attribute=spec.attribute, + ) + path_prefix = spec.path.split("/")[-1].lower() or component + for sys_id, members in result.items(): + if sys_id not in self._sys[component]: + self._sys[component][sys_id] = {} + for member_id, data in members.items(): + key = ( + member_id + if use_single_key + else f"{path_prefix}_{member_id}" + ) + self._sys[component][sys_id][key] = data + except Exception as e: + self.log.debug( + "Skipping %s path %s (not available on this hardware): %s", + component, + spec.path, + e, + ) def _update_network(self) -> None: self._run_update("network") diff --git a/src/ceph-node-proxy/ceph_node_proxy/redfish.py b/src/ceph-node-proxy/ceph_node_proxy/redfish.py index 02a1f698e15..d51bf48d104 100644 --- a/src/ceph-node-proxy/ceph_node_proxy/redfish.py +++ b/src/ceph-node-proxy/ceph_node_proxy/redfish.py @@ -123,9 +123,7 @@ class Endpoint: except KeyError as e: self.log.error(f"KeyError while querying {url}: {e}") except HTTPError as e: - self.log.error( - f"HTTP error while querying {url} - {e.code} - {e.reason}" - ) + self.log.error(f"HTTP error while querying {url} - {e.code} - {e.reason}") except json.JSONDecodeError as e: self.log.error(f"JSON decode error while querying {url}: {e}") except Exception as e: @@ -276,19 +274,15 @@ def _resolve_path(endpoint: Endpoint, path: str) -> Endpoint: return current -def update_component( +def get_component_data( endpoints: EndpointMgr, collection: str, - component: str, path: str, fields: List[str], - _sys: Dict[str, Any], log: Any, attribute: Optional[str] = None, -) -> None: - """Update _sys[component] from Redfish endpoints using the given spec. - path can be a single segment ('Memory') or multiple ('PowerSubsystem/PowerSupplies'). - """ +) -> Dict[str, Any]: + """Build component data from Redfish endpoints. Returns dict sys_id -> member_id -> data.""" members: List[str] = endpoints[collection].get_members_names() result: Dict[str, Any] = {} if not members: @@ -307,6 +301,24 @@ def update_component( data=data, fields=fields, log=log, attribute=attribute ) except HTTPError as e: - log.error(f"Error while updating {component}: {e}") + log.error(f"Error while updating {path}: {e}") continue - _sys[component] = result + return result + + +def update_component( + endpoints: EndpointMgr, + collection: str, + component: str, + path: str, + fields: List[str], + _sys: Dict[str, Any], + log: Any, + attribute: Optional[str] = None, +) -> None: + """Update _sys[component] from Redfish endpoints using the given spec. + path can be a single segment ('Memory') or multiple ('PowerSubsystem/PowerSupplies'). + """ + _sys[component] = get_component_data( + endpoints, collection, path, fields, log, attribute=attribute + ) diff --git a/src/ceph-node-proxy/tests/test_baseredfishsystem.py b/src/ceph-node-proxy/tests/test_baseredfishsystem.py index e3d3382b1c8..cb693fc9f66 100644 --- a/src/ceph-node-proxy/tests/test_baseredfishsystem.py +++ b/src/ceph-node-proxy/tests/test_baseredfishsystem.py @@ -137,33 +137,39 @@ class TestBaseRedfishSystemGetSystem: assert "firmwares" in result -class TestBaseRedfishSystemGetUpdateSpec: - - def test_get_update_spec_network(self, system): - spec = system.get_update_spec("network") - assert isinstance(spec, ComponentUpdateSpec) - assert spec.collection == "systems" - assert spec.path == "EthernetInterfaces" - assert "Name" in spec.fields - assert spec.attribute is None - - def test_get_update_spec_memory(self, system): - spec = system.get_update_spec("memory") - assert spec.collection == "systems" - assert spec.path == "Memory" - assert "CapacityMiB" in spec.fields - - def test_get_update_spec_power(self, system): - spec = system.get_update_spec("power") - assert spec.collection == "chassis" - assert "PowerSubsystem" in spec.path - assert spec.attribute is None - - def test_get_update_spec_fans(self, system): - spec = system.get_update_spec("fans") - assert spec.collection == "chassis" - assert spec.path == "Thermal" - assert spec.attribute == "Fans" +class TestBaseRedfishSystemGetSpecs: + + def test_get_specs_network(self, system): + specs = system.get_specs("network") + assert len(specs) == 2 + assert all(isinstance(s, ComponentUpdateSpec) for s in specs) + paths = [s.path for s in specs] + assert "EthernetInterfaces" in paths + assert "NetworkInterfaces" in paths + assert specs[0].collection == "systems" + assert "Name" in specs[0].fields + assert specs[0].attribute is None + + def test_get_specs_memory(self, system): + specs = system.get_specs("memory") + assert len(specs) == 1 + assert specs[0].collection == "systems" + assert specs[0].path == "Memory" + assert "CapacityMiB" in specs[0].fields + + def test_get_specs_power(self, system): + specs = system.get_specs("power") + assert len(specs) == 1 + assert specs[0].collection == "chassis" + assert "PowerSubsystem" in specs[0].path + assert specs[0].attribute is None + + def test_get_specs_fans(self, system): + specs = system.get_specs("fans") + assert len(specs) == 1 + assert specs[0].collection == "chassis" + assert specs[0].path == "Thermal" + assert specs[0].attribute == "Fans" def test_get_component_spec_overrides_empty(self, system): assert system.get_component_spec_overrides() == {}