2
0
mirror of https://opendev.org/x/pyghmi synced 2025-10-26 08:55:20 +00:00

Add OEM Lenovo Firmware

For Lenovo equipment, improve compatibility
with the ipmi interface and provide
more info than redfish provides.

Change-Id: I1b5ad99c89d8dec0abd18b1f794dabdb0aed13c3
This commit is contained in:
Jarrod Johnson
2019-05-24 13:25:01 -04:00
parent 2879867002
commit 710b12658c
4 changed files with 202 additions and 46 deletions

View File

@@ -28,6 +28,7 @@ import time
import pyghmi.exceptions as exc
import pyghmi.constants as const
import pyghmi.util.webclient as webclient
from pyghmi.util.parse import parse_time
import pyghmi.redfish.oem.lookup as oem
import re
from dateutil import tz
@@ -80,49 +81,6 @@ _healthmap = {
}
def _parse_time(timeval):
if timeval is None:
return None
try:
retval = datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%SZ')
return retval.replace(tzinfo=tz.tzutc())
except ValueError:
pass
try:
positive = None
offset = None
if '+' in timeval:
timeval, offset = timeval.split('+', 1)
positive = 1
elif len(timeval.split('-')) > 3:
timeval, offset = timeval.rsplit('-', 1)
positive = -1
if positive:
hrs, mins = offset.split(':', 1)
secs = int(hrs) * 60 + int(mins)
secs = secs * 60 * positive
ms = None
if '.' in timeval:
timeval, ms = timeval.split('.', 1)
ms = int(ms)
ms = timedelta(0, 0, 0, ms)
retval = datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%S')
if ms:
retval += ms
return retval.replace(tzinfo=tz.tzoffset('', secs))
except ValueError:
pass
try:
return datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%S')
except ValueError:
pass
try:
return datetime.strptime(timeval, '%Y-%m-%d')
except ValueError:
pass
return None
def _mask_to_cidr(mask):
maskn = socket.inet_pton(socket.AF_INET, mask)
maskn = struct.unpack('!I', maskn)[0]
@@ -275,6 +233,8 @@ class Command(object):
self.wc.set_header('OData-Version', '4.0')
overview = self.wc.grab_json_response('/redfish/v1/')
self.wc.set_basic_credentials(userid, password)
self.username = userid
self.password = password
self.wc.set_header('Content-Type', 'application/json')
systems = overview['Systems']['@odata.id']
res = self.wc.grab_json_response_with_status(systems)
@@ -401,7 +361,7 @@ class Command(object):
errmsg = ','.join(errmsg)
raise exc.RedfishError(errmsg)
except (ValueError, KeyError):
raise exc.PyghmiException(res[0])
raise exc.PyghmiException(str(url) + ":" + res[0])
if payload is None and method is None:
self._urlcache[url] = {'contents': res[0],
'vintage': os.times()[4]}
@@ -827,6 +787,11 @@ class Command(object):
return netcfg['HostName']
def get_firmware(self, components=()):
try:
for firminfo in self.oem.get_firmware_inventory(components):
yield firminfo
except exc.BypassGenericBehavior:
return
fwlist = self._do_web_request(self._fwinventory)
fwurls = [x['@odata.id'] for x in fwlist.get('Members', [])]
self._fwnamemap = {}
@@ -850,7 +815,7 @@ class Command(object):
currinf['name'] = fwname
currinf['id'] = fwi.get('Id', None)
currinf['version'] = fwi.get('Version', 'Unknown')
currinf['date'] = _parse_time(fwi.get('ReleaseDate', ''))
currinf['date'] = parse_time(fwi.get('ReleaseDate', ''))
if not (currinf['version'] or currinf['date']):
return None, None
# TODO: OEM extended data with buildid
@@ -1017,6 +982,7 @@ class Command(object):
if not self._oem:
self._oem = oem.get_oem_handler(
self.sysinfo, self.sysurl, self.wc, self._urlcache)
self._oem.set_credentials(self.username, self.password)
return self._oem
def get_description(self):
@@ -1150,7 +1116,7 @@ class Command(object):
newloginfo = self._do_web_request(lurl, cache=False)
for log in entries.get('Members', []):
record = {}
entime = _parse_time(log.get('Created', '')) + correction
entime = parse_time(log.get('Created', '')) + correction
entime = entime.astimezone(tz.gettz())
record['timestamp'] = entime.strftime('%Y-%m-%dT%H:%M:%S')
record['message'] = log.get('Message', None)

View File

@@ -32,6 +32,13 @@ class OEMHandler(object):
def get_description(self):
return {}
def get_firmware_inventory(self, components):
return []
def set_credentials(self, username, password):
self.username = username
self.password = password
def _do_web_request(self, url, payload=None, method=None, cache=True):
res = None

View File

