From 5d5ad821f4ebb47e4088f10d9aa2fdefa51adf2e Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 1 Sep 2026 23:51:32 +0200 Subject: [PATCH 1/2] Read health from a service that publishes no PCIe PCIeDevices and PCIeFunctions are optional, and get_health indexed both without checking. _get_adp_urls in the same file already spells it .get('PCIeDevices', []), so these two were the outliers. Power and cooling equipment has no PCIe at all. The KeyError escaped the health read and reached the user as "Unexpected Error" with a traceback behind it. Reproduces offline against DMTF's public-rackmount1 mockup. --- confluent_server/aiohmi/redfish/oem/generic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/confluent_server/aiohmi/redfish/oem/generic.py b/confluent_server/aiohmi/redfish/oem/generic.py index b44bc253..763d0383 100644 --- a/confluent_server/aiohmi/redfish/oem/generic.py +++ b/confluent_server/aiohmi/redfish/oem/generic.py @@ -789,11 +789,14 @@ class OEMHandler(object): meminfo = sysinfo['MemorySummary'] meminfo['Name'] = 'Memory' summary['badreadings'].append(SensorReading(meminfo)) - for adapter in sysinfo['PCIeDevices']: + # Both are optional in Redfish. A PDU or a cooling unit has no + # PCIe at all, and indexing them raised KeyError out of a health + # read. + for adapter in sysinfo.get('PCIeDevices', []): adpinfo = await fishclient._do_web_request(adapter['@odata.id']) if adpinfo['Status']['Health'] not in ('OK', None): summary['badreadings'].append(SensorReading(adpinfo)) - for fun in sysinfo['PCIeFunctions']: + for fun in sysinfo.get('PCIeFunctions', []): funinfo = await fishclient._do_web_request(fun['@odata.id']) if funinfo['Status']['Health'] not in ('OK', None): summary['badreadings'].append(SensorReading(funinfo)) From 26151c506f5f6d1e76fc752021055394b2939add Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 1 Sep 2026 23:51:32 +0200 Subject: [PATCH 2/2] Read power and boot from a service that has no system A Redfish service may publish no Systems collection, and power and cooling equipment does exactly that. sysurl is then None, and get_power and get_bootdev handed it straight to a request, raising TypeError from inside the url library. sysinfo already guarded the same field. Both now ask through _system_url and get a refusal naming what is missing. DMTF publish three services of this shape, which is why they sit commented out in inventory-dmtf.yaml. --- confluent_server/aiohmi/redfish/command.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/confluent_server/aiohmi/redfish/command.py b/confluent_server/aiohmi/redfish/command.py index 9fb4fdd3..3459cc9d 100644 --- a/confluent_server/aiohmi/redfish/command.py +++ b/confluent_server/aiohmi/redfish/command.py @@ -501,6 +501,18 @@ class Command(object): 'BMC does not implement extended firmware information') return self._varfwinventory + def _system_url(self): + """The system this client acts on, or a refusal that says so. + + Power and cooling equipment publishes no Systems collection, which is + legal. sysurl is then None, and handing that to a request raised + TypeError from inside the url library. + """ + if not self.sysurl: + raise exc.UnsupportedFunctionality( + 'this service publishes no computer system to act on') + return self.sysurl + async def sysinfo(self): if not self.sysurl: return {} @@ -517,7 +529,7 @@ class Command(object): return await self._do_web_request(bmcurl) async def get_power(self): - currinfo = await self._do_web_request(self.sysurl, cache=False) + currinfo = await self._do_web_request(self._system_url(), cache=False) return {'powerstate': str(currinfo['PowerState'].lower())} async def reseat_bay(self, bay): @@ -640,7 +652,7 @@ class Command(object): :raises: PyghmiException on error :returns: dict """ - result = await self._do_web_request(self.sysurl) + result = await self._do_web_request(self._system_url()) overridestate = result.get('Boot', {}).get( 'BootSourceOverrideEnabled', None) if overridestate == 'Disabled':