• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kontron / python-ipmi / 14906099362

08 May 2025 12:09PM UTC coverage: 69.897% (+0.03%) from 69.867%
14906099362

Pull #191

github

web-flow
Merge 4aecbc232 into 7f6abde74
Pull Request #191: Feature/fru ignore checksum

31 of 40 new or added lines in 1 file covered. (77.5%)

4 existing lines in 1 file now uncovered.

4732 of 6770 relevant lines covered (69.9%)

6.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.84
/pyipmi/fru.py
1
# Copyright (c) 2014  Kontron Europe GmbH
2
#
3
# This library is free software; you can redistribute it and/or
4
# modify it under the terms of the GNU Lesser General Public
5
# License as published by the Free Software Foundation; either
6
# version 2.1 of the License, or (at your option) any later version.
7
#
8
# This library is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11
# Lesser General Public License for more details.
12
#
13
# You should have received a copy of the GNU Lesser General Public
14
# License along with this library; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
16

17
import array
10✔
18
import codecs
10✔
19
import datetime
10✔
20
import os
10✔
21

22
from .errors import DecodingError, CompletionCodeError
10✔
23
from .msgs import constants
10✔
24
from .utils import bcd_search, chunks, py3_array_tobytes
10✔
25
from .fields import FruTypeLengthString
10✔
26

27
codecs.register(bcd_search)
10✔
28

29

30
class Fru(object):
10✔
31
    def __init__(self):
10✔
32
        self.write_length = 16
10✔
33

34
    def get_fru_inventory_area_info(self, fru_id=0):
10✔
35
        rsp = self.send_message_with_name('GetFruInventoryAreaInfo',
×
36
                                          fru_id=fru_id)
37
        return rsp.area_size
×
38

39
    def write_fru_data(self, data, offset=0, fru_id=0):
10✔
40
        for chunk in chunks(data, self.write_length):
×
41
            write_rsp = self.send_message_with_name('WriteFruData',
×
42
                                                    fru_id=fru_id,
43
                                                    offset=offset,
44
                                                    data=chunk)
45

46
            # check if device wrote the same number of bytes sent
47
            if write_rsp.count_written != len(chunk):
×
48
                raise Exception('sent {:} bytes but device wrote {:} bytes'
×
49
                                .format(len(chunk), write_rsp.count_written))
50

51
            offset += len(chunk)
×
52

53
    def read_fru_data(self, offset=None, count=None, fru_id=0):
10✔
54
        req_size = 32
×
55
        data = array.array('B')
×
56

57
        # first check for maximum area size
58
        if offset is None:
×
59
            area_size = self.get_fru_inventory_area_info(fru_id)
×
60
            off = 0
×
61
        else:
62
            area_size = offset + count
×
63
            off = offset
×
64

65
        while off < area_size:
×
66
            if (off + req_size) > area_size:
×
67
                req_size = area_size - off
×
68

69
            try:
×
70
                rsp = self.send_message_with_name('ReadFruData', fru_id=fru_id,
×
71
                                                  offset=off, count=req_size)
72
            except CompletionCodeError as ex:
×
73
                if ex.cc in (constants.CC_CANT_RET_NUM_REQ_BYTES,
×
74
                             constants.CC_REQ_DATA_FIELD_EXCEED,
75
                             constants.CC_PARAM_OUT_OF_RANGE):
76
                    req_size -= 2
×
77
                    if req_size <= 0:
×
78
                        raise
×
79
                    continue
×
80
                else:
81
                    raise
×
82

83
            data.extend(rsp.data)
×
84
            off += rsp.count
×
85

86
        return py3_array_tobytes(data)
×
87

88
    def read_fru_data_full(self, fru_id=0):
10✔
89
        return self.read_fru_data(fru_id=fru_id)
×
90

91
    def get_fru_inventory_header(self, fru_id=0, ignore_checksum=False):
10✔
92
        data = self.read_fru_data(offset=0, count=8, fru_id=fru_id)
×
NEW
93
        return InventoryCommonHeader(data, ignore_checksum=ignore_checksum)
×
94

