From a513bfc04f35656f99cf3ae9ac888377a1d105c0 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 23:48:19 +0200 Subject: [PATCH 1/3] Read the sdr partial read codes from the exception raw_command raises on any nonzero completion code, so the 0xCA and 0xC5 checks in get_sdr could not run: a bmc that will not return a whole record in one go, or whose reservation went stale, failed the sensor load outright instead of being retried. Read the code from the exception, which carries it. The back off also had a fixed point, size // 2 + 2 being 3 for a size of 3, so a bmc that kept refusing would have been asked the same question for ever. Give up when the request cannot get any smaller. A header read cannot go under 5 bytes either, since that is where the record length sits, so give up there rather than parse a reply too short to index. The stale reservation retry could not end on its own either: it cleared the id and left taking a new one to the top of the loop, which only reserves for a partial read. Take one where the code is handled. --- confluent_server/aiohmi/ipmi/sdr.py | 41 ++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/confluent_server/aiohmi/ipmi/sdr.py b/confluent_server/aiohmi/ipmi/sdr.py index fcf05d3c..923bb810 100644 --- a/confluent_server/aiohmi/ipmi/sdr.py +++ b/confluent_server/aiohmi/ipmi/sdr.py @@ -769,24 +769,35 @@ class SDR(object): rqdata = [rsvid & 0xff, rsvid >> 8, recid & 0xff, recid >> 8, offset, size] - sdrrec = await self.ipmicmd.raw_command(netfn=0x0a, command=0x23, - data=rqdata) - if sdrrec['code'] == 0xca: - if size == 0xff: # get just 5 to get header to know length - size = 5 - elif size > 5: - size //= 2 + try: + sdrrec = await self.ipmicmd.raw_command( + netfn=0x0a, command=0x23, data=rqdata) + except exc.IpmiException as ie: + # The two codes this loop negotiates with, rather than + # failures: more asked for than the bmc will return at once, + # and a reservation that has gone stale + if ie.ipmicode == 0xca: + if size == 0xff: # get just 5 to get header to know length + size = 5 + continue # push things over such that it's less # likely to be just 1 short of a read # and incur a whole new request - size += 2 - chunksize = size - continue - if sdrrec['code'] == 0xc5: # need a new reservation id - rsvid = 0 - continue - if sdrrec['code'] != 0: - raise exc.IpmiException(sdrrec['error']) + smaller = size // 2 + 2 + if smaller >= size or (currlen == 0 and smaller < 5): + # Nothing left to give up, and asking again + # unchanged would never end. A header read has to + # be 5 bytes to carry the record length, so a + # smaller one could not be parsed either + raise + size = chunksize = smaller + continue + if ie.ipmicode == 0xc5: # need a new reservation id + # Take one here rather than leaving it to the top of + # the loop, which only reserves for a partial read + rsvid = await self.get_sdr_reservation() + continue + raise if newrecid == 0: newrecid = (sdrrec['data'][1] << 8) + sdrrec['data'][0] if currlen == 0: From d5e9be5abbe9bd90278158ec270406e361b70635 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 23:48:19 +0200 Subject: [PATCH 2/3] Skip an absent optional sensor again, and read the ipv6 answer Completion code 203 on a sensor reading means the sensor is not present, which is expected of an optional device, but the check for it sat after a call that raises first, so one absent sensor ended the whole sensor sweep. _supports_standard_ipv6 read rsp['code'] the same way, so it could only ever answer True; a platform without the standard parameters raised instead. A completion code is that platform's answer, while a timeout or a lost session is not and must not be cached as one. raw_command's docstring still described itself as the other call it was renamed from, which is how these checks came to be written against the wrong contract. --- confluent_server/aiohmi/ipmi/command.py | 34 +++++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/confluent_server/aiohmi/ipmi/command.py b/confluent_server/aiohmi/ipmi/command.py index 8cdc0ca9..9bb12410 100644 --- a/confluent_server/aiohmi/ipmi/command.py +++ b/confluent_server/aiohmi/ipmi/command.py @@ -453,9 +453,11 @@ class Command(object): retry=True, timeout=None, rslun=0): """Send raw ipmi command to BMC, raising exception on error - This is identical to raw_command, except it raises exceptions + This is identical to oldraw_command, except it raises exceptions on IPMI errors and returns data as a buffer. This is the recommend - function to use. The response['data'] being a buffer allows + function to use, and a caller that has to act on a particular + completion code reads it from the exception's ipmicode. The + response['data'] being a buffer allows traditional indexed access as well as works nicely with struct.unpack_from when certain data is coming back. @@ -1089,13 +1091,14 @@ class Command(object): await self.init_sdr() for sensor in self._sdr.get_sensor_numbers(): currsensor = self._sdr.sensors[sensor] - rsp = await self.raw_command(command=0x2d, netfn=4, - rslun=currsensor.sensor_lun, - data=(currsensor.sensor_number,)) - if 'error' in rsp: - if rsp['code'] == 203: # Sensor does not exist, optional dev + try: + rsp = await self.raw_command(command=0x2d, netfn=4, + rslun=currsensor.sensor_lun, + data=(currsensor.sensor_number,)) + except exc.IpmiException as ie: + if ie.ipmicode == 203: # Sensor does not exist, optional dev continue - raise exc.IpmiException(rsp['error'], code=rsp['code']) + raise yield await self._sdr.sensors[sensor].\ decode_sensor_reading(self, rsp['data']) await self.oem_init() @@ -1304,9 +1307,18 @@ class Command(object): # handler of commands lanchan = await self.get_network_channel() if self._ipv6support is None: - rsp = await self.raw_command(netfn=0xc, command=0x2, data=(2, lanchan, - 0x32, 0, 0)) - self._ipv6support = rsp['code'] == 0 + try: + await self.raw_command(netfn=0xc, command=0x2, + data=(2, lanchan, 0x32, 0, 0)) + self._ipv6support = True + except exc.IpmiException as ie: + # A platform without the standard parameter says so in the + # completion code, which is the answer. Anything else, a + # timeout or a lost session, is not, and must not be remembered + # as the platform's answer for the rest of the session. + if not 0 < ie.ipmicode <= 0xff: + raise + self._ipv6support = False return self._ipv6support async def set_alert_destination(self, ip=None, acknowledge_required=None, From 7c68758761e4e39a94cb562ecb119c46de47b57e Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 23:48:19 +0200 Subject: [PATCH 3/3] Back off a fru read the bmc will not serve in one piece Completion codes 201 and 202 mean the chunk asked for was too big, and the check for them sat after a call that raises, so a bmc that cannot serve 224 bytes at once failed the fru read rather than being asked for less. The retry could not terminate either: chunksize // 2 + 2 is 4 for a chunksize of 4, so the chunksize == 3 guard was unreachable and a bmc that kept refusing would have been asked for 4 bytes for ever. --- confluent_server/aiohmi/ipmi/fru.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/confluent_server/aiohmi/ipmi/fru.py b/confluent_server/aiohmi/ipmi/fru.py index 50fa90c9..a83c320c 100644 --- a/confluent_server/aiohmi/ipmi/fru.py +++ b/confluent_server/aiohmi/ipmi/fru.py @@ -150,8 +150,6 @@ class FRU(object): async def fetch_fru(self, fruid): response = await self.ipmicmd.raw_command( netfn=0xa, command=0x10, data=[fruid]) - if 'error' in response: - raise iexc.IpmiException(response['error'], code=response['code']) frusize = response['data'][0] | (response['data'][1] << 8) # In our case, we don't need to think too hard about whether # the FRU is word or byte, we just process what we get back in the @@ -165,20 +163,21 @@ class FRU(object): offset = 0 self.rawfru = bytearray([]) while chunksize: - response = await self.ipmicmd.raw_command( - netfn=0xa, command=0x11, data=[fruid, offset & 0xff, - offset >> 8, chunksize]) - if response['code'] in (201, 202): + try: + response = await self.ipmicmd.raw_command( + netfn=0xa, command=0x11, data=[fruid, offset & 0xff, + offset >> 8, chunksize]) + except iexc.IpmiException as ie: # if it was too big, back off and try smaller # Try just over half to mitigate the chance of # one request becoming three rather than just two - if chunksize == 3: - raise iexc.IpmiException(response['error']) - chunksize //= 2 - chunksize += 2 + smaller = chunksize // 2 + 2 + if ie.ipmicode not in (201, 202) or smaller >= chunksize: + # Nothing left to give up, and asking again unchanged would + # never end + raise + chunksize = smaller continue - elif 'error' in response: - raise iexc.IpmiException(response['error'], response['code']) offset += response['data'][0] if response['data'][0] == 0: break