]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
node-proxy: add temperatures and fan speed
authorGuillaume Abrioux <gabrioux@ibm.com>
Wed, 10 Jun 2026 10:51:14 +0000 (12:51 +0200)
committerGuillaume Abrioux <gabrioux@ibm.com>
Wed, 24 Jun 2026 02:17:12 +0000 (04:17 +0200)
This adds the temperatures category and fan speed information.

Fixes: https://tracker.ceph.com/issues/77408
Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
13 files changed:
doc/hardware-monitoring/index.rst
src/ceph-node-proxy/ceph_node_proxy/api.py
src/ceph-node-proxy/ceph_node_proxy/baseredfishsystem.py
src/ceph-node-proxy/ceph_node_proxy/basesystem.py
src/ceph-node-proxy/ceph_node_proxy/protocols.py
src/ceph-node-proxy/ceph_node_proxy/redfish.py
src/ceph-node-proxy/tests/test_baseredfishsystem.py
src/pybind/mgr/cephadm/agent.py
src/pybind/mgr/cephadm/tests/node_proxy_data.py
src/pybind/mgr/cephadm/tests/test_node_proxy.py
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/hardware.enum.ts
src/pybind/mgr/dashboard/services/hardware.py
src/pybind/mgr/orchestrator/module.py

index de94f6ee6aa93aa05bffd64e9c63f6ed42a95a22..033b89aa6107a07b0c7dc47e3bea118e73b9f6d9 100644 (file)
@@ -88,6 +88,7 @@ Supported categories are:
 * network
 * power
 * fans
+* temperatures
 * firmwares
 * criticals
 
@@ -187,6 +188,7 @@ For Developers
 .. automethod:: NodeProxyEndpoint.power
 .. automethod:: NodeProxyEndpoint.processors
 .. automethod:: NodeProxyEndpoint.fans
+.. automethod:: NodeProxyEndpoint.temperatures
 .. automethod:: NodeProxyEndpoint.firmwares
 .. automethod:: NodeProxyEndpoint.led
 
index 0bb700820f04c1bc695634cd7f2923b1e51d1907..e51a1a3397734b0a3f87d3e8e6c98e8d66a4e81f 100644 (file)
@@ -110,6 +110,12 @@ class API(Server):
     def fans(self) -> Dict[str, Any]:
         return {"fans": self.backend.get_fans()}
 
+    @cherrypy.expose
+    @cherrypy.tools.allow(methods=["GET"])
+    @cherrypy.tools.json_out()
+    def temperatures(self) -> Dict[str, Any]:
+        return {"temperatures": self.backend.get_temperatures()}
+
     @cherrypy.expose
     @cherrypy.tools.allow(methods=["GET"])
     @cherrypy.tools.json_out()
index 4c4051951f1d72fefdd68ffeb0f01cc3c160aadf..15b513da6f0ac57f34860186b7afac279ca07abd 100644 (file)
@@ -33,7 +33,20 @@ class BaseRedfishSystem(BaseSystem):
         "Status",
     ]
     POWER_FIELDS: List[str] = ["Name", "Model", "Manufacturer", "Status"]
