From ee35fba8bf224a7e42c2aee683b5f2c9b2ae7719 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 02:26:29 +0200 Subject: [PATCH] Read sensor data records from a bmc that has no repository A bmc may keep its sensor data records on the sensor device instead of in a repository, and this one does, so it had no sensors, no health and only a partial inventory over ipmi. The records themselves are identical, version 0x51 and the same types, so everything that decodes them is reused as is. Only the fetching differs: a command of its own, a reservation of its own, and records held per lun rather than in one place. The luns to ask, and a change indicator to cache on, come from Get Device SDR Info. The fetch is written out rather than shared with the repository one. The loops are alike, but nothing available here has a repository to test against, and the price of factoring them together is that a mistake would land on every bmc that works today rather than only on those that do not work at all. Records are cached in memory and on disk exactly as repository records are, keyed on the change indicator, and a device that offers no such indicator is read afresh each time rather than cached wrongly. Names are stripped of the nulls that pad a fixed width field. This bmc pads every name out to sixteen bytes, and a name carrying them cannot be matched by a caller asking for a sensor by name. On the bmc this was written for: 163 sensors and 9 frus, against the 163 the device reports it has. The 54 temperatures and 36 fans it now reads match what the same machine reports over redfish, to within the precision each side gives. --- confluent_server/aiohmi/ipmi/sdr.py | 167 ++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 12 deletions(-) diff --git a/confluent_server/aiohmi/ipmi/sdr.py b/confluent_server/aiohmi/ipmi/sdr.py index 2fc573f7..eaadb65b 100644 --- a/confluent_server/aiohmi/ipmi/sdr.py +++ b/confluent_server/aiohmi/ipmi/sdr.py @@ -620,12 +620,12 @@ class SDREntry(object): return "" if ipmitype == 0: # Unicode per 43.15 in ipmi 2.0 spec # the spec is not specific about encoding, assuming utf8 - return struct.pack("%dB" % len(data), *data).decode("utf-8") + ret = struct.pack("%dB" % len(data), *data).decode("utf-8") elif ipmitype == 1: # BCD '+' tmpl = "%02X" * len(data) tstr = tmpl % tuple(data) tstr = tstr.replace("A", " ").replace("B", "-").replace("C", ".") - return tstr.replace("D", ":").replace("E", ",").replace("F", "_") + ret = tstr.replace("D", ":").replace("E", ",").replace("F", "_") elif ipmitype == 2: # 6 bit ascii, start at 0x20 # the ordering is very peculiar and is best understood from # IPMI SPEC "6-bit packed ascii example @@ -637,12 +637,14 @@ class SDREntry(object): tstr += chr((data[2] >> 2) + 0x20) if not isinstance(tstr, str): tstr = tstr.decode('utf-8') - return tstr - elif ipmitype == 3: # ACSII+LATIN1 + ret = tstr + else: # ipmitype == 3, ASCII+LATIN1 ret = struct.pack("%dB" % len(data), *data) if not isinstance(ret, str): ret = ret.decode('utf-8') - return ret + # A fixed width field is padded out with nulls. They are not part of + # the name, and would stop a caller ever matching on it. + return ret.rstrip('\x00') class SDR(object): @@ -684,13 +686,9 @@ class SDR(object): # device, so we are meant to use an alternative mechanism to get # SDR data if rsp['data'][5] & 1: - # The device has sensor device support, so in theory we should - # be able to proceed - # However at the moment, we haven't done so - raise exc.UnsupportedFunctionality( - 'This bmc keeps its sensor data records on the sensor ' - 'device rather than in a repository, which is not ' - 'supported') + # The device has sensor device support, so the records are + # read from the sensor device a lun at a time instead + return await self.get_device_sdr() # We have Device SDR, without SDR Repository device, but # also without sensor device support, no idea how to # continue @@ -704,6 +702,151 @@ class SDR(object): raise exc.IpmiException(rsp['error']) return rsp['data'][0] + (rsp['data'][1] << 8) + async def get_device_sdr_reservation(self): + rsp = await self.ipmicmd.raw_command(netfn=4, command=0x22) + if rsp['code'] != 0: + raise exc.IpmiException(rsp['error']) + return rsp['data'][0] + (rsp['data'][1] << 8) + + async def get_device_sdr_info(self): + """Ask the sensor device about its records + + Answers the luns that have sensors and a change indicator to cache on, + which is only present when the device says its sensors are populated + dynamically. + """ + rsp = await self.ipmicmd.raw_command(netfn=4, command=0x20, data=(1,)) + if rsp['code'] != 0: + raise exc.IpmiException(rsp['error']) + data = bytearray(rsp['data']) + luns = [lun for lun in range(4) if data[1] & (1 << lun)] + if not luns: + # A device that names no lun still has to answer for lun 0, and + # some do not set the bit for it + luns = [0] + modtime = 0 + if data[1] & 0b10000000 and len(data) >= 6: + modtime = struct.unpack('> 8, + recid & 0xff, recid >> 8, + offset, size] + sdrrec = await self.ipmicmd.raw_command( + netfn=4, command=0x21, data=rqdata, rslun=lun) + if sdrrec['code'] == 0xca: + if size == 0xff: # get just 5 to get header to know length + size = 5 + elif size > 5: + size //= 2 + # 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']) + if newrecid == 0: + newrecid = (sdrrec['data'][1] << 8) + sdrrec['data'][0] + if currlen == 0: + currlen = sdrrec['data'][6] + 5 # compensate for header + sdrdata.extend(sdrrec['data'][2:]) + offset += size + if offset >= currlen: + break + if size == 5 and offset == 5: + # bump up size after header retrieval + size = chunksize + if (offset + size) > currlen: + size = currlen - offset + await self.add_sdr(sdrdata) + if sdrraw is not None: + sdrraw.append(bytes(sdrdata)) + offset = 0 + if size != 0xff: + size = 5 + if newrecid == recid: + raise exc.BmcErrorException("Incorrect SDR record id from BMC") + recid = newrecid + async def get_sdr(self): repinfo = await self.ipmicmd.raw_command(netfn=0x0a, command=0x20) repinfo['data'] = bytearray(repinfo['data'])