Skip to content

Commit 62ef9ab

Browse files
#1602 groundwork for adding a SOAP style client
1 parent 1f0bf76 commit 62ef9ab

19 files changed

Lines changed: 1818 additions & 1381 deletions

SoftLayer/transports.py

Lines changed: 0 additions & 589 deletions
This file was deleted.

SoftLayer/transports/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
SoftLayer.transports
3+
~~~~~~~~~~~~~~~~~~~~
4+
XML-RPC transport layer that uses the requests library.
5+
6+
:license: MIT, see LICENSE for more details.
7+
"""
8+
9+
10+
import requests
11+
12+
13+
# Required imports to not break existing code.
14+
from .rest import RestTransport
15+
from .xmlrpc import XmlRpcTransport
16+
from .fixture import FixtureTransport
17+
from .timing import TimingTransport
18+
from .debug import DebugTransport
19+
20+
from .transport import Request
21+
from .transport import SoftLayerListResult as SoftLayerListResult
22+
23+
24+
# transports.Request does have a lot of instance attributes. :(
25+
# pylint: disable=too-many-instance-attributes, no-self-use
26+
27+
__all__ = [
28+
'Request',
29+
'XmlRpcTransport',
30+
'RestTransport',
31+
'TimingTransport',
32+
'DebugTransport',
33+
'FixtureTransport',
34+
'SoftLayerListResult'
35+
]
36+
37+
38+
39+
40+
41+
42+
43+
44+
45+

SoftLayer/transports/debug.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
SoftLayer.transports.debug
3+
~~~~~~~~~~~~~~~~~~~~
4+
Debugging transport. Will print out verbose logging information.
5+
6+
:license: MIT, see LICENSE for more details.
7+
"""
8+
9+
import logging
10+
import time
11+
12+
from SoftLayer import exceptions
13+
14+
15+
class DebugTransport(object):
16+
"""Transport that records API call timings."""
17+
18+
def __init__(self, transport):
19+
self.transport = transport
20+
21+
#: List All API calls made during a session
22+
self.requests = []
23+
self.logger = logging.getLogger(__name__)
24+
25+
def __call__(self, call):
26+
call.start_time = time.time()
27+
28+
self.pre_transport_log(call)
29+
try:
30+
call.result = self.transport(call)
31+
except (exceptions.SoftLayerAPIError, exceptions.TransportError) as ex:
32+
call.exception = ex
33+
34+
self.post_transport_log(call)
35+
36+
call.end_time = time.time()
37+
self.requests.append(call)
38+
39+
if call.exception is not None:
40+
self.logger.debug(self.print_reproduceable(call))
41+
raise call.exception
42+
43+
return call.result
44+
45+
def pre_transport_log(self, call):
46+
"""Prints a warning before calling the API """
47+
output = "Calling: {})".format(call)
48+
self.logger.warning(output)
49+
50+
def post_transport_log(self, call):
51+
"""Prints the result "Returned Data: \n%s" % (call.result)of an API call"""
52+
output = "Returned Data: \n{}".format(call.result)
53+
self.logger.debug(output)
54+
55+
def get_last_calls(self):
56+
"""Returns all API calls for a session"""
57+
return self.requests
58+
59+
def print_reproduceable(self, call):
60+
"""Prints a reproduceable debugging output"""
61+
return self.transport.print_reproduceable(call)

