From 8a7a909b2ef6a5056225562c0c26a0df84bf1f6f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 18 Apr 2016 14:00:08 -0400 Subject: [PATCH] Add system X firmware information This commit adds support for fetching extended System X firmware information. This includes, as supported by the model, extended IMM info, backup IMM, UEFI (primary, backup, and pending), as well as available firmware from agentless system x options. Change-Id: Iea09af0dd54938dbfe54b64c0a1084cb7ad2264f --- pyghmi/ipmi/command.py | 5 +- pyghmi/ipmi/oem/generic.py | 7 +- pyghmi/ipmi/oem/lenovo/handler.py | 26 ++++- pyghmi/ipmi/oem/lenovo/imm.py | 151 ++++++++++++++++++++++++++++++ pyghmi/util/webclient.py | 25 ++++- 5 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 pyghmi/ipmi/oem/lenovo/imm.py diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 3ad9c835..6f217902 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -1690,7 +1690,10 @@ class Command(object): """Retrieve OEM Firmware information """ self.oem_init() - return self._oem.get_oem_firmware() + mcinfo = self.xraw_command(netfn=6, command=1) + bmcver = '{0}.{1}'.format( + ord(mcinfo['data'][2]), hex(ord(mcinfo['data'][3]))[2:]) + return self._oem.get_oem_firmware(bmcver) def get_capping_enabled(self): """Get PSU based power capping status diff --git a/pyghmi/ipmi/oem/generic.py b/pyghmi/ipmi/oem/generic.py index 082d4513..3cf5d2cc 100644 --- a/pyghmi/ipmi/oem/generic.py +++ b/pyghmi/ipmi/oem/generic.py @@ -175,10 +175,13 @@ class OEMHandler(object): fru['oem_parser'] = None return fru - def get_oem_firmware(self): + def get_oem_firmware(self, bmcver): """Get Firmware information. """ - return () + # Here the bmc version is passed into the OEM handler, to allow + # the handler to enrich the data. For the generic case, just + # provide the generic BMC version, which is all that is possible + yield ('BMC Version', {'version': bmcver}) def get_oem_capping_enabled(self): """Get PSU based power capping status diff --git a/pyghmi/ipmi/oem/lenovo/handler.py b/pyghmi/ipmi/oem/lenovo/handler.py index 510d13f0..0003be4f 100755 --- a/pyghmi/ipmi/oem/lenovo/handler.py +++ b/pyghmi/ipmi/oem/lenovo/handler.py @@ -30,6 +30,7 @@ from pyghmi.ipmi.oem.lenovo import dimm from pyghmi.ipmi.oem.lenovo import drive from pyghmi.ipmi.oem.lenovo import firmware +from pyghmi.ipmi.oem.lenovo import imm from pyghmi.ipmi.oem.lenovo import inventory from pyghmi.ipmi.oem.lenovo import nextscale from pyghmi.ipmi.oem.lenovo import pci @@ -37,6 +38,7 @@ from pyghmi.ipmi.oem.lenovo import psu from pyghmi.ipmi.oem.lenovo import raid_controller from pyghmi.ipmi.oem.lenovo import raid_drive + import pyghmi.util.webclient as wc import socket @@ -132,6 +134,8 @@ class OEMHandler(generic.OEMHandler): self._has_megarac = None self.oem_inventory_info = None self._mrethidx = None + self._hasimm = None + self._immbuildinfo = None @property def _megarac_eth_index(self): @@ -419,11 +423,31 @@ class OEMHandler(generic.OEMHandler): fru['oem_parser'] = None return fru - def get_oem_firmware(self): + @property + def has_imm(self): + if self._hasimm is not None: + return self._hasimm + try: + bdata = self.ipmicmd.xraw_command(netfn=0x3a, command=0x50) + except pygexc.IpmiException: + self._hasimm = False + return False + if len(bdata['data'][:]) != 30: + self._hasimm = False + return False + self._hasimm = True + self._immbuildinfo = bdata['data'][:] + return True + + def get_oem_firmware(self, bmcver): if self.has_tsm: command = firmware.get_categories()["firmware"] rsp = self.ipmicmd.xraw_command(**command["command"]) return command["parser"](rsp["data"]) + elif self.has_imm: + return imm.get_firmware_inventory(self.ipmicmd, bmcver, + self._immbuildinfo, + self._certverify) return () def get_oem_capping_enabled(self): diff --git a/pyghmi/ipmi/oem/lenovo/imm.py b/pyghmi/ipmi/oem/lenovo/imm.py new file mode 100644 index 00000000..34b48784 --- /dev/null +++ b/pyghmi/ipmi/oem/lenovo/imm.py @@ -0,0 +1,151 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2016 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 datetime import datetime +import json +import pyghmi.util.webclient as webclient +import urllib + + +def get_imm_property(ipmicmd, propname): + propname = propname.encode('utf-8') + proplen = len(propname) | 0b10000000 + cmdlen = len(propname) + 1 + cdata = bytearray([0, 0, cmdlen, proplen]) + propname + rsp = ipmicmd.xraw_command(netfn=0x3a, command=0xc4, data=cdata) + rsp['data'] = bytearray(rsp['data']) + if rsp['data'][0] != 0: + return None + propdata = rsp['data'][3:] # second two bytes are size, don't need it + if propdata[0] & 0b10000000: # string, for now assume length valid + return str(propdata[1:]).rstrip(' \x00') + else: + raise Exception('Unknown format for property: ' + repr(propdata)) + + +def get_imm_webclient(imm, certverify, uid, password): + wc = webclient.SecureHTTPConnection(imm, 443, + verifycallback=certverify) + try: + wc.connect() + except Exception: + return None + adata = urllib.urlencode({'user': uid, + 'password': password, + 'SessionTimeout': 60 + }) + headers = {'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded'} + wc.request('POST', '/data/login', adata, headers) + rsp = wc.getresponse() + if rsp.status == 200: + rspdata = json.loads(rsp.read()) + if rspdata['authResult'] == '0' and rspdata['status'] == 'ok': + return wc + + +def parse_imm_buildinfo(buildinfo): + buildid = buildinfo[:9].rstrip(' \x00') + bdt = ' '.join(buildinfo[9:].replace('\x00', ' ').split()) + bdate = datetime.strptime(bdt, '%Y/%m/%d %H:%M:%S') + return (buildid, bdate) + + +def datefromprop(propstr): + if propstr is None: + return None + return datetime.strptime(propstr, '%Y/%m/%d') + + +def fetch_grouped_properties(ipmicmd, groupinfo): + retdata = {} + for keyval in groupinfo: + retdata[keyval] = get_imm_property(ipmicmd, groupinfo[keyval]) + if keyval == 'date': + retdata[keyval] = datefromprop(retdata[keyval]) + returnit = False + for keyval in list(retdata): + if retdata[keyval] in (None, ''): + del retdata[keyval] + else: + returnit = True + if returnit: + return retdata + + +def fetch_adapter_firmware(wc): + wc.request('GET', '/designs/imm/dataproviders/imm_adapters.php') + rsp = wc.getresponse() + if rsp.status == 200: + adapterdata = json.loads(rsp.read()) + for adata in adapterdata['items']: + aname = adata['adapter.adapterName'] + donenames = set([]) + for fundata in adata['adapter.functions']: + fdata = fundata.get('firmwares', ()) + for firm in fdata: + fname = firm['firmwareName'] + if '.' in fname: + fname = firm['description'] + if fname in donenames: + # ignore redundant entry + continue + donenames.add(fname) + bdata = {} + bdata['version'] = firm['versionStr'] + if 'releaseDate' in firm and firm['releaseDate'] != 'N/A': + bdata['date'] = datetime.strptime(firm['releaseDate'], + '%m/%d/%Y') + yield ('{0} {1}'.format(aname, fname), bdata) + + +def get_firmware_inventory(ipmicmd, bmcver, immbuildinfo, certverify): + # First we fetch the system firmware found in imm properties + # then check for agentless, if agentless, get adapter info using + # https, using the caller TLS verification scheme + immverdata = parse_imm_buildinfo(immbuildinfo) + bdata = {'version': bmcver, 'build': immverdata[0], 'date': immverdata[1]} + yield ('IMM', bdata) + bdata = fetch_grouped_properties(ipmicmd, { + 'build': '/v2/ibmc/dm/fw/imm2/backup_build_id', + 'version': '/v2/ibmc/dm/fw/imm2/backup_build_version', + 'date': '/v2/ibmc/dm/fw/imm2/backup_build_date'}) + if bdata: + yield ('IMM Backup', bdata) + bdata = fetch_grouped_properties(ipmicmd, { + 'build': '/v2/bios/build_id', + 'version': '/v2/bios/build_version', + 'date': '/v2/bios/build_date'}) + if bdata: + yield ('UEFI', bdata) + bdata = fetch_grouped_properties(ipmicmd, { + 'build': '/v2/ibmc/dm/fw/bios/backup_build_id', + 'version': '/v2/ibmc/dm/fw/bios/backup_build_version'}) + if bdata: + yield ('UEFI Backup', bdata) + # Note that the next pending could be pending for either primary + # or backup, so can't promise where it will go + bdata = fetch_grouped_properties(ipmicmd, { + 'build': '/v2/bios/pending_build_id'}) + if bdata: + yield ('UEFI Pending Update', bdata) + wc = get_imm_webclient(ipmicmd.bmc, certverify, + ipmicmd.ipmi_session.userid, + ipmicmd.ipmi_session.password) + if wc: + for firm in fetch_adapter_firmware(wc): + yield firm + wc.request('GET', '/data/logout') diff --git a/pyghmi/util/webclient.py b/pyghmi/util/webclient.py index 3f455147..f1a1725b 100644 --- a/pyghmi/util/webclient.py +++ b/pyghmi/util/webclient.py @@ -17,13 +17,15 @@ # 2.6 as is found in commonly used enterprise linux distributions. __author__ = 'jjohnson2' + +import Cookie import httplib import pyghmi.exceptions as pygexc import socket import ssl -class SecureHTTPConnection(httplib.HTTPConnection): +class SecureHTTPConnection(httplib.HTTPConnection, object): default_port = httplib.HTTPS_PORT def __init__(self, host, port=None, key_file=None, cert_file=None, @@ -31,6 +33,7 @@ class SecureHTTPConnection(httplib.HTTPConnection): httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs) self.cert_reqs = ssl.CERT_NONE # verification will be done ssh style.. self._certverify = verifycallback + self.cookies = {} def connect(self): plainsock = socket.create_connection((self.host, self.port)) @@ -40,3 +43,23 @@ class SecureHTTPConnection(httplib.HTTPConnection): if not self._certverify(bincert): raise pygexc.UnrecognizedCertificate('Unknown certificate', bincert) + + def getresponse(self): + rsp = super(SecureHTTPConnection, self).getresponse() + for hdr in rsp.msg.headers: + if hdr.startswith('Set-Cookie:'): + c = Cookie.BaseCookie(hdr[11:]) + for k in c: + self.cookies[k] = c[k].value + return rsp + + def request(self, method, url, body=None, headers=None): + if headers is None: + headers = {} + if self.cookies: + cookies = [] + for ckey in self.cookies: + cookies.append('{0}={1}'.format(ckey, self.cookies[ckey])) + headers['Cookie'] = '; '.join(cookies) + return super(SecureHTTPConnection, self).request(method, url, body, + headers)