Skip to content

Commit

Permalink
Merge pull request #640 from sudorandom/rest-support
Browse files Browse the repository at this point in the history
Improve rest support, add total items to list results
  • Loading branch information
briancline committed Nov 20, 2015
2 parents c0ec9f7 + ae68813 commit 3e65e68
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 42 deletions.
20 changes: 15 additions & 5 deletions SoftLayer/API.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,21 @@ def create_client_from_env(username=None,

# If we have enough information to make an auth driver, let's do it
if auth is None and settings.get('username') and settings.get('api_key'):

auth = slauth.BasicAuthentication(
settings.get('username'),
settings.get('api_key'),
)
# NOTE(kmcdonald): some transports mask other transports, so this is
# a way to find the 'real' one
real_transport = getattr(transport, 'transport', transport)

if isinstance(real_transport, transports.XmlRpcTransport):
auth = slauth.BasicAuthentication(
settings.get('username'),
settings.get('api_key'),
)

elif isinstance(real_transport, transports.RestTransport):
auth = slauth.BasicHTTPAuthentication(
settings.get('username'),
settings.get('api_key'),
)

return BaseClient(auth=auth, transport=transport)

Expand Down
1 change: 1 addition & 0 deletions SoftLayer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def get_client_settings_args(**kwargs):
timeout = kwargs.get('timeout')
if timeout is not None:
timeout = float(timeout)

return {
'endpoint_url': kwargs.get('endpoint_url'),
'timeout': timeout,
Expand Down
2 changes: 1 addition & 1 deletion SoftLayer/managers/load_balancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def edit_service(self, loadbal_id, service_id, ip_address_id=None,
if ip_address_id is not None:
service['ipAddressId'] = ip_address_id

template = {'virtualServers': virtual_servers}
template = {'virtualServers': list(virtual_servers)}

load_balancer = self.lb_svc.editObject(template, id=loadbal_id)
return load_balancer
Expand Down
136 changes: 109 additions & 27 deletions SoftLayer/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
'FixtureTransport',
]

REST_SPECIAL_METHODS = {
'deleteObject': 'DELETE',
'createObject': 'POST',
'createObjects': 'POST',
'editObject': 'PUT',
'editObjects': 'PUT',
}


class Request(object):
"""Transport request object."""
Expand Down Expand Up @@ -76,6 +84,14 @@ def __init__(self):
self.offset = None


class SoftLayerListResult(list):
"""A SoftLayer list result."""

def __init__(self, items, total_count):
self.total_count = total_count
super(SoftLayerListResult, self).__init__(items)


class XmlRpcTransport(object):
"""XML-RPC transport."""
def __init__(self,
Expand Down Expand Up @@ -104,7 +120,8 @@ def __call__(self, request):
headers[header_name] = {'id': request.identifier}

if request.mask is not None:
headers.update(_format_object_mask(request.mask, request.service))
headers.update(_format_object_mask_xmlrpc(request.mask,
request.service))

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

try:
response = requests.request('POST', url,
data=payload,
headers=request.transport_headers,
timeout=self.timeout,
verify=request.verify,
cert=request.cert,
proxies=_proxies_dict(self.proxy))
resp = requests.request('POST', url,
data=payload,
headers=request.transport_headers,
timeout=self.timeout,
verify=request.verify,
cert=request.cert,
proxies=_proxies_dict(self.proxy))
LOGGER.debug("=== RESPONSE ===")
LOGGER.debug(response.headers)
LOGGER.debug(response.content)
response.raise_for_status()
return utils.xmlrpc_client.loads(response.content)[0][0]
LOGGER.debug(resp.headers)
LOGGER.debug(resp.content)
resp.raise_for_status()
result = utils.xmlrpc_client.loads(resp.content)[0][0]
if isinstance(result, list):
return SoftLayerListResult(
result, int(resp.headers.get('softlayer-total-items', 0)))
else:
return result
except utils.xmlrpc_client.Fault as ex:
# These exceptions are formed from the XML-RPC spec
# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
Expand Down Expand Up @@ -190,26 +212,68 @@ def __call__(self, request):
:param request request: Request object
"""
request.transport_headers.setdefault('Content-Type',
'application/json')
request.transport_headers.setdefault('User-Agent', self.user_agent)

params = request.headers.copy()
if request.mask:
params['objectMask'] = _format_object_mask(request.mask)

if request.limit:
params['limit'] = request.limit

if request.offset:
params['offset'] = request.offset

if request.filter:
params['objectFilter'] = json.dumps(request.filter)

auth = None
if request.transport_user:
auth = requests.auth.HTTPBasicAuth(
request.transport_user,
request.transport_password,
)

method = REST_SPECIAL_METHODS.get(request.method)
is_special_method = True
if method is None:
is_special_method = False
method = 'GET'

body = {}
if request.args:
# NOTE(kmcdonald): force POST when there are arguments because
# the request body is ignored otherwise.
method = 'POST'
body['parameters'] = request.args

raw_body = None
if body:
raw_body = json.dumps(body)

url_parts = [self.endpoint_url, request.service]
if request.identifier is not None:
url_parts.append(str(request.identifier))
if request.method is not None:
url_parts.append(request.method)
for arg in request.args:
url_parts.append(str(arg))

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

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

LOGGER.debug("=== REQUEST ===")
LOGGER.info(url)
LOGGER.debug(request.transport_headers)
LOGGER.debug(raw_body)
try:
resp = requests.request('GET', url,
resp = requests.request(method, url,
auth=auth,
headers=request.transport_headers,
params=params,
data=raw_body,
timeout=self.timeout,
verify=request.verify,
cert=request.cert,
Expand All @@ -218,7 +282,13 @@ def __call__(self, request):
LOGGER.debug(resp.headers)
LOGGER.debug(resp.content)
resp.raise_for_status()
return json.loads(resp.content)
result = json.loads(resp.content)

if isinstance(result, list):
return SoftLayerListResult(
result, int(resp.headers.get('softlayer-total-items', 0)))
else:
return result
except requests.HTTPError as ex:
content = json.loads(ex.response.content)
raise exceptions.SoftLayerAPIError(ex.response.status_code,
Expand Down Expand Up @@ -279,7 +349,7 @@ def _proxies_dict(proxy):
return {'http': proxy, 'https': proxy}


def _format_object_mask(objectmask, service):
def _format_object_mask_xmlrpc(objectmask, service):
"""Format new and old style object masks into proper headers.
:param objectmask: a string- or dict-based object mask
Expand All @@ -290,10 +360,22 @@ def _format_object_mask(objectmask, service):
mheader = '%sObjectMask' % service
else:
mheader = 'SoftLayer_ObjectMask'

objectmask = objectmask.strip()
if (not objectmask.startswith('mask') and
not objectmask.startswith('[')):
objectmask = "mask[%s]" % objectmask
objectmask = _format_object_mask(objectmask)

return {mheader: {'mask': objectmask}}


def _format_object_mask(objectmask):
"""Format the new style object mask.
This wraps the user mask with mask[USER_MASK] if it does not already
have one. This makes it slightly easier for users.
:param objectmask: a string-based object mask
"""
objectmask = objectmask.strip()
if (not objectmask.startswith('mask') and
not objectmask.startswith('[')):
objectmask = "mask[%s]" % objectmask
return objectmask
4 changes: 2 additions & 2 deletions docs/dev/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ versions every time you push new code.

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

::

nosetests
py.test tests


Documentation
Expand Down
Loading

0 comments on commit 3e65e68

Please sign in to comment.