Add OEM extensibility framework to pyghmi redfish

This enables OEM specific override for enhancements beyond the specification
when avaible.

Change-Id: I6a10f1fa58a425b2abfbeb9c85dd72c3b7d4cbba
This commit is contained in:
Jarrod Johnson 2019-04-16 15:27:14 -04:00
parent a05c044874
commit f710b1d30a
8 changed files with 182 additions and 36 deletions

View File

@ -18,15 +18,16 @@
# for redfish compliant endpoints
from datetime import datetime
from dateutil import tz
import json
import os
import pyghmi.exceptions as exc
import pyghmi.constants as const
import pyghmi.util.webclient as webclient
import socket
import struct
import time
import pyghmi.exceptions as exc
import pyghmi.constants as const
import pyghmi.util.webclient as webclient
import pyghmi.redfish.oem.lookup as oem
from dateutil import tz
powerstates = {
'on': 'On',
@ -71,6 +72,7 @@ _healthmap = {
'OK': const.Health.Ok,
}
def _parse_time(timeval):
if timeval is None:
return None
@ -116,10 +118,11 @@ def _mask_to_cidr(mask):
maskn >>= 1
return cidr
def _cidr_to_mask(cidr):
return socket.inet_ntop(
socket.AF_INET, struct.pack(
'!I', (2**32-1) ^ (2**(32-cidr)-1)))
'!I', (2**32 - 1) ^ (2**(32 - cidr) - 1)))
class SensorReading(object):
@ -132,6 +135,7 @@ class SensorReading(object):
self.imprecision = None
self.units = None
class Command(object):
def __init__(self, bmc, userid, password, verifycallback, sysurl=None,
@ -149,6 +153,7 @@ class Command(object):
self._varresetbmcurl = None
self._varupdateservice = None
self._varfwinventory = None
self._oem = None
self._gpool = pool
self.wc.set_header('Accept', 'application/json')
self.wc.set_header('User-Agent', 'pyghmi')
@ -165,10 +170,10 @@ class Command(object):
break
else:
raise exc.PyghmiException(
'Specified sysurl not found: '.format(sysurl))
'Specified sysurl not found: {0}'.format(sysurl))
else:
if len(systems) != 1:
raise pygexc.PyghmiException(
raise exc.PyghmiException(
'Multi system manager, sysurl is required parameter')
self.sysurl = systems[0]['@odata.id']
self.powerurl = self.sysinfo.get('Actions', {}).get(
@ -196,15 +201,10 @@ class Command(object):
'BMC does not implement extended firmware information')
return self._varfwinventory
@property
def sysinfo(self):
return self._do_web_request(self.sysurl)
def get_power(self):
currinfo = self._do_web_request(self.sysurl, cache=False)
return {'powerstate': str(currinfo['PowerState'].lower())}
@ -226,8 +226,8 @@ class Command(object):
if reqpowerstate in ('softoff', 'shutdown'):
reqpowerstate = 'off'
timeout = os.times()[4] + 300
while (self.get_power()['powerstate'] != reqpowerstate and
os.times()[4] < timeout):
while (self.get_power()['powerstate'] != reqpowerstate
and os.times()[4] < timeout):
time.sleep(1)
if self.get_power()['powerstate'] != reqpowerstate:
raise exc.PyghmiException(
@ -264,7 +264,7 @@ class Command(object):
wc = self.wc.dupe()
res = wc.grab_json_response_with_status(url, payload,
method=method)
if res[1] < 200 or res[1] >=300:
if res[1] < 200 or res[1] >= 300:
raise exc.PyghmiException(res[0])
if payload is None and method is None:
self._urlcache[url] = {'contents': res[0],
@ -288,19 +288,19 @@ class Command(object):
elif overridestate == 'Continuous':
persistent = True
else:
raise exc.PyghmiException('Unrecognized Boot state: ' +
repr(overridestate))
raise exc.PyghmiException('Unrecognized Boot state: '
+ repr(overridestate))
uefimode = result.get('Boot', {}).get('BootSourceOverrideMode', None)
if uefimode == 'UEFI':
uefimode = True
elif uefimode == 'Legacy':
elif uefimode == 'Legacy':
uefimode = False
else:
raise exc.PyghmiException('Unrecognized mode: ' + uefimode)
bootdev = result.get('Boot', {}).get('BootSourceOverrideTarget', None)
if bootdev not in boot_devices_read:
raise exc.PyghmiException('Unrecognized boot target: ' +
repr(bootdev))
raise exc.PyghmiException('Unrecognized boot target: '
+ repr(bootdev))
bootdev = boot_devices_read[bootdev]
return {'bootdev': bootdev, 'persistent': persistent,
'uefimode': uefimode}
@ -325,16 +325,17 @@ class Command(object):
:returns: dict or True -- If callback is not provided, the response
"""
reqbootdev = bootdev
if (bootdev not in boot_devices_write and
bootdev not in boot_devices_read):
raise exc.InvalidParameterValue('Unsupported device ' +
repr(bootdev))
if (bootdev not in boot_devices_write
and bootdev not in boot_devices_read):
raise exc.InvalidParameterValue('Unsupported device '
+ repr(bootdev))
bootdev = boot_devices_write.get(bootdev, bootdev)
if bootdev == 'None':
payload = {'Boot': {'BootSourceOverrideEnabled': 'Disabled'}}
else:
payload = {'Boot': {
'BootSourceOverrideEnabled': 'Continuous' if persist else 'Once',
'BootSourceOverrideEnabled': 'Continuous' if persist
else 'Once',
'BootSourceOverrideTarget': bootdev,
}}
if uefiboot is not None:
@ -347,7 +348,7 @@ class Command(object):
def _biosurl(self):
if not self._varbiosurl:
self._varbiosurl = self.sysinfo.get('Bios', {}).get('@odata.id',
None)
None)
if self._varbiosurl is None:
raise exc.UnsupportedFunctionality(
'Bios management not detected on this platform')
@ -359,7 +360,7 @@ class Command(object):
biosinfo = self._do_web_request(self._biosurl)
self._varsetbiosurl = biosinfo.get(
'@Redfish.Settings', {}).get('SettingsObject', {}).get(
'@odata.id', None)
'@odata.id', None)
if self._varsetbiosurl is None:
raise exc.UnsupportedFunctionality('Ability to set BIOS settings '
'not detected on this platform')
@ -397,7 +398,6 @@ class Command(object):
self._varbmcnicurl = lastnicurl
return self._varbmcnicurl
@property
def _bmcreseturl(self):
if not self._varresetbmcurl:
@ -502,8 +502,8 @@ class Command(object):
ipinfo['Gateway'] = ipv4_gateway
if ipv4_configuration.lower() == 'dhcp':
patch['DHCPv4'] = {'DHCPEnabled': True}
elif (ipv4_configuration == 'static' or
'IPv4StaticAddresses' in patch):
elif (ipv4_configuration == 'static'
or 'IPv4StaticAddresses' in patch):
patch['DHCPv4'] = {'DHCPEnabled': False}
if patch:
self._do_web_request(self._bmcnicurl, patch, 'PATCH')
@ -515,8 +515,9 @@ class Command(object):
raise exc.PyghmiException('Unable to locate network information')
retval = {}
if len(netcfg['IPv4Addresses']) != 1:
netcfg['IPv4Addresses'] = [x for x in
netcfg['IPv4Addresses'] if x['Address'] != '0.0.0.0']
netcfg['IPv4Addresses'] = [
x for x in netcfg['IPv4Addresses']
if x['Address'] != '0.0.0.0']
if len(netcfg['IPv4Addresses']) != 1:
raise exc.PyghmiException('Multiple IP addresses not supported')
currip = netcfg['IPv4Addresses'][0]
@ -608,8 +609,8 @@ class Command(object):
def _get_adp_inventory(self, onlyname=False, withids=False, urls=None):
if not urls:
urls = self._get_adp_urls()
if not urls:
return
if not urls:
return
for inf in self._do_bulk_requests(urls):
adpinfo, url = inf
aname = adpinfo.get('Name', 'Unknown')
@ -622,7 +623,8 @@ class Command(object):
if onlyname:
if withids:
yield aname, adpinfo.get('Id', aname)
yield aname
else:
yield aname
continue
functions = adpinfo.get('Links', {}).get('PCIeFunctions', [])
nicidx = 1
@ -659,6 +661,16 @@ class Command(object):
urls = []
return urls
@property
def oem(self):
if not self._oem:
self._oem = oem.get_oem_handler(
self.sysinfo, self.sysurl, self.wc, self._urlcache)
return self._oem
def get_description(self):
return self.oem.get_description()
def _get_cpu_inventory(self, onlynames=False, withids=False, urls=None):
if not urls:
urls = self._get_cpu_urls()

View File

View File

@ -0,0 +1,51 @@
# 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.
import os
import pyghmi.exceptions as exc
class OEMHandler(object):
def __init__(self, sysinfo, sysurl, webclient, cache):
self._varsysinfo = sysinfo
self._varsysurl = sysurl
self._urlcache = cache
self.webclient = webclient
def _get_cache(self, url):
now = os.times()[4]
cachent = self._urlcache.get(url, None)
if cachent and cachent['vintage'] > now - 30:
return cachent['contents']
return None
def get_description(self):
return {}
def _do_web_request(self, url, payload=None, method=None, cache=True):
res = None
if cache and payload is None and method is None:
res = self._get_cache(url)
if res:
return res
wc = self.webclient.dupe()
res = wc.grab_json_response_with_status(url, payload, method=method)
if res[1] < 200 or res[1] >= 300:
raise exc.PyghmiException(res[0])
if payload is None and method is None:
self._urlcache[url] = {
'contents': res[0],
'vintage': os.times()[4]
}
return res[0]

View File

View File

@ -0,0 +1,24 @@
# 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.
import pyghmi.redfish.oem.generic as generic
from pyghmi.redfish.oem.lenovo import xcc
def get_handler(sysinfo, sysurl, webclient, cache):
leninf = sysinfo.get('Oem', {}).get('Lenovo', {})
if 'FrontPanelUSB' in leninf:
return xcc.OEMHandler(sysinfo, sysurl, webclient, cache)
else:
return generic.OEMHandler(sysinfo, sysurl, webclient, cache)

View File

@ -0,0 +1,32 @@
# 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.
import pyghmi.redfish.oem.generic as generic
class OEMHandler(generic.OEMHandler):
def get_description(self):
description = self._do_web_request('/DeviceDescription.json')
if description:
description = description[0]
u_height = description.get('u-height', '')
if not u_height and description.get(
'enclosure-machinetype-model', '').startswith('7Y36'):
u_height = '2'
if not u_height:
return {}
u_height = int(u_height)
slot = description.get('slot', '0')
slot = int(slot)
return {'height': u_height, 'slot': slot}

View File

@ -0,0 +1,26 @@
# 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.
import pyghmi.redfish.oem.generic as generic
import pyghmi.redfish.oem.lenovo.main as lenovo
OEMMAP = {
'Lenovo': lenovo,
}
def get_oem_handler(sysinfo, sysurl, webclient, cache):
for oem in sysinfo.get('Oem', {}):
if oem in OEMMAP:
return OEMMAP[oem].get_handler(sysinfo, sysurl, webclient, cache)
return generic.OEMHandler(sysinfo, sysurl, webclient, cache)

View File

@ -25,6 +25,7 @@ setuptools.setup(
author_email='jjohnson2@lenovo.com',
packages=['pyghmi', 'pyghmi.util', 'pyghmi.ipmi', 'pyghmi.cmd',
'pyghmi.redfish', 'pyghmi.ipmi.private', 'pyghmi.ipmi.oem',
'pyghmi.ipmi.oem.lenovo'],
'pyghmi.ipmi.oem.lenovo', 'pyghmi.redfish.oem',
'pyghmi.redfish.oem.lenovo'],
license='Apache License, Version 2.0')