431 lines
17 KiB
Python
431 lines
17 KiB
Python
#
|
|
# 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.
|
|
|
|
"""
|
|
DRAC Bios specific methods
|
|
"""
|
|
|
|
import re
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
|
|
from ironic.common import exception
|
|
from ironic.common.i18n import _, _LE, _LW
|
|
from ironic.conductor import task_manager
|
|
from ironic.drivers.modules.drac import client as wsman_client
|
|
from ironic.drivers.modules.drac import management
|
|
from ironic.drivers.modules.drac import resource_uris
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def _val_or_none(item):
|
|
"""Test to see if an XML element should be treated as None.
|
|
|
|
If the element contains an XML Schema namespaced nil attribute that
|
|
has a value of True, return None. Otherwise, return whatever the
|
|
text of the element is.
|
|
|
|
:param item: an XML element.
|
|
:returns: None or the test of the XML element.
|
|
"""
|
|
|
|
if item is None:
|
|
return
|
|
itemnil = item.attrib.get('{%s}nil' % resource_uris.CIM_XmlSchema)
|
|
if itemnil == "true":
|
|
return
|
|
else:
|
|
return item.text
|
|
|
|
|
|
def _parse_common(item, ns):
|
|
"""Parse common values that all attributes must have.
|
|
|
|
:param item: an XML element.
|
|
:param ns: the namespace to search.
|
|
:returns: a dictionary containing the parsed attributes of the element.
|
|
:raises: DracOperationFailed if the given element had no AttributeName
|
|
value.
|
|
"""
|
|
searches = {'current_value': './{%s}CurrentValue' % ns,
|
|
'read_only': './{%s}IsReadOnly' % ns,
|
|
'pending_value': './{%s}PendingValue' % ns}
|
|
LOG.debug("Handing %(ns)s for %(xml)s", {
|
|
'ns': ns,
|
|
'xml': ET.tostring(item),
|
|
})
|
|
name = item.findtext('./{%s}AttributeName' % ns)
|
|
if not name:
|
|
raise exception.DracOperationFailed(
|
|
message=_('Item has no name: "%s"') % ET.tostring(item))
|
|
res = {}
|
|
res['name'] = name
|
|
|
|
for k in searches:
|
|
if k == 'read_only':
|
|
res[k] = item.findtext(searches[k]) == 'true'
|
|
else:
|
|
res[k] = _val_or_none(item.find(searches[k]))
|
|
return res
|
|
|
|
|
|
def _format_error_msg(invalid_attribs_msgs, read_only_keys):
|
|
"""Format a combined error message.
|
|
|
|
This method creates a combined error message from a list of error messages
|
|
and a list of read-only keys.
|
|
|
|
:param invalid_attribs_msgs: a list of invalid attribute error messages.
|
|
:param read_only_keys: a list of read only keys that were attempted to be
|
|
written to.
|
|
:returns: a formatted error message.
|
|
"""
|
|
msg = '\n'.join(invalid_attribs_msgs)
|
|
if invalid_attribs_msgs and read_only_keys:
|
|
msg += '\n'
|
|
if read_only_keys:
|
|
msg += (_('Cannot set read-only BIOS settings "%r"') % read_only_keys)
|
|
return msg
|
|
|
|
|
|
def parse_enumeration(item, ns):
|
|
"""Parse an attribute that has a set of distinct values.
|
|
|
|
:param item: an XML element.
|
|
:param ns: the namespace to search.
|
|
:returns: a dictionary containing the parsed attributes of the element.
|
|
:raises: DracOperationFailed if the given element had no AttributeName
|
|
value.
|
|
"""
|
|
res = _parse_common(item, ns)
|
|
res['possible_values'] = sorted(
|
|
[v.text for v in item.findall('./{%s}PossibleValues' % ns)])
|
|
|
|
return res
|
|
|
|
|
|
def parse_string(item, ns):
|
|
"""Parse an attribute that should be a freeform string.
|
|
|
|
:param item: an XML element.
|
|
:param ns: the namespace to search.
|
|
:returns: a dictionary containing the parsed attributes of the element.
|
|
:raises: DracOperationFailed if the given element had no AttributeName
|
|
value.
|
|
"""
|
|
res = _parse_common(item, ns)
|
|
searches = {'min_length': './{%s}MinLength' % ns,
|
|
'max_length': './{%s}MaxLength' % ns,
|
|
'pcre_regex': './{%s}ValueExpression' % ns}
|
|
for k in searches:
|
|
if k == 'pcre_regex':
|
|
res[k] = _val_or_none(item.find(searches[k]))
|
|
else:
|
|
res[k] = int(item.findtext(searches[k]))
|
|
|
|
# Workaround for a BIOS bug in one of the 13 gen boxes
|
|
badval = re.compile(r"MAX_ASSET_TAG_LEN")
|
|
if (res['pcre_regex'] is not None and
|
|
res['name'] == 'AssetTag' and
|
|
badval.search(res['pcre_regex'])):
|
|
res['pcre_regex'] = badval.sub("%d" % res['max_length'],
|
|
res['pcre_regex'])
|
|
return res
|
|
|
|
|
|
def parse_integer(item, ns):
|
|
"""Parse an attribute that should be an integer.
|
|
|
|
:param item: an XML element.
|
|
:param ns: the namespace to search.
|
|
:returns: a dictionary containing the parsed attributes of the element.
|
|
:raises: DracOperationFailed if the given element had no AttributeName
|
|
value.
|
|
"""
|
|
res = _parse_common(item, ns)
|
|
for k in ['current_value', 'pending_value']:
|
|
if res[k]:
|
|
res[k] = int(res[k])
|
|
searches = {'lower_bound': './{%s}LowerBound' % ns,
|
|
'upper_bound': './{%s}UpperBound' % ns}
|
|
for k in searches:
|
|
res[k] = int(item.findtext(searches[k]))
|
|
|
|
return res
|
|
|
|
|
|
def _get_config(node, resource):
|
|
"""Helper for get_config.
|
|
|
|
Handles getting BIOS config values for a single namespace
|
|
|
|
:param node: an ironic node object.
|
|
:param resource: the namespace.
|
|
:returns: a dictionary that maps the name of each attribute to a dictionary
|
|
of values of that attribute.
|
|
:raises: InvalidParameterValue if some information required to connnect
|
|
to the DRAC is missing on the node or the value of one or more
|
|
required parameters is invalid.
|
|
:raises: DracClientError on an error from pywsman library.
|
|
:raises: DracOperationFailed if the specified resource is unknown.
|
|
"""
|
|
res = {}
|
|
client = wsman_client.get_wsman_client(node)
|
|
try:
|
|
doc = client.wsman_enumerate(resource)
|
|
except exception.DracClientError as exc:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('DRAC driver failed to get BIOS settings '
|
|
'for resource %(resource)s '
|
|
'from node %(node_uuid)s. '
|
|
'Reason: %(error)s.'),
|
|
{'node_uuid': node.uuid,
|
|
'resource': resource,
|
|
'error': exc})
|
|
items = doc.find('.//{%s}Items' % resource_uris.CIM_WSMAN)
|
|
for item in items:
|
|
if resource == resource_uris.DCIM_BIOSEnumeration:
|
|
attribute = parse_enumeration(item, resource)
|
|
elif resource == resource_uris.DCIM_BIOSString:
|
|
attribute = parse_string(item, resource)
|
|
elif resource == resource_uris.DCIM_BIOSInteger:
|
|
attribute = parse_integer(item, resource)
|
|
else:
|
|
raise exception.DracOperationFailed(
|
|
message=_('Unknown namespace %(ns)s for item: "%(item)s"') % {
|
|
'item': ET.tostring(item), 'ns': resource})
|
|
res[attribute['name']] = attribute
|
|
return res
|
|
|
|
|
|
def get_config(node):
|
|
"""Get the BIOS configuration from a Dell server using WSMAN
|
|
|
|
:param node: an ironic node object.
|
|
:raises: DracClientError on an error from pywsman.
|
|
:raises: DracOperationFailed when a BIOS setting cannot be parsed.
|
|
:returns: a dictionary containing BIOS settings in the form of:
|
|
{'EnumAttrib': {'name': 'EnumAttrib',
|
|
'current_value': 'Value',
|
|
'pending_value': 'New Value', # could also be None
|
|
'read_only': False,
|
|
'possible_values': ['Value', 'New Value', 'None']},
|
|
'StringAttrib': {'name': 'StringAttrib',
|
|
'current_value': 'Information',
|
|
'pending_value': None,
|
|
'read_only': False,
|
|
'min_length': 0,
|
|
'max_length': 255,
|
|
'pcre_regex': '^[0-9A-Za-z]{0,255}$'},
|
|
'IntegerAttrib': {'name': 'IntegerAttrib',
|
|
'current_value': 0,
|
|
'pending_value': None,
|
|
'read_only': True,
|
|
'lower_bound': 0,
|
|
'upper_bound': 65535}
|
|
}
|
|
|
|
The above values are only examples, of course. BIOS attributes exposed via
|
|
this API will always be either an enumerated attribute, a string attribute,
|
|
or an integer attribute. All attributes have the following parameters:
|
|
:name: is the name of the BIOS attribute.
|
|
:current_value: is the current value of the attribute.
|
|
It will always be either an integer or a string.
|
|
:pending_value: is the new value that we want the attribute to have.
|
|
None means that there is no pending value.
|
|
:read_only: indicates whether this attribute can be changed. Trying to
|
|
change a read-only value will result in an error.
|
|
The read-only flag can change depending on other attributes.
|
|
A future version of this call may expose the dependencies
|
|
that indicate when that may happen.
|
|
|
|
Enumerable attributes also have the following parameters:
|
|
:possible_values: is an array of values it is permissible to set
|
|
the attribute to.
|
|
|
|
String attributes also have the following parameters:
|
|
:min_length: is the minimum length of the string.
|
|
:max_length: is the maximum length of the string.
|
|
:pcre_regex: is a PCRE compatible regular expression that the string
|
|
must match. It may be None if the string is read only
|
|
or if the string does not have to match any particular
|
|
regular expression.
|
|
|
|
Integer attributes also have the following parameters:
|
|
:lower_bound: is the minimum value the attribute can have.
|
|
:upper_bound: is the maximum value the attribute can have.
|
|
|
|
"""
|
|
res = {}
|
|
for ns in [resource_uris.DCIM_BIOSEnumeration,
|
|
resource_uris.DCIM_BIOSString,
|
|
resource_uris.DCIM_BIOSInteger]:
|
|
attribs = _get_config(node, ns)
|
|
if not set(res).isdisjoint(set(attribs)):
|
|
raise exception.DracOperationFailed(
|
|
message=_('Colliding attributes %r') % (
|
|
set(res) & set(attribs)))
|
|
res.update(attribs)
|
|
return res
|
|
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def set_config(task, **kwargs):
|
|
"""Sets the pending_value parameter for each of the values passed in.
|
|
|
|
:param task: an ironic task object.
|
|
:param kwargs: a dictionary of {'AttributeName': 'NewValue'}
|
|
:raises: DracOperationFailed if any new values are invalid.
|
|
:raises: DracOperationFailed if any of the attributes are read-only.
|
|
:raises: DracOperationFailed if any of the attributes cannot be set for
|
|
any other reason.
|
|
:raises: DracClientError on an error from the pywsman library.
|
|
:returns: A boolean indicating whether commit_config needs to be
|
|
called to make the changes.
|
|
|
|
"""
|
|
node = task.node
|
|
management.check_for_config_job(node)
|
|
current = get_config(node)
|
|
unknown_keys = set(kwargs) - set(current)
|
|
if unknown_keys:
|
|
LOG.warning(_LW('Ignoring unknown BIOS attributes "%r"'),
|
|
unknown_keys)
|
|
|
|
candidates = set(kwargs) - unknown_keys
|
|
read_only_keys = []
|
|
unchanged_attribs = []
|
|
invalid_attribs_msgs = []
|
|
attrib_names = []
|
|
|
|
for k in candidates:
|
|
if str(kwargs[k]) == str(current[k]['current_value']):
|
|
unchanged_attribs.append(k)
|
|
elif current[k]['read_only']:
|
|
read_only_keys.append(k)
|
|
else:
|
|
if 'possible_values' in current[k]:
|
|
if str(kwargs[k]) not in current[k]['possible_values']:
|
|
m = _('Attribute %(attr)s cannot be set to value %(val)s.'
|
|
' It must be in %(ok)r') % {
|
|
'attr': k,
|
|
'val': kwargs[k],
|
|
'ok': current[k]['possible_values']}
|
|
invalid_attribs_msgs.append(m)
|
|
continue
|
|
if ('pcre_regex' in current[k] and
|
|
current[k]['pcre_regex'] is not None):
|
|
regex = re.compile(current[k]['pcre_regex'])
|
|
if regex.search(str(kwargs[k])) is None:
|
|
# TODO(victor-lowther)
|
|
# Leave untranslated for now until the unicode
|
|
# issues that the test suite exposes are straightened out.
|
|
m = ('Attribute %(attr)s cannot be set to value %(val)s.'
|
|
' It must match regex %(re)s.') % {
|
|
'attr': k,
|
|
'val': kwargs[k],
|
|
're': current[k]['pcre_regex']}
|
|
invalid_attribs_msgs.append(m)
|
|
continue
|
|
if 'lower_bound' in current[k]:
|
|
lower = current[k]['lower_bound']
|
|
upper = current[k]['upper_bound']
|
|
val = int(kwargs[k])
|
|
if val < lower or val > upper:
|
|
m = _('Attribute %(attr)s cannot be set to value %(val)d.'
|
|
' It must be between %(lower)d and %(upper)d.') % {
|
|
'attr': k,
|
|
'val': val,
|
|
'lower': lower,
|
|
'upper': upper}
|
|
invalid_attribs_msgs.append(m)
|
|
continue
|
|
attrib_names.append(k)
|
|
|
|
if unchanged_attribs:
|
|
LOG.warn(_LW('Ignoring unchanged BIOS settings %r'),
|
|
unchanged_attribs)
|
|
|
|
if invalid_attribs_msgs or read_only_keys:
|
|
raise exception.DracOperationFailed(
|
|
_format_error_msg(invalid_attribs_msgs, read_only_keys))
|
|
|
|
if not attrib_names:
|
|
return False
|
|
|
|
client = wsman_client.get_wsman_client(node)
|
|
selectors = {'CreationClassName': 'DCIM_BIOSService',
|
|
'Name': 'DCIM:BIOSService',
|
|
'SystemCreationClassName': 'DCIM_ComputerSystem',
|
|
'SystemName': 'DCIM:ComputerSystem'}
|
|
properties = {'Target': 'BIOS.Setup.1-1',
|
|
'AttributeName': attrib_names,
|
|
'AttributeValue': map(lambda k: kwargs[k], attrib_names)}
|
|
doc = client.wsman_invoke(resource_uris.DCIM_BIOSService,
|
|
'SetAttributes',
|
|
selectors,
|
|
properties)
|
|
# Yes, we look for RebootRequired. In this context, that actually means
|
|
# that we need to create a lifecycle controller config job and then reboot
|
|
# so that the lifecycle controller can commit the BIOS config changes that
|
|
# we have proposed.
|
|
set_results = doc.findall(
|
|
'.//{%s}RebootRequired' % resource_uris.DCIM_BIOSService)
|
|
return any(str(res.text) == 'Yes' for res in set_results)
|
|
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def commit_config(task):
|
|
"""Commits pending changes added by set_config
|
|
|
|
:param task: is the ironic task for running the config job.
|
|
:raises: DracClientError on an error from pywsman library.
|
|
:raises: DracPendingConfigJobExists if the job is already created.
|
|
:raises: DracOperationFailed if the client received response with an
|
|
error message.
|
|
:raises: DracUnexpectedReturnValue if the client received a response
|
|
with unexpected return value
|
|
|
|
"""
|
|
node = task.node
|
|
management.check_for_config_job(node)
|
|
management.create_config_job(node)
|
|
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def abandon_config(task):
|
|
"""Abandons uncommitted changes added by set_config
|
|
|
|
:param task: is the ironic task for abandoning the changes.
|
|
:raises: DracClientError on an error from pywsman library.
|
|
:raises: DracOperationFailed on error reported back by DRAC.
|
|
:raises: DracUnexpectedReturnValue if the drac did not report success.
|
|
|
|
"""
|
|
node = task.node
|
|
client = wsman_client.get_wsman_client(node)
|
|
selectors = {'CreationClassName': 'DCIM_BIOSService',
|
|
'Name': 'DCIM:BIOSService',
|
|
'SystemCreationClassName': 'DCIM_ComputerSystem',
|
|
'SystemName': 'DCIM:ComputerSystem'}
|
|
properties = {'Target': 'BIOS.Setup.1-1'}
|
|
|
|
client.wsman_invoke(resource_uris.DCIM_BIOSService,
|
|
'DeletePendingConfiguration',
|
|
selectors,
|
|
properties,
|
|
wsman_client.RET_SUCCESS)
|