@@ -13,9 +13,20 @@
# limitations under the License.
import pyghmi.redfish.oem.generic as generic
from pyghmi.util.parse import parse_time
import errno
import json
import socket
import pyghmi.ipmi.private.util as util
import pyghmi.exceptions as pygexc
class OEMHandler(generic.OEMHandler):
def __init__(self, sysinfo, sysurl, webclient, cache):
super(OEMHandler, self).__init__(sysinfo, sysurl, webclient, cache)
self._wc = None
def get_description(self):
description = self._do_web_request('/DeviceDescription.json')
if description:
@@ -30,3 +41,116 @@ class OEMHandler(generic.OEMHandler):
slot = description.get('slot', '0')
slot = int(slot)
return {'height': u_height, 'slot': slot}
def _get_agentless_firmware(self, components):
adata = self.wc.grab_json_response('/api/dataset/imm_adapters?params=pci_GetAdapters')
anames = set()
for adata in adata.get('items', []):
baseaname = adata['adapterName']
aname = baseaname
idx = 1
while aname in anames:
aname = '{0} {1}'.format(baseaname, idx)
idx += 1
anames.add(aname)
donenames = set()
for fundata in adata['functions']:
for firm in fundata.get('firmwares', []):
fname = firm['firmwareName'].rstrip()
if '.' in fname:
fname = firm['description'].rstrip()
if fname in donenames:
# ignore redundant entry
continue
if not fname:
continue
donenames.add(fname)
bdata = {}
if 'versionStr' in firm and firm['versionStr']:
bdata['version'] = firm['versionStr']
if ('releaseDate' in firm and
firm['releaseDate'] and
firm['releaseDate'] != 'N/A'):
try:
bdata['date'] = parse_time(firm['releaseDate'])
except ValueError:
pass
yield ('{0} {1}'.format(aname, fname), bdata)
def _get_disk_firmware_single(self, diskent, prefix=''):
bdata = {}
if not prefix and diskent.get('location', '').startswith('M.2'):
prefix = 'M.2-'
diskname = 'Disk {1}{0}'.format(diskent['slotNo'], prefix)
bdata['model'] = diskent[
'productName'].rstrip()
bdata['version'] = diskent['fwVersion']
return (diskname, bdata)
def _get_disk_firmware(self, coponents):
storagedata = storagedata = self.wc.grab_json_response(
'/api/function/raid_alldevices?params=storage_GetAllDisks')
for adp in storagedata.get('items', []):
for diskent in adp.get('disks', ()):
yield self._get_disk_firmware_single(diskent)
for diskent in adp.get('aimDisks', ()):
yield self._get_disk_firmware_single(diskent)
def get_firmware_inventory(self, components):
sysinf = self.wc.grab_json_response('/api/dataset/sys_info')
for item in sysinf.get('items', {}):
for firm in item.get('firmware', []):
firminfo = {
'version': firm['version'],
'build': firm['build'],
'date': parse_time(firm['release_date']),
}
if firm['type'] == 5:
yield ('XCC', firminfo)
elif firm['type'] == 6:
yield ('XCC Backup', firminfo)
elif firm['type'] == 0:
yield ('UEFI', firminfo)
elif firm['type'] == 7:
yield ('LXPM', firminfo)
elif firm['type'] == 8:
yield ('LXPM Windows Driver Bundle', firminfo)
elif firm['type'] == 9:
yield ('LXPM Linux Driver Bundle', firminfo)
for adpinfo in self._get_agentless_firmware(components):
yield adpinfo
for adpinfo in self._get_disk_firmware(components):
yield adpinfo
raise pygexc.BypassGenericBehavior()
@property
def wc(self):
if (not self._wc or (self._wc.vintage and
self._wc.vintage < util._monotonic_time() - 30)):
self._wc = self.get_webclient()
return self._wc
def get_webclient(self, login=True):
wc = self.webclient.dupe()
wc.vintage = util._monotonic_time()
try:
wc.connect()
except socket.error as se:
if se.errno != errno.ECONNREFUSED:
raise
return None
if not login:
return wc
adata = json.dumps({'username': self.username,
'password': self.password
})
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
if rsp.status == 200:
rspdata = json.loads(rsp.read())
wc.set_header('Content-Type', 'application/json')
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
if '_csrf_token' in wc.cookies:
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
return wc

59
pyghmi/util/parse.py Normal file
View File

@@ -0,0 +1,59 @@
# Copyright 2019 Lenovo Corporation
#
# 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 dateutil import tz
from datetime import datetime, timedelta
def parse_time(timeval):
if timeval is None:
return None
try:
retval = datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%SZ')
return retval.replace(tzinfo=tz.tzutc())
except ValueError:
pass
try:
positive = None
offset = None
if '+' in timeval:
timeval, offset = timeval.split('+', 1)
positive = 1
elif len(timeval.split('-')) > 3:
timeval, offset = timeval.rsplit('-', 1)
positive = -1
if positive:
hrs, mins = offset.split(':', 1)
secs = int(hrs) * 60 + int(mins)
secs = secs * 60 * positive
ms = None
if '.' in timeval:
timeval, ms = timeval.split('.', 1)
ms = int(ms)
ms = timedelta(0, 0, 0, ms)
retval = datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%S')
if ms:
retval += ms
return retval.replace(tzinfo=tz.tzoffset('', secs))
except ValueError:
pass
try:
return datetime.strptime(timeval, '%Y-%m-%dT%H:%M:%S')
except ValueError:
pass
try:
return datetime.strptime(timeval, '%Y-%m-%d')
except ValueError:
pass
return None