-    FANS_FIELDS: List[str] = ["Name", "PhysicalContext", "Status"]
+    FANS_FIELDS: List[str] = [
+        "Name",
+        "PhysicalContext",
+        "Reading",
+        "ReadingUnits",
+        "Status",
+    ]
+    TEMPERATURES_FIELDS: List[str] = [
+        "Name",
+        "PhysicalContext",
+        "Reading",
+        "ReadingUnits",
+        "Status",
+    ]
     FIRMWARES_FIELDS: List[str] = [
         "Name",
         "Description",
@@ -63,6 +76,11 @@ class BaseRedfishSystem(BaseSystem):
         "fans": [
             ComponentUpdateSpec("chassis", "Thermal", FANS_FIELDS, "Fans"),
         ],
+        "temperatures": [
+            ComponentUpdateSpec(
+                "chassis", "Thermal", TEMPERATURES_FIELDS, "Temperatures"
+            ),
+        ],
         "firmwares": [
             ComponentUpdateSpec(
                 "update_service", "FirmwareInventory", FIRMWARES_FIELDS, None
@@ -102,6 +120,7 @@ class BaseRedfishSystem(BaseSystem):
                 "memory",
                 "power",
                 "fans",
+                "temperatures",
                 "network",
                 "processors",
                 "storage",
@@ -209,6 +228,7 @@ class BaseRedfishSystem(BaseSystem):
                 "memory": self.get_memory(),
                 "power": self.get_power(),
                 "fans": self.get_fans(),
+                "temperatures": self.get_temperatures(),
             },
             "firmwares": self.get_firmwares(),
         }
@@ -251,6 +271,9 @@ class BaseRedfishSystem(BaseSystem):
     def get_fans(self) -> Dict[str, Dict[str, Dict]]:
         return dict(self._sys.get("fans", {}))
 
+    def get_temperatures(self) -> Dict[str, Dict[str, Dict]]:
+        return dict(self._sys.get("temperatures", {}))
+
     def get_component_spec_overrides(self) -> Dict[str, Dict[str, Any]]:
         return {}
 
@@ -361,6 +384,9 @@ class BaseRedfishSystem(BaseSystem):
     def _update_fans(self) -> None:
         self._run_update("fans")
 
+    def _update_temperatures(self) -> None:
+        self._run_update("temperatures")
+
     def _update_firmwares(self) -> None:
         self._run_update("firmwares")
 
index 3d02a7d4fad0ccdc28403de5848ce5f382ff829d..025638b5cde3fdd5ccc3122eb6f7ebf71f56fcc3 100644 (file)
@@ -36,6 +36,9 @@ class BaseSystem(BaseThread):
     def get_fans(self) -> Dict[str, Dict[str, Dict]]:
         raise NotImplementedError()
 
+    def get_temperatures(self) -> Dict[str, Dict[str, Dict]]:
+        raise NotImplementedError()
+
     def get_power(self) -> Dict[str, Dict[str, Dict]]:
         raise NotImplementedError()
 
index e8b91d760514517e030994b64e14f52ee06ebb00..2b322e8ed7e8bf772e194fbfd3e3c7e637c20ead 100644 (file)
@@ -16,6 +16,8 @@ class SystemBackend(Protocol):
 
     def get_fans(self) -> Dict[str, Any]: ...
 
+    def get_temperatures(self) -> Dict[str, Any]: ...
+
     def get_firmwares(self) -> Dict[str, Any]: ...
 
     def get_led(self) -> Dict[str, Any]: ...
index d51bf48d1048dfdcc2eb640f2122fd5962706b74..bbf17dad71e1b65368464a35927bcf98663851a9 100644 (file)
@@ -231,6 +231,14 @@ class Endpoint:
         )
 
 
