From ea3960c22692929272f67453383191152ea2a76a Mon Sep 17 00:00:00 2001 From: Allan Vidal Date: Fri, 14 Aug 2015 12:04:55 -0300 Subject: [PATCH] Add custom OEM inventory parsing for Lenovo ThinkServers Parse OEM data for CPU, DIMM, PCI, drive and PSU inventory. Change-Id: Ic75d03fb1b05c13702321d174c8dbb9a96a08ae4 --- bin/pyghmiutil | 3 + pyghmi/ipmi/command.py | 2 +- pyghmi/ipmi/oem/lenovo/__init__.py | 0 pyghmi/ipmi/oem/lenovo/cpu.py | 48 ++++++ pyghmi/ipmi/oem/lenovo/dimm.py | 52 ++++++ pyghmi/ipmi/oem/lenovo/drive.py | 73 ++++++++ pyghmi/ipmi/oem/lenovo/handler.py | 248 ++++++++++++++++++++++++++++ pyghmi/ipmi/oem/lenovo/inventory.py | 124 ++++++++++++++ pyghmi/ipmi/oem/lenovo/pci.py | 64 +++++++ pyghmi/ipmi/oem/lenovo/psu.py | 123 ++++++++++++++ pyghmi/ipmi/oem/lookup.py | 2 +- 11 files changed, 737 insertions(+), 2 deletions(-) create mode 100755 pyghmi/ipmi/oem/lenovo/__init__.py create mode 100755 pyghmi/ipmi/oem/lenovo/cpu.py create mode 100755 pyghmi/ipmi/oem/lenovo/dimm.py create mode 100755 pyghmi/ipmi/oem/lenovo/drive.py create mode 100755 pyghmi/ipmi/oem/lenovo/handler.py create mode 100755 pyghmi/ipmi/oem/lenovo/inventory.py create mode 100755 pyghmi/ipmi/oem/lenovo/pci.py create mode 100755 pyghmi/ipmi/oem/lenovo/psu.py mode change 100644 => 100755 pyghmi/ipmi/oem/lookup.py diff --git a/bin/pyghmiutil b/bin/pyghmiutil index a68b4f4d..4f45843e 100755 --- a/bin/pyghmiutil +++ b/bin/pyghmiutil @@ -64,6 +64,9 @@ def docommand(result, ipmisession): print repr(reading) elif cmmand == 'health': print repr(ipmisession.get_health()) + elif cmmand == 'inventory': + for item in ipmisession.get_inventory(): + print repr(item) elif cmmand == 'raw': print ipmisession.raw_command(netfn=int(args[0]), command=int(args[1]), diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index d37e267c..2df49736 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -1417,7 +1417,7 @@ class Command(object): channel = self.get_network_channel() names = {} max_ids = self.get_channel_max_user_count(channel) - for uid in range(1, max_ids): + for uid in range(1, max_ids+1): name = self.get_user_name(uid=uid) if name is not None: names[uid] = self.get_user(uid=uid, channel=channel) diff --git a/pyghmi/ipmi/oem/lenovo/__init__.py b/pyghmi/ipmi/oem/lenovo/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/pyghmi/ipmi/oem/lenovo/cpu.py b/pyghmi/ipmi/oem/lenovo/cpu.py new file mode 100755 index 00000000..4228ac57 --- /dev/null +++ b/pyghmi/ipmi/oem/lenovo/cpu.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \ + parse_inventory_category_entry + +cpu_fields = ( + EntryField("index", "B"), + EntryField("Cores", "B"), + EntryField("Threads", "B"), + EntryField("Manufacturer", "13s"), + EntryField("Family", "30s"), + EntryField("Model", "30s"), + EntryField("Stepping", "3s"), + EntryField("Maximum Frequency", "> 3 + if slotnumber: + event['component'] += ' {0}'.format(slotnumber) + event['event'], event['severity'] = \ + firmware_event[oemdata[1] & 0b111] + event['event_data'] = '{0}.{1}'.format(oemdata[2], oemdata[3]) + elif oemtype == 0xd1: # BIOS recovery + event['severity'] = pygconst.Health.Warning + event['component'] = 'BIOS/UEFI' + event['component_type'] = ipmiconst.sensor_type_codes[0xf] + status = oemdata[0] + method = (status & 0b11110000) >> 4 + status = (status & 0b1111) + if method == 1: + event['event'] = 'Automatic recovery' + elif method == 2: + event['event'] = 'Manual recovery' + if status == 0: + event['event'] += '- Failed' + event['severity'] = pygconst.Health.Failed + if oemdata[1] == 0x1: + event['event'] += '- BIOS recovery image not found' + event['event_data'] = '{0}.{1}'.format(oemdata[2], oemdata[3]) + elif oemtype == 0xd2: # eMMC status + if oemdata[0] == 1: + event['component'] = 'eMMC' + event['component_type'] = ipmiconst.sensor_type_codes[0xc] + if oemdata[0] == 1: + event['event'] = 'eMMC Format error' + event['severity'] = pygconst.Health.Failed + elif oemtype == 0xd3: + if oemdata[0] == 1: + event['event'] = 'User privilege modification' + event['severity'] = pygconst.Health.Ok + event['component'] = 'User Privilege' + event['component_type'] = ipmiconst.sensor_type_codes[6] + event['event_data'] = \ + 'User {0} on channel {1} had privilege changed ' \ + 'from {2} to {3}'.format( + oemdata[2], oemdata[1], oemdata[3] & 0b1111, + (oemdata[3] & 0b11110000) >> 4 + ) + else: + event['event'] = 'OEM event: {0}'.format( + ' '.join(format(x, '02x') for x in event['oemdata'])) + del event['oemdata'] + return + evdata = event['event_data_bytes'] + if event['event_type_byte'] == 0x75: # ME event + event['component'] = 'ME Firmware' + event['component_type'] = ipmiconst.sensor_type_codes[0xf] + event['event'], event['severity'] = me_status.get( + evdata[1], ('Unknown', pygconst.Health.Warning)) + if evdata[1] == 3: + event['event'], event['severity'] = me_flash_status.get( + evdata[2], ('Unknown state', pygconst.Health.Warning)) + elif evdata[1] == 9: + event['event'] += ' (0x{0:2x})'.format(evdata[2]) + elif evdata[1] == 0xf and evdata[2] & 0b10000000: + event['event'] = 'Auto configuration failed' + event['severity'] = pygconst.Health.Critical + # For HDD bay events, the event data 2 is the bay, modify + # the description to be more specific + if (event['event_type_byte'] == 0x6f and + (evdata[0] & 0b11000000) == 0b10000000 and + event['component_type_id'] == 13): + event['component'] += ' {0}'.format(evdata[1] & 0b11111) + + @property + def has_tsm(self): + """True if this particular server have a TSM based service processor + """ + return (self.oemid['manufacturer_id'] == 19046 and + self.oemid['device_id'] == 32) + + def get_oem_inventory_descriptions(self): + if self.has_tsm: + # Thinkserver with TSM + if not self.oem_inventory_info: + self._collect_tsm_inventory() + return iter(self.oem_inventory_info) + return () + + def get_oem_inventory(self): + if self.has_tsm: + self._collect_tsm_inventory() + for compname in self.oem_inventory_info: + yield (compname, self.oem_inventory_info[compname]) + + def get_inventory_of_component(self, component): + if self.has_tsm: + self._collect_tsm_inventory() + return self.oem_inventory_info.get(component, None) + + def _collect_tsm_inventory(self): + self.oem_inventory_info = {} + for catid, catspec in inventory.categories.items(): + try: + rsp = self.ipmicmd.xraw_command(**catspec["command"]) + except pygexc.IpmiException: + continue + else: + try: + items = inventory.parse_inventory_category(catid, rsp) + except Exception: + # If we can't parse an inventory category, ignore it + print traceback.print_exc() + continue + + for item in items: + try: + key = catspec["idstr"].format(item["index"]) + del item["index"] + self.oem_inventory_info[key] = item + except Exception: + # If we can't parse an inventory item, ignore it + print traceback.print_exc() + continue + + def process_fru(self, fru): + if fru is None: + return fru + if self.has_tsm: + fru['oem_parser'] = 'lenovo' + # Thinkserver lays out specific interpretation of the + # board extra fields + _, _, wwn1, wwn2, mac1, mac2 = fru['board_extra'] + if wwn1 not in ('0000000000000000', ''): + fru['WWN 1'] = wwn1 + if wwn2 not in ('0000000000000000', ''): + fru['WWN 2'] = wwn2 + if mac1 not in ('00:00:00:00:00:00', ''): + fru['MAC Address 1'] = mac1 + if mac2 not in ('00:00:00:00:00:00', ''): + fru['MAC Address 2'] = mac2 + try: + # The product_extra field is UUID as the system would present + # in DMI. This is different than the two UUIDs that + # it returns for get device and get system uuid... + byteguid = fru['product_extra'][0] + # It can present itself as claiming to be ASCII when it + # is actually raw hex. As a result it triggers the mechanism + # to strip \x00 from the end of text strings. Work around this + # by padding with \x00 to the right if less than 16 long + byteguid.extend('\x00' * (16 - len(byteguid))) + fru['UUID'] = util.decode_wireformat_uuid(byteguid) + except (AttributeError, KeyError): + pass + return fru + else: + fru['oem_parser'] = None + return fru diff --git a/pyghmi/ipmi/oem/lenovo/inventory.py b/pyghmi/ipmi/oem/lenovo/inventory.py new file mode 100755 index 00000000..7014c40a --- /dev/null +++ b/pyghmi/ipmi/oem/lenovo/inventory.py @@ -0,0 +1,124 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +import struct + +categories = {} + + +def register_inventory_category(module): + c = module.get_categories() + for id in c: + categories[id] = c[id] + + +class EntryField(object): + """Store inventory field parsing options. + + Represents an inventory field and its options for the custom requests to a + ThinkServer's BMC. + + :param name: the name of the field + :param fmt: the format of the field (see struct module for details) + :param include: whether to include the field in the parse output + :param mapper: a dictionary mapping values to new values for the parse + output + :param valuefunc: a function to be called to change the value in the last + step of the build process. + """ + def __init__(self, name, fmt, include=True, mapper=None, valuefunc=None, + multivaluefunc=False): + self.name = name + self.fmt = fmt + self.include = include + self.mapper = mapper + self.valuefunc = valuefunc + self.multivaluefunc = multivaluefunc + + +# General parameter parsing functions +def parse_inventory_category(name, info): + """Parses every entry in an inventory category (CPU, memory, PCI, drives, + etc). + + Expects the first byte to be a count of the number of entries, followed + by a list of elements to be parsed by a dedicated parser (below). + + :param name: the name of the parameter (e.g.: "cpu") + :param info: a list of integers with raw data read from an IPMI requests + + :returns: dict -- a list of entries in the category. + """ + raw = info["data"][1:] + + cur = 0 + count = struct.unpack("B", raw[cur])[0] + cur += 1 + + entries = [] + while cur < len(raw): + read, cpu = categories[name]["parser"](raw[cur:]) + cur = cur + read + entries.append(cpu) + + # TODO(avidal): raise specific exception to point that there's data left in + # the buffer + if cur != len(raw): + raise Exception + # TODO(avidal): raise specific exception to point that the number of + # entries is different than the expected + if count != len(entries): + raise Exception + return entries + + +def parse_inventory_category_entry(raw, fields): + """Parses one entry in an inventory category. + + :param raw: the raw data to the entry. May contain more than one entry, + only one entry will be read in that case. + :param fields: an iterable of EntryField objects to be used for parsing the + entry. + + :returns: dict -- a tuple with the number of bytes read and a dictionary + representing the entry. + """ + r = raw + + obj = {} + bytes_read = 0 + for field in fields: + value = struct.unpack_from(field.fmt, r)[0] + read = struct.calcsize(field.fmt) + bytes_read += read + r = r[read:] + if not field.include: + continue + + if (field.fmt[-1] == "s"): + value = value.rstrip("\x00") + if (field.mapper and value in field.mapper): + value = field.mapper[value] + if (field.valuefunc): + value = field.valuefunc(value) + + if not field.multivaluefunc: + obj[field.name] = value + else: + for key in value: + obj[key] = value[key] + + return bytes_read, obj diff --git a/pyghmi/ipmi/oem/lenovo/pci.py b/pyghmi/ipmi/oem/lenovo/pci.py new file mode 100755 index 00000000..c875107f --- /dev/null +++ b/pyghmi/ipmi/oem/lenovo/pci.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \ + parse_inventory_category_entry + +pci_fields = ( + EntryField("index", "B"), + EntryField("PCIType", "B", mapper={ + 0x0: "On board slot", + 0x1: "Riser Type 1", + 0x2: "Riser Type 2", + 0x3: "Riser Type 3", + 0x4: "Riser Type 4", + 0x5: "Riser Type 5", + 0x6: "Riser Type 6a", + 0x7: "Riser Type 6b", + 0x8: "ROC", + 0x9: "Mezz" + }), + EntryField("BusNumber", "B"), + EntryField("DeviceFunction", "B"), + EntryField("VendorID", "