Replacing pywsman with a simple wsman client
Change-Id: I3d87528f1d5286a53e6aba221766aae72513abdb
This commit is contained in:
parent
7be96d5f82
commit
d833babfe8
35
dracclient/exceptions.py
Normal file
35
dracclient/exceptions.py
Normal 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
23
dracclient/tests/base.py
Normal 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
|
278
dracclient/tests/test_wsman.py
Normal file
278
dracclient/tests/test_wsman.py
Normal 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
42
dracclient/tests/utils.py
Normal 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'),
|
||||
]
|
||||
}
|
15
dracclient/tests/wsman_mocks/wsman-enum_context-1.xml
Normal file
15
dracclient/tests/wsman_mocks/wsman-enum_context-1.xml
Normal 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>
|
22
dracclient/tests/wsman_mocks/wsman-enum_context-2.xml
Normal file
22
dracclient/tests/wsman_mocks/wsman-enum_context-2.xml
Normal 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>
|
24
dracclient/tests/wsman_mocks/wsman-enum_context-3.xml
Normal file
24
dracclient/tests/wsman_mocks/wsman-enum_context-3.xml
Normal 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>
|
21
dracclient/tests/wsman_mocks/wsman-enum_context-4.xml
Normal file
21
dracclient/tests/wsman_mocks/wsman-enum_context-4.xml
Normal 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
374
dracclient/wsman.py
Normal 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
|
@ -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
|
||||
|
@ -5,4 +5,5 @@
|
||||
coverage>=3.6
|
||||
doc8
|
||||
hacking>=0.10.0,<0.11
|
||||
mock>=1.2
|
||||
mock>=1.2
|
||||
requests-mock>=0.6
|
||||
|
Loading…
Reference in New Issue
Block a user