+def is_absent_member(item: Dict[str, Any]) -> bool:
+    status = item.get("Status")
+    if not isinstance(status, dict):
+        return False
+    state = status.get("State")
+    return isinstance(state, str) and state.lower() == "absent"
+
+
 def build_data(
     data: Dict[str, Any],
     fields: List[str],
@@ -247,6 +255,10 @@ def build_data(
             except KeyError:
                 log.debug(f"Could not find field: {field} in data: {d}")
                 out[to_snake_case(field)] = None
+        if out.get("reading") is None and "ReadingCelsius" in d:
+            out["reading"] = d["ReadingCelsius"]
+            if out.get("reading_units") is None:
+                out["reading_units"] = "Cel"
         return out
 
     try:
@@ -256,8 +268,9 @@ def build_data(
             data_items = [{"MemberId": k, **v} for k, v in data.items()]
         log.debug(f"build_data: data_items count={len(data_items)}")
         for d in data_items:
+            if is_absent_member(d):
+                continue
             member_id = d.get("MemberId")
-            result[member_id] = {}
             result[member_id] = process_data(member_id, fields, d)
     except (KeyError, TypeError, AttributeError) as e:
         log.error(f"Can't build data: {e}")
index adac813a54cd2535582bdcd105f01a055a4e4e44..2939081935ced6e0af618a42e3f2e3157542ff9c 100644 (file)
@@ -48,6 +48,7 @@ class TestBaseRedfishSystemInit:
         assert "storage" in system.component_list
         assert "firmwares" in system.component_list
         assert "fans" in system.component_list
+        assert "temperatures" in system.component_list
 
     def test_init_update_funcs_populated(self, system):
         # Should have one callable per component that has _update_<name>
@@ -106,6 +107,9 @@ class TestBaseRedfishSystemGetters:
     def test_get_fans_empty(self, system):
         assert system.get_fans() == {}
 
+    def test_get_temperatures_empty(self, system):
+        assert system.get_temperatures() == {}
+
     def test_get_firmwares_empty(self, system):
         assert system.get_firmwares() == {}
 
@@ -122,6 +126,7 @@ class TestBaseRedfishSystemGetSystem:
         system._sys["storage"] = {}
         system._sys["power"] = {}
         system._sys["fans"] = {}
+        system._sys["temperatures"] = {}
         system._sys["firmwares"] = {}
         result = system.get_system()
         assert "host" in result
@@ -134,6 +139,7 @@ class TestBaseRedfishSystemGetSystem:
         assert result["status"]["storage"] == {}
         assert result["status"]["power"] == {}
         assert result["status"]["fans"] == {}
+        assert result["status"]["temperatures"] == {}
         assert "firmwares" in result
 
 
@@ -170,6 +176,15 @@ class TestBaseRedfishSystemGetSpecs:
         assert specs[0].collection == "chassis"
         assert specs[0].path == "Thermal"
         assert specs[0].attribute == "Fans"
+        assert "Reading" in specs[0].fields
+
+    def test_get_specs_temperatures(self, system):
+        specs = system.get_specs("temperatures")
+        assert len(specs) == 1
+        assert specs[0].collection == "chassis"
+        assert specs[0].path == "Thermal"
+        assert specs[0].attribute == "Temperatures"
+        assert "Reading" in specs[0].fields
 
     def test_get_component_spec_overrides_empty(self, system):
         assert system.get_component_spec_overrides() == {}
@@ -232,6 +247,7 @@ class TestBaseRedfishSystemComponentSpecs:
         assert "memory" in BaseRedfishSystem.COMPONENT_SPECS
         assert "power" in BaseRedfishSystem.COMPONENT_SPECS
         assert "fans" in BaseRedfishSystem.COMPONENT_SPECS
+        assert "temperatures" in BaseRedfishSystem.COMPONENT_SPECS
         assert "firmwares" in BaseRedfishSystem.COMPONENT_SPECS
 
     def test_field_lists_non_empty(self):
index d767f1b24b57f94d60e36c57e5d0eb0d47131c36..166d9ba48c06e6314b494f4e475e9cd0d9b0179b 100644 (file)
@@ -612,6 +612,16 @@ class NodeProxyEndpoint:
             raise cherrypy.HTTPError(404, f"{kw.get('hostname')} not found.")
         return results
 
+    @cherrypy.expose
+    @cherrypy.tools.allow(methods=['GET'])
+    @cherrypy.tools.json_out()
+    def temperatures(self, **kw: Any) -> Dict[str, Any]:
+        try:
+            results = self.mgr.node_proxy_cache.common('temperatures', **kw)
+        except (KeyError, OrchestratorError):
+            raise cherrypy.HTTPError(404, f"{kw.get('hostname')} not found.")
+        return results
+
     @cherrypy.expose
     @cherrypy.tools.allow(methods=['GET'])
     @cherrypy.tools.json_out()
index fa768f1d4c65c8e1985c6e15f4e9a45edcb8dfe4..6f103dbeba1a74d2c96dd515c1ed64153a091564 100644 (file)
@@ -1,3 +1,3 @@
 full_set_with_critical = {'host': 'host01', 'sn': '12345', 'status': {'storage': {'1': {'disk.bay.0:enclosure.internal.0-1:raid.integrated.1-1': {'description': 'Solid State Disk 0:1:0', 'entity': 'RAID.Integrated.1-1', 'capacity_bytes': 959656755200, 'model': 'KPM5XVUG960G', 'protocol': 'SAS', 'serial_number': '8080A1CRTP5F', 'status': {'health': 'Critical', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 0, 'locationtype': 'Slot'}}}, 'disk.bay.9:enclosure.internal.0-1': {'description': 'PCIe SSD in Slot 9 in Bay 1', 'entity': 'CPU.1', 'capacity_bytes': 1600321314816, 'model': 'Dell Express Flash NVMe P4610 1.6TB SFF', 'protocol': 'PCIe', 'serial_number': 'PHLN035305MN1P6AGN', 'status': {'health': 'Critical', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 9, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 20, 'total_threads': 40, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'nic.slot.1-1-1': {'description': 'NIC in Slot 1 Port 1 Partition 1', 'name': 'System Ethernet Interface', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'StandbyOffline'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 31237, 'status': {'health': 'Critical', 'state': 'Enabled'}}}}}, 'firmwares': {}}
 mgr_inventory_cache = {'host01': {'hostname': 'host01', 'addr': '10.10.10.11', 'labels': ['_admin'], 'status': '', 'oob': {'hostname': '10.10.10.11', 'username': 'root', 'password': 'ceph123'}}, 'host02': {'hostname': 'host02', 'addr': '10.10.10.12', 'labels': [], 'status': '', 'oob': {'hostname': '10.10.10.12', 'username': 'root', 'password': 'ceph123'}}}
-full_set = {'host01': {'host': 'host01', 'sn': 'FR8Y5X3', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmwares': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'host02': {'host': 'host02', 'sn': 'FR8Y5X4', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmwares': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}
+full_set = {'host01': {'host': 'host01', 'sn': 'FR8Y5X3', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmwares': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'host02': {'host': 'host02', 'sn': 'FR8Y5X4', 'status': {'storage': {'1': {'disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1': {'description': 'Disk 8 in Backplane 1 of Storage Controller in Slot 2', 'entity': 'NonRAID.Slot.2-1', 'capacity_bytes': 20000588955136, 'model': 'ST20000NM008D-3D', 'protocol': 'SATA', 'serial_number': 'ZVT99QLL', 'status': {'health': 'OK', 'healthrollup': 'OK', 'state': 'Enabled'}, 'physical_location': {'partlocation': {'locationordinalvalue': 8, 'locationtype': 'Slot'}}}}}, 'processors': {'1': {'cpu.socket.2': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}, 'cpu.socket.1': {'description': 'Represents the properties of a Processor attached to this System', 'total_cores': 16, 'total_threads': 32, 'processor_type': 'CPU', 'model': 'Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz', 'status': {'health': 'OK', 'state': 'Enabled'}, 'manufacturer': 'Intel'}}}, 'network': {'1': {'oslogicalnetwork.2': {'description': 'eno8303', 'name': 'eno8303', 'speed_mbps': 0, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'memory': {'1': {'dimm.socket.a1': {'description': 'DIMM A1', 'memory_device_type': 'DDR4', 'capacity_mi_b': 16384, 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'power': {'1': {'0': {'name': 'PS1 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}, '1': {'name': 'PS2 Status', 'model': 'PWR SPLY,800W,RDNT,LTON', 'manufacturer': 'DELL', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'fans': {'1': {'0': {'name': 'System Board Fan1A', 'physical_context': 'SystemBoard', 'reading': 4800, 'reading_units': 'RPM', 'status': {'health': 'OK', 'state': 'Enabled'}}}}, 'temperatures': {'1': {'0': {'name': 'System Board Inlet Temp', 'physical_context': 'Intake', 'reading': 24, 'reading_units': 'Cel', 'status': {'health': 'OK', 'state': 'Enabled'}}}}}, 'firmwares': {'installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1': {'name': 'Lifecycle Controller', 'description': 'Represents Firmware Inventory', 'release_date': '00:00:00Z', 'version': '6.10.30.20', 'updateable': True, 'status': {'health': 'OK', 'state': 'Enabled'}}}}}
index 75fe7d6fab916bd28c3386089cae9d5b47b10772..dd6593fd273682ba2fff410578ec87901a7638a6 100644 (file)
@@ -319,6 +319,18 @@ class TestNodeProxyEndpoint(helper.CPWebCase):
         self.getPage("/host03/fans", method="GET")
         self.assertStatus('404 Not Found')
 
+    def test_temperatures_with_valid_hostname(self):
+        self.getPage("/host02/temperatures", method="GET")
+        self.assertStatus('200 OK')
+
+    def test_temperatures_no_hostname(self):
+        self.getPage("/temperatures", method="GET")
+        self.assertStatus('200 OK')
+
+    def test_temperatures_with_invalid_hostname(self):
+        self.getPage("/host03/temperatures", method="GET")
+        self.assertStatus('404 Not Found')
+
     def test_firmwares_with_valid_hostname(self):
         self.getPage("/host02/firmwares", method="GET")
         self.assertStatus('200 OK')
index 7956dfa5d7c000212d73bb857e6636347e3d70ea..ae115d05001a3b137caf0b1f994b57a54f55b23c 100644 (file)
@@ -4,5 +4,6 @@ export enum HardwareNameMapping {
   processors = 'CPU',
   network = 'Network',
   power = 'Power supply',
-  fans = 'Fan module'
+  fans = 'Fan module',
+  temperatures = 'Temperature'
 }
index 31054ab4cc7af4a125e9e5b7e7359910c5fdeb36..2aa89f422a38eef35c5faaccfb98ad29e306d084 100644 (file)
@@ -75,7 +75,7 @@ class HardwareService(object):
     @staticmethod
     def validate_categories(categories: Optional[List[str]]) -> List[str]:
         categories_list = ['memory', 'storage', 'processors',
-                           'network', 'power', 'fans']
+                           'network', 'power', 'fans', 'temperatures']
 
         if isinstance(categories, str):
             categories = [categories]
index 8a1cbea42fc558c093f3ed7d9a7e4d721d111fec..abb5f20543507274a5199d8ce550af5b29855f0c 100644 (file)
@@ -534,7 +534,8 @@ class OrchestratorCli(OrchestratorClientMixin, MgrModule):
             'processors': ['HOST', 'SYS_ID', 'NAME', 'MODEL', 'CORES', 'THREADS', 'STATUS', 'STATE'],
             'network': ['HOST', 'SYS_ID', 'NAME', 'SPEED', 'STATUS', 'STATE'],
             'power': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'MODEL', 'MANUFACTURER', 'STATUS', 'STATE'],
-            'fans': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'STATUS', 'STATE']
+            'fans': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'READING', 'UNITS', 'STATUS', 'STATE'],
+            'temperatures': ['HOST', 'CHASSIS_ID', 'ID', 'NAME', 'READING', 'UNITS', 'STATUS', 'STATE'],
         }
 
         if category not in table_heading_mapping.keys():
@@ -619,7 +620,8 @@ class OrchestratorCli(OrchestratorClientMixin, MgrModule):
             'processors': ('model', 'total_cores', 'total_threads', 'health', 'state'),
             'network': ('name', 'speed_mbps', 'health', 'state'),
             'power': ('name', 'model', 'manufacturer', 'health', 'state'),
-            'fans': ('name', 'health', 'state')
+            'fans': ('name', 'reading', 'reading_units', 'health', 'state'),
+            'temperatures': ('name', 'reading', 'reading_units', 'health', 'state'),
         }
 
         fields = mapping.get(category, ())
@@ -634,7 +636,7 @@ class OrchestratorCli(OrchestratorClientMixin, MgrModule):
                             row.append(v['status'][field])
                         else:
                             row.append('')
-                    if category in ('power', 'fans', 'processors'):
+                    if category in ('power', 'fans', 'temperatures', 'processors'):
                         table.add_row((host, sys_id,) + (k,) + tuple(row))
                     else:
                         table.add_row((host, sys_id,) + tuple(row))