File: fru.py

package info (click to toggle)
python-pyghmi 1.6.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,376 kB
  • sloc: python: 21,736; sh: 35; makefile: 18
file content (347 lines) | stat: -rw-r--r-- 14,430 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# 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 pyghmi.exceptions as iexc
import pyghmi.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
        if self.rawfru is not None:
            self.parsedata()
        elif ipmicmd is not None:
            self.ipmicmd = weakref.proxy(ipmicmd)
            # Use the ipmicmd to fetch the data
            try:
                self.fetch_fru(fruid)
            except iexc.IpmiException as ie:
                if ie.ipmicode in (195, 201, 203, 129):
                    return
                raise
            self.parsedata()
        else:
            raise TypeError('Either rawdata or ipmicmd must be specified')

    def fetch_fru(self, fruid):
        response = 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 = 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 pyghmi 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