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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
59
pyghmi/util/parse.py
Normal 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
|
||||
Reference in New Issue
Block a user