# 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(' 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