95
    def _read_fru_area(self, offset, fru_id=0):
10✔
96
        # read the area header
97
        data = self.read_fru_data(offset=offset, count=5, fru_id=fru_id)
×
98
        # get the whole area data
99
        count = data[1] * 8
×
100
        return self.read_fru_data(offset=offset, count=count, fru_id=fru_id)
×
101

102
    def get_fru_chassis_area(self, fru_id=0, ignore_checksum=False):
10✔
NEW
103
        header = self.get_fru_inventory_header(fru_id=fru_id,
×
104
                                               ignore_checksum=ignore_checksum)
UNCOV
105
        data = self._read_fru_area(offset=header.chassis_info_area_offset,
×
106
                                   fru_id=fru_id)
NEW
107
        return InventoryChassisInfoArea(data, ignore_checksum=ignore_checksum)
×
108

109
    def get_fru_board_area(self, fru_id=0, ignore_checksum=False):
10✔
NEW
110
        header = self.get_fru_inventory_header(fru_id=fru_id,
×
111
                                               ignore_checksum=ignore_checksum)
UNCOV
112
        data = self._read_fru_area(offset=header.board_info_area_offset,
×
113
                                   fru_id=fru_id)
NEW
114
        return InventoryBoardInfoArea(data, ignore_checksum=ignore_checksum)
×
115

116
    def get_fru_product_area(self, fru_id=0, ignore_checksum=False):
10✔
NEW
117
        header = self.get_fru_inventory_header(fru_id=fru_id,
×
118
                                               ignore_checksum=ignore_checksum)
UNCOV
119
        data = self._read_fru_area(offset=header.product_info_area_offset,
×
120
                                   fru_id=fru_id)
NEW
121
        return InventoryProductInfoArea(data, ignore_checksum=ignore_checksum)
×
122

123
    def get_fru_multirecord_area(self, fru_id=0, ignore_checksum=False):
10✔
NEW
124
        header = self.get_fru_inventory_header(fru_id=fru_id,
×
125
                                               ignore_checksum=ignore_checksum)
126

127
        # we have to determine the length of the area first
128
        offset = header.multirecord_area_offset
×
129
        count = 0
×
130

131
        while True:
132
            # read the header
133
            data = self.read_fru_data(offset=offset, count=5)
×
134
            end_of_list = bool(data[1] & 0x80)
×
135
            length = data[2]
×
136
            count += length + 5
×
137
            offset += length + 5
×
138
            if end_of_list:
×
139
                break
×
140

141
        # now read the full area
142
        offset = header.multirecord_area_offset
×
143
        data = self.read_fru_data(offset=offset, count=count)
×
NEW
144
        return InventoryMultiRecordArea(data, ignore_checksum=ignore_checksum)
×
145

146
    def get_fru_inventory(self, fru_id=0, ignore_checksum=False):
10✔
147
        """
148
        Get the full parsed FRU inventory data.
149
        """
150
        fru = FruInventory()
×
151
        header = self.get_fru_inventory_header(fru_id=fru_id)
×
152

153
        if header.chassis_info_area_offset:
×
154
            fru.chassis_info_area = self.get_fru_chassis_area(fru_id=fru_id)
×
155

156
        if header.board_info_area_offset:
×
157
            fru.board_info_area = self.get_fru_board_area(fru_id=fru_id)
×
158

159
        if header.product_info_area_offset:
×
160
            fru.product_info_area = self.get_fru_product_area(fru_id=fru_id)
×
161

162
        if header.multirecord_area_offset:
×
163
            fru.multirecord_area = self.get_fru_multirecord_area(fru_id=fru_id)
×
164

165
        return fru
×
166

167

168
def get_fru_inventory_from_file(filename, ignore_checksum=False):
10✔
169
    try:
10✔
170
        file = open(filename, "rb")
10✔
171
    except IOError:
×
172
        print('Error open file "%s"' % filename)
×
173

174
    ################################
175
    # get file size
176
    file_size = os.stat(filename).st_size
10✔
177
    file_data = file.read(file_size)
10✔
178
    data = array.array('B', file_data)
10✔
179
    file.close()
