# # 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. """ Wrapper for pywsman.Client """ import time from xml.etree import ElementTree from oslo_log import log as logging from oslo_utils import importutils from ironic.common import exception from ironic.common.i18n import _LW from ironic.drivers.modules.drac import common as drac_common pywsman = importutils.try_import('pywsman') LOG = logging.getLogger(__name__) _SOAP_ENVELOPE_URI = 'http://www.w3.org/2003/05/soap-envelope' # Filter Dialects, see (Section 2.3.1): # http://en.community.dell.com/techcenter/extras/m/white_papers/20439105.aspx _FILTER_DIALECT_MAP = {'cql': 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf', 'wql': 'http://schemas.microsoft.com/wbem/wsman/1/WQL'} # ReturnValue constants RET_SUCCESS = '0' RET_ERROR = '2' RET_CREATED = '4096' RETRY_COUNT = 5 RETRY_DELAY = 5 def get_wsman_client(node): """Return a DRAC client object. Given an ironic node object, this method gives back a Client object which is a wrapper for pywsman.Client. :param node: an ironic node object. :returns: a Client object. :raises: InvalidParameterValue if some mandatory information is missing on the node or on invalid inputs. """ driver_info = drac_common.parse_driver_info(node) client = Client(**driver_info) return client def retry_on_empty_response(client, action, *args, **kwargs): """Wrapper to retry an action on failure.""" func = getattr(client, action) for i in range(RETRY_COUNT): response = func(*args, **kwargs) if response: return response else: LOG.warning(_LW('Empty response on calling %(action)s on client. ' 'Last error (cURL error code): %(last_error)s, ' 'fault string: "%(fault_string)s" ' 'response_code: %(response_code)s. ' 'Retry attempt %(count)d') % {'action': action, 'last_error': client.last_error(), 'fault_string': client.fault_string(), 'response_code': client.response_code(), 'count': i + 1}) time.sleep(RETRY_DELAY) class Client(object): def __init__(self, drac_host, drac_port, drac_path, drac_protocol, drac_username, drac_password): pywsman_client = pywsman.Client(drac_host, drac_port, drac_path, drac_protocol, drac_username, drac_password) # TODO(ifarkas): Add support for CACerts pywsman.wsman_transport_set_verify_peer(pywsman_client, False) pywsman.wsman_transport_set_verify_host(pywsman_client, False) self.client = pywsman_client def wsman_enumerate(self, resource_uri, filter_query=None, filter_dialect='cql'): """Enumerates a remote WS-Man class. :param resource_uri: URI of the resource. :param filter_query: the query string. :param filter_dialect: the filter dialect. Valid options are: 'cql' and 'wql'. Defaults to 'cql'. :raises: DracClientError on an error from pywsman library. :raises: DracInvalidFilterDialect if an invalid filter dialect was specified. :returns: an ElementTree object of the response received. """ options = pywsman.ClientOptions() filter_ = None if filter_query is not None: try: filter_dialect = _FILTER_DIALECT_MAP[filter_dialect] except KeyError: valid_opts = ', '.join(_FILTER_DIALECT_MAP) raise exception.DracInvalidFilterDialect( invalid_filter=filter_dialect, supported=valid_opts) filter_ = pywsman.Filter() filter_.simple(filter_dialect, filter_query) options.set_flags(pywsman.FLAG_ENUMERATION_OPTIMIZATION) options.set_max_elements(100) doc = retry_on_empty_response(self.client, 'enumerate', options, filter_, resource_uri) root = self._get_root(doc) final_xml = root find_query = './/{%s}Body' % _SOAP_ENVELOPE_URI insertion_point = final_xml.find(find_query) while doc.context() is not None: doc = retry_on_empty_response(self.client, 'pull', options, None, resource_uri, str(doc.context())) root = self._get_root(doc) for result in root.findall(find_query): for child in list(result): insertion_point.append(child) return final_xml def wsman_invoke(self, resource_uri, method, selectors=None, properties=None, expected_return_value=RET_SUCCESS): """Invokes a remote WS-Man method. :param resource_uri: URI of the resource. :param method: name of the method to invoke. :param selectors: dictionary of selectors. :param properties: dictionary of properties. :param expected_return_value: expected return value. :raises: DracClientError on an error from pywsman library. :raises: DracOperationFailed on error reported back by DRAC. :raises: DracUnexpectedReturnValue on return value mismatch. :returns: an ElementTree object of the response received. """ if selectors is None: selectors = {} if properties is None: properties = {} options = pywsman.ClientOptions() for name, value in selectors.items(): options.add_selector(name, value) # NOTE(ifarkas): manually constructing the XML doc should be deleted # once pywsman supports passing a list as a property. # For now this is only a fallback method: in case no # list provided, the supported pywsman API will be used. list_included = any([isinstance(prop_item, list) for prop_item in properties.values()]) if list_included: xml_doc = pywsman.XmlDoc('%s_INPUT' % method, resource_uri) xml_root = xml_doc.root() for name, value in properties.items(): if isinstance(value, list): for item in value: xml_root.add(resource_uri, name, item) else: xml_root.add(resource_uri, name, value) else: xml_doc = None for name, value in properties.items(): options.add_property(name, value) doc = retry_on_empty_response(self.client, 'invoke', options, resource_uri, method, xml_doc) root = self._get_root(doc) return_value = drac_common.find_xml(root, 'ReturnValue', resource_uri).text if return_value != expected_return_value: if return_value == RET_ERROR: message = drac_common.find_xml(root, 'Message', resource_uri).text raise exception.DracOperationFailed(message=message) else: raise exception.DracUnexpectedReturnValue( expected_return_value=expected_return_value, actual_return_value=return_value) return root def _get_root(self, doc): if doc is None or doc.root() is None: raise exception.DracClientError( last_error=self.client.last_error(), fault_string=self.client.fault_string(), response_code=self.client.response_code()) root = doc.root() return ElementTree.fromstring(root.string())