SoftLayer/transports/fixture.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
SoftLayer.transports.fixture
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
Fixture transport, used for unit tests
5+
6+
:license: MIT, see LICENSE for more details.
7+
"""
8+
9+
import importlib
10+
11+
class FixtureTransport(object):
12+
"""Implements a transport which returns fixtures."""
13+
14+
def __call__(self, call):
15+
"""Load fixture from the default fixture path."""
16+
try:
17+
module_path = 'SoftLayer.fixtures.%s' % call.service
18+
module = importlib.import_module(module_path)
19+
except ImportError as ex:
20+
message = '{} fixture is not implemented'.format(call.service)
21+
raise NotImplementedError(message) from ex
22+
try:
23+
return getattr(module, call.method)
24+
except AttributeError as ex:
25+
message = '{}::{} fixture is not implemented'.format(call.service, call.method)
26+
raise NotImplementedError(message) from ex
27+
28+
def print_reproduceable(self, call):
29+
"""Not Implemented"""
30+
return call.service

SoftLayer/transports/rest.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
SoftLayer.transports.rest
3+
~~~~~~~~~~~~~~~~~~~~
4+
REST Style transport library
5+
6+
:license: MIT, see LICENSE for more details.
7+
"""
8+
9+
import json
10+
import logging
11+
import requests
12+
13+
from SoftLayer import consts
14+
from SoftLayer import exceptions
15+
16+
from .transport import _format_object_mask
17+
from .transport import _proxies_dict
18+
from .transport import ComplexEncoder
19+
from .transport import get_session
20+
from .transport import SoftLayerListResult
21+
22+
REST_SPECIAL_METHODS = {
23+
# 'deleteObject': 'DELETE',
24+
'createObject': 'POST',
25+
'createObjects': 'POST',
26+
'editObject': 'PUT',
27+
'editObjects': 'PUT',
28+
}
29+
30+
31+
class RestTransport(object):
32+
"""REST transport.
33+
34+
REST calls should mostly work, but is not fully tested.
35+
XML-RPC should be used when in doubt
36+
"""
37+
38+
def __init__(self, endpoint_url=None, timeout=None, proxy=None, user_agent=None, verify=True):
39+
40+
self.endpoint_url = (endpoint_url or consts.API_PUBLIC_ENDPOINT_REST).rstrip('/')
41+
self.timeout = timeout or None
42+
self.proxy = proxy
43+
self.user_agent = user_agent or consts.USER_AGENT
44+
self.verify = verify
45+
self._client = None
46+
self.logger = logging.getLogger(__name__)
47+
48+
@property
49+
def client(self):
50+
"""Returns client session object"""
51+
52+
if self._client is None:
53+
self._client = get_session(self.user_agent)
54+
return self._client
55+
56+
def __call__(self, request):
57+
"""Makes a SoftLayer API call against the REST endpoint.
58+
59+
REST calls should mostly work, but is not fully tested.
60+
XML-RPC should be used when in doubt
61+
62+
:param request request: Request object
63+
"""
64+
params = request.headers.copy()
65+
if request.mask:
66+
request.mask = _format_object_mask(request.mask)
67+
params['objectMask'] = request.mask
68+
69+
if request.limit or request.offset:
70+
limit = request.limit or 0
71+
offset = request.offset or 0
72+
params['resultLimit'] = "%d,%d" % (offset, limit)
73+
74+
if request.filter:
75+
params['objectFilter'] = json.dumps(request.filter)
76+
77+
request.params = params
78+
79+
auth = None
80+
if request.transport_user:
81+
auth = requests.auth.HTTPBasicAuth(
82+
request.transport_user,
83+
request.transport_password,
84+
)
85+
86+
method = REST_SPECIAL_METHODS.get(request.method)
87+
88+
if method is None:
89+
method = 'GET'
90+
91+
body = {}
92+
if request.args:
93+
# NOTE(kmcdonald): force POST when there are arguments because
94+
# the request body is ignored otherwise.
95+
method = 'POST'
96+
body['parameters'] = request.args
97+
98+
if body:
99+
request.payload = json.dumps(body, cls=ComplexEncoder)
100+
101+
url_parts = [self.endpoint_url, request.service]
102+
if request.identifier is not None:
103+
url_parts.append(str(request.identifier))
104+
105+
if request.method is not None:
106+
url_parts.append(request.method)
107+
108+
request.url = '%s.%s' % ('/'.join(url_parts), 'json')
109+
110+
# Prefer the request setting, if it's not None
111+
112+
if request.verify is None:
113+
request.verify = self.verify
114+
115+
try:
116+
resp = self.client.request(method, request.url,
117+
auth=auth,
118+
headers=request.transport_headers,
119+
params=request.params,
120+
data=request.payload,
121+
timeout=self.timeout,
122+
verify=request.verify,
123+
cert=request.cert,
124+
proxies=_proxies_dict(self.proxy))
125+
126+
request.url = resp.url
127+
128+
resp.raise_for_status()
129+
130+
if resp.text != "":
131+
try:
132+
result = json.loads(resp.text)
133+
except ValueError as json_ex:
134+
self.logger.warning(json_ex)
135+
raise exceptions.SoftLayerAPIError(resp.status_code, str(resp.text))
136+
else:
137+
raise exceptions.SoftLayerAPIError(resp.status_code, "Empty response.")
138+
139+
request.result = result
140+
141+
if isinstance(result, list):
142+
return SoftLayerListResult(
143+
result, int(resp.headers.get('softlayer-total-items', 0)))
144+
else:
145+
return result
146+
except requests.HTTPError as ex:
147+
try:
148+
message = json.loads(ex.response.text)['error']
149+
request.url = ex.response.url
150+
except ValueError as json_ex:
151+
if ex.response.text == "":
152+
raise exceptions.SoftLayerAPIError(resp.status_code, "Empty response.")
153+
self.logger.warning(json_ex)
154+
raise exceptions.SoftLayerAPIError(resp.status_code, ex.response.text)
155+
156+
raise exceptions.SoftLayerAPIError(ex.response.status_code, message)
157+
except requests.RequestException as ex:
158+
raise exceptions.TransportError(0, str(ex))
159+
160+
def print_reproduceable(self, request):
161+
"""Prints out the minimal python code to reproduce a specific request
162+
163+
The will also automatically replace the API key so its not accidently exposed.
164+
165+
:param request request: Request object
166+
"""
167+
command = "curl -u $SL_USER:$SL_APIKEY -X {method} -H {headers} {data} '{uri}'"
168+
169+
method = REST_SPECIAL_METHODS.get(request.method)
170+
171+
if method is None:
172+
method = 'GET'
173+
if request.args:
174+
method = 'POST'
175+
176+
data = ''
177+
if request.payload is not None:
178+
data = "-d '{}'".format(request.payload)
179+
180+
headers = ['"{0}: {1}"'.format(k, v) for k, v in request.transport_headers.items()]
181+
headers = " -H ".join(headers)
182+
return command.format(method=method, headers=headers, data=data, uri=request.url)

0 commit comments

Comments
 (0)