Skip to content

Commit 3e65e68

Browse files
committed
Merge pull request softlayer#640 from sudorandom/rest-support
Improve rest support, add total items to list results
2 parents c0ec9f7 + ae68813 commit 3e65e68

6 files changed

Lines changed: 236 additions & 42 deletions

File tree

SoftLayer/API.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,21 @@ def create_client_from_env(username=None,
9595

9696
# If we have enough information to make an auth driver, let's do it
9797
if auth is None and settings.get('username') and settings.get('api_key'):
98-
99-
auth = slauth.BasicAuthentication(
100-
settings.get('username'),
101-
settings.get('api_key'),
102-
)
98+
# NOTE(kmcdonald): some transports mask other transports, so this is
99+
# a way to find the 'real' one
100+
real_transport = getattr(transport, 'transport', transport)
101+
102+
if isinstance(real_transport, transports.XmlRpcTransport):
103+
auth = slauth.BasicAuthentication(
104+
settings.get('username'),
105+
settings.get('api_key'),
106+
)
107+
108+
elif isinstance(real_transport, transports.RestTransport):
109+
auth = slauth.BasicHTTPAuthentication(
110+
settings.get('username'),
111+
settings.get('api_key'),
112+
)
103113

104114
return BaseClient(auth=auth, transport=transport)
105115

SoftLayer/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def get_client_settings_args(**kwargs):
1919
timeout = kwargs.get('timeout')
2020
if timeout is not None:
2121
timeout = float(timeout)
22+
2223
return {
2324
'endpoint_url': kwargs.get('endpoint_url'),
2425
'timeout': timeout,

SoftLayer/managers/load_balancer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None,
204204
if ip_address_id is not None:
205205
service['ipAddressId'] = ip_address_id
206206

207-
template = {'virtualServers': virtual_servers}
207+
template = {'virtualServers': list(virtual_servers)}
208208

209209
load_balancer = self.lb_svc.editObject(template, id=loadbal_id)
210210
return load_balancer

SoftLayer/transports.py

Lines changed: 109 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
'FixtureTransport',
2929
]
3030

31+
REST_SPECIAL_METHODS = {
32+
'deleteObject': 'DELETE',
33+
'createObject': 'POST',
34+
'createObjects': 'POST',
35+
'editObject': 'PUT',
36+
'editObjects': 'PUT',
37+
}
38+
3139

3240
class Request(object):
3341
"""Transport request object."""
@@ -76,6 +84,14 @@ def __init__(self):
7684
self.offset = None
7785

7886

87+
class SoftLayerListResult(list):
88+
"""A SoftLayer list result."""
89+
90+
def __init__(self, items, total_count):
91+
self.total_count = total_count
92+
super(SoftLayerListResult, self).__init__(items)
93+
94+
7995
class XmlRpcTransport(object):
8096
"""XML-RPC transport."""
8197
def __init__(self,
@@ -104,7 +120,8 @@ def __call__(self, request):
104120
headers[header_name] = {'id': request.identifier}
105121

106122
if request.mask is not None:
107-
headers.update(_format_object_mask(request.mask, request.service))
123+
headers.update(_format_object_mask_xmlrpc(request.mask,
124+
request.service))
108125

109126
if request.filter is not None:
110127
headers['%sObjectFilter' % request.service] = request.filter
@@ -129,18 +146,23 @@ def __call__(self, request):
129146
LOGGER.debug(payload)
130147

131148
try:
132-
response = requests.request('POST', url,
133-
data=payload,
134-
headers=request.transport_headers,
135-
timeout=self.timeout,
136-
verify=request.verify,
137-
cert=request.cert,
138-
proxies=_proxies_dict(self.proxy))
149+
resp = requests.request('POST', url,
150+
data=payload,
151+
headers=request.transport_headers,
152+
timeout=self.timeout,
153+
verify=request.verify,
154+
cert=request.cert,
155+
proxies=_proxies_dict(self.proxy))
139156
LOGGER.debug("=== RESPONSE ===")
140-
LOGGER.debug(response.headers)
141-
LOGGER.debug(response.content)
142-
response.raise_for_status()
143-
return utils.xmlrpc_client.loads(response.content)[0][0]
157+
LOGGER.debug(resp.headers)
158+
LOGGER.debug(resp.content)
159+
resp.raise_for_status()
160+
result = utils.xmlrpc_client.loads(resp.content)[0][0]
161+
if isinstance(result, list):
162+
return SoftLayerListResult(
163+
result, int(resp.headers.get('softlayer-total-items', 0)))
164+
else:
165+
return result
144166
except utils.xmlrpc_client.Fault as ex:
145167
# These exceptions are formed from the XML-RPC spec
146168
# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
@@ -190,26 +212,68 @@ def __call__(self, request):
190212
191213
:param request request: Request object
192214
"""
215+
request.transport_headers.setdefault('Content-Type',
216+
'application/json')
217+
request.transport_headers.setdefault('User-Agent', self.user_agent)
218+
219+
params = request.headers.copy()
220+
if request.mask:
221+
params['objectMask'] = _format_object_mask(request.mask)
222+
223+
if request.limit:
224+
params['limit'] = request.limit
225+
226+
if request.offset:
227+
params['offset'] = request.offset
228+
229+
if request.filter:
230+
params['objectFilter'] = json.dumps(request.filter)
231+
232+
auth = None
233+
if request.transport_user:
234+
auth = requests.auth.HTTPBasicAuth(
235+
request.transport_user,
236+
request.transport_password,
237+
)
238+
239+
method = REST_SPECIAL_METHODS.get(request.method)
240+
is_special_method = True
241+
if method is None:
242+
is_special_method = False
243+
method = 'GET'
244+
245+
body = {}
246+
if request.args:
247+
# NOTE(kmcdonald): force POST when there are arguments because
248+
# the request body is ignored otherwise.
249+
method = 'POST'
250+
body['parameters'] = request.args
251+
252+
raw_body = None
253+
if body:
254+
raw_body = json.dumps(body)
255+
193256
url_parts = [self.endpoint_url, request.service]
194257
if request.identifier is not None:
195258
url_parts.append(str(request.identifier))
196-
if request.method is not None:
197-
url_parts.append(request.method)
198-
for arg in request.args:
199-
url_parts.append(str(arg))
200259

201-
request.transport_headers.setdefault('Content-Type',
202-
'application/json')
203-
request.transport_headers.setdefault('User-Agent', self.user_agent)
260+
# Special methods (createObject, editObject, etc) use the HTTP verb
261+
# to determine the action on the resource
262+
if request.method is not None and not is_special_method:
263+
url_parts.append(request.method)
204264

205265
url = '%s.%s' % ('/'.join(url_parts), 'json')
206266

207267
LOGGER.debug("=== REQUEST ===")
208268
LOGGER.info(url)
209269
LOGGER.debug(request.transport_headers)
270+
LOGGER.debug(raw_body)
210271
try:
211-
resp = requests.request('GET', url,
272+
resp = requests.request(method, url,
273+
auth=auth,
212274
headers=request.transport_headers,
275+
params=params,
276+
data=raw_body,
213277
timeout=self.timeout,
214278
verify=request.verify,
215279
cert=request.cert,
@@ -218,7 +282,13 @@ def __call__(self, request):
218282
LOGGER.debug(resp.headers)
219283
LOGGER.debug(resp.content)
220284
resp.raise_for_status()
221-
return json.loads(resp.content)
285+
result = json.loads(resp.content)
286+
287+
if isinstance(result, list):
288+
return SoftLayerListResult(
289+
result, int(resp.headers.get('softlayer-total-items', 0)))
290+
else:
291+
return result
222292
except requests.HTTPError as ex:
223293
content = json.loads(ex.response.content)
224294
raise exceptions.SoftLayerAPIError(ex.response.status_code,
@@ -279,7 +349,7 @@ def _proxies_dict(proxy):
279349
return {'http': proxy, 'https': proxy}
280350

281351

282-
def _format_object_mask(objectmask, service):
352+
def _format_object_mask_xmlrpc(objectmask, service):
283353
"""Format new and old style object masks into proper headers.
284354
285355
:param objectmask: a string- or dict-based object mask
@@ -290,10 +360,22 @@ def _format_object_mask(objectmask, service):
290360
mheader = '%sObjectMask' % service
291361
else:
292362
mheader = 'SoftLayer_ObjectMask'
293-
294-
objectmask = objectmask.strip()
295-
if (not objectmask.startswith('mask') and
296-
not objectmask.startswith('[')):
297-
objectmask = "mask[%s]" % objectmask
363+
objectmask = _format_object_mask(objectmask)
298364

299365
return {mheader: {'mask': objectmask}}
366+
367+
368+
def _format_object_mask(objectmask):
369+
"""Format the new style object mask.
370+
371+
This wraps the user mask with mask[USER_MASK] if it does not already
372+
have one. This makes it slightly easier for users.
373+
374+
:param objectmask: a string-based object mask
375+
376+
"""
377+
objectmask = objectmask.strip()
378+
if (not objectmask.startswith('mask') and
379+
not objectmask.startswith('[')):
380+
objectmask = "mask[%s]" % objectmask
381+
return objectmask

docs/dev/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ versions every time you push new code.
7575

7676
Using tox to running the tests in multiple environments can be very time
7777
consuming. If you wish to quickly run the tests in your own environment, you
78-
may do so using `nose <https://nose.readthedocs.org>`_. The command to do that
78+
may do so using `py.test <http://pytest.org/>`_. The command to do that
7979
is:
8080

8181
::
8282

83-
nosetests
83+
py.test tests
8484

8585

8686
Documentation

0 commit comments

Comments
 (0)