You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
19 KiB
462 lines
19 KiB
# |
|
# 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. |
|
|
|
""" |
|
Common functionalities shared between different DRAC modules. |
|
""" |
|
|
|
from dracclient import constants |
|
import logging |
|
|
|
from dracclient import exceptions |
|
from dracclient import wsman |
|
|
|
LOG = logging.getLogger(__name__) |
|
|
|
NS_XMLSchema_Instance = 'http://www.w3.org/2001/XMLSchema-instance' |
|
|
|
# ReturnValue constants |
|
RET_SUCCESS = '0' |
|
RET_ERROR = '2' |
|
RET_CREATED = '4096' |
|
|
|
REBOOT_REQUIRED = { |
|
'yes': constants.RebootRequired.true, |
|
'no': constants.RebootRequired.false, |
|
'optional': constants.RebootRequired.optional |
|
} |
|
|
|
|
|
def find_xml(doc, item, namespace, find_all=False): |
|
"""Find the first or all elements in an ElementTree object. |
|
|
|
:param doc: the element tree object. |
|
:param item: the element name. |
|
:param namespace: the namespace of the element. |
|
:param find_all: Boolean value, if True find all elements, if False |
|
find only the first one. Defaults to False. |
|
:returns: if find_all is False the element object will be returned |
|
if found, None if not found. If find_all is True a list of |
|
element objects will be returned or an empty list if no |
|
elements were found. |
|
|
|
""" |
|
query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace, |
|
'item': item}) |
|
if find_all: |
|
return doc.findall(query) |
|
return doc.find(query) |
|
|
|
|
|
def _is_attr_non_nil(elem): |
|
"""Return whether an element is non-nil. |
|
|
|
:param elem: the element object. |
|
:returns: whether the element is nil. |
|
""" |
|
return elem.attrib.get('{%s}nil' % NS_XMLSchema_Instance) != 'true' |
|
|
|
|
|
def get_wsman_resource_attr(doc, resource_uri, attr_name, nullable=False, |
|
allow_missing=False): |
|
"""Find an attribute of a resource in an ElementTree object. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:param attr_name: the name of the attribute. |
|
:param nullable: enables checking if the element contains an |
|
XMLSchema-instance namespaced nil attribute that has a |
|
value of True. In this case, it will return None. |
|
:param allow_missing: if set to True, attributes missing from the XML |
|
document will return None instead of raising |
|
DRACMissingResponseField. |
|
:raises: DRACMissingResponseField if the attribute is missing from the XML |
|
doc and allow_missing is False. |
|
:raises: DRACEmptyResponseField if the attribute is present in the XML doc |
|
but it has no text and nullable is False. |
|
:returns: value of the attribute |
|
""" |
|
item = find_xml(doc, attr_name, resource_uri) |
|
|
|
if item is None: |
|
if allow_missing: |
|
return |
|
else: |
|
raise exceptions.DRACMissingResponseField(attr=attr_name) |
|
|
|
if not nullable: |
|
if item.text is None: |
|
raise exceptions.DRACEmptyResponseField(attr=attr_name) |
|
return item.text.strip() |
|
else: |
|
if _is_attr_non_nil(item): |
|
return item.text.strip() |
|
|
|
|
|
def get_all_wsman_resource_attrs(doc, resource_uri, attr_name, nullable=False): |
|
"""Find all instances of an attribute of a resource in an ElementTree. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:param attr_name: the name of the attribute. |
|
:param nullable: enables checking if any of the elements contain an |
|
XMLSchema-instance namespaced nil attribute that has a |
|
value of True. In this case, these elements will not be |
|
returned. |
|
:raises: DRACEmptyResponseField if any of the attributes in the XML doc |
|
have no text and nullable is False. |
|
:returns: a list containing the value of each of the instances of the |
|
attribute. |
|
""" |
|
items = find_xml(doc, attr_name, resource_uri, find_all=True) |
|
|
|
if not nullable: |
|
for item in items: |
|
if item.text is None: |
|
raise exceptions.DRACEmptyResponseField(attr=attr_name) |
|
return [item.text.strip() for item in items] |
|
else: |
|
|
|
return [item.text.strip() for item in items if _is_attr_non_nil(item)] |
|
|
|
|
|
def build_return_dict(doc, resource_uri, |
|
is_commit_required_value=None, |
|
is_reboot_required_value=None, |
|
commit_required_value=None, |
|
include_commit_required=False): |
|
"""Builds a dictionary to be returned |
|
|
|
Build a dictionary to be returned from WSMAN operations that are not |
|
read-only. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:param is_commit_required_value: The value to be returned for |
|
is_commit_required, or None if the value should be determined |
|
from the doc. |
|
:param is_reboot_required_value: The value to be returned for |
|
is_reboot_required, or None if the value should be determined |
|
from the doc. |
|
:param commit_required_value: The value to be returned for |
|
commit_required, or None if the value should be determined |
|
from the doc. |
|
:parm include_commit_required: Indicates if the deprecated commit_required |
|
should be returned in the result. |
|
:returns: a dictionary containing: |
|
- is_commit_required: indicates if a commit is required. |
|
- is_reboot_required: indicates if a reboot is required. |
|
- commit_required: a deprecated key indicating if a commit is |
|
required. This key actually has a value that indicates if a |
|
reboot is required. |
|
""" |
|
|
|
if is_reboot_required_value is not None and \ |
|
is_reboot_required_value not in constants.RebootRequired.all(): |
|
msg = ("is_reboot_required_value must be a member of the " |
|
"RebootRequired enumeration or None. The passed value was " |
|
"%(is_reboot_required_value)s" % { |
|
'is_reboot_required_value': is_reboot_required_value}) |
|
raise exceptions.InvalidParameterValue(reason=msg) |
|
|
|
result = {} |
|
if is_commit_required_value is None: |
|
is_commit_required_value = is_commit_required(doc, resource_uri) |
|
|
|
result['is_commit_required'] = is_commit_required_value |
|
|
|
if is_reboot_required_value is None: |
|
is_reboot_required_value = reboot_required(doc, resource_uri) |
|
|
|
result['is_reboot_required'] = is_reboot_required_value |
|
|
|
# Include commit_required in the response for backwards compatibility |
|
# TBD: Remove this parameter in the future |
|
if include_commit_required: |
|
if commit_required_value is None: |
|
commit_required_value = is_reboot_required(doc, resource_uri) |
|
|
|
result['commit_required'] = commit_required_value |
|
|
|
return result |
|
|
|
|
|
def is_commit_required(doc, resource_uri): |
|
"""Check the response document if commit is required. |
|
|
|
If SetResult contains "pending" in the response then a commit is required. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:returns: a boolean value indicating commit is required or not. |
|
""" |
|
|
|
commit_required = find_xml(doc, 'SetResult', resource_uri) |
|
return "pendingvalue" in commit_required.text.lower() |
|
|
|
|
|
def is_reboot_required(doc, resource_uri): |
|
"""Check the response document if reboot is requested. |
|
|
|
RebootRequired attribute in the response indicates whether a config job |
|
needs to be created and the node needs to be rebooted, so that the |
|
Lifecycle controller can commit the pending changes. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:returns: a boolean value indicating reboot was requested or not. |
|
""" |
|
|
|
reboot_required = find_xml(doc, 'RebootRequired', resource_uri) |
|
return reboot_required.text.lower() == 'yes' |
|
|
|
|
|
def reboot_required(doc, resource_uri): |
|
"""Check the response document if reboot is requested. |
|
|
|
RebootRequired attribute in the response indicates whether node needs to |
|
be rebooted, so that the pending changes can be committed. |
|
|
|
:param doc: the element tree object. |
|
:param resource_uri: the resource URI of the namespace. |
|
:returns: True if reboot is required, False if it is not, and the string |
|
"optional" if reboot is optional. |
|
""" |
|
|
|
reboot_required_value = find_xml(doc, 'RebootRequired', resource_uri) |
|
return REBOOT_REQUIRED[reboot_required_value.text.lower()] |
|
|
|
|
|
def validate_integer_value(value, attr_name, error_msgs): |
|
"""Validate integer value""" |
|
|
|
if value is None: |
|
error_msgs.append("'%s' is not supplied" % attr_name) |
|
return |
|
|
|
try: |
|
int(value) |
|
except ValueError: |
|
error_msgs.append("'%s' is not an integer value" % attr_name) |
|
|
|
|
|
def list_settings(client, namespaces, by_name=True, fqdd_filter=None, |
|
name_formatter=None, wait_for_idrac=True): |
|
"""List the configuration settings |
|
|
|
:param client: an instance of WSManClient. |
|
:param namespaces: a list of URI/class pairs to retrieve. |
|
:param by_name: controls whether returned dictionary uses |
|
attribute name or instance_id as key. |
|
:param fqdd_filter: An FQDD used to filter the instances. Note that |
|
this is only used when by_name is True. |
|
:param name_formatter: a method used to format the keys in the |
|
returned dictionary. By default, |
|
attribute.name will be used. |
|
:param wait_for_idrac: indicates whether or not to wait for the |
|
iDRAC to be ready to accept commands before |
|
issuing the command. |
|
:returns: a dictionary with the settings using name or instance_id as |
|
the key. |
|
:raises: WSManRequestFailure on request failures |
|
:raises: WSManInvalidResponse when receiving invalid response |
|
:raises: DRACOperationFailed on error reported back by the DRAC |
|
interface |
|
""" |
|
|
|
result = {} |
|
for (namespace, attr_cls) in namespaces: |
|
attribs = _get_config(client, namespace, attr_cls, by_name, |
|
fqdd_filter, name_formatter, wait_for_idrac) |
|
if not set(result).isdisjoint(set(attribs)): |
|
raise exceptions.DRACOperationFailed( |
|
drac_messages=('Colliding attributes %r' % ( |
|
set(result) & set(attribs)))) |
|
result.update(attribs) |
|
return result |
|
|
|
|
|
def _get_config(client, resource, attr_cls, by_name, fqdd_filter, |
|
name_formatter, wait_for_idrac): |
|
result = {} |
|
|
|
doc = client.enumerate(resource, wait_for_idrac=wait_for_idrac) |
|
items = doc.find('.//{%s}Items' % wsman.NS_WSMAN) |
|
|
|
for item in items: |
|
attribute = attr_cls.parse(item) |
|
if by_name: |
|
# Filter out all instances without a matching FQDD |
|
if fqdd_filter is None or fqdd_filter == attribute.fqdd: |
|
if name_formatter is None: |
|
name = attribute.name |
|
else: |
|
name = name_formatter(attribute) |
|
|
|
result[name] = attribute |
|
else: |
|
result[attribute.instance_id] = attribute |
|
|
|
return result |
|
|
|
|
|
def set_settings(settings_type, |
|
client, |
|
namespaces, |
|
new_settings, |
|
resource_uri, |
|
cim_creation_class_name, |
|
cim_name, |
|
target, |
|
name_formatter=None, |
|
include_commit_required=False, |
|
wait_for_idrac=True, |
|
by_name=True): |
|
"""Generically handles setting various types of settings on the iDRAC |
|
|
|
This method pulls the current list of settings from the iDRAC then compares |
|
that list against the passed new settings to determine if there are any |
|
errors. If no errors exist then the settings are sent to the iDRAC using |
|
the passed resource, target, etc. |
|
|
|
:param settings_type: a string indicating the settings type |
|
:param client: an instance of WSManClient |
|
:param namespaces: a list of URI/class pairs to retrieve. |
|
:param new_settings: a dictionary containing the proposed values, with |
|
each key being the name of attribute and the |
|
value being the proposed value. |
|
:param resource_uri: URI of resource to invoke |
|
:param cim_creation_class_name: creation class name of the CIM object |
|
:param cim_name: name of the CIM object |
|
:param target: target device |
|
:param name_formatter: a method used to format the keys in the |
|
returned dictionary. By default, |
|
attribute.name will be used. |
|
:parm include_commit_required: Indicates if the deprecated commit_required |
|
should be returned in the result. |
|
:param wait_for_idrac: indicates whether or not to wait for the |
|
iDRAC to be ready to accept commands before issuing |
|
the command |
|
:param by_name: Controls whether returned dictionary uses RAID |
|
attribute name or instance_id as key. |
|
:returns: a dictionary containing: |
|
- The commit_required key with a boolean value indicating |
|
whether a config job must be created for the values to be |
|
applied. This key actually has a value that indicates if |
|
a reboot is required. This key has been deprecated and |
|
will be removed in a future release. |
|
- The is_commit_required key with a boolean value indicating |
|
whether a config job must be created for the values to be |
|
applied. |
|
- The is_reboot_required key with a RebootRequired enumerated |
|
value indicating whether the server must be rebooted for the |
|
values to be applied. Possible values are true and false. |
|
:raises: WSManRequestFailure on request failures |
|
:raises: WSManInvalidResponse when receiving invalid response |
|
:raises: DRACOperationFailed on new settings with invalid values or |
|
attempting to set read-only settings or when an error is reported |
|
back by the iDRAC interface |
|
:raises: DRACUnexpectedReturnValue on return value mismatch |
|
:raises: InvalidParameterValue on invalid new setting |
|
""" |
|
current_settings = list_settings(client, namespaces, by_name=by_name, |
|
name_formatter=name_formatter, |
|
wait_for_idrac=wait_for_idrac) |
|
|
|
unknown_keys = set(new_settings) - set(current_settings) |
|
if unknown_keys: |
|
msg = ('Unknown %(settings_type)s attributes found: %(unknown_keys)r' % |
|
{'settings_type': settings_type, 'unknown_keys': unknown_keys}) |
|
raise exceptions.InvalidParameterValue(reason=msg) |
|
|
|
read_only_keys = [] |
|
unchanged_attribs = [] |
|
invalid_attribs_msgs = [] |
|
attrib_names = [] |
|
candidates = set(new_settings) |
|
|
|
for attr in candidates: |
|
# There are RAID settings that can have multiple values, |
|
# however these are all read-only attributes. |
|
# Filter out all read-only attributes first so that we exclude |
|
# these settings from further consideration |
|
current_setting_value = current_settings[attr].current_value |
|
if type(current_setting_value) is list: |
|
current_setting_value = current_setting_value[0] |
|
|
|
if current_settings[attr].read_only: |
|
read_only_keys.append(attr) |
|
elif str(new_settings[attr]) == str(current_setting_value): |
|
unchanged_attribs.append(attr) |
|
else: |
|
validation_msg = current_settings[attr].validate( |
|
new_settings[attr]) |
|
if not validation_msg: |
|
attrib_names.append(attr) |
|
else: |
|
invalid_attribs_msgs.append(validation_msg) |
|
|
|
if unchanged_attribs: |
|
LOG.debug('Ignoring unchanged %(settings_type)s attributes: ' |
|
'%(unchanged_attribs)r' % |
|
{'settings_type': settings_type, |
|
'unchanged_attribs': unchanged_attribs}) |
|
|
|
if invalid_attribs_msgs or read_only_keys: |
|
if read_only_keys: |
|
read_only_msg = ['Cannot set read-only %(settings_type)s ' |
|
'attributes: %(read_only_keys)r.' % |
|
{'settings_type': settings_type, |
|
'read_only_keys': read_only_keys}] |
|
else: |
|
read_only_msg = [] |
|
|
|
drac_messages = '\n'.join(invalid_attribs_msgs + read_only_msg) |
|
raise exceptions.DRACOperationFailed( |
|
drac_messages=drac_messages) |
|
|
|
if not attrib_names: |
|
return build_return_dict( |
|
None, |
|
resource_uri, |
|
include_commit_required=include_commit_required, |
|
is_commit_required_value=False, |
|
is_reboot_required_value=constants.RebootRequired.false, |
|
commit_required_value=False) |
|
|
|
selectors = {'CreationClassName': cim_creation_class_name, |
|
'Name': cim_name, |
|
'SystemCreationClassName': 'DCIM_ComputerSystem', |
|
'SystemName': 'DCIM:ComputerSystem'} |
|
|
|
properties = {'Target': target, |
|
'AttributeValue': [new_settings[attr] for attr |
|
in attrib_names]} |
|
# To set RAID settings, above we fetched list raid settings using |
|
# instance_id to retrieve attribute values. When we pass instance_id in |
|
# setattribute method for setting any new RAID settings, wsman raises |
|
# an error. So another approach to set those settings is to list raid |
|
# settings using instance_id and for settings new settings, pass the |
|
# attribute names in list to SetAttributes method along with the target. |
|
# That's the reason, we need to handle RAID specific settings like below |
|
if settings_type == 'RAID': |
|
properties['AttributeName'] = [current_settings[attr].name for |
|
attr in attrib_names] |
|
else: |
|
properties['AttributeName'] = attrib_names |
|
doc = client.invoke(resource_uri, 'SetAttributes', |
|
selectors, properties, |
|
wait_for_idrac=wait_for_idrac) |
|
|
|
return build_return_dict(doc, resource_uri, |
|
include_commit_required=include_commit_required)
|
|
|