summaryrefslogtreecommitdiff
path: root/openstackclient/common
diff options
context:
space:
mode:
authorDean Troyer <dtroyer@gmail.com>2013-08-13 17:14:42 -0500
committerDean Troyer <dtroyer@gmail.com>2013-08-23 12:08:32 -0500
commit17f13f7bf4cea80e8e1380fbc8295318de5be383 (patch)
tree611a7a512eae39341911db529b36035d737730e2 /openstackclient/common
parentb440986e6e0e90c220fe3e52f893bc5dd51cae5a (diff)
downloadpython-openstackclient-17f13f7bf4cea80e8e1380fbc8295318de5be383.tar.gz
Create a new base REST API interface
* restapi module provides basic REST API support * uses dicts rather than Resource classes * JSON serialization/deserialization * log requests in 'curl' format * basic API boilerplate for create/delete/list/set/show verbs * ignore H302 due to urllib import Change-Id: I3cb91e44e631ee19e9f5dea19b6bac5d599d19ce
Diffstat (limited to 'openstackclient/common')
-rw-r--r--openstackclient/common/restapi.py188
-rw-r--r--openstackclient/common/utils.py24
2 files changed, 212 insertions, 0 deletions
diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py
new file mode 100644
index 00000000..4cea5a06
--- /dev/null
+++ b/openstackclient/common/restapi.py
@@ -0,0 +1,188 @@
+# Copyright 2013 Nebula Inc.
+#
+# 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.
+#
+
+"""REST API bits"""
+
+import json
+import logging
+import requests
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+_logger = logging.getLogger(__name__)
+
+
+class RESTApi(object):
+ """A REST api client that handles the interface from us to the server
+
+ RESTApi is an extension of a requests.Session that knows
+ how to do:
+ * JSON serialization/deserialization
+ * log requests in 'curl' format
+ * basic API boilerplate for create/delete/list/set/show verbs
+
+ * authentication is handled elsewhere and a token is passed in
+
+ The expectation that there will be a RESTApi object per authentication
+ token in use, i.e. project/username/auth_endpoint
+
+ On the other hand, a Client knows details about the specific REST Api that
+ it communicates with, such as the available endpoints, API versions, etc.
+ """
+
+ USER_AGENT = 'RAPI'
+
+ def __init__(
+ self,
+ os_auth=None,
+ user_agent=USER_AGENT,
+ debug=None,
+ **kwargs
+ ):
+ self.set_auth(os_auth)
+ self.debug = debug
+ self.session = requests.Session(**kwargs)
+
+ self.set_header('User-Agent', user_agent)
+ self.set_header('Content-Type', 'application/json')
+
+ def set_auth(self, os_auth):
+ """Sets the current auth blob"""
+ self.os_auth = os_auth
+
+ def set_header(self, header, content):
+ """Sets passed in headers into the session headers
+
+ Replaces existing headers!!
+ """
+ if content is None:
+ del self.session.headers[header]
+ else:
+ self.session.headers[header] = content
+
+ def request(self, method, url, **kwargs):
+ if self.os_auth:
+ self.session.headers.setdefault('X-Auth-Token', self.os_auth)
+ if 'data' in kwargs and isinstance(kwargs['data'], type({})):
+ kwargs['data'] = json.dumps(kwargs['data'])
+ log_request(method, url, headers=self.session.headers, **kwargs)
+ response = self.session.request(method, url, **kwargs)
+ log_response(response)
+ return self._error_handler(response)
+
+ def create(self, url, data=None, response_key=None, **kwargs):
+ response = self.request('POST', url, data=data, **kwargs)
+ if response_key:
+ return response.json()[response_key]
+ else:
+ return response.json()
+
+ #with self.completion_cache('human_id', self.resource_class, mode="a"):
+ # with self.completion_cache('uuid', self.resource_class, mode="a"):
+ # return self.resource_class(self, body[response_key])
+
+ def delete(self, url):
+ self.request('DELETE', url)
+
+ def list(self, url, data=None, response_key=None, **kwargs):
+ if data:
+ response = self.request('POST', url, data=data, **kwargs)
+ else:
+ kwargs.setdefault('allow_redirects', True)
+ response = self.request('GET', url, **kwargs)
+
+ return response.json()[response_key]
+
+ ###hack this for keystone!!!
+ #data = body[response_key]
+ # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+ # unlike other services which just return the list...
+ #if isinstance(data, dict):
+ # try:
+ # data = data['values']
+ # except KeyError:
+ # pass
+
+ #with self.completion_cache('human_id', obj_class, mode="w"):
+ # with self.completion_cache('uuid', obj_class, mode="w"):
+ # return [obj_class(self, res, loaded=True)
+ # for res in data if res]
+
+ def set(self, url, data=None, response_key=None, **kwargs):
+ response = self.request('PUT', url, data=data)
+ if data:
+ if response_key:
+ return response.json()[response_key]
+ else:
+ return response.json()
+ else:
+ return None
+
+ def show(self, url, response_key=None, **kwargs):
+ response = self.request('GET', url, **kwargs)
+ if response_key:
+ return response.json()[response_key]
+ else:
+ return response.json()
+
+ def _error_handler(self, response):
+ if response.status_code < 200 or response.status_code >= 300:
+ _logger.debug(
+ "ERROR: %s",
+ response.text,
+ )
+ response.raise_for_status()
+ return response
+
+
+def log_request(method, url, **kwargs):
+ # put in an early exit if debugging is not enabled?
+ if 'params' in kwargs and kwargs['params'] != {}:
+ url += '?' + urlencode(kwargs['params'])
+
+ string_parts = [
+ "curl -i",
+ "-X '%s'" % method,
+ "'%s'" % url,
+ ]
+
+ for element in kwargs['headers']:
+ header = " -H '%s: %s'" % (element, kwargs['headers'][element])
+ string_parts.append(header)
+
+ _logger.debug("REQ: %s" % " ".join(string_parts))
+ if 'data' in kwargs:
+ _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
+
+
+def log_response(response):
+ _logger.debug(
+ "RESP: [%s] %s\n",
+ response.status_code,
+ response.headers,
+ )
+ if response._content_consumed:
+ _logger.debug(
+ "RESP BODY: %s\n",
+ response.text,
+ )
+ _logger.debug(
+ "encoding: %s",
+ response.encoding,
+ )
diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py
index f72bb505..91a20895 100644
--- a/openstackclient/common/utils.py
+++ b/openstackclient/common/utils.py
@@ -115,6 +115,30 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
return tuple(row)
+def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
+ """Return a tuple containing the item properties.
+
+ :param item: a single dict resource
+ :param fields: tuple of strings with the desired field names
+ :param mixed_case_fields: tuple of field names to preserve case
+ :param formatters: dictionary mapping field names to callables
+ to format the values
+ """
+ row = []
+
+ for field in fields:
+ if field in mixed_case_fields:
+ field_name = field.replace(' ', '_')
+ else:
+ field_name = field.lower().replace(' ', '_')
+ data = item[field_name] if field_name in item else ''
+ if field in formatters:
+ row.append(formatters[field](data))
+ else:
+ row.append(data)
+ return tuple(row)
+
+
def string_to_bool(arg):
return arg.strip().lower() in ('t', 'true', 'yes', '1')