]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
node-proxy: allow multiple sources per component
authorGuillaume Abrioux <gabrioux@ibm.com>
Thu, 12 Feb 2026 14:00:11 +0000 (15:00 +0100)
committerGuillaume Abrioux <gabrioux@ibm.com>
Wed, 18 Feb 2026 08:52:38 +0000 (09:52 +0100)
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 <gabrioux@ibm.com>
src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py
src/ceph-node-proxy/ceph_node_proxy/redfish.py
src/ceph-node-proxy/tests/test_baseredfishsystem.py

index 7b0ea9186e3e9d7850388970cdea7b684c522e20..b5103f0d8aec4a423d480a383efe168f903b4285 100644 (file)
@@ -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")
index 02a1f698e154a3328e9e16c15a0c2314e8a650c5..d51bf48d1048dfdcc2eb640f2122fd5962706b74 100644 (file)
@@ -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
+    )
index e3d3382b1c85c6faaaa1280036fabf7a3cbf8b05..cb693fc9f669d3bb5a83242ac846a56214bd8693 100644 (file)
@@ -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() == {}