10✔
180
    return FruInventory(data, ignore_checksum=ignore_checksum)
10✔
181

182

183
CUSTOM_FIELD_END = 0xc1
10✔
184

185

186
def _decode_custom_fields(data):
10✔
187
    offset = 0
10✔
188
    fields = []
10✔
189
    while data[offset] != CUSTOM_FIELD_END:
10✔
190
        field = FruTypeLengthString(data, offset)
10✔
191
        fields.append(field)
10✔
192
        offset += field.length + 1
10✔
193
    return fields
10✔
194

195

196
class FruData(object):
10✔
197
    def __init__(self, data=None, ignore_checksum=False):
10✔
198
        if data:
10✔
199
            if isinstance(data, str):
10✔
200
                data = [ord(c) for c in data]
10✔
201
            self.data = data
10✔
202
            if hasattr(self, '_from_data'):
10✔
203
                self._from_data(data, ignore_checksum=ignore_checksum)
10✔
204

205

206
class InventoryCommonHeader(FruData):
10✔
207
    def _from_data(self, data, ignore_checksum=False):
10✔
208
        if len(data) < 8:
10✔
UNCOV
209
            raise DecodingError('InventoryCommonHeader length != 8')
×
210
        self.format_version = data[0] & 0x0f
10✔
211
        self.internal_use_area_offset = data[1] * 8 or None
10✔
212
        self.chassis_info_area_offset = data[2] * 8 or None
10✔
213
        self.board_info_area_offset = data[3] * 8 or None
10✔
214
        self.product_info_area_offset = data[4] * 8 or None
10✔
215
        self.multirecord_area_offset = data[5] * 8 or None
10✔
216
        if sum(data[:8]) % 256 != 0 and ignore_checksum is False:
10✔
217
            raise DecodingError(f'InventoryCommonHeader checksum failed {sum(data) % 0x10}')
10✔
218

219

220
class CommonInfoArea(FruData):
10✔
221
    def _from_data(self, data, ignore_checksum=False):
10✔
222
        self.format_version = data[0] & 0x0f
10✔
223
        if self.format_version != 1:
10✔
224
            raise DecodingError('unsupported format version (%d)' %
×
225
                                self.format_version)
226
        self.length = data[1] * 8
10✔
227
        if sum(data[:self.length]) % 256 != 0 and ignore_checksum is False:
10✔
228
            raise DecodingError('checksum failed')
10✔
229

230

231
class InventoryChassisInfoArea(CommonInfoArea):
10✔
232
    TYPE_OTHER = 1
10✔
233
    TYPE_UNKNOWN = 2
10✔
234
    TYPE_DESKTOP = 3
10✔
235
    TYPE_LOW_PROFILE_DESKTOP = 4
10✔
236
    TYPE_PIZZA_BOX = 5
10✔
237
    TYPE_MINI_TOWER = 6
10✔
238
    TYPE_TOWER = 7
10✔
239
    TYPE_PORTABLE = 8
10✔
240
    TYPE_LAPTOP = 9
10✔
241
    TYPE_NOTEBOOK = 10
10✔
242
    TYPE_HAND_HELD = 11
10✔
243
    TYPE_DOCKING_STATION = 12
10✔
244
    TYPE_ALL_IN_ONE = 13
10✔
245
    TYPE_SUB_NOTEBOOK = 14
10✔
246
    TYPE_SPACE_SAVING = 15
10✔
247
    TYPE_LUNCH_BOX = 16
10✔
248
    TYPE_MAIN_SERVER_CHASSIS = 17
10✔
249
    TYPE_EXPANSION_CHASSIS = 18
10✔
250
    TYPE_SUB_CHASSIS = 19
10✔
251
    TYPE_BUS_EXPANSION_CHASSIS = 20
10✔
252
    TYPE_PERIPHERAL_CHASSIS = 21
10✔
253
    TYPE_RAID_CHASSIS = 22
10✔
254
    TYPE_RACK_MOUNT_CHASSIS = 23
10✔
255

256
    def _from_data(self, data, ignore_checksum=False):
