mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-05 04:27:56 +00:00
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.
This commit is contained in:
@@ -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('<I', bytes(data[2:6]))[0]
|
||||
return luns, modtime
|
||||
|
||||
async def get_device_sdr(self):
|
||||
"""Read the sensor data records from the sensor device
|
||||
|
||||
For a bmc with no sdr repository. The records themselves are the same,
|
||||
so only the fetching differs: a different command, a reservation of its
|
||||
own, and the records are held per lun rather than in one place.
|
||||
"""
|
||||
luns, modtime = await self.get_device_sdr_info()
|
||||
self.broken_sensor_ids = {}
|
||||
cachekey = (self.fw_major, self.fw_minor, self.mfg_id, self.prod_id,
|
||||
self.device_id, 'device', modtime)
|
||||
cachefilename = None
|
||||
if modtime:
|
||||
# Without a change indicator there is nothing to notice a change
|
||||
# by, so such a device is read afresh every time
|
||||
try:
|
||||
csdrs = shared_sdrs[cachekey]
|
||||
self.sensors = csdrs['sensors']
|
||||
self.fru = csdrs['fru']
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
if self.cachedir:
|
||||
cachefilename = os.path.join(
|
||||
self.cachedir,
|
||||
'sdrcache-2.device.{0}.{1}.{2}.{3}.{4}.{5}'.format(
|
||||
self.mfg_id, self.prod_id, self.device_id,
|
||||
self.fw_major, self.fw_minor, modtime))
|
||||
if cachefilename and os.path.isfile(cachefilename):
|
||||
with open(cachefilename, 'rb') as cfile:
|
||||
csdrlen = cfile.read(2)
|
||||
while csdrlen:
|
||||
csdrlen = struct.unpack('!H', csdrlen)[0]
|
||||
await self.add_sdr(cfile.read(csdrlen))
|
||||
csdrlen = cfile.read(2)
|
||||
else:
|
||||
sdrraw = [] if cachefilename else None
|
||||
for lun in luns:
|
||||
await self._read_device_sdr_lun(lun, sdrraw)
|
||||
if cachefilename:
|
||||
self._write_sdr_cache(cachefilename, sdrraw)
|
||||
for sid in self.broken_sensor_ids:
|
||||
try:
|
||||
del self.sensors[sid]
|
||||
except KeyError:
|
||||
pass
|
||||
if modtime:
|
||||
shared_sdrs[cachekey] = {
|
||||
'sensors': self.sensors,
|
||||
'fru': self.fru,
|
||||
}
|
||||
|
||||
def _write_sdr_cache(self, cachefilename, sdrraw):
|
||||
suffix = ''.join(
|
||||
random.choice(string.ascii_lowercase) for _ in range(12))
|
||||
with open(cachefilename + '.' + suffix, 'wb') as cfile:
|
||||
for csdr in sdrraw:
|
||||
cfile.write(struct.pack('!H', len(csdr)))
|
||||
cfile.write(csdr)
|
||||
os.rename(cachefilename + '.' + suffix, cachefilename)
|
||||
|
||||
async def _read_device_sdr_lun(self, lun, sdrraw=None):
|
||||
recid = 0
|
||||
rsvid = 0
|
||||
offset = 0
|
||||
size = 0xff
|
||||
chunksize = 128
|
||||
while recid != 0xffff: # per 35.3 Get Device SDR, 0xffff marks the end
|
||||
newrecid = 0
|
||||
currlen = 0
|
||||
sdrdata = bytearray()
|
||||
while True: # loop until SDR fetched wholly
|
||||
if size != 0xff and rsvid == 0:
|
||||
rsvid = await self.get_device_sdr_reservation()
|
||||
rqdata = [rsvid & 0xff, rsvid >> 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'])
|
||||
|
||||
Reference in New Issue
Block a user