2
0
mirror of https://opendev.org/x/pyghmi synced 2026-04-01 07:43:39 +00:00

Refactor system configuration management to OEM

Permit OEM override of the standard redfish BIOS attribute
mechanism

Change-Id: I26e63d12ed830fb7747fc5786c0c9be955671de0
This commit is contained in:
Jarrod Johnson
2022-10-31 15:33:35 -04:00
parent c78d1883e3
commit 79ac63f409
2 changed files with 253 additions and 243 deletions

View File

@@ -145,80 +145,6 @@ class SensorReading(object):
self.unavailable = unavailable
class AttrDependencyHandler(object):
def __init__(self, dependencies, currsettings, pendingsettings):
self.dependencymap = {}
for dep in dependencies.get('Dependencies', [{}]):
if 'Dependency' not in dep:
continue
if dep['Type'] != 'Map':
continue
if dep['DependencyFor'] in self.dependencymap:
self.dependencymap[
dep['DependencyFor']].append(dep['Dependency'])
else:
self.dependencymap[
dep['DependencyFor']] = [dep['Dependency']]
self.curr = currsettings
self.pend = pendingsettings
self.reg = dependencies['Attributes']
def get_overrides(self, setting):
overrides = {}
blameattrs = []
if setting not in self.dependencymap:
return {}, []
for depinfo in self.dependencymap[setting]:
lastoper = None
lastcond = None
for mapfrom in depinfo.get('MapFrom', []):
if lastcond is not None and not lastoper:
break # MapTerm required to make sense of this, give up
currattr = mapfrom['MapFromAttribute']
blameattrs.append(currattr)
currprop = mapfrom['MapFromProperty']
if currprop == 'CurrentValue':
if currattr in self.pend:
currval = self.pend[currattr]
else:
currval = self.curr[currattr]
else:
currval = self.reg[currattr][currprop]
lastcond = self.process(currval, mapfrom, lastcond, lastoper)
lastoper = mapfrom.get('MapTerms', None)
if lastcond:
if setting not in overrides:
overrides[setting] = {}
if depinfo['MapToAttribute'] not in overrides[setting]:
overrides[depinfo['MapToAttribute']] = {}
overrides[depinfo['MapToAttribute']][
depinfo['MapToProperty']] = depinfo['MapToValue']
return overrides, blameattrs
def process(self, currval, mapfrom, lastcond, lastoper):
newcond = None
mfc = mapfrom['MapFromCondition']
if mfc == 'EQU':
newcond = currval == mapfrom['MapFromValue']
if mfc == 'NEQ':
newcond = currval != mapfrom['MapFromValue']
if mfc == 'GEQ':
newcond = float(currval) >= float(mapfrom['MapFromValue'])
if mfc == 'GTR':
newcond = float(currval) > float(mapfrom['MapFromValue'])
if mfc == 'LEQ':
newcond = float(currval) <= float(mapfrom['MapFromValue'])
if mfc == 'LSS':
newcond = float(currval) < float(mapfrom['MapFromValue'])
if lastcond is not None:
if lastoper == 'AND':
return lastcond and newcond
elif lastoper == 'OR':
return lastcond or newcond
return None
return newcond
class Command(object):
def __init__(self, bmc, userid, password, verifycallback, sysurl=None,
@@ -902,40 +828,6 @@ class Command(object):
summary['badreadings'].append(unkinf)
return summary
def _get_biosreg(self, url):
addon = {}
valtodisplay = {}
displaytoval = {}
reg = self._do_web_request(url)
reg = reg['RegistryEntries']
for attr in reg['Attributes']:
vals = attr.get('Value', [])
if vals:
valtodisplay[attr['AttributeName']] = {}
displaytoval[attr['AttributeName']] = {}
for val in vals:
valtodisplay[
attr['AttributeName']][val['ValueName']] = val[
'ValueDisplayName']
displaytoval[
attr['AttributeName']][val['ValueDisplayName']] = val[
'ValueName']
defaultval = attr.get('DefaultValue', None)
defaultval = valtodisplay.get(attr['AttributeName'], {}).get(
defaultval, defaultval)
if attr['Type'] == 'Integer' and defaultval:
defaultval = int(defaultval)
if attr['Type'] == 'Boolean':
vals = [{'ValueDisplayName': 'True'},
{'ValueDisplayName': 'False'}]
addon[attr['AttributeName']] = {
'default': defaultval,
'help': attr.get('HelpText', None),
'sortid': attr.get('DisplayOrder', None),
'possible': [x['ValueDisplayName'] for x in vals],
}
return addon, valtodisplay, displaytoval, reg
def get_bmc_configuration(self):
"""Get miscellaneous BMC configuration
@@ -960,6 +852,9 @@ class Command(object):
# For now, this is a stub, no implementation for redfish currently
return self.oem.set_bmc_configuration(changeset)
def set_system_configuration(self, changeset):
return self.oem.set_system_configuration(changeset, self)
def clear_bmc_configuration(self):
"""Reset BMC to factory default
@@ -971,64 +866,7 @@ class Command(object):
'Clear BMC configuration not supported in redfish yet')
def get_system_configuration(self, hideadvanced=True):
return self._getsyscfg()[0]
def _getsyscfg(self):
biosinfo = self._do_web_request(self._biosurl, cache=False)
reginfo = ({}, {}, {}, {})
extrainfo = {}
valtodisplay = {}
self.attrdeps = {'Dependencies': [], 'Attributes': []}
if 'AttributeRegistry' in biosinfo:
overview = self._do_web_request('/redfish/v1/')
reglist = overview['Registries']['@odata.id']
reglist = self._do_web_request(reglist)
regurl = None
for cand in reglist.get('Members', []):
cand = cand.get('@odata.id', '')
candname = cand.split('/')[-1]
if candname == '': # implementation uses trailing slash
candname = cand.split('/')[-2]
if candname == biosinfo['AttributeRegistry']:
regurl = cand
break
if not regurl:
# Workaround a vendor bug where they link to a
# non-existant name
for cand in reglist.get('Members', []):
cand = cand.get('@odata.id', '')
candname = cand.split('/')[-1]
candname = candname.split('.')[0]
if candname == biosinfo[
'AttributeRegistry'].split('.')[0]:
regurl = cand
break
if regurl:
reginfo = self._do_web_request(regurl)
for reg in reginfo.get('Location', []):
if reg.get('Language', 'en').startswith('en'):
reguri = reg['Uri']
reginfo = self._get_biosreg(reguri)
extrainfo, valtodisplay, _, self.attrdeps = reginfo
currsettings = {}
try:
pendingsettings = self._do_web_request(self._setbiosurl)
except exc.UnsupportedFunctionality:
pendingsettings = {}
pendingsettings = pendingsettings.get('Attributes', {})
for setting in biosinfo.get('Attributes', {}):
val = biosinfo['Attributes'][setting]
currval = val
if setting in pendingsettings:
val = pendingsettings[setting]
val = valtodisplay.get(setting, {}).get(val, val)
currval = valtodisplay.get(setting, {}).get(currval, currval)
val = {'value': val}
if currval != val['value']:
val['active'] = currval
val.update(**extrainfo.get(setting, {}))
currsettings[setting] = val
return currsettings, reginfo
return self.oem.get_system_configuration(hideadvanced, self)
def clear_system_configuration(self):
"""Clear the BIOS/UEFI configuration
@@ -1057,83 +895,6 @@ class Command(object):
parms = {'Action': 'Bios.ResetBios'}
self._do_web_request(rb, parms)
def set_system_configuration(self, changeset):
while True:
try:
self._set_system_configuration(changeset)
return
except exc.RedfishError as re:
if ('etag' not in re.msgid.lower()
and 'PreconditionFailed' not in re.msgid):
raise
def _set_system_configuration(self, changeset):
currsettings, reginfo = self._getsyscfg()
rawsettings = self._do_web_request(self._biosurl, cache=False)
rawsettings = rawsettings.get('Attributes', {})
pendingsettings = self._do_web_request(self._setbiosurl)
etag = pendingsettings.get('@odata.etag', None)
pendingsettings = pendingsettings.get('Attributes', {})
dephandler = AttrDependencyHandler(self.attrdeps, rawsettings,
pendingsettings)
for change in list(changeset):
if change not in currsettings:
found = False
for attr in currsettings:
if fnmatch(attr.lower(), change.lower()):
found = True
changeset[attr] = changeset[change]
if fnmatch(attr.lower(),
change.replace('.', '_').lower()):
found = True
changeset[attr] = changeset[change]
if found:
del changeset[change]
for change in changeset:
changeval = changeset[change]
overrides, blameattrs = dephandler.get_overrides(change)
meta = {}
for attr in self.attrdeps['Attributes']:
if attr['AttributeName'] == change:
meta = dict(attr)
break
meta.update(**overrides.get(change, {}))
if meta.get('ReadOnly', False) or meta.get('GrayOut', False):
errstr = '{0} is read only'.format(change)
if blameattrs:
errstr += (' due to one of the following settings: '
'{0}'.format(','.join(sorted(blameattrs)))
)
raise exc.InvalidParameterValue(errstr)
if (currsettings.get(change, {}).get('possible', [])
and changeval not in currsettings[change]['possible']):
normval = changeval.lower()
normval = re.sub(r'\s+', ' ', normval)
if not normval.endswith('*'):
normval += '*'
for cand in currsettings[change]['possible']:
if fnmatch(cand.lower().replace(' ', ''),
normval.replace(' ', '')):
changeset[change] = cand
break
else:
raise exc.InvalidParameterValue(
'{0} is not a valid value for {1} ({2})'.format(
changeval, change, ','.join(
currsettings[change]['possible'])))
if changeset[change] in reginfo[2].get(change, {}):
changeset[change] = reginfo[2][change][changeset[change]]
for regentry in reginfo[3].get('Attributes', []):
if change in (regentry.get('AttributeName', ''),
regentry.get('DisplayName', '')):
if regentry.get('Type', None) == 'Integer':
changeset[change] = int(changeset[change])
if regentry.get('Type', None) == 'Boolean':
changeset[change] = _to_boolean(changeset[change])
redfishsettings = {'Attributes': changeset}
self._do_web_request(self._setbiosurl, redfishsettings, 'PATCH',
etag=etag)
def set_net_configuration(self, ipv4_address=None, ipv4_configuration=None,
ipv4_gateway=None, name=None):
patch = {}

View File

@@ -12,8 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from fnmatch import fnmatch
import json
import os
import re
import pyghmi.exceptions as exc
import pyghmi.media as media
@@ -47,6 +49,80 @@ boot_devices_read = {
}
class AttrDependencyHandler(object):
def __init__(self, dependencies, currsettings, pendingsettings):
self.dependencymap = {}
for dep in dependencies.get('Dependencies', [{}]):
if 'Dependency' not in dep:
continue
if dep['Type'] != 'Map':
continue
if dep['DependencyFor'] in self.dependencymap:
self.dependencymap[
dep['DependencyFor']].append(dep['Dependency'])
else:
self.dependencymap[
dep['DependencyFor']] = [dep['Dependency']]
self.curr = currsettings
self.pend = pendingsettings
self.reg = dependencies['Attributes']
def get_overrides(self, setting):
overrides = {}
blameattrs = []
if setting not in self.dependencymap:
return {}, []
for depinfo in self.dependencymap[setting]:
lastoper = None
lastcond = None
for mapfrom in depinfo.get('MapFrom', []):
if lastcond is not None and not lastoper:
break # MapTerm required to make sense of this, give up
currattr = mapfrom['MapFromAttribute']
blameattrs.append(currattr)
currprop = mapfrom['MapFromProperty']
if currprop == 'CurrentValue':
if currattr in self.pend:
currval = self.pend[currattr]
else:
currval = self.curr[currattr]
else:
currval = self.reg[currattr][currprop]
lastcond = self.process(currval, mapfrom, lastcond, lastoper)
lastoper = mapfrom.get('MapTerms', None)
if lastcond:
if setting not in overrides:
overrides[setting] = {}
if depinfo['MapToAttribute'] not in overrides[setting]:
overrides[depinfo['MapToAttribute']] = {}
overrides[depinfo['MapToAttribute']][
depinfo['MapToProperty']] = depinfo['MapToValue']
return overrides, blameattrs
def process(self, currval, mapfrom, lastcond, lastoper):
newcond = None
mfc = mapfrom['MapFromCondition']
if mfc == 'EQU':
newcond = currval == mapfrom['MapFromValue']
if mfc == 'NEQ':
newcond = currval != mapfrom['MapFromValue']
if mfc == 'GEQ':
newcond = float(currval) >= float(mapfrom['MapFromValue'])
if mfc == 'GTR':
newcond = float(currval) > float(mapfrom['MapFromValue'])
if mfc == 'LEQ':
newcond = float(currval) <= float(mapfrom['MapFromValue'])
if mfc == 'LSS':
newcond = float(currval) < float(mapfrom['MapFromValue'])
if lastcond is not None:
if lastoper == 'AND':
return lastcond and newcond
elif lastoper == 'OR':
return lastcond or newcond
return None
return newcond
class OEMHandler(object):
hostnic = None
@@ -128,6 +204,179 @@ class OEMHandler(object):
raise exc.UnsupportedFunctionality(
'Platform does not support setting bmc attributes')
def _get_biosreg(self, url, fishclient):
addon = {}
valtodisplay = {}
displaytoval = {}
reg = fishclient._do_web_request(url)
reg = reg['RegistryEntries']
for attr in reg['Attributes']:
vals = attr.get('Value', [])
if vals:
valtodisplay[attr['AttributeName']] = {}
displaytoval[attr['AttributeName']] = {}
for val in vals:
valtodisplay[
attr['AttributeName']][val['ValueName']] = val[
'ValueDisplayName']
displaytoval[
attr['AttributeName']][val['ValueDisplayName']] = val[
'ValueName']
defaultval = attr.get('DefaultValue', None)
defaultval = valtodisplay.get(attr['AttributeName'], {}).get(
defaultval, defaultval)
if attr['Type'] == 'Integer' and defaultval:
defaultval = int(defaultval)
if attr['Type'] == 'Boolean':
vals = [{'ValueDisplayName': 'True'},
{'ValueDisplayName': 'False'}]
addon[attr['AttributeName']] = {
'default': defaultval,
'help': attr.get('HelpText', None),
'sortid': attr.get('DisplayOrder', None),
'possible': [x['ValueDisplayName'] for x in vals],
}
return addon, valtodisplay, displaytoval, reg
def get_system_configuration(self, hideadvanced=True, fishclient=None):
return self._getsyscfg(fishclient)[0]
def _getsyscfg(self, fishclient):
biosinfo = self._do_web_request(fishclient._biosurl, cache=False)
reginfo = ({}, {}, {}, {})
extrainfo = {}
valtodisplay = {}
self.attrdeps = {'Dependencies': [], 'Attributes': []}
if 'AttributeRegistry' in biosinfo:
overview = fishclient._do_web_request('/redfish/v1/')
reglist = overview['Registries']['@odata.id']
reglist = fishclient._do_web_request(reglist)
regurl = None
for cand in reglist.get('Members', []):
cand = cand.get('@odata.id', '')
candname = cand.split('/')[-1]
if candname == '': # implementation uses trailing slash
candname = cand.split('/')[-2]
if candname == biosinfo['AttributeRegistry']:
regurl = cand
break
if not regurl:
# Workaround a vendor bug where they link to a
# non-existant name
for cand in reglist.get('Members', []):
cand = cand.get('@odata.id', '')
candname = cand.split('/')[-1]
candname = candname.split('.')[0]
if candname == biosinfo[
'AttributeRegistry'].split('.')[0]:
regurl = cand
break
if regurl:
reginfo = fishclient._do_web_request(regurl)
for reg in reginfo.get('Location', []):
if reg.get('Language', 'en').startswith('en'):
reguri = reg['Uri']
reginfo = self._get_biosreg(reguri, fishclient)
extrainfo, valtodisplay, _, self.attrdeps = reginfo
currsettings = {}
try:
pendingsettings = fishclient._do_web_request(
fishclient._setbiosurl)
except exc.UnsupportedFunctionality:
pendingsettings = {}
pendingsettings = pendingsettings.get('Attributes', {})
for setting in biosinfo.get('Attributes', {}):
val = biosinfo['Attributes'][setting]
currval = val
if setting in pendingsettings:
val = pendingsettings[setting]
val = valtodisplay.get(setting, {}).get(val, val)
currval = valtodisplay.get(setting, {}).get(currval, currval)
val = {'value': val}
if currval != val['value']:
val['active'] = currval
val.update(**extrainfo.get(setting, {}))
currsettings[setting] = val
return currsettings, reginfo
def set_system_configuration(self, changeset, fishclient):
while True:
try:
self._set_system_configuration(changeset, fishclient)
return
except exc.RedfishError as re:
if ('etag' not in re.msgid.lower()
and 'PreconditionFailed' not in re.msgid):
raise
def _set_system_configuration(self, changeset, fishclient):
currsettings, reginfo = self._getsyscfg(fishclient)
rawsettings = fishclient._do_web_request(fishclient._biosurl,
cache=False)
rawsettings = rawsettings.get('Attributes', {})
pendingsettings = fishclient._do_web_request(fishclient._setbiosurl)
etag = pendingsettings.get('@odata.etag', None)
pendingsettings = pendingsettings.get('Attributes', {})
dephandler = AttrDependencyHandler(self.attrdeps, rawsettings,
pendingsettings)
for change in list(changeset):
if change not in currsettings:
found = False
for attr in currsettings:
if fnmatch(attr.lower(), change.lower()):
found = True
changeset[attr] = changeset[change]
if fnmatch(attr.lower(),
change.replace('.', '_').lower()):
found = True
changeset[attr] = changeset[change]
if found:
del changeset[change]
for change in changeset:
changeval = changeset[change]
overrides, blameattrs = dephandler.get_overrides(change)
meta = {}
for attr in self.attrdeps['Attributes']:
if attr['AttributeName'] == change:
meta = dict(attr)
break
meta.update(**overrides.get(change, {}))
if meta.get('ReadOnly', False) or meta.get('GrayOut', False):
errstr = '{0} is read only'.format(change)
if blameattrs:
errstr += (' due to one of the following settings: '
'{0}'.format(','.join(sorted(blameattrs)))
)
raise exc.InvalidParameterValue(errstr)
if (currsettings.get(change, {}).get('possible', [])
and changeval not in currsettings[change]['possible']):
normval = changeval.lower()
normval = re.sub(r'\s+', ' ', normval)
if not normval.endswith('*'):
normval += '*'
for cand in currsettings[change]['possible']:
if fnmatch(cand.lower().replace(' ', ''),
normval.replace(' ', '')):
changeset[change] = cand
break
else:
raise exc.InvalidParameterValue(
'{0} is not a valid value for {1} ({2})'.format(
changeval, change, ','.join(
currsettings[change]['possible'])))
if changeset[change] in reginfo[2].get(change, {}):
changeset[change] = reginfo[2][change][changeset[change]]
for regentry in reginfo[3].get('Attributes', []):
if change in (regentry.get('AttributeName', ''),
regentry.get('DisplayName', '')):
if regentry.get('Type', None) == 'Integer':
changeset[change] = int(changeset[change])
if regentry.get('Type', None) == 'Boolean':
changeset[change] = _to_boolean(changeset[change])
redfishsettings = {'Attributes': changeset}
fishclient._do_web_request(
fishclient._setbiosurl, redfishsettings, 'PATCH', etag=etag)
def attach_remote_media(self, url, username, password, vmurls):
return None