From 54b90439e752ecfc2ada2336538b52662b53a355 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 29 Apr 2015 10:49:06 -0400 Subject: [PATCH] Add system UUID to inventory While not strictly in the FRU area, it is often desirable to have the system UUID available. The intent is for the UUID to match what dmidecode would return. If a manufacturer does it right, that UUID will be unique. For ThinkServers, override with the UUID from the OEM FRU fields rather than using the get system UUID result. Change-Id: Ie9a1b7e8fee2cb40ab679cbf2df04db61fd4e42f --- pyghmi/ipmi/command.py | 25 ++++++++++++++++++++----- pyghmi/ipmi/fru.py | 9 ++++++--- pyghmi/ipmi/oem/generic.py | 2 +- pyghmi/ipmi/oem/lenovo.py | 14 ++++++++++++-- pyghmi/ipmi/private/util.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 pyghmi/ipmi/private/util.py diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 506d4db9..bee7f43a 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -22,6 +22,7 @@ import pyghmi.exceptions as exc import pyghmi.ipmi.fru as fru from pyghmi.ipmi.oem.lookup import get_oem_handler from pyghmi.ipmi.private import session +import pyghmi.ipmi.private.util as pygutil import pyghmi.ipmi.sdr as sdr import struct @@ -384,7 +385,7 @@ class Command(object): """ self.oem_init() if component == 'System': - return self._oem.process_fru(fru.FRU(ipmicmd=self).info) + return self._get_zero_fru() if self._sdr is None: self._sdr = sdr.SDR(self) for fruid in self._sdr.fru: @@ -392,6 +393,23 @@ class Command(object): return self._oem.process_fru(fru.FRU( ipmicmd=self, fruid=fruid, sdr=self._sdr.fru[fruid]).info) + def _get_zero_fru(self): + # It is expected that a manufacturer matches SMBIOS to IPMI + # get system uuid return data. If a manufacturer does not + # do so, they should handle either deletion or fixup in the + # OEM processing pass. Code optimistically assumes that if + # data is returned, than the vendor is properly using it. + zerofru = fru.FRU(ipmicmd=self).info + if zerofru is None: + zerofru = {} + guiddata = self.raw_command(netfn=6, command=0x37) + if 'error' not in guiddata: + zerofru['UUID'] = pygutil.decode_wireformat_uuid( + guiddata['data']) + if not zerofru: + zerofru = None + return self._oem.process_fru(zerofru) + def get_inventory(self): """Retrieve inventory of system @@ -403,10 +421,7 @@ class Command(object): or None for items not present. """ self.oem_init() - zerofru = fru.FRU(ipmicmd=self).info - if zerofru is not None: - zerofru = self._oem.process_fru(zerofru) - yield ("System", zerofru) + yield ("System", self._get_zero_fru()) if self._sdr is None: self._sdr = sdr.SDR(self) for fruid in self._sdr.fru: diff --git a/pyghmi/ipmi/fru.py b/pyghmi/ipmi/fru.py index 9823b837..6b6257ee 100644 --- a/pyghmi/ipmi/fru.py +++ b/pyghmi/ipmi/fru.py @@ -221,7 +221,11 @@ class FRU(object): retinfo = retinfo.decode('utf-16le') except UnicodeDecodeError: pass - retinfo = retinfo.replace('\x00', '').strip() + # 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('\x00 ') return retinfo, newoffset elif currtype == 1: # BCD 'plus' retdata = '' @@ -232,8 +236,7 @@ class FRU(object): retdata = retdata.strip() return retdata, newoffset elif currtype == 2: # 6-bit ascii - retinfo = unpack6bitascii(retinfo) - retinfo = retinfo.strip() + retinfo = unpack6bitascii(retinfo).strip() return retinfo, newoffset def _parse_chassis(self): diff --git a/pyghmi/ipmi/oem/generic.py b/pyghmi/ipmi/oem/generic.py index 9603517d..d0b6ffb6 100644 --- a/pyghmi/ipmi/oem/generic.py +++ b/pyghmi/ipmi/oem/generic.py @@ -37,7 +37,7 @@ class OEMHandler(object): board/product/chassis_extra_data arrays if 'oem_parser' is None, and mask those fields if not None. It is expected that OEMs leave the fields intact so that if client code hard codes around the - ordered lists that their expectations are not broken by an update + ordered lists that their expectations are not broken by an update. """ # In the generic case, just pass through if fru is None: diff --git a/pyghmi/ipmi/oem/lenovo.py b/pyghmi/ipmi/oem/lenovo.py index 290b662e..b396678e 100644 --- a/pyghmi/ipmi/oem/lenovo.py +++ b/pyghmi/ipmi/oem/lenovo.py @@ -15,9 +15,11 @@ # limitations under the License. import pyghmi.ipmi.oem.generic as generic +import pyghmi.ipmi.private.util as util class OEMHandler(generic.OEMHandler): + # noinspection PyUnusedLocal def __init__(self, oemid, ipmicmd): # will need to retain data to differentiate # variations. For example System X versus Thinkserver @@ -40,8 +42,16 @@ class OEMHandler(generic.OEMHandler): fru['MAC Address 1'] = mac1 if mac2 not in ('00:00:00:00:00:00', ''): fru['MAC Address 2'] = mac2 - # The product_extra is just UUID, we have that plenty of other ways - # So for now, leave that portion of the data alone + # 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 the string is not 16 long + byteguid.extend('\x00' * (16 - len(byteguid))) + fru['UUID'] = util.decode_wireformat_uuid(byteguid) return fru else: fru['oem_parser'] = None diff --git a/pyghmi/ipmi/private/util.py b/pyghmi/ipmi/private/util.py new file mode 100644 index 00000000..40afcda3 --- /dev/null +++ b/pyghmi/ipmi/private/util.py @@ -0,0 +1,31 @@ +# 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 + + +def decode_wireformat_uuid(rawguid): + """Decode a wire format UUID + + It handles the rather particular scheme where half is little endian + and half is big endian. It returns a string like dmidecode would output. + """ + if isinstance(rawguid, list): + rawguid = bytearray(rawguid) + lebytes = struct.unpack_from('HHI', buffer(rawguid[8:])) + return '{0:04X}-{1:02X}-{2:02X}-{3:02X}-{4:02X}{5:04X}'.format( + lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2])