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.
447 lines
17 KiB
447 lines
17 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. |
|
|
|
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 "<wsman:Items>" |
|
find_items_wsman_query = './/{%s}Items' % NS_WSMAN |
|
|
|
# Successive pulls return "<wsen:Items>" |
|
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
|
|
|