From d8f40491155dd893f3c2d5e790756e1380472cce Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 05:42:25 +0200 Subject: [PATCH 1/9] Fix password change handling in EUREKA discovery util.json_loads does not exist, so the PasswordChangeRequired flow raised AttributeError on every 401, silently swallowed by the blanket except. Use json.loads, which accepts the bytes body directly. --- confluent_server/confluent/discovery/handlers/eureka.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/handlers/eureka.py b/confluent_server/confluent/discovery/handlers/eureka.py index 3bb466f0..7488a8f7 100644 --- a/confluent_server/confluent/discovery/handlers/eureka.py +++ b/confluent_server/confluent/discovery/handlers/eureka.py @@ -21,6 +21,8 @@ sensor readings. It does NOT support IPMI, NIC configuration, firmware update, or other BMC-specific operations. """ +import asyncio +import json import confluent.discovery.handlers.generic as generic import confluent.util as util import aiohmi.util.webclient as webclient @@ -105,7 +107,7 @@ class NodeHandler(generic.NodeHandler): elif status == 401: self.trieddefault = True try: - errinfo = util.json_loads(body) + errinfo = json.loads(body) for msg in errinfo.get('@Message.ExtendedInfo', []): if 'PasswordChangeRequired' in msg.get('MessageId', ''): chgurl = msg.get('MessageArgs', [None])[0] From 5399ba25a0735f67dedf94e63a288a002d421a85 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 05:42:43 +0200 Subject: [PATCH 2/9] Fix EUREKA CPU temperature readings The sensor URL was built as BMC{N}CpuCPU{X}Temp instead of BMC{N}CPU{X}Temp, so every request returned 404. The entries also referenced const.SensorUnits, which does not exist, and used a dict shape the only consumer, get_average_processor_temperature, cannot read - it expects thermal-style dicts with ReadingCelsius. --- confluent_server/aiohmi/redfish/oem/megware/eureka.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 0c377099..98db5946 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -96,15 +96,12 @@ class OEMHandler(generic.OEMHandler): continue for cpu in ('CPU0', 'CPU1'): try: - sensor_url = '/redfish/v1/Chassis/1/Sensors/BMC{}Cpu{}Temp'.format(nodeid, cpu) + sensor_url = '/redfish/v1/Chassis/1/Sensors/BMC{}{}Temp'.format(nodeid, cpu) sensor = await fishclient._do_web_request(sensor_url) if sensor and 'Reading' in sensor: cputemps.append({ - 'name': 'CPU {} Node {}'.format(cpu, nodeid), - 'value': float(sensor['Reading']), - 'state_ids': [], - 'units': const.SensorUnits.Celsius, - 'imprecision': None, + 'Name': '{} Node {}'.format(cpu, nodeid), + 'ReadingCelsius': float(sensor['Reading']), }) except Exception: pass From 4d952892b170ce8263ae7da48fdc2b8d16cd86f8 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 05:43:18 +0200 Subject: [PATCH 3/9] Report detail and severity from EUREKA health get_health collected an issues list but returned it nowhere, and flattened every problem to Warning. Return the findings as badreadings using SensorReading, honor verbose, and map chassis health through _healthmap so Critical is no longer downgraded. --- .../aiohmi/redfish/oem/megware/eureka.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 98db5946..6d0e64b6 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -124,14 +124,24 @@ class OEMHandler(generic.OEMHandler): async def get_health(self, fishclient, verbose=True): """Gather health status for the EUREKA chassis and all nodes.""" - issues = [] + summary = {'badreadings': [], 'health': const.Health.Ok} + + def note_issue(name, health, state): + summary['health'] |= health + if verbose: + reading = generic.SensorReading(None, {'name': name}) + reading.health = health + reading.states = [state] + summary['badreadings'].append(reading) + try: chassis = await self._do_web_request('/redfish/v1/Chassis/1') health = chassis.get('Status', {}).get('Health', 'OK') if health != 'OK': - issues.append('Chassis health: {}'.format(health)) + note_issue('Chassis', generic._healthmap.get( + health, const.Health.Warning), health) except Exception: - issues.append('Cannot reach chassis health endpoint') + note_issue('Chassis', const.Health.Warning, 'Unreachable') for sysurl in self._allsysurls: try: @@ -140,12 +150,6 @@ class OEMHandler(generic.OEMHandler): continue state = sysinfo.get('Status', {}).get('State', 'Absent') name = sysurl.rstrip('/').rsplit('/', 1)[-1] - if state == 'Absent': - issues.append('{}: Absent'.format(name)) - elif state != 'Enabled': - issues.append('{}: {}'.format(name, state)) - - health = 0 - if issues: - health = 1 - return {'badreadings': [], 'health': health} + if state != 'Enabled': + note_issue(name, const.Health.Warning, state) + return summary From e59230c63a267a9cc043256e0bb8985cea445626 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 05:43:35 +0200 Subject: [PATCH 4/9] Pass rootinfo through to EUREKA OEM handler The service root was already fetched by the caller; dropping it forced OEMHandler.create to request /redfish/v1/ again. --- confluent_server/aiohmi/redfish/oem/megware/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/main.py b/confluent_server/aiohmi/redfish/oem/megware/main.py index fc2b324d..31b5f453 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/main.py +++ b/confluent_server/aiohmi/redfish/oem/megware/main.py @@ -20,4 +20,5 @@ import aiohmi.redfish.oem.megware.eureka as eureka async def get_handler(sysinfo, sysurl, webclient, cache, cmd, rootinfo={}): - return await eureka.OEMHandler.create(sysinfo, sysurl, webclient, cache, gpool=cmd._gpool) + return await eureka.OEMHandler.create(sysinfo, sysurl, webclient, cache, + gpool=cmd._gpool, rootinfo=rootinfo) From 7cc8972170d5bd6f3d93ff9419210689ba7438fd Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 13:33:20 +0200 Subject: [PATCH 5/9] Skip CPU temperatures from unbooted EUREKA node BMCs A node whose BMC is not reporting returns Reading 0 from its temperature sensors, dragging averages down with meaningless values. The ComputerSystem Oem data flags this via HasBMCMetrics; skip such nodes, and keep collecting when the flag is absent. --- confluent_server/aiohmi/redfish/oem/megware/eureka.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 6d0e64b6..587d4f67 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -86,7 +86,9 @@ class OEMHandler(generic.OEMHandler): """Read CPU temperatures from EUREKA BMC sensor endpoints. Reads BMC{N}CPU0Temp and BMC{N}CPU1Temp sensors from - /redfish/v1/Chassis/1/Sensors/. + /redfish/v1/Chassis/1/Sensors/. Nodes whose BMC is not + currently reporting (HasBMCMetrics false) are skipped, as + their sensors read a meaningless 0. """ cputemps = [] for sysurl in self._allsysurls: @@ -94,6 +96,13 @@ class OEMHandler(generic.OEMHandler): nodeid = nodeid.replace('Node', '') if not nodeid.isdigit(): continue + try: + sysinfo = await fishclient._do_web_request(sysurl) + except Exception: + continue + if not sysinfo.get('Oem', {}).get('Megware', {}).get( + 'HasBMCMetrics', True): + continue for cpu in ('CPU0', 'CPU1'): try: sensor_url = '/redfish/v1/Chassis/1/Sensors/BMC{}{}Temp'.format(nodeid, cpu) From 859a8db8738dfaa2ff6c8f77beda855e2e6743b8 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 24 Sep 2026 19:04:28 +0200 Subject: [PATCH 6/9] Reseat EUREKA nodes with the Reseat reset type ForceRestart only restarts the host, so the node BMC kept running and a reseat did not recover a hung one. The EUREKA firmware provides a Reseat reset type that removes all power from the slot. --- confluent_server/aiohmi/redfish/oem/megware/eureka.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 587d4f67..719815fe 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -117,10 +117,12 @@ class OEMHandler(generic.OEMHandler): return cputemps async def reseat_bay(self, bay): - """Power cycle a specific node in the EUREKA enclosure. + """Reseat a specific node in the EUREKA enclosure. - Uses ComputerSystem.Reset with ForceRestart on the target node. - bay=-1 (enclosure-level) is not supported. + Uses ComputerSystem.Reset with the EUREKA specific Reseat type, + which removes all power from the slot, node BMC included. + ForceRestart only restarts the host. bay=-1 (enclosure-level) + is not supported. """ if bay == -1: raise exc.UnsupportedFunctionality( @@ -128,7 +130,7 @@ class OEMHandler(generic.OEMHandler): nodeurl = '/redfish/v1/Systems/Node{}'.format(bay) await self._do_web_request( nodeurl + '/Actions/ComputerSystem.Reset', - {'ResetType': 'ForceRestart'}, + {'ResetType': 'Reseat'}, method='POST') async def get_health(self, fishclient, verbose=True): From 38e94c73d6afad2619b3a62179460330390b68bc Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 24 Sep 2026 20:43:13 +0200 Subject: [PATCH 7/9] Read EUREKA sensors with $expand when the firmware supports it The sensor collection has 668 members, and reading each one on its own takes about 92 seconds and rebuilds on every nodesensors call. Opt in to $expand=. for that collection, checking once whether the firmware really inlines the members, so firmware that ignores $expand keeps the per-member reads. --- .../aiohmi/redfish/oem/megware/eureka.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 719815fe..16de37b7 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -27,6 +27,28 @@ import aiohmi.constants as const class OEMHandler(generic.OEMHandler): usegenericsensors = True + async def supports_expand(self, url): + """Whether url answers $expand=. with its members inlined. + + Only the sensor collection is worth it (668 members, one GET + each otherwise). Firmware that ignores $expand answers with + plain links, so the answer is probed once per collection and + such firmware keeps the per-member reads. + """ + if not url.rstrip('/').endswith('/Sensors'): + return False + if not hasattr(self, '_expandsupport'): + self._expandsupport = {} + if url not in self._expandsupport: + try: + rsp = await self._do_web_request(url + '?$expand=.') + except Exception: + rsp = {} + members = rsp.get('Members', []) + self._expandsupport[url] = bool(members) and all( + 'Name' in member for member in members) + return self._expandsupport[url] + async def get_default_sysurl(self): """Return the system URL for the first available node. From 1bf8c01867cfb3750e7e20a963d6dd058512e745 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 25 Sep 2026 19:22:38 +0200 Subject: [PATCH 8/9] Report node health and unreachable nodes from EUREKA health get_health looked only at each node's State, so an Enabled node whose Health was Warning or Critical left the chassis reported as ok. It also skipped a node whose resource could not be read, counting it as healthy while an unreadable chassis was flagged. Report the node's Health when it is not OK, and a node that cannot be read as Unreachable. --- confluent_server/aiohmi/redfish/oem/megware/eureka.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/confluent_server/aiohmi/redfish/oem/megware/eureka.py b/confluent_server/aiohmi/redfish/oem/megware/eureka.py index 16de37b7..ec79816c 100644 --- a/confluent_server/aiohmi/redfish/oem/megware/eureka.py +++ b/confluent_server/aiohmi/redfish/oem/megware/eureka.py @@ -177,12 +177,18 @@ class OEMHandler(generic.OEMHandler): note_issue('Chassis', const.Health.Warning, 'Unreachable') for sysurl in self._allsysurls: + name = sysurl.rstrip('/').rsplit('/', 1)[-1] try: sysinfo = await self._do_web_request(sysurl) except Exception: + note_issue(name, const.Health.Warning, 'Unreachable') continue - state = sysinfo.get('Status', {}).get('State', 'Absent') - name = sysurl.rstrip('/').rsplit('/', 1)[-1] + status = sysinfo.get('Status', {}) + state = status.get('State', 'Absent') + health = status.get('Health', 'OK') if state != 'Enabled': note_issue(name, const.Health.Warning, state) + elif health != 'OK': + note_issue(name, generic._healthmap.get( + health, const.Health.Warning), health) return summary From 913029e19b2717ee64a1c578fe065726b6b22463 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 25 Sep 2026 19:22:38 +0200 Subject: [PATCH 9/9] Send If-Match only with the EUREKA discovery PATCH requests The password change and the account update set If-Match: * on the connection itself, and after a forced password change that connection is handed back, so every later request carried it. Pass the header with the PATCH alone. Also drop an unused asyncio import. --- .../confluent/discovery/handlers/eureka.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/discovery/handlers/eureka.py b/confluent_server/confluent/discovery/handlers/eureka.py index 7488a8f7..88ee1dc2 100644 --- a/confluent_server/confluent/discovery/handlers/eureka.py +++ b/confluent_server/confluent/discovery/handlers/eureka.py @@ -21,7 +21,6 @@ sensor readings. It does NOT support IPMI, NIC configuration, firmware update, or other BMC-specific operations. """ -import asyncio import json import confluent.discovery.handlers.generic as generic import confluent.util as util @@ -113,10 +112,12 @@ class NodeHandler(generic.NodeHandler): chgurl = msg.get('MessageArgs', [None])[0] if chgurl and self.targpass and self.targpass != defpass: wc.set_basic_credentials(defuser, defpass) - wc.set_header('If-Match', '*') + # If-Match only on the PATCH: wc is handed + # back for every later request rsp, chgstatus = await wc.grab_json_response_with_status( chgurl, {'Password': self.targpass}, + headers=dict(wc.stdheaders, **{'If-Match': '*'}), method='PATCH') if chgstatus >= 200 and chgstatus < 300: body, status, headers = \ @@ -228,9 +229,10 @@ class NodeHandler(generic.NodeHandler): acctdata, acctstatus = \ await wc.grab_json_response_with_status(accturl) if acctdata.get('UserName') == self.curruser: - wc.set_header('If-Match', '*') rsp, status = await wc.grab_json_response_with_status( - accturl, authupdate, method='PATCH') + accturl, authupdate, + headers=dict(wc.stdheaders, **{'If-Match': '*'}), + method='PATCH') if status >= 200 and status < 300: self.curruser = user self.currpass = passwd