10✔
257
        CommonInfoArea._from_data(self, data)
×
258
        self.type = data[2]
×
259
        offset = 3
×
260
        self.part_number = FruTypeLengthString(data, offset)
×
261
        offset += self.part_number.length + 1
×
262
        self.serial_number = FruTypeLengthString(data, offset, True)
×
263
        offset += self.serial_number.length + 1
×
264
        self.custom_chassis_info = _decode_custom_fields(data[offset:])
×
265

266

267
class InventoryBoardInfoArea(CommonInfoArea):
10✔
268
    def _from_data(self, data, ignore_checksum=False):
10✔
269
        CommonInfoArea._from_data(self, data, ignore_checksum=ignore_checksum)
10✔
270
        self.language_code = data[2]
10✔
271
        minutes = data[5] << 16 | data[4] << 8 | data[3]
10✔
272
        self.mfg_date = (datetime.datetime(1996, 1, 1)
10✔
273
                         + datetime.timedelta(minutes=minutes))
274
        offset = 6
10✔
275
        self.manufacturer = FruTypeLengthString(data, offset)
10✔
276
        offset += self.manufacturer.length + 1
10✔
277
        self.product_name = FruTypeLengthString(data, offset)
10✔
278
        offset += self.product_name.length + 1
10✔
279
        self.serial_number = FruTypeLengthString(data, offset, True)
10✔
280
        offset += self.serial_number.length + 1
10✔
281
        self.part_number = FruTypeLengthString(data, offset)
10✔
282
        offset += self.part_number.length + 1
10✔
283
        self.fru_file_id = FruTypeLengthString(data, offset, True)
10✔
284
        offset += self.fru_file_id.length + 1
10✔
285
        self.custom_mfg_info = _decode_custom_fields(data[offset:])
10✔
286

287

288
class InventoryProductInfoArea(CommonInfoArea):
10✔
289
    def _from_data(self, data, ignore_checksum=False):
10✔
290
        CommonInfoArea._from_data(self, data)
10✔
291
        self.language_code = data[2]
10✔
292
        offset = 3
10✔
293
        self.manufacturer = FruTypeLengthString(data, offset)
10✔
294
        offset += self.manufacturer.length + 1
10✔
295
        self.name = FruTypeLengthString(data, offset)
10✔
296
        offset += self.name.length + 1
10✔
297
        self.part_number = FruTypeLengthString(data, offset)
10✔
298
        offset += self.part_number.length + 1
10✔
299
        self.version = FruTypeLengthString(data, offset)
10✔
300
        offset += self.version.length + 1
10✔
301
        self.serial_number = FruTypeLengthString(data, offset, True)
10✔
302
        offset += self.serial_number.length + 1
10✔
303
        self.asset_tag = FruTypeLengthString(data, offset)
10✔
304
        offset += self.asset_tag.length + 1
10✔
305
        self.fru_file_id = FruTypeLengthString(data, offset, True)
10✔
306
        offset += self.fru_file_id.length + 1
10✔
307
        self.custom_mfg_info = list()
10✔
308
        self.custom_mfg_info = _decode_custom_fields(data[offset:])
10✔
309

310

311
class FruDataMultiRecord(FruData):
10✔
312
    TYPE_POWER_SUPPLY_INFORMATION = 0
10✔
313
    TYPE_DC_OUTPUT = 1
10✔
314
    TYPE_DC_LOAD = 2
10✔
315
    TYPE_MANAGEMENT_ACCESS_RECORD = 3
10✔
316
    TYPE_BASE_COMPATIBILITY_RECORD = 4
10✔
317
    TYPE_EXTENDED_COMPATIBILITY_RECORD = 5
10✔
318
    TYPE_OEM = list(range(0x0c, 0x100))
10✔
319
    TYPE_OEM_PICMG = 0xc0
10✔
320

321
    def __str__(self):
10✔
322
        return '%02x: %s' % (self.record_type_id,
×
323
                             ' '.join('%02x' % b for b in self.raw))
324

325
    def _from_data(self, data, ignore_checksum=False):
10✔
326
        if len(data) < 5:
