# # 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 logging import re import six import time import uuid from lxml import etree as ElementTree import requests.exceptions from dracclient import constants from dracclient import exceptions LOG = logging.getLogger(__name__) NS_SOAP_ENV = 'http://www.w3.org/2003/05/soap-envelope' NS_WS_ADDR = 'http://schemas.xmlsoap.org/ws/2004/08/addressing' NS_WS_ADDR_ANONYM_ROLE = ('http://schemas.xmlsoap.org/ws/2004/08/addressing/' 'role/anonymous') NS_WSMAN = 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd' NS_WSMAN_ENUM = 'http://schemas.xmlsoap.org/ws/2004/09/enumeration' NS_MAP = {'s': NS_SOAP_ENV, 'wsa': NS_WS_ADDR, 'wsman': NS_WSMAN} FILTER_DIALECT_MAP = {'cql': 'http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf', 'wql': 'http://schemas.microsoft.com/wbem/wsman/1/WQL'} class Client(object): """Simple client for talking over WSMan protocol.""" def __init__(self, host, username, password, port=443, path='/wsman', protocol='https', ssl_retries=constants.DEFAULT_WSMAN_SSL_ERROR_RETRIES, ssl_retry_delay=( constants.DEFAULT_WSMAN_SSL_ERROR_RETRY_DELAY_SEC)): """Creates client object :param host: hostname or IP of the DRAC interface :param username: username for accessing the DRAC interface :param password: password for accessing the DRAC interface :param port: port for accessing the DRAC interface :param path: path for accessing the DRAC interface :param protocol: protocol for accessing the DRAC interface :param ssl_retries: number of resends to attempt on SSL failures :param ssl_retry_delay: number of seconds to wait between retries on SSL failures """ self.host = host self.username = username self.password = password self.port = port self.path = path self.protocol = protocol self.ssl_retries = ssl_retries self.ssl_retry_delay = ssl_retry_delay self.endpoint = ('%(protocol)s://%(host)s:%(port)s%(path)s' % { 'protocol': self.protocol, 'host': self.host, 'port': self.port, 'path': self.path}) def _do_request(self, payload): payload = payload.build() LOG.debug('Sending request to %(endpoint)s: %(payload)s', {'endpoint': self.endpoint, 'payload': payload}) num_tries = 1 while num_tries <= self.ssl_retries: try: resp = requests.post( self.endpoint, auth=requests.auth.HTTPBasicAuth(self.username, self.password), data=payload, # TODO(ifarkas): enable cert verification verify=False) break except (requests.exceptions.ConnectionError, requests.exceptions.SSLError) as ex: error_msg = "A {error_type} error occurred while " \ " communicating with {host}, attempt {num_tries} of " \ "{retries}".format( error_type=type(ex).__name__, host=self.host, num_tries=num_tries, retries=self.ssl_retries) if num_tries == self.ssl_retries: LOG.error(error_msg) raise exceptions.WSManRequestFailure( "A {error_type} error occurred while communicating " "with {host}: {error}".format( error_type=type(ex).__name__, host=self.host, error=ex)) else: LOG.warning(error_msg) num_tries += 1 if self.ssl_retry_delay > 0 and num_tries <= self.ssl_retries: time.sleep(self.ssl_retry_delay) except requests.exceptions.RequestException as ex: error_msg = "A {error_type} error occurred while " \ "communicating with {host}: {error}".format( error_type=type(ex).__name__, host=self.host, error=ex) LOG.error(error_msg) raise exceptions.WSManRequestFailure(error_msg) LOG.debug('Received response from %(endpoint)s: %(payload)s', {'endpoint': self.endpoint, 'payload': resp.content}) if not resp.ok: raise exceptions.WSManInvalidResponse( status_code=resp.status_code, reason=resp.reason) else: return resp def enumerate(self, resource_uri, optimization=True, max_elems=100, auto_pull=True, filter_query=None, filter_dialect='cql'): """Executes enumerate operation over WSMan. :param resource_uri: URI of resource to enumerate. :param optimization: flag to enable enumeration optimization. If disabled, the enumeration returns only an enumeration context. :param max_elems: maximum number of elements returned by the operation. :param auto_pull: flag to enable automatic pull on the enumeration context, merging the items returned. :param filter_query: filter query string. :param filter_dialect: filter dialect. Valid options are: 'cql' and 'wql'. :returns: an lxml.etree.Element object of the response received. :raises: WSManRequestFailure on request failures :raises: WSManInvalidResponse when receiving invalid response """ payload = _EnumeratePayload(self.endpoint, resource_uri, optimization, max_elems, filter_query, filter_dialect) resp = self._do_request(payload) try: resp_xml = ElementTree.fromstring(resp.content) except ElementTree.XMLSyntaxError: LOG.warning('Received invalid content from iDRAC. Filtering out ' 'unprintable characters: ' + repr(resp.content)) # Filter out everything except for printable ASCII characters and # tab resp_xml = ElementTree.fromstring(re.sub(six.b('[^\x20-\x7e\t]'), six.b(''), resp.content)) if auto_pull: # The first response returns "" find_items_wsman_query = './/{%s}Items' % NS_WSMAN # Successive pulls return "" find_items_enum_query = './/{%s}Items' % NS_WSMAN_ENUM full_resp_xml = resp_xml items_xml = full_resp_xml.find(find_items_wsman_query) context = self._enum_context(full_resp_xml) while context is not None: resp_xml = self.pull(resource_uri, context, max_elems) context = self._enum_context(resp_xml) # Merge in next batch of enumeration items for item in resp_xml.find(find_items_enum_query): items_xml.append(item) # remove enumeration context because items are already merged enum_context_elem = full_resp_xml.find('.//{%s}EnumerationContext' % NS_WSMAN_ENUM) if enum_context_elem is not None: enum_context_elem.getparent().remove(enum_context_elem) return full_resp_xml else: return resp_xml def pull(self, resource_uri, context, max_elems=100): """Executes pull operation over WSMan. :param resource_uri: URI of resource to pull :param context: enumeration context :param max_elems: maximum number of elements returned by the operation :returns: an lxml.etree.Element object of the response received :raises: WSManRequestFailure on request failures :raises: WSManInvalidResponse when receiving invalid response """ payload = _PullPayload(self.endpoint, resource_uri, context, max_elems) resp = self._do_request(payload) resp_xml = ElementTree.fromstring(resp.content) return resp_xml def invoke(self, resource_uri, method, selectors, properties): """Executes invoke operation over WSMan. :param resource_uri: URI of resource to invoke :param method: name of the method to invoke :param selector: dict of selectors :param properties: dict of properties :returns: an lxml.etree.Element object of the response received. :raises: WSManRequestFailure on request failures :raises: WSManInvalidResponse when receiving invalid response """ payload = _InvokePayload(self.endpoint, resource_uri, method, selectors, properties) resp = self._do_request(payload) resp_xml = ElementTree.fromstring(resp.content) return resp_xml def _enum_context(self, resp): context_elem = resp.find('.//{%s}EnumerationContext' % NS_WSMAN_ENUM) if context_elem is not None: return context_elem.text class _Payload(object): """Payload generation for WSMan requests.""" def build(self): request = self._create_envelope() self._add_header(request) self._add_body(request) return ElementTree.tostring(request) def _create_envelope(self): return ElementTree.Element('{%s}Envelope' % NS_SOAP_ENV, nsmap=NS_MAP) def _add_header(self, envelope): header = ElementTree.SubElement(envelope, '{%s}Header' % NS_SOAP_ENV) qn_must_understand = ElementTree.QName(NS_SOAP_ENV, 'mustUnderstand') to_elem = ElementTree.SubElement(header, '{%s}To' % NS_WS_ADDR) to_elem.set(qn_must_understand, 'true') to_elem.text = self.endpoint resource_elem = ElementTree.SubElement(header, '{%s}ResourceURI' % NS_WSMAN) resource_elem.set(qn_must_understand, 'true') resource_elem.text = self.resource_uri msg_id_elem = ElementTree.SubElement(header, '{%s}MessageID' % NS_WS_ADDR) msg_id_elem.set(qn_must_understand, 'true') msg_id_elem.text = 'uuid:%s' % uuid.uuid4() reply_to_elem = ElementTree.SubElement(header, '{%s}ReplyTo' % NS_WS_ADDR) reply_to_addr_elem = ElementTree.SubElement(reply_to_elem, '{%s}Address' % NS_WS_ADDR) reply_to_addr_elem.text = NS_WS_ADDR_ANONYM_ROLE return header def _add_body(self, envelope): return ElementTree.SubElement(envelope, '{%s}Body' % NS_SOAP_ENV) class _EnumeratePayload(_Payload): """Payload generation for WSMan enumerate operation.""" def __init__(self, endpoint, resource_uri, optimization=True, max_elems=100, filter_query=None, filter_dialect=None): self.endpoint = endpoint self.resource_uri = resource_uri self.filter_dialect = None self.filter_query = None self.optimization = optimization self.max_elems = max_elems if filter_query is not None: try: self.filter_dialect = FILTER_DIALECT_MAP[filter_dialect] except KeyError: valid_opts = ', '.join(FILTER_DIALECT_MAP) raise exceptions.WSManInvalidFilterDialect( invalid_filter=filter_dialect, supported=valid_opts) self.filter_query = filter_query def _add_header(self, envelope): header = super(_EnumeratePayload, self)._add_header(envelope) action_elem = ElementTree.SubElement(header, '{%s}Action' % NS_WS_ADDR) action_elem.set('{%s}mustUnderstand' % NS_SOAP_ENV, 'true') action_elem.text = NS_WSMAN_ENUM + '/Enumerate' return header def _add_body(self, envelope): body = super(_EnumeratePayload, self)._add_body(envelope) enum_elem = ElementTree.SubElement(body, '{%s}Enumerate' % NS_WSMAN_ENUM, nsmap={'wsen': NS_WSMAN_ENUM}) if self.filter_query is not None: self._add_filter(enum_elem) if self.optimization: self._add_enum_optimization(enum_elem) return body def _add_enum_optimization(self, enum_elem): ElementTree.SubElement(enum_elem, '{%s}OptimizeEnumeration' % NS_WSMAN) max_elem_elem = ElementTree.SubElement(enum_elem, '{%s}MaxElements' % NS_WSMAN) max_elem_elem.text = str(self.max_elems) def _add_filter(self, enum_elem): filter_elem = ElementTree.SubElement(enum_elem, '{%s}Filter' % NS_WSMAN) filter_elem.set('Dialect', self.filter_dialect) filter_elem.text = self.filter_query class _PullPayload(_Payload): """Payload generation for WSMan pull operation.""" def __init__(self, endpoint, resource_uri, context, max_elems=100): self.endpoint = endpoint self.resource_uri = resource_uri self.context = context self.max_elems = max_elems def _add_header(self, envelope): header = super(_PullPayload, self)._add_header(envelope) action_elem = ElementTree.SubElement(header, '{%s}Action' % NS_WS_ADDR) action_elem.set('{%s}mustUnderstand' % NS_SOAP_ENV, 'true') action_elem.text = NS_WSMAN_ENUM + '/Pull' return header def _add_body(self, envelope): body = super(_PullPayload, self)._add_body(envelope) pull_elem = ElementTree.SubElement(body, '{%s}Pull' % NS_WSMAN_ENUM, nsmap={'wsen': NS_WSMAN_ENUM}) enum_context_elem = ElementTree.SubElement( pull_elem, '{%s}EnumerationContext' % NS_WSMAN_ENUM) enum_context_elem.text = self.context self._add_enum_optimization(pull_elem) return body def _add_enum_optimization(self, pull_elem): max_elem_elem = ElementTree.SubElement(pull_elem, '{%s}MaxElements' % NS_WSMAN) max_elem_elem.text = str(self.max_elems) class _InvokePayload(_Payload): """Payload generation for WSMan invoke operation.""" def __init__(self, endpoint, resource_uri, method, selectors=None, properties=None): self.endpoint = endpoint self.resource_uri = resource_uri self.method = method self.selectors = selectors self.properties = properties def _add_header(self, envelope): header = super(_InvokePayload, self)._add_header(envelope) action_elem = ElementTree.SubElement(header, '{%s}Action' % NS_WS_ADDR) action_elem.set('{%s}mustUnderstand' % NS_SOAP_ENV, 'true') action_elem.text = ('%(resource_uri)s/%(method)s' % {'resource_uri': self.resource_uri, 'method': self.method}) self._add_selectors(header) return header def _add_body(self, envelope): body = super(_InvokePayload, self)._add_body(envelope) self._add_properties(body) return body def _add_selectors(self, header): selector_set_elem = ElementTree.SubElement( header, '{%s}SelectorSet' % NS_WSMAN) for (name, value) in self.selectors.items(): selector_elem = ElementTree.SubElement(selector_set_elem, '{%s}Selector' % NS_WSMAN) selector_elem.set('Name', name) selector_elem.text = value def _add_properties(self, body): method_elem = ElementTree.SubElement( body, ('{%(resource_uri)s}%(method)s_INPUT' % {'resource_uri': self.resource_uri, 'method': self.method})) for (name, value) in self.properties.items(): if not isinstance(value, list): value = [value] for item in value: property_elem = ElementTree.SubElement( method_elem, ('{%(resource_uri)s}%(name)s' % {'resource_uri': self.resource_uri, 'name': name})) property_elem.text = item