Replacing pywsman with a simple wsman client

Change-Id: I3d87528f1d5286a53e6aba221766aae72513abdb
This commit is contained in:
Imre Farkas 2015-09-11 16:08:45 +02:00
parent 7be96d5f82
commit d833babfe8
11 changed files with 838 additions and 2 deletions

35
dracclient/exceptions.py Normal file
View File

@ -0,0 +1,35 @@
#
# 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.
class BaseClientException(Exception):
msg_fmt = 'An unknown exception occurred'
def __init__(self, message=None, **kwargs):
message = self.msg_fmt % kwargs
super(BaseClientException, self).__init__(message)
class WSManRequestFailure(BaseClientException):
msg_fmt = ('WSMan request failed.')
class WSManInvalidResponse(BaseClientException):
msg_fmt = ('Invalid response received. Status code: "%(status_code)s", '
'reason: "%(reason)s"')
class WSManInvalidFilterDialect(BaseClientException):
msg_fmt = ('Invalid filter dialect "%(invalid_filter)s". '
'Supported options are %(supported)s')

23
dracclient/tests/base.py Normal file
View File

@ -0,0 +1,23 @@
#
# 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 sys
import unittest
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
class BaseTest(unittest.TestCase):
pass

View File

@ -0,0 +1,278 @@
#
# 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 collections
import uuid
import lxml.etree
import lxml.objectify
import mock
import requests_mock
from dracclient import exceptions
from dracclient.tests import base
from dracclient.tests import utils as test_utils
import dracclient.wsman
class ClientTestCase(base.BaseTest):
def setUp(self):
super(ClientTestCase, self).setUp()
self.client = dracclient.wsman.Client(**test_utils.FAKE_ENDPOINT)
@requests_mock.Mocker()
def test_enumerate(self, mock_requests):
expected_resp = '<result>yay!</result>'
mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)
resp = self.client.enumerate('resource', auto_pull=False)
self.assertEqual('yay!', resp.text)
def test_enumerate_with_request_failure(self):
self.client = dracclient.wsman.Client('malformed://^@*', 'user',
'pass')
self.assertRaises(exceptions.WSManRequestFailure,
self.client.enumerate, 'resource')
@requests_mock.Mocker()
def test_enumerate_with_invalid_status_code(self, mock_requests):
mock_requests.post('https://1.2.3.4:443/wsman', status_code=500,
reason='dumb request')
self.assertRaises(exceptions.WSManInvalidResponse,
self.client.enumerate, 'resource')
@requests_mock.Mocker()
def test_enumerate_with_auto_pull(self, mock_requests):
mock_requests.post(
'https://1.2.3.4:443/wsman',
[{'text': test_utils.WSManEnumerations['context'][0]},
{'text': test_utils.WSManEnumerations['context'][1]},
{'text': test_utils.WSManEnumerations['context'][2]},
{'text': test_utils.WSManEnumerations['context'][3]}])
resp_xml = self.client.enumerate('FooResource')
foo_resource_uri = 'http://FooResource'
bar_resource_uri = 'http://BarResource'
self.assertEqual(
3, len(resp_xml.findall('.//{%s}FooResource' % foo_resource_uri)))
self.assertEqual(
1, len(resp_xml.findall('.//{%s}BazResource' % bar_resource_uri)))
self.assertEqual(
0, len(resp_xml.findall(
'.//{%s}EnumerationContext' % dracclient.wsman.NS_WSMAN_ENUM)))
@requests_mock.Mocker()
@mock.patch.object(dracclient.wsman.Client, 'pull', autospec=True)
def test_enumerate_with_auto_pull_without_optimization(self, mock_requests,
mock_pull):
mock_requests.post('https://1.2.3.4:443/wsman',
text=test_utils.WSManEnumerations['context'][0])
mock_pull.return_value = lxml.etree.fromstring(
test_utils.WSManEnumerations['context'][3])
self.client.enumerate('FooResource', optimization=False, max_elems=42)
mock_pull.assert_called_once_with(self.client, 'FooResource',
'enum-context-uuid', 42)
@requests_mock.Mocker()
def test_pull(self, mock_requests):
expected_resp = '<result>yay!</result>'
mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)
resp = self.client.pull('resource', 'context-uuid')
self.assertEqual('yay!', resp.text)
@requests_mock.Mocker()
def test_invoke(self, mock_requests):
expected_resp = '<result>yay!</result>'
mock_requests.post('https://1.2.3.4:443/wsman', text=expected_resp)
resp = self.client.invoke('http://resource', 'method',
{'selector': 'foo'}, {'property': 'bar'})
self.assertEqual('yay!', resp.text)
class PayloadTestCase(base.BaseTest):
def setUp(self):
super(PayloadTestCase, self).setUp()
dracclient.wsman.NS_MAP = collections.OrderedDict([
('s', dracclient.wsman.NS_SOAP_ENV),
('wsa', dracclient.wsman.NS_WS_ADDR),
('wsman', dracclient.wsman.NS_WSMAN)])
@mock.patch.object(uuid, 'uuid4', autospec=True)
def test_build_enum(self, mock_uuid):
expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
<wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
<wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</wsa:Action>
</s:Header>
<s:Body>
<wsen:Enumerate xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
<wsman:OptimizeEnumeration/>
<wsman:MaxElements>100</wsman:MaxElements>
</wsen:Enumerate>
</s:Body>
</s:Envelope>
""" # noqa
expected_payload_obj = lxml.objectify.fromstring(expected_payload)
mock_uuid.return_value = '1234-12'
payload = dracclient.wsman._EnumeratePayload(
'http://host:443/wsman', 'http://resource_uri').build()
payload_obj = lxml.objectify.fromstring(payload)
self.assertEqual(lxml.etree.tostring(expected_payload_obj),
lxml.etree.tostring(payload_obj))
def test_enumerate_without_optimization(self):
payload = dracclient.wsman._EnumeratePayload(
'http://host:443/wsman', 'http://resource_uri', optimization=False,
max_elems=42).build()
payload_xml = lxml.etree.fromstring(payload)
optimize_enum_elems = payload_xml.findall(
'.//{%s}OptimizeEnumeration' % dracclient.wsman.NS_WSMAN)
max_elem_elems = payload_xml.findall(
'.//{%s}MaxElements' % dracclient.wsman.NS_WSMAN)
self.assertEqual([], optimize_enum_elems)
self.assertEqual([], max_elem_elems)
@mock.patch.object(uuid, 'uuid4', autospec=True)
def test_build_enum_with_filter(self, mock_uuid):
expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
<wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
<wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</wsa:Action>
</s:Header>
<s:Body>
<wsen:Enumerate xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
<wsman:Filter Dialect="http://schemas.dmtf.org/wbem/cql/1/dsp0202.pdf">DROP TABLE users</wsman:Filter>
<wsman:OptimizeEnumeration/>
<wsman:MaxElements>100</wsman:MaxElements>
</wsen:Enumerate>
</s:Body>
</s:Envelope>
""" # noqa
expected_payload_obj = lxml.objectify.fromstring(expected_payload)
mock_uuid.return_value = '1234-12'
payload = dracclient.wsman._EnumeratePayload(
'http://host:443/wsman', 'http://resource_uri',
filter_query='DROP TABLE users', filter_dialect='cql').build()
payload_obj = lxml.objectify.fromstring(payload)
self.assertEqual(lxml.etree.tostring(expected_payload_obj),
lxml.etree.tostring(payload_obj))
def test_build_enum_with_invalid_filter_dialect(self):
invalid_dialect = 'foo'
self.assertRaises(exceptions.WSManInvalidFilterDialect,
dracclient.wsman._EnumeratePayload,
'http://host:443/wsman', 'http://resource_uri',
filter_query='DROP TABLE users',
filter_dialect=invalid_dialect)
@mock.patch.object(uuid, 'uuid4', autospec=True)
def test_build_pull(self, mock_uuid):
expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
<wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
<wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull</wsa:Action>
</s:Header>
<s:Body>
<wsen:Pull xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
<wsen:EnumerationContext>context-uuid</wsen:EnumerationContext>
<wsman:MaxElements>100</wsman:MaxElements>
</wsen:Pull>
</s:Body>
</s:Envelope>
""" # noqa
expected_payload_obj = lxml.objectify.fromstring(expected_payload)
mock_uuid.return_value = '1234-12'
payload = dracclient.wsman._PullPayload('http://host:443/wsman',
'http://resource_uri',
'context-uuid').build()
payload_obj = lxml.objectify.fromstring(payload)
self.assertEqual(lxml.etree.tostring(expected_payload_obj),
lxml.etree.tostring(payload_obj))
@mock.patch.object(uuid, 'uuid4', autospec=True)
def test_build_invoke(self, mock_uuid):
expected_payload = """<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<wsa:To s:mustUnderstand="true">http://host:443/wsman</wsa:To>
<wsman:ResourceURI s:mustUnderstand="true">http://resource_uri</wsman:ResourceURI>
<wsa:MessageID s:mustUnderstand="true">uuid:1234-12</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:Action s:mustUnderstand="true">http://resource_uri/method</wsa:Action>
<wsman:SelectorSet>
<wsman:Selector Name="selector">foo</wsman:Selector>
</wsman:SelectorSet>
</s:Header>
<s:Body>
<ns0:method_INPUT xmlns:ns0="http://resource_uri">
<ns0:property>bar</ns0:property>
</ns0:method_INPUT>
</s:Body>
</s:Envelope>
""" # noqa
expected_payload_obj = lxml.objectify.fromstring(expected_payload)
mock_uuid.return_value = '1234-12'
payload = dracclient.wsman._InvokePayload(
'http://host:443/wsman', 'http://resource_uri', 'method',
{'selector': 'foo'}, {'property': 'bar'}).build()
payload_obj = lxml.objectify.fromstring(payload)
self.assertEqual(lxml.etree.tostring(expected_payload_obj),
lxml.etree.tostring(payload_obj))

42
dracclient/tests/utils.py Normal file
View File

@ -0,0 +1,42 @@
#
# 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 os
FAKE_ENDPOINT = {
'host': '1.2.3.4',
'port': '443',
'path': '/wsman',
'protocol': 'https',
'username': 'admin',
'password': 's3cr3t'
}
def load_wsman_xml(name):
"""Helper function to load a WSMan XML response from a file."""
with open(os.path.join(os.path.dirname(__file__), 'wsman_mocks',
'%s.xml' % name), 'r') as f:
xml_body = f.read()
return xml_body
WSManEnumerations = {
'context': [
load_wsman_xml('wsman-enum_context-1'),
load_wsman_xml('wsman-enum_context-2'),
load_wsman_xml('wsman-enum_context-3'),
load_wsman_xml('wsman-enum_context-4'),
]
}

View File

@ -0,0 +1,15 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/enumeration/EnumerateResponse</wsa:Action>
<wsa:RelatesTo>uuid:89afbea0-2005-1005-8002-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:babd467b-2009-1009-8096-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<wsen:EnumerateResponse>
<wsen:EnumerationContext>enum-context-uuid</wsen:EnumerationContext>
</wsen:EnumerateResponse>
</s:Body>
</s:Envelope>

View File

@ -0,0 +1,22 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:n1="http://FooResource"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</wsa:Action>
<wsa:RelatesTo>uuid:89ec4d7c-2005-1005-8003-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:bac57212-2009-1009-8097-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<wsen:PullResponse>
<wsen:EnumerationContext>enum-context-uuid</wsen:EnumerationContext>
<wsen:Items>
<n1:FooResource>
<n1:InstanceID>1</n1:InstanceID>
</n1:FooResource>
</wsen:Items>
</wsen:PullResponse>
</s:Body>
</s:Envelope>

View File

@ -0,0 +1,24 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:n1="http://FooResource">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</wsa:Action>
<wsa:RelatesTo>uuid:89f3b75a-2005-1005-8004-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:baccc4b1-2009-1009-8098-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<wsen:PullResponse>
<wsen:EnumerationContext>enum-context-uuid</wsen:EnumerationContext>
<wsen:Items>
<n1:FooResource>
<n1:InstanceID>2</n1:InstanceID>
</n1:FooResource>
<n1:FooResource>
<n1:InstanceID>3</n1:InstanceID>
</n1:FooResource>
</wsen:Items>
</wsen:PullResponse>
</s:Body>
</s:Envelope>

View File

@ -0,0 +1,21 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:n1="http://BarResource">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</wsa:Action>
<wsa:RelatesTo>uuid:8b0bcd65-2005-1005-8026-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:bbe513cd-2009-1009-80ba-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<wsen:PullResponse>
<wsen:Items>
<n1:BazResource>
<n1:InstanceID>4</n1:InstanceID>
</n1:BazResource>
</wsen:Items>
<wsen:EndOfSequence/>
</wsen:PullResponse>
</s:Body>
</s:Envelope>

374
dracclient/wsman.py Normal file
View File

@ -0,0 +1,374 @@
#
# 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 uuid
from lxml import etree as ElementTree
import requests
import requests.exceptions
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'):
self.host = host
self.username = username
self.password = password
self.port = port
self.path = path
self.protocol = protocol
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})
try:
resp = requests.post(
self.endpoint,
auth=requests.auth.HTTPBasicAuth(self.username, self.password),
data=payload,
# TODO(ifarkas): enable cert verification
verify=False)
except requests.exceptions.RequestException:
LOG.exception('Request failed')
raise exceptions.WSManRequestFailure()
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)
resp_xml = ElementTree.fromstring(resp.content)
if auto_pull:
find_items_query = './/{%s}Items' % NS_WSMAN_ENUM
full_resp_xml = resp_xml
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)
items_xml = full_resp_xml.find(find_items_query)
if items_xml is not None:
# merge enumeration items
for item in resp_xml.find(find_items_query):
items_xml.append(item)
else:
full_resp_xml = resp_xml
# 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():
property_elem = ElementTree.SubElement(
method_elem,
('{%(resource_uri)s}%(name)s' %
{'resource_uri': self.resource_uri,
'name': name}))
property_elem.text = value

View File

@ -2,4 +2,5 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pywsman>=2.3
lxml>=2.3
requests>=2.5.2

View File

@ -6,3 +6,4 @@ coverage>=3.6
doc8
hacking>=0.10.0,<0.11
mock>=1.2
requests-mock>=0.6