10✔
327
            raise DecodingError('data too short')
×
328
        self.record_type_id = data[0]
10✔
329
        self.format_version = data[1] & 0x0f
10✔
330
        self.end_of_list = bool(data[1] & 0x80)
10✔
331
        self.length = data[2]
10✔
332
        if sum(data[:5]) % 256 != 0 and ignore_checksum is False:
10✔
333
            raise DecodingError('FruDataMultiRecord header checksum failed')
×
334
        self.raw = data[5:5+self.length]
10✔
335
        if (sum(self.raw) + data[3]) % 256 != 0 and ignore_checksum is False:
10✔
336
            raise DecodingError('FruDataMultiRecord record checksum failed')
×
337

338
    @staticmethod
10✔
339
    def create_from_record_id(data):
9✔
340
        if data[0] == FruDataMultiRecord.TYPE_OEM_PICMG:
10✔
341
            return FruPicmgRecord.create_from_record_id(data)
10✔
342
        else:
343
            return FruDataUnknown(data)
×
344

345

346
class FruDataUnknown(FruDataMultiRecord):
10✔
347
    """This class is used to indicate undecoded picmg record."""
3✔
348

349
    pass
10✔
350

351

352
class FruPicmgRecord(FruDataMultiRecord):
10✔
353
    PICMG_RECORD_ID_BACKPLANE_PTP_CONNECTIVITY = 0x04
10✔
354
    PICMG_RECORD_ID_ADDRESS_TABLE = 0x10
10✔
355
    PICMG_RECORD_ID_SHELF_POWER_DISTRIBUTION = 0x11
10✔
356
    PICMG_RECORD_ID_SHMC_ACTIVATION_MANAGEMENT = 0x12
10✔
357
    PICMG_RECORD_ID_SHMC_IP_CONNECTION = 0x13
10✔
358
    PICMG_RECORD_ID_BOARD_PTP_CONNECTIVITY = 0x14
10✔
359
    PICMG_RECORD_ID_RADIAL_IPMB0_LINK_MAPPING = 0x15
10✔
360
    PICMG_RECORD_ID_MODULE_CURRENT_REQUIREMENTS = 0x16
10✔
361
    PICMG_RECORD_ID_CARRIER_ACTIVATION_MANAGEMENT = 0x17
10✔
362
    PICMG_RECORD_ID_CARRIER_PTP_CONNECTIVITY = 0x18
10✔
363
    PICMG_RECORD_ID_AMC_PTP_CONNECTIVITY = 0x19
10✔
364
    PICMG_RECORD_ID_CARRIER_INFORMATION = 0x1a
10✔
365
    PICMG_RECORD_ID_MTCA_FRU_INFORMATION_PARTITION = 0x20
10✔
366
    PICMG_RECORD_ID_MTCA_CARRIER_MANAGER_IP_LINK = 0x21
10✔
367
    PICMG_RECORD_ID_MTCA_CARRIER_INFORMATION = 0x22
10✔
368
    PICMG_RECORD_ID_MTCA_SHELF_INFORMATION = 0x23
10✔
369
    PICMG_RECORD_ID_MTCA_SHELF_MANAGER_IP_LINK = 0x24
10✔
370
    PICMG_RECORD_ID_MTCA_CARRIER_POWER_POLICY = 0x25
10✔
371
    PICMG_RECORD_ID_MTCA_CARRIER_ACTIVATION_AND_POWER = 0x26
10✔
372
    PICMG_RECORD_ID_MTCA_POWER_MODULE_CAPABILITY = 0x27
10✔
373
    PICMG_RECORD_ID_MTCA_FAN_GEOGRAPHY = 0x28
10✔
374
    PICMG_RECORD_ID_OEM_MODULE_DESCRIPTION = 0x29
10✔
375
    PICMG_RECORD_ID_CARRIER_CLOCK_PTP_CONNECTIVITY = 0x2C
10✔
376
    PICMG_RECORD_ID_CLOCK_CONFIGURATION = 0x2d
10✔
377
    PICMG_RECORD_ID_ZONE_3_INTERFACE_COMPATIBILITY = 0x30
10✔
378
    PICMG_RECORD_ID_CARRIER_BUSED_CONNECTIVITY = 0x31
10✔
379
    PICMG_RECORD_ID_ZONE_3_INTERFACE_DOCUMENTATION = 0x32
10✔
380

381
    def __init__(self, data):
10✔
382
        FruDataMultiRecord.__init__(self, data)
10✔
383

384
    @staticmethod
10✔
385
    def create_from_record_id(data):
9✔
386
        picmg_record = FruPicmgRecord(data)
10✔
387
        if picmg_record.picmg_record_type_id ==\
10✔
388
                FruPicmgRecord.PICMG_RECORD_ID_MTCA_POWER_MODULE_CAPABILITY:
389
            return FruPicmgPowerModuleCapabilityRecord(data)
10✔
390

391
        return FruPicmgRecord(data)
10✔
392

393
    def _from_data(self, data, ignore_checksum=False):
10✔
394
        if len(data) < 10:
10✔
395
            raise DecodingError('data too short')
×
396
        data = array.array('B', data)
10✔
397
        FruDataMultiRecord._from_data(self, data, ignore_checksum=ignore_checksum)
10✔
398
        self.manufacturer_id = \
10✔
399
            data[5] | data[6] << 8 | data[7] << 16
400
        self.picmg_record_type_id = data[8]
10✔
401
        self.format_version = data[9]
10✔
402

403

404
class FruPicmgPowerModuleCapabilityRecord(FruPicmgRecord):
10✔
405
    def _from_data(self, data, ignore_checksum=False):
10✔
406
        if len(data) < 12:
10✔
407
            raise DecodingError('data too short')
×
408
        FruPicmgRecord._from_data(self, data)
10✔
409
        maximum_current_output = data[10] | data[11] << 8
10✔
410
        self.maximum_current_output = float(maximum_current_output/10)
10✔
411

412

413
class InventoryMultiRecordArea(object):
10✔
414
    def __init__(self, data, ignore_checksum=False):
10✔
415
        if data:
10✔
416
            self._from_data(data)
10✔
417

418
    def _from_data(self, data, ignore_checksum=False):
10✔
419
        self.records = list()
10✔
420
        offset = 0
10✔
421
        while True:
6✔
422
            record = FruDataMultiRecord.create_from_record_id(data[offset:])
10✔
423
            self.records.append(record)
10✔
424
            offset += record.length + 5
10✔
425
            if record.end_of_list:
10✔
426
                break
10✔
427

428

429
class FruInventory(object):
10✔
430
    def __init__(self, data=None, ignore_checksum=False):
10✔
431
        self.chassis_info_area = None
10✔
432
        self.board_info_area = None
10✔
433
        self.product_info_area = None
10✔
434
        self.multirecord_area = None
10✔
435

436
        if data:
10✔
437
            self._from_data(data, ignore_checksum=ignore_checksum)
10✔
438

439
    def _from_data(self, data, ignore_checksum=False):
10✔
440
        self.raw = data
10✔
441
        self.common_header = InventoryCommonHeader(data[:8])
10✔
442

443
        if self.common_header.chassis_info_area_offset:
10✔
444
            self.chassis_info_area = InventoryChassisInfoArea(
×
445
                data[self.common_header.chassis_info_area_offset:],
446
                ignore_checksum=ignore_checksum)
447

448
        if self.common_header.board_info_area_offset:
10✔
449
            self.board_info_area = InventoryBoardInfoArea(
10✔
450
                data[self.common_header.board_info_area_offset:],
451
                ignore_checksum=ignore_checksum)
452

453
        if self.common_header.product_info_area_offset:
10✔
454
            self.product_info_area = InventoryProductInfoArea(
10✔
455
                data[self.common_header.product_info_area_offset:],
456
                ignore_checksum=ignore_checksum)
457

458
        if self.common_header.multirecord_area_offset:
10✔
459
            self.multirecord_area = InventoryMultiRecordArea(
10✔
460
                data[self.common_header.multirecord_area_offset:],
461
                ignore_checksum=ignore_checksum)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc