mirror of
https://github.com/xcat2/confluent.git
synced 2026-05-01 04:47:45 +00:00
If someone asks for it independently, we can break it out again. But for now, assume it's only for confluent.
353 lines
14 KiB
Python
353 lines
14 KiB
Python
# Copyright 2015 Lenovo
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""This module provides access to SDR offered by a BMC
|
|
|
|
This data is common between 'sensors' and 'inventory' modules since SDR
|
|
is both used to enumerate sensors for sensor commands and FRU ids for FRU
|
|
commands
|
|
|
|
For now, we will not offer persistent SDR caching as we do in xCAT's IPMI
|
|
code. Will see if it is adequate to advocate for high object reuse in a
|
|
persistent process for the moment.
|
|
|
|
Focus is at least initially on the aspects that make the most sense for a
|
|
remote client to care about. For example, smbus information is being
|
|
skipped for now
|
|
|
|
This file handles parsing of fru format records as presented by IPMI
|
|
devices. This format is documented in the 'Platform Management FRU
|
|
Information Storage Definition (Document Revision 1.2)
|
|
"""
|
|
|
|
import struct
|
|
import time
|
|
import weakref
|
|
|
|
import aiohmi.exceptions as iexc
|
|
import aiohmi.ipmi.private.spd as spd
|
|
|
|
|
|
fruepoch = 820454400 # 1/1/1996, 0:00
|
|
|
|
# This is from SMBIOS specification Table 16
|
|
enclosure_types = {
|
|
0: 'Unspecified',
|
|
1: 'Other',
|
|
2: 'Unknown',
|
|
3: 'Desktop',
|
|
4: 'Low Profile Desktop',
|
|
5: 'Pizza Box',
|
|
6: 'Mini Tower',
|
|
7: 'Tower',
|
|
8: 'Portable',
|
|
9: 'Laptop',
|
|
0xa: 'Notebook',
|
|
0xb: 'Hand Held',
|
|
0xc: 'Docking Station',
|
|
0xd: 'All in One',
|
|
0xe: 'Sub Notebook',
|
|
0xf: 'Space-saving',
|
|
0x10: 'Lunch Box',
|
|
0x11: 'Main Server Chassis',
|
|
0x12: 'Expansion Chassis',
|
|
0x13: 'SubChassis',
|
|
0x14: 'Bus Expansion Chassis',
|
|
0x15: 'Peripheral Chassis',
|
|
0x16: 'RAID Chassis',
|
|
0x17: 'Rack Mount Chassis',
|
|
0x18: 'Sealed-case PC',
|
|
0x19: 'Multi-system Chassis',
|
|
0x1a: 'Compact PCI',
|
|
0x1b: 'Advanced TCA',
|
|
0x1c: 'Blade',
|
|
0x1d: 'Blade Enclosure',
|
|
}
|
|
|
|
|
|
def unpack6bitascii(inputdata):
|
|
# This is a text encoding scheme that seems unique
|
|
# to IPMI FRU. It seems to be relatively rare in practice
|
|
result = ''
|
|
while len(inputdata) > 0:
|
|
currchunk = inputdata[:3]
|
|
del inputdata[:3]
|
|
currchar = currchunk[0] & 0b111111
|
|
result += chr(0x20 + currchar)
|
|
currchar = (currchunk[0] & 0b11000000) >> 6
|
|
currchar |= (currchunk[1] & 0b1111) << 2
|
|
result += chr(0x20 + currchar)
|
|
currchar = (currchunk[1] & 0b11110000) >> 4
|
|
currchar |= (currchunk[2] & 0b11) << 4
|
|
result += chr(0x20 + currchar)
|
|
currchar = (currchunk[2] & 0b11111100) >> 2
|
|
result += chr(0x20 + currchar)
|
|
return result
|
|
|
|
|
|
def decode_fru_date(datebytes):
|
|
# Returns ISO
|
|
datebytes.append(0)
|
|
minutesfromepoch = struct.unpack('<I', struct.pack('4B', *datebytes))[0]
|
|
# Some data in the field has had some data less than 800
|
|
# At this juncture, it's far more likely for this noise
|
|
# to be incorrect than anything in particular
|
|
if minutesfromepoch < 800:
|
|
return None
|
|
return time.strftime('%Y-%m-%dT%H:%M',
|
|
time.gmtime((minutesfromepoch * 60) + fruepoch))
|
|
|
|
|
|
class FRU(object):
|
|
"""An object representing structure
|
|
|
|
FRU (Field Replaceable Unit) is the usual format for inventory in IPMI
|
|
devices. This covers most standards compliant inventory data
|
|
as well as presenting less well defined fields in a structured way.
|
|
|
|
:param rawdata: A binary string/bytearray of raw data from BMC or dump
|
|
:param ipmicmd: An ipmi command object to fetch data live
|
|
:param fruid: The identifier number of the FRU
|
|
:param sdr: The sdr locator entry to help clarify how to parse data
|
|
"""
|
|
|
|
def __init__(self, rawdata=None, ipmicmd=None, fruid=0, sdr=None):
|
|
self.rawfru = rawdata
|
|
self.databytes = None
|
|
self.info = None
|
|
self.sdr = sdr
|
|
self.ipmicmd = None
|
|
self.fruid = fruid
|
|
if self.rawfru is not None:
|
|
self.parsedata()
|
|
elif ipmicmd is not None:
|
|
self.ipmicmd = weakref.proxy(ipmicmd)
|
|
else:
|
|
raise TypeError('Either rawdata or ipmicmd must be specified')
|
|
|
|
async def initialize(self):
|
|
if self.ipmicmd is not None:
|
|
try:
|
|
await self.fetch_fru(self.fruid)
|
|
except iexc.IpmiException as ie:
|
|
if ie.ipmicode in (195, 201, 203, 129):
|
|
return
|
|
raise
|
|
self.parsedata()
|
|
|
|
|
|
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
|
|
# payload
|
|
chunksize = 224
|
|
# Selected as it is accomodated by most tested things
|
|
# and many tested things broke after going much
|
|
# bigger
|
|
if chunksize > frusize:
|
|
chunksize = frusize
|
|
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):
|
|
# 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
|
|
continue
|
|
elif 'error' in response:
|
|
raise iexc.IpmiException(response['error'], response['code'])
|
|
offset += response['data'][0]
|
|
if response['data'][0] == 0:
|
|
break
|
|
# move down to avoid exception when data[0] is zero
|
|
self.rawfru.extend(response['data'][1:])
|
|
if offset + chunksize > frusize:
|
|
chunksize = frusize - offset
|
|
|
|
def parsedata(self):
|
|
self.info = {}
|
|
rawdata = self.rawfru
|
|
self.databytes = bytearray(rawdata)
|
|
if self.sdr is not None:
|
|
frutype = self.sdr.fru_type_and_modifier >> 8
|
|
frusubtype = self.sdr.fru_type_and_modifier & 0xff
|
|
if frutype > 0x10 or frutype < 0x8 or frusubtype not in (0, 1, 2):
|
|
return
|
|
# TODO(jjohnson2): strict mode to detect aiohmi and BMC
|
|
# gaps
|
|
# raise iexc.PyghmiException(
|
|
# 'Unsupported FRU device: {0:x}h, {1:x}h'.format(frutype,
|
|
# frusubtype
|
|
# ))
|
|
elif frusubtype == 1:
|
|
self.myspd = spd.SPD(self.databytes)
|
|
self.info = self.myspd.info
|
|
return
|
|
if self.databytes[0] != 1:
|
|
return
|
|
# TODO(jjohnson2): strict mode to flag potential BMC errors
|
|
# raise iexc.BmcErrorException("Invalid/Unsupported FRU format")
|
|
# Ignore the internal use even if present.
|
|
self._parse_chassis()
|
|
self._parse_board()
|
|
self._parse_prod()
|
|
# TODO(jjohnson2): Multi Record area
|
|
|
|
def _decode_tlv(self, offset, lang=0):
|
|
currtlv = self.databytes[offset]
|
|
currlen = currtlv & 0b111111
|
|
currtype = (currtlv & 0b11000000) >> 6
|
|
retinfo = self.databytes[offset + 1:offset + currlen + 1]
|
|
newoffset = offset + currlen + 1
|
|
if currlen == 0:
|
|
return None, newoffset
|
|
if currtype == 0:
|
|
# return it as a bytearray, not much to be done for it
|
|
return retinfo, newoffset
|
|
elif currtype == 3: # text string
|
|
# Sometimes BMCs have FRU data with 0xff termination
|
|
# contrary to spec, but can be tolerated
|
|
# also in case something null terminates, handle that too
|
|
# strictly speaking, \xff should be a y with diaeresis, but
|
|
# erring on the side of that not being very relevant in practice
|
|
# to fru info, particularly the last values
|
|
# Additionally 0xfe has been observed, which should be a thorn, but
|
|
# again assuming termination of string is more likely than thorn.
|
|
retinfo = retinfo.rstrip(b'\xfe\xff\x10\x03\x00 ')
|
|
retinfo = retinfo.replace(b'\x00', b'')
|
|
if lang in (0, 25):
|
|
try:
|
|
retinfo = retinfo.decode('iso-8859-1')
|
|
except (UnicodeError, LookupError):
|
|
pass
|
|
else:
|
|
try:
|
|
retinfo = retinfo.decode('utf-16le')
|
|
except (UnicodeDecodeError, LookupError):
|
|
pass
|
|
# Some things lie about being text. Do the best we can by
|
|
# removing trailing spaces and nulls like makes sense for text
|
|
# and rely on vendors to workaround deviations in their OEM
|
|
# module
|
|
# retinfo = retinfo.rstrip(b'\x00 ')
|
|
return retinfo, newoffset
|
|
elif currtype == 1: # BCD 'plus'
|
|
retdata = ''
|
|
for byte in retinfo:
|
|
byte = hex(byte).replace('0x', '').replace('a', ' ').replace(
|
|
'b', '-').replace('c', '.')
|
|
retdata += byte
|
|
retdata = retdata.strip()
|
|
return retdata, newoffset
|
|
elif currtype == 2: # 6-bit ascii
|
|
retinfo = unpack6bitascii(retinfo).strip()
|
|
return retinfo, newoffset
|
|
|
|
def _parse_chassis(self):
|
|
offset = 8 * self.databytes[2]
|
|
if offset == 0:
|
|
return
|
|
if self.databytes[offset] & 0b1111 != 1:
|
|
raise iexc.BmcErrorException("Invalid/Unsupported chassis area")
|
|
inf = self.info
|
|
# ignore length field, just process the data
|
|
# add check to avoid exception
|
|
if self.databytes[offset + 2] in enclosure_types.keys():
|
|
inf['Chassis type'] = enclosure_types[self.databytes[offset + 2]]
|
|
inf['Chassis part number'], offset = self._decode_tlv(offset + 3)
|
|
inf['Chassis serial number'], offset = self._decode_tlv(offset)
|
|
inf['chassis_extra'] = []
|
|
self.extract_extra(inf['chassis_extra'], offset)
|
|
|
|
def extract_extra(self, target, offset, language=0):
|
|
try:
|
|
while self.databytes[offset] != 0xc1:
|
|
fielddata, offset = self._decode_tlv(offset, language)
|
|
target.append(fielddata)
|
|
except IndexError:
|
|
# If we overrun the end due to malformed FRU,
|
|
# return at least what decoded right
|
|
return
|
|
|
|
def _parse_board(self):
|
|
offset = 8 * self.databytes[3]
|
|
if offset == 0:
|
|
return
|
|
if self.databytes[offset] & 0b1111 != 1:
|
|
raise iexc.BmcErrorException("Invalid/Unsupported board info area")
|
|
inf = self.info
|
|
language = self.databytes[offset + 2]
|
|
inf['Board manufacture date'] = decode_fru_date(
|
|
self.databytes[offset + 3:offset + 6])
|
|
inf['Board manufacturer'], offset = self._decode_tlv(offset + 6)
|
|
inf['Board product name'], offset = self._decode_tlv(offset, language)
|
|
inf['Board serial number'], offset = self._decode_tlv(offset, language)
|
|
inf['Board model'], offset = self._decode_tlv(offset, language)
|
|
inf['Board FRU Id'], offset = self._decode_tlv(offset, language)
|
|
inf['board_extra'] = []
|
|
self.extract_extra(inf['board_extra'], offset, language)
|
|
|
|
def _parse_prod(self):
|
|
offset = 8 * self.databytes[4]
|
|
if offset == 0:
|
|
return
|
|
inf = self.info
|
|
language = self.databytes[offset + 2]
|
|
inf['Manufacturer'], offset = self._decode_tlv(offset + 3,
|
|
language)
|
|
inf['Product name'], offset = self._decode_tlv(offset, language)
|
|
inf['Model'], offset = self._decode_tlv(offset, language)
|
|
inf['Hardware Version'], offset = self._decode_tlv(offset, language)
|
|
inf['Serial Number'], offset = self._decode_tlv(offset, language)
|
|
inf['Asset Number'], offset = self._decode_tlv(offset, language)
|
|
inf['FRU ID'], offset = self._decode_tlv(offset, language)
|
|
inf['product_extra'] = []
|
|
self.extract_extra(inf['product_extra'], offset, language)
|
|
|
|
def __repr__(self):
|
|
return repr(self.info)
|
|
# retdata = 'Chassis data\n'
|
|
# retdata += ' Type: ' + repr(self.chassis_type) + '\n'
|
|
# retdata += ' Part Number: ' + repr(self.chassis_part_number) + '\n'
|
|
# retdata += ' Serial Number: ' + repr(self.chassis_serial) + '\n'
|
|
# retdata += ' Extra: ' + repr(self.chassis_extra) + '\n'
|
|
# retdata += 'Board data\n'
|
|
# retdata += ' Manufacturer: ' + repr(self.board_manufacturer) + '\n'
|
|
# retdata += ' Date: ' + repr(self.board_mfg_date) + '\n'
|
|
# retdata += ' Product' + repr(self.board_product) + '\n'
|
|
# retdata += ' Serial: ' + repr(self.board_serial) + '\n'
|
|
# retdata += ' Model: ' + repr(self.board_model) + '\n'
|
|
# retdata += ' Extra: ' + repr(self.board_extra) + '\n'
|
|
# retdata += 'Product data\n'
|
|
# retdata += ' Manufacturer: ' + repr(self.product_manufacturer)+'\n'
|
|
# retdata += ' Name: ' + repr(self.product_name) + '\n'
|
|
# retdata += ' Model: ' + repr(self.product_model) + '\n'
|
|
# retdata += ' Version: ' + repr(self.product_version) + '\n'
|
|
# retdata += ' Serial: ' + repr(self.product_serial) + '\n'
|
|
# retdata += ' Asset: ' + repr(self.product_asset) + '\n'
|
|
# retdata += ' Extra: ' + repr(self.product_extra) + '\n'
|
|
# return retdata
|