This adds the temperatures category and fan speed information.
Fixes: https://tracker.ceph.com/issues/77408
Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
* network
* power
* fans
+* temperatures
* firmwares
* criticals
.. automethod:: NodeProxyEndpoint.power
.. automethod:: NodeProxyEndpoint.processors
.. automethod:: NodeProxyEndpoint.fans
+.. automethod:: NodeProxyEndpoint.temperatures
.. automethod:: NodeProxyEndpoint.firmwares
.. automethod:: NodeProxyEndpoint.led
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()
"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",
"fans": [
ComponentUpdateSpec("chassis", "Thermal", FANS_FIELDS, "Fans"),
],
+ "temperatures": [
+ ComponentUpdateSpec(
+ "chassis", "Thermal", TEMPERATURES_FIELDS, "Temperatures"
+ ),
+ ],
"firmwares": [
ComponentUpdateSpec(
"update_service", "FirmwareInventory", FIRMWARES_FIELDS, None
"memory",
"power",
"fans",
+ "temperatures",
"network",
"processors",
"storage",
"memory": self.get_memory(),
"power": self.get_power(),
"fans": self.get_fans(),
+ "temperatures": self.get_temperatures(),
},
"firmwares": self.get_firmwares(),
}
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 {}
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")
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()
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]: ...
)
+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],
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:
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}")
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>
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() == {}
system._sys["storage"] = {}
system._sys["power"] = {}
system._sys["fans"] = {}
+ system._sys["temperatures"] = {}
system._sys["firmwares"] = {}
result = system.get_system()
assert "host" in result
assert result["status"]["storage"] == {}
assert result["status"]["power"] == {}
assert result["status"]["fans"] == {}
+ assert result["status"]["temperatures"] == {}
assert "firmwares" in result
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() == {}
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):
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()
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'}}}}}
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')
processors = 'CPU',
network = 'Network',
power = 'Power supply',
- fans = 'Fan module'
+ fans = 'Fan module',
+ temperatures = 'Temperature'
}
@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]
'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():
'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, ())
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))