diff options
| author | Dean Troyer <dtroyer@gmail.com> | 2013-08-13 17:14:42 -0500 |
|---|---|---|
| committer | Dean Troyer <dtroyer@gmail.com> | 2013-08-23 12:08:32 -0500 |
| commit | 17f13f7bf4cea80e8e1380fbc8295318de5be383 (patch) | |
| tree | 611a7a512eae39341911db529b36035d737730e2 /openstackclient/common | |
| parent | b440986e6e0e90c220fe3e52f893bc5dd51cae5a (diff) | |
| download | python-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.py | 188 | ||||
| -rw-r--r-- | openstackclient/common/utils.py | 24 |
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') |
