Add basic client work + regions support
This commit is contained in:
		
							
								
								
									
										536
									
								
								cratonclient/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										536
									
								
								cratonclient/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,536 @@ | |||||||
|  | # Copyright 2010 Jacob Kaplan-Moss | ||||||
|  | # Copyright 2011 OpenStack Foundation | ||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # All Rights Reserved. | ||||||
|  | # | ||||||
|  | #    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. | ||||||
|  |  | ||||||
|  | """Base utilities to build API operation managers and objects on top of.""" | ||||||
|  |  | ||||||
|  | import abc | ||||||
|  | import copy | ||||||
|  | import functools | ||||||
|  | import warnings | ||||||
|  |  | ||||||
|  | from oslo_utils import strutils | ||||||
|  | import six | ||||||
|  | from six.moves import urllib | ||||||
|  |  | ||||||
|  | from keystoneclient import auth | ||||||
|  | from keystoneclient import exceptions | ||||||
|  | from keystoneclient.i18n import _ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def getid(obj): | ||||||
|  |     """Return id if argument is a Resource. | ||||||
|  |  | ||||||
|  |     Abstracts the common pattern of allowing both an object or an object's ID | ||||||
|  |     (UUID) as a parameter when dealing with relationships. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         if obj.uuid: | ||||||
|  |             return obj.uuid | ||||||
|  |     except AttributeError:  # nosec(cjschaef): 'obj' doesn't contain attribute | ||||||
|  |         # 'uuid', return attribute 'id' or the 'obj' | ||||||
|  |         pass | ||||||
|  |     try: | ||||||
|  |         return obj.id | ||||||
|  |     except AttributeError: | ||||||
|  |         return obj | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def filter_none(**kwargs): | ||||||
|  |     """Remove any entries from a dictionary where the value is None.""" | ||||||
|  |     return dict((k, v) for k, v in six.iteritems(kwargs) if v is not None) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def filter_kwargs(f): | ||||||
|  |     @functools.wraps(f) | ||||||
|  |     def func(*args, **kwargs): | ||||||
|  |         new_kwargs = {} | ||||||
|  |         for key, ref in six.iteritems(kwargs): | ||||||
|  |             if ref is None: | ||||||
|  |                 # drop null values | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             id_value = getid(ref) | ||||||
|  |             if id_value != ref: | ||||||
|  |                 # If an object with an id was passed, then use the id, e.g.: | ||||||
|  |                 #     user: user(id=1) becomes user_id: 1 | ||||||
|  |                 key = '%s_id' % key | ||||||
|  |  | ||||||
|  |             new_kwargs[key] = id_value | ||||||
|  |  | ||||||
|  |         return f(*args, **new_kwargs) | ||||||
|  |     return func | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Manager(object): | ||||||
|  |     """Basic manager type providing common operations. | ||||||
|  |  | ||||||
|  |     Managers interact with a particular type of API (servers, flavors, images, | ||||||
|  |     etc.) and provide CRUD operations for them. | ||||||
|  |  | ||||||
|  |     :param client: instance of BaseClient descendant for HTTP requests | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     resource_class = None | ||||||
|  |  | ||||||
|  |     def __init__(self, client): | ||||||
|  |         super(Manager, self).__init__() | ||||||
|  |         self.client = client | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def api(self): | ||||||
|  |         """The client. | ||||||
|  |  | ||||||
|  |         .. warning:: | ||||||
|  |  | ||||||
|  |             This property is deprecated as of the 1.7.0 release in favor of | ||||||
|  |             :meth:`client` and may be removed in the 2.0.0 release. | ||||||
|  |  | ||||||
|  |         """ | ||||||
|  |         warnings.warn( | ||||||
|  |             'api is deprecated as of the 1.7.0 release in favor of client and ' | ||||||
|  |             'may be removed in the 2.0.0 release', DeprecationWarning) | ||||||
|  |         return self.client | ||||||
|  |  | ||||||
|  |     def _list(self, url, response_key, obj_class=None, body=None, **kwargs): | ||||||
|  |         """List the collection. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param obj_class: class for constructing the returned objects | ||||||
|  |             (self.resource_class will be used by default) | ||||||
|  |         :param body: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         if body: | ||||||
|  |             resp, body = self.client.post(url, body=body, **kwargs) | ||||||
|  |         else: | ||||||
|  |             resp, body = self.client.get(url, **kwargs) | ||||||
|  |  | ||||||
|  |         if obj_class is None: | ||||||
|  |             obj_class = self.resource_class | ||||||
|  |  | ||||||
|  |         data = body[response_key] | ||||||
|  |         # NOTE(ja): keystone returns values as list as {'values': [ ... ]} | ||||||
|  |         #           unlike other services which just return the list... | ||||||
|  |         try: | ||||||
|  |             data = data['values'] | ||||||
|  |         except (KeyError, TypeError):  # nosec(cjschaef): keystone data values | ||||||
|  |             # not as expected (see comment above), assumption is that values | ||||||
|  |             # are already returned in a list (so simply utilize that list) | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         return [obj_class(self, res, loaded=True) for res in data if res] | ||||||
|  |  | ||||||
|  |     def _get(self, url, response_key, **kwargs): | ||||||
|  |         """Get an object from collection. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'server' | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         resp, body = self.client.get(url, **kwargs) | ||||||
|  |         return self.resource_class(self, body[response_key], loaded=True) | ||||||
|  |  | ||||||
|  |     def _head(self, url, **kwargs): | ||||||
|  |         """Retrieve request headers for an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         resp, body = self.client.head(url, **kwargs) | ||||||
|  |         return resp.status_code == 204 | ||||||
|  |  | ||||||
|  |     def _post(self, url, body, response_key, return_raw=False, **kwargs): | ||||||
|  |         """Create an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param body: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param return_raw: flag to force returning raw JSON instead of | ||||||
|  |             Python object of self.resource_class | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         resp, body = self.client.post(url, body=body, **kwargs) | ||||||
|  |         if return_raw: | ||||||
|  |             return body[response_key] | ||||||
|  |         return self.resource_class(self, body[response_key]) | ||||||
|  |  | ||||||
|  |     def _put(self, url, body=None, response_key=None, **kwargs): | ||||||
|  |         """Update an object with PUT method. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param body: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         resp, body = self.client.put(url, body=body, **kwargs) | ||||||
|  |         # PUT requests may not return a body | ||||||
|  |         if body is not None: | ||||||
|  |             if response_key is not None: | ||||||
|  |                 return self.resource_class(self, body[response_key]) | ||||||
|  |             else: | ||||||
|  |                 return self.resource_class(self, body) | ||||||
|  |  | ||||||
|  |     def _patch(self, url, body=None, response_key=None, **kwargs): | ||||||
|  |         """Update an object with PATCH method. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param body: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         resp, body = self.client.patch(url, body=body, **kwargs) | ||||||
|  |         if response_key is not None: | ||||||
|  |             return self.resource_class(self, body[response_key]) | ||||||
|  |         else: | ||||||
|  |             return self.resource_class(self, body) | ||||||
|  |  | ||||||
|  |     def _delete(self, url, **kwargs): | ||||||
|  |         """Delete an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers/my-server' | ||||||
|  |         :param kwargs: Additional arguments will be passed to the request. | ||||||
|  |         """ | ||||||
|  |         return self.client.delete(url, **kwargs) | ||||||
|  |  | ||||||
|  |     def _update(self, url, body=None, response_key=None, method="PUT", | ||||||
|  |                 **kwargs): | ||||||
|  |         methods = {"PUT": self.client.put, | ||||||
|  |                    "POST": self.client.post, | ||||||
|  |                    "PATCH": self.client.patch} | ||||||
|  |         try: | ||||||
|  |             resp, body = methods[method](url, body=body, | ||||||
|  |                                          **kwargs) | ||||||
|  |         except KeyError: | ||||||
|  |             raise exceptions.ClientException(_("Invalid update method: %s") | ||||||
|  |                                              % method) | ||||||
|  |         # PUT requests may not return a body | ||||||
|  |         if body: | ||||||
|  |             return self.resource_class(self, body[response_key]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @six.add_metaclass(abc.ABCMeta) | ||||||
|  | class ManagerWithFind(Manager): | ||||||
|  |     """Manager with additional `find()`/`findall()` methods.""" | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def list(self): | ||||||
|  |         pass  # pragma: no cover | ||||||
|  |  | ||||||
|  |     def find(self, **kwargs): | ||||||
|  |         """Find a single item with attributes matching ``**kwargs``. | ||||||
|  |  | ||||||
|  |         This isn't very efficient: it loads the entire list then filters on | ||||||
|  |         the Python side. | ||||||
|  |         """ | ||||||
|  |         rl = self.findall(**kwargs) | ||||||
|  |         num = len(rl) | ||||||
|  |  | ||||||
|  |         if num == 0: | ||||||
|  |             msg = _("No %(name)s matching %(kwargs)s.") % { | ||||||
|  |                 'name': self.resource_class.__name__, 'kwargs': kwargs} | ||||||
|  |             raise exceptions.NotFound(404, msg) | ||||||
|  |         elif num > 1: | ||||||
|  |             raise exceptions.NoUniqueMatch | ||||||
|  |         else: | ||||||
|  |             return rl[0] | ||||||
|  |  | ||||||
|  |     def findall(self, **kwargs): | ||||||
|  |         """Find all items with attributes matching ``**kwargs``. | ||||||
|  |  | ||||||
|  |         This isn't very efficient: it loads the entire list then filters on | ||||||
|  |         the Python side. | ||||||
|  |         """ | ||||||
|  |         found = [] | ||||||
|  |         searches = kwargs.items() | ||||||
|  |  | ||||||
|  |         for obj in self.list(): | ||||||
|  |             try: | ||||||
|  |                 if all(getattr(obj, attr) == value | ||||||
|  |                        for (attr, value) in searches): | ||||||
|  |                     found.append(obj) | ||||||
|  |             except AttributeError: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |         return found | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CrudManager(Manager): | ||||||
|  |     """Base manager class for manipulating Keystone entities. | ||||||
|  |  | ||||||
|  |     Children of this class are expected to define a `collection_key` and `key`. | ||||||
|  |  | ||||||
|  |     - `collection_key`: Usually a plural noun by convention (e.g. `entities`); | ||||||
|  |       used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON | ||||||
|  |       objects containing a list of member resources (e.g. `{'entities': [{}, | ||||||
|  |       {}, {}]}`). | ||||||
|  |     - `key`: Usually a singular noun by convention (e.g. `entity`); used to | ||||||
|  |       refer to an individual member of the collection. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     collection_key = None | ||||||
|  |     key = None | ||||||
|  |     base_url = None | ||||||
|  |  | ||||||
|  |     def build_url(self, dict_args_in_out=None): | ||||||
|  |         """Build a resource URL for the given kwargs. | ||||||
|  |  | ||||||
|  |         Given an example collection where `collection_key = 'entities'` and | ||||||
|  |         `key = 'entity'`, the following URL's could be generated. | ||||||
|  |  | ||||||
|  |         By default, the URL will represent a collection of entities, e.g.:: | ||||||
|  |  | ||||||
|  |             /entities | ||||||
|  |  | ||||||
|  |         If kwargs contains an `entity_id`, then the URL will represent a | ||||||
|  |         specific member, e.g.:: | ||||||
|  |  | ||||||
|  |             /entities/{entity_id} | ||||||
|  |  | ||||||
|  |         If a `base_url` is provided, the generated URL will be appended to it. | ||||||
|  |  | ||||||
|  |         If a 'tail' is provided, it will be appended to the end of the URL. | ||||||
|  |  | ||||||
|  |         """ | ||||||
|  |         if dict_args_in_out is None: | ||||||
|  |             dict_args_in_out = {} | ||||||
|  |  | ||||||
|  |         url = dict_args_in_out.pop('base_url', None) or self.base_url or '' | ||||||
|  |         url += '/%s' % self.collection_key | ||||||
|  |  | ||||||
|  |         # do we have a specific entity? | ||||||
|  |         entity_id = dict_args_in_out.pop('%s_id' % self.key, None) | ||||||
|  |         if entity_id is not None: | ||||||
|  |             url += '/%s' % entity_id | ||||||
|  |  | ||||||
|  |         if dict_args_in_out.get('tail'): | ||||||
|  |             url += dict_args_in_out['tail'] | ||||||
|  |  | ||||||
|  |         return url | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def create(self, **kwargs): | ||||||
|  |         url = self.build_url(dict_args_in_out=kwargs) | ||||||
|  |         return self._post( | ||||||
|  |             url, | ||||||
|  |             {self.key: kwargs}, | ||||||
|  |             self.key) | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def get(self, **kwargs): | ||||||
|  |         return self._get( | ||||||
|  |             self.build_url(dict_args_in_out=kwargs), | ||||||
|  |             self.key) | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def head(self, **kwargs): | ||||||
|  |         return self._head(self.build_url(dict_args_in_out=kwargs)) | ||||||
|  |  | ||||||
|  |     def _build_query(self, params): | ||||||
|  |         return '?%s' % urllib.parse.urlencode(params) if params else '' | ||||||
|  |  | ||||||
|  |     def build_key_only_query(self, params_list): | ||||||
|  |         """Build a query that does not include values, just keys. | ||||||
|  |  | ||||||
|  |         The Identity API has some calls that define queries without values, | ||||||
|  |         this can not be accomplished by using urllib.parse.urlencode(). This | ||||||
|  |         method builds a query using only the keys. | ||||||
|  |         """ | ||||||
|  |         return '?%s' % '&'.join(params_list) if params_list else '' | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def list(self, fallback_to_auth=False, **kwargs): | ||||||
|  |         if 'id' in kwargs.keys(): | ||||||
|  |             # Ensure that users are not trying to call things like | ||||||
|  |             # ``domains.list(id='default')`` when they should have used | ||||||
|  |             # ``[domains.get(domain_id='default')]`` instead. Keystone supports | ||||||
|  |             # ``GET /v3/domains/{domain_id}``, not ``GET | ||||||
|  |             # /v3/domains?id={domain_id}``. | ||||||
|  |             raise TypeError( | ||||||
|  |                 _("list() got an unexpected keyword argument 'id'. To " | ||||||
|  |                   "retrieve a single object using a globally unique " | ||||||
|  |                   "identifier, try using get() instead.")) | ||||||
|  |  | ||||||
|  |         url = self.build_url(dict_args_in_out=kwargs) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             query = self._build_query(kwargs) | ||||||
|  |             url_query = '%(url)s%(query)s' % {'url': url, 'query': query} | ||||||
|  |             return self._list( | ||||||
|  |                 url_query, | ||||||
|  |                 self.collection_key) | ||||||
|  |         except exceptions.EmptyCatalog: | ||||||
|  |             if fallback_to_auth: | ||||||
|  |                 return self._list( | ||||||
|  |                     url_query, | ||||||
|  |                     self.collection_key, | ||||||
|  |                     endpoint_filter={'interface': auth.AUTH_INTERFACE}) | ||||||
|  |             else: | ||||||
|  |                 raise | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def put(self, **kwargs): | ||||||
|  |         return self._update( | ||||||
|  |             self.build_url(dict_args_in_out=kwargs), | ||||||
|  |             method='PUT') | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def update(self, **kwargs): | ||||||
|  |         url = self.build_url(dict_args_in_out=kwargs) | ||||||
|  |  | ||||||
|  |         return self._update( | ||||||
|  |             url, | ||||||
|  |             {self.key: kwargs}, | ||||||
|  |             self.key, | ||||||
|  |             method='PATCH') | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def delete(self, **kwargs): | ||||||
|  |         return self._delete( | ||||||
|  |             self.build_url(dict_args_in_out=kwargs)) | ||||||
|  |  | ||||||
|  |     @filter_kwargs | ||||||
|  |     def find(self, **kwargs): | ||||||
|  |         """Find a single item with attributes matching ``**kwargs``.""" | ||||||
|  |         url = self.build_url(dict_args_in_out=kwargs) | ||||||
|  |  | ||||||
|  |         query = self._build_query(kwargs) | ||||||
|  |         rl = self._list( | ||||||
|  |             '%(url)s%(query)s' % { | ||||||
|  |                 'url': url, | ||||||
|  |                 'query': query, | ||||||
|  |             }, | ||||||
|  |             self.collection_key) | ||||||
|  |         num = len(rl) | ||||||
|  |  | ||||||
|  |         if num == 0: | ||||||
|  |             msg = _("No %(name)s matching %(kwargs)s.") % { | ||||||
|  |                 'name': self.resource_class.__name__, 'kwargs': kwargs} | ||||||
|  |             raise exceptions.NotFound(404, msg) | ||||||
|  |         elif num > 1: | ||||||
|  |             raise exceptions.NoUniqueMatch | ||||||
|  |         else: | ||||||
|  |             return rl[0] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Resource(object): | ||||||
|  |     """Base class for OpenStack resources (tenant, user, etc.). | ||||||
|  |  | ||||||
|  |     This is pretty much just a bag for attributes. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     HUMAN_ID = False | ||||||
|  |     NAME_ATTR = 'name' | ||||||
|  |  | ||||||
|  |     def __init__(self, manager, info, loaded=False): | ||||||
|  |         """Populate and bind to a manager. | ||||||
|  |  | ||||||
|  |         :param manager: BaseManager object | ||||||
|  |         :param info: dictionary representing resource attributes | ||||||
|  |         :param loaded: prevent lazy-loading if set to True | ||||||
|  |         """ | ||||||
|  |         self.manager = manager | ||||||
|  |         self._info = info | ||||||
|  |         self._add_details(info) | ||||||
|  |         self._loaded = loaded | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         """Return string representation of resource attributes.""" | ||||||
|  |         reprkeys = sorted(k | ||||||
|  |                           for k in self.__dict__.keys() | ||||||
|  |                           if k[0] != '_' and k != 'manager') | ||||||
|  |         info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) | ||||||
|  |         return "<%s %s>" % (self.__class__.__name__, info) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def human_id(self): | ||||||
|  |         """Human-readable ID which can be used for bash completion.""" | ||||||
|  |         if self.HUMAN_ID: | ||||||
|  |             name = getattr(self, self.NAME_ATTR, None) | ||||||
|  |             if name is not None: | ||||||
|  |                 return strutils.to_slug(name) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def _add_details(self, info): | ||||||
|  |         for (k, v) in six.iteritems(info): | ||||||
|  |             try: | ||||||
|  |                 setattr(self, k, v) | ||||||
|  |                 self._info[k] = v | ||||||
|  |             except AttributeError:  # nosec(cjschaef): we already defined the | ||||||
|  |                 # attribute on the class | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |     def __getattr__(self, k): | ||||||
|  |         """Checking attrbiute existence.""" | ||||||
|  |         if k not in self.__dict__: | ||||||
|  |             # NOTE(bcwaldon): disallow lazy-loading if already loaded once | ||||||
|  |             if not self.is_loaded(): | ||||||
|  |                 self.get() | ||||||
|  |                 return self.__getattr__(k) | ||||||
|  |  | ||||||
|  |             raise AttributeError(k) | ||||||
|  |         else: | ||||||
|  |             return self.__dict__[k] | ||||||
|  |  | ||||||
|  |     def get(self): | ||||||
|  |         """Support for lazy loading details. | ||||||
|  |  | ||||||
|  |         Some clients, such as novaclient have the option to lazy load the | ||||||
|  |         details, details which can be loaded with this function. | ||||||
|  |         """ | ||||||
|  |         # set_loaded() first ... so if we have to bail, we know we tried. | ||||||
|  |         self.set_loaded(True) | ||||||
|  |         if not hasattr(self.manager, 'get'): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         new = self.manager.get(self.id) | ||||||
|  |         if new: | ||||||
|  |             self._add_details(new._info) | ||||||
|  |             self._add_details( | ||||||
|  |                 {'x_request_id': self.manager.client.last_request_id}) | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         """Define equality for resources.""" | ||||||
|  |         if not isinstance(other, Resource): | ||||||
|  |             return NotImplemented | ||||||
|  |         # two resources of different types are not equal | ||||||
|  |         if not isinstance(other, self.__class__): | ||||||
|  |             return False | ||||||
|  |         return self._info == other._info | ||||||
|  |  | ||||||
|  |     def is_loaded(self): | ||||||
|  |         return self._loaded | ||||||
|  |  | ||||||
|  |     def set_loaded(self, val): | ||||||
|  |         self._loaded = val | ||||||
|  |  | ||||||
|  |     def to_dict(self): | ||||||
|  |         return copy.deepcopy(self._info) | ||||||
|  |  | ||||||
|  |     def delete(self): | ||||||
|  |         return self.manager.delete(self) | ||||||
							
								
								
									
										177
									
								
								cratonclient/crud.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								cratonclient/crud.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | """Client for CRUD operations.""" | ||||||
|  |  | ||||||
|  | class CRUDClient(object): | ||||||
|  |     """Class that handles the basic create, read, upload, delete workflow.""" | ||||||
|  |  | ||||||
|  |     key = None | ||||||
|  |     base_path = None | ||||||
|  |     resource_class = None | ||||||
|  |  | ||||||
|  |     def __init__(self, session, url): | ||||||
|  |         self.session = session | ||||||
|  |         self.url = url.rstrip('/') | ||||||
|  |  | ||||||
|  |     def build_url(self, path_arguments=None): | ||||||
|  |         """Build a complete URL from the url, base_path, and arguments. | ||||||
|  |  | ||||||
|  |         A CRUDClient is constructed with the base URL, e.g. | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             RegionManager(url='https://10.1.1.0:8080/v1', ...) | ||||||
|  |  | ||||||
|  |         The child class of the CRUDClient may set the ``base_path``, e.g., | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |              | ||||||
|  |             base_path = '/regions' | ||||||
|  |  | ||||||
|  |         And it's ``key``, e.g., | ||||||
|  |          | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             key = 'region' | ||||||
|  |  | ||||||
|  |         And based on the ``path_arguments`` parameter we will construct a | ||||||
|  |         complete URL. For example, if someone calls: | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             self.build_url(path_arguments={'region_id': 1}) | ||||||
|  |  | ||||||
|  |         with the hypothetical values above, we would return | ||||||
|  |  | ||||||
|  |             https://10.1.1.0:8080/v1/regions/1 | ||||||
|  |  | ||||||
|  |         Users can also override ``base_path`` in ``path_arguments``. | ||||||
|  |         """ | ||||||
|  |         if path_arguments is None: | ||||||
|  |             path_arguments = {} | ||||||
|  |  | ||||||
|  |         base_path = path_arguments.pop('base_path', None) or self.base_path | ||||||
|  |         item_id = path_arguments.pop('{0}_id'.format(self.key), None) | ||||||
|  |  | ||||||
|  |         url = self.url + base_path | ||||||
|  |  | ||||||
|  |         if item_id is not None: | ||||||
|  |             url += '/{0}'.format(item_id) | ||||||
|  |  | ||||||
|  |         return url | ||||||
|  |  | ||||||
|  |     def create(self, **kwargs): | ||||||
|  |         """Create a new item based on the keyword arguments provided.""" | ||||||
|  |         url = self.build_url(path_arguments=kwargs) | ||||||
|  |         response = self.session.post(url, json=kwargs) | ||||||
|  |         return self.resource_class(self, response.json()) | ||||||
|  |  | ||||||
|  |     def get(self, **kwargs): | ||||||
|  |         """Retrieve the item based on the keyword arguments provided.""" | ||||||
|  |         url = self.build_url(path_arguments=kwargs) | ||||||
|  |         response = self.session.get(url) | ||||||
|  |         return self.resource_class(self, response.json()) | ||||||
|  |  | ||||||
|  |     def list(self, **kwargs): | ||||||
|  |         """List the items from this endpoint.""" | ||||||
|  |         url = self.build_url(path_arguments=kwargs) | ||||||
|  |         response = self.session.get(url, params=kwargs) | ||||||
|  |         return [self.resource_class(self, item) for item in response.json()] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # NOTE(sigmavirus24): Credit for this Resource object goes to the | ||||||
|  | # keystoneclient developers and contributors. | ||||||
|  | class Resource(object): | ||||||
|  |     """Base class for OpenStack resources (tenant, user, etc.). | ||||||
|  |  | ||||||
|  |     This is pretty much just a bag for attributes. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     HUMAN_ID = False | ||||||
|  |     NAME_ATTR = 'name' | ||||||
|  |  | ||||||
|  |     def __init__(self, manager, info, loaded=False): | ||||||
|  |         """Populate and bind to a manager. | ||||||
|  |  | ||||||
|  |         :param manager: BaseManager object | ||||||
|  |         :param info: dictionary representing resource attributes | ||||||
|  |         :param loaded: prevent lazy-loading if set to True | ||||||
|  |         """ | ||||||
|  |         self.manager = manager | ||||||
|  |         self._info = info | ||||||
|  |         self._add_details(info) | ||||||
|  |         self._loaded = loaded | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         """Return string representation of resource attributes.""" | ||||||
|  |         reprkeys = sorted(k | ||||||
|  |                           for k in self.__dict__.keys() | ||||||
|  |                           if k[0] != '_' and k != 'manager') | ||||||
|  |         info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) | ||||||
|  |         return "<%s %s>" % (self.__class__.__name__, info) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def human_id(self): | ||||||
|  |         """Human-readable ID which can be used for bash completion.""" | ||||||
|  |         if self.HUMAN_ID: | ||||||
|  |             name = getattr(self, self.NAME_ATTR, None) | ||||||
|  |             if name is not None: | ||||||
|  |                 return strutils.to_slug(name) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def _add_details(self, info): | ||||||
|  |         for (k, v) in info.items(): | ||||||
|  |             try: | ||||||
|  |                 setattr(self, k, v) | ||||||
|  |                 self._info[k] = v | ||||||
|  |             except AttributeError:  # nosec(cjschaef): we already defined the | ||||||
|  |                 # attribute on the class | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |     def __getattr__(self, k): | ||||||
|  |         """Checking attrbiute existence.""" | ||||||
|  |         if k not in self.__dict__: | ||||||
|  |             # NOTE(bcwaldon): disallow lazy-loading if already loaded once | ||||||
|  |             if not self.is_loaded(): | ||||||
|  |                 self.get() | ||||||
|  |                 return self.__getattr__(k) | ||||||
|  |  | ||||||
|  |             raise AttributeError(k) | ||||||
|  |         else: | ||||||
|  |             return self.__dict__[k] | ||||||
|  |  | ||||||
|  |     def get(self): | ||||||
|  |         """Support for lazy loading details. | ||||||
|  |  | ||||||
|  |         Some clients, such as novaclient have the option to lazy load the | ||||||
|  |         details, details which can be loaded with this function. | ||||||
|  |         """ | ||||||
|  |         # set_loaded() first ... so if we have to bail, we know we tried. | ||||||
|  |         self.set_loaded(True) | ||||||
|  |         if not hasattr(self.manager, 'get'): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         new = self.manager.get(self.id) | ||||||
|  |         if new: | ||||||
|  |             self._add_details(new._info) | ||||||
|  |             self._add_details( | ||||||
|  |                 {'x_request_id': self.manager.client.last_request_id}) | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         """Define equality for resources.""" | ||||||
|  |         if not isinstance(other, Resource): | ||||||
|  |             return NotImplemented | ||||||
|  |         # two resources of different types are not equal | ||||||
|  |         if not isinstance(other, self.__class__): | ||||||
|  |             return False | ||||||
|  |         return self._info == other._info | ||||||
|  |  | ||||||
|  |     def is_loaded(self): | ||||||
|  |         return self._loaded | ||||||
|  |  | ||||||
|  |     def set_loaded(self, val): | ||||||
|  |         self._loaded = val | ||||||
|  |  | ||||||
|  |     def to_dict(self): | ||||||
|  |         return copy.deepcopy(self._info) | ||||||
|  |  | ||||||
|  |     def delete(self): | ||||||
|  |         return self.manager.delete(self) | ||||||
							
								
								
									
										256
									
								
								cratonclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								cratonclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | # 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. | ||||||
|  | """Exception classes and logic for cratonclient.""" | ||||||
|  |  | ||||||
|  | class ClientException(Exception): | ||||||
|  |     """Base exception class for all exceptions in cratonclient.""" | ||||||
|  |  | ||||||
|  |     message = None | ||||||
|  |  | ||||||
|  |     def __init__(self, message=None): | ||||||
|  |         """Initialize our exception instance with our class level message.""" | ||||||
|  |         if message is None: | ||||||
|  |             if self.message is None: | ||||||
|  |                 message = self.__class__.__name__ | ||||||
|  |             else: | ||||||
|  |                 message = self.message | ||||||
|  |         super(ClientException, self).__init__(message) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Timeout(ClientException): | ||||||
|  |     """Catch-all class for connect and read timeouts from requests.""" | ||||||
|  |  | ||||||
|  |     message = 'Request timed out' | ||||||
|  |      | ||||||
|  |     def __init__(self, message=None, **kwargs): | ||||||
|  |         self.original_exception = kwargs.pop('exception', None) | ||||||
|  |         super(Timeout, self).__init__(message) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPError(ClientException): | ||||||
|  |     """Base exception class for all HTTP related exceptions in.""" | ||||||
|  |  | ||||||
|  |     message = "An error occurred while talking to the remote server." | ||||||
|  |     status_code = None | ||||||
|  |  | ||||||
|  |     def __init__(self, message=None, **kwargs): | ||||||
|  |         self.response = kwargs.pop('response', None) | ||||||
|  |         self.original_exception = kwargs.pop('exception', None) | ||||||
|  |         self.status_code = (self.status_code | ||||||
|  |                             or getattr(self.response, 'status_code', None)) | ||||||
|  |         super(HTTPError, self).__init__(message) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def status_code(self): | ||||||
|  |         """Shim to provide a similar API to other OpenStack clients.""" | ||||||
|  |         return self.status_code | ||||||
|  |  | ||||||
|  |     @status_code.setter | ||||||
|  |     def status_code(self, code): | ||||||
|  |         self.status_code = code | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ConnectionFailed(HTTPError): | ||||||
|  |     """Connecting to the server failed.""" | ||||||
|  |  | ||||||
|  |     message = "An error occurred while connecting to the server.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPClientError(HTTPError): | ||||||
|  |     """Base exception for client side errors (4xx status codes).""" | ||||||
|  |  | ||||||
|  |     message = "Something went wrong with the request.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BadRequest(HTTPClientError): | ||||||
|  |     """Client sent a malformed request.""" | ||||||
|  |  | ||||||
|  |     status_code = 400 | ||||||
|  |     message = "The request sent to the server was invalid." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Unauthorized(HTTPClientError): | ||||||
|  |     """Client is unauthorized to access the resource in question.""" | ||||||
|  |      | ||||||
|  |     status_code = 401 | ||||||
|  |     message = ("The user has either provided insufficient parameters for " | ||||||
|  |                "authentication or is not authorized to access this resource.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Forbidden(HTTPClientError): | ||||||
|  |     """Client is forbidden to access the resource.""" | ||||||
|  |  | ||||||
|  |     status_code = 403 | ||||||
|  |     message = ("The user was unable to access the resource because they are " | ||||||
|  |                "forbidden.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotFound(HTTPClientError): | ||||||
|  |     """Resource could not be found.""" | ||||||
|  |  | ||||||
|  |     status_code = 404 | ||||||
|  |     message = "The requested resource was not found.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MethodNotAllowed(HTTPClientError): | ||||||
|  |     """The request method is not supported.""" | ||||||
|  |  | ||||||
|  |     status_code = 405 | ||||||
|  |     message = "The method used in the request is not supported." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotAcceptable(HTTPClientError): | ||||||
|  |     """The requested resource can not respond with acceptable content. | ||||||
|  |  | ||||||
|  |     Based on the Accept headers specified by the client, the resource can not | ||||||
|  |     generate content that is an acceptable content-type. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     status_code = 406 | ||||||
|  |     message = "The resource can not return acceptable content." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ProxyAuthenticationRequired(HTTPClientError): | ||||||
|  |     """The client must first authenticate itself with the proxy.""" | ||||||
|  |  | ||||||
|  |     status_code = 407 | ||||||
|  |     message = "The client must first authenticate itself with a proxy." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Conflict(HTTPClientError): | ||||||
|  |     """The request presents a conflict.""" | ||||||
|  |  | ||||||
|  |     status_code = 409 | ||||||
|  |     message = "The request could not be processed due to a conflict." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Gone(HTTPClientError): | ||||||
|  |     """The requested resource is no longer available. | ||||||
|  |  | ||||||
|  |     The resource requested is no longer available and will not be available | ||||||
|  |     again. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     status_code = 410 | ||||||
|  |     message = ("The resource requested is no longer available and will not be" | ||||||
|  |                " available again.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LengthRequired(HTTPClientError): | ||||||
|  |     """The request did not specify a Content-Length header.""" | ||||||
|  |  | ||||||
|  |     status_code = 411 | ||||||
|  |     message = ("The request did not contain a Content-Length header but one" | ||||||
|  |                " was required by the resource.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PreconditionFailed(HTTPClientError): | ||||||
|  |     """The server failed to meet one of the preconditions in the request.""" | ||||||
|  |  | ||||||
|  |     status_code = 412 | ||||||
|  |     message = ("The server failed to meet one of the preconditions in the " | ||||||
|  |                "request.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestEntityTooLarge(HTTPClientError): | ||||||
|  |     """The request is larger than the server is willing or able to process.""" | ||||||
|  |  | ||||||
|  |     status_code = 413 | ||||||
|  |     message = ("The request is larger than the server is willing or able to " | ||||||
|  |                "process.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestUriTooLong(HTTPClientError): | ||||||
|  |     """The URI provided was too long for the server to process.""" | ||||||
|  |  | ||||||
|  |     status_code = 414 | ||||||
|  |     message = "The URI provided was too long for the server to process." | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnsupportedMediaType(HTTPClientError): | ||||||
|  |     """The request entity has a media type which is unsupported.""" | ||||||
|  |  | ||||||
|  |     status_code = 415 | ||||||
|  |     message = ("The request entity has a media type which is unsupported by " | ||||||
|  |                "the server or resource.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestedRangeNotSatisfiable(HTTPClientError): | ||||||
|  |     """The requestor wanted a range but the server was unable to provide it.""" | ||||||
|  |  | ||||||
|  |     status_code = 416 | ||||||
|  |     message = ("The requestor wanted a range but the server was unable to " | ||||||
|  |                "provide it.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnprocessableEntity(HTTPClientError): | ||||||
|  |     """There were semantic errors in the request.""" | ||||||
|  |  | ||||||
|  |     status_code = 422 | ||||||
|  |     message = ("The request is of a valid content-type and structure but " | ||||||
|  |                "semantically invalid.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _4xx_classes = [ | ||||||
|  |     BadRequest, | ||||||
|  |     Unauthorized, | ||||||
|  |     Forbidden, | ||||||
|  |     NotFound, | ||||||
|  |     MethodNotAllowed, | ||||||
|  |     NotAcceptable, | ||||||
|  |     ProxyAuthenticationRequired, | ||||||
|  |     Conflict, | ||||||
|  |     Gone, | ||||||
|  |     LengthRequired, | ||||||
|  |     PreconditionFailed, | ||||||
|  |     RequestEntityTooLarge, | ||||||
|  |     RequestUriTooLong, | ||||||
|  |     UnsupportedMediaType, | ||||||
|  |     RequestedRangeNotSatisfiable, | ||||||
|  |     UnprocessableEntity, | ||||||
|  | ] | ||||||
|  | _4xx_codes = {cls.status_code: cls for cls in _4xx_classes} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPServerError(HTTPError): | ||||||
|  |     """The server encountered an error it could not recover from.""" | ||||||
|  |  | ||||||
|  |     message = "HTTP Server-side Error" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InternalServerError(HTTPServerError): | ||||||
|  |     """The server encountered an error it could not recover from.""" | ||||||
|  |  | ||||||
|  |     status_code = 500 | ||||||
|  |     message = ("There was an internal server error that could not be recovered" | ||||||
|  |                " from.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _5xx_classes = [ | ||||||
|  |     InternalServerError, | ||||||
|  |     # NOTE(sigmavirus24): Allow for future expansion | ||||||
|  | ] | ||||||
|  | _5xx_codes = {cls.status_code: cls for cls in _5xx_classes} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def error_from(response): | ||||||
|  |     """Find an error code that matches a response status_code.""" | ||||||
|  |     if 400 <= response.status_code < 500: | ||||||
|  |         cls = _4xx_codes.get(response.status_code, HTTPClientError) | ||||||
|  |     elif 500 <= response.status_code < 600: | ||||||
|  |         cls = _5xx_codes.get(response.status_code, HTTPServerError) | ||||||
|  |     else: | ||||||
|  |         cls = HTTPError | ||||||
|  |  | ||||||
|  |     return cls(response=response) | ||||||
							
								
								
									
										150
									
								
								cratonclient/session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								cratonclient/session.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | # 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. | ||||||
|  | """Craton-specific session details.""" | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  | from requests import exceptions as requests_exc | ||||||
|  |  | ||||||
|  | import cratonclient | ||||||
|  | from cratonclient import exceptions as exc | ||||||
|  |  | ||||||
|  | LOG = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Session(object): | ||||||
|  |     """Management class to allow different types of sessions to be used. | ||||||
|  |  | ||||||
|  |     If an instance of Craton is deployed with Keystone Middleware, this allows | ||||||
|  |     for a keystoneauth session to be used so authentication will happen | ||||||
|  |     immediately. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, session=None, username=None, token=None, project_id=None): | ||||||
|  |         if session is None: | ||||||
|  |             session = requests.Session() | ||||||
|  |  | ||||||
|  |         self._session = session | ||||||
|  |         self._session.headers['X-Auth-User'] = username | ||||||
|  |         self._session.headers['X-Auth-Project'] = str(project_id) | ||||||
|  |         self._session.headers['X-Auth-Token'] = token | ||||||
|  |  | ||||||
|  |         craton_version = 'python-cratonclient/{0} '.format( | ||||||
|  |             cratonclient.__version__) | ||||||
|  |         old_user_agent = self._session.headers['User-Agent'] | ||||||
|  |  | ||||||
|  |         self._session.headers['User-Agent'] = craton_version + old_user_agent | ||||||
|  |         self._session.headers['Accept'] = 'application/json' | ||||||
|  |  | ||||||
|  |     def delete(self, url, **kwargs): | ||||||
|  |         return self.request('DELETE', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def get(self, url, **kwargs): | ||||||
|  |         return self.request('GET', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def head(self, url, **kwargs): | ||||||
|  |         return self.request('HEAD', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def options(self, url, **kwargs): | ||||||
|  |         return self.request('OPTIONS', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def post(self, url, **kwargs): | ||||||
|  |         return self.request('POST', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def put(self, url, **kwargs): | ||||||
|  |         return self.request('PUT', url, **kwargs) | ||||||
|  |  | ||||||
|  |     def request(self, method, url, **kwargs): | ||||||
|  |         self._http_log_request(method=method, | ||||||
|  |                           url=url, | ||||||
|  |                           data=kwargs.get('data'), | ||||||
|  |                           headers=kwargs.get('headers', {}).copy()) | ||||||
|  |         try: | ||||||
|  |             response = self._session.request(method=method, | ||||||
|  |                                              url=url, | ||||||
|  |                                              **kwargs) | ||||||
|  |         except requests_exc.HTTPError as err: | ||||||
|  |             raise exc.HTTPError(exception=err, response=err.response) | ||||||
|  |         # NOTE(sigmavirus24): The ordering of Timeout before ConnectionError | ||||||
|  |         # is important on requests 2.x. The ConnectTimeout exception inherits | ||||||
|  |         # from both ConnectionError and Timeout. To catch both connect and | ||||||
|  |         # read timeouts similarly, we need to catch this one first. | ||||||
|  |         except requests_exc.Timeout as err: | ||||||
|  |             raise exc.Timeout(exception=err) | ||||||
|  |         except requests_exc.ConnectionError: | ||||||
|  |             raise exc.ConnectionFailed(exception=err) | ||||||
|  |  | ||||||
|  |         self._http_log_response(response) | ||||||
|  |         if response.status_code >= 400: | ||||||
|  |             raise exc.error_from(response) | ||||||
|  |  | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def _http_log_request(self, url, method=None, data=None, | ||||||
|  |                           headers=None, logger=LOG): | ||||||
|  |         if not logger.isEnabledFor(logging.DEBUG): | ||||||
|  |             # NOTE(morganfainberg): This whole debug section is expensive, | ||||||
|  |             # there is no need to do the work if we're not going to emit a | ||||||
|  |             # debug log. | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         string_parts = ['REQ: curl -g -i'] | ||||||
|  |  | ||||||
|  |         # NOTE(jamielennox): None means let requests do its default validation | ||||||
|  |         # so we need to actually check that this is False. | ||||||
|  |         if self.verify is False: | ||||||
|  |             string_parts.append('--insecure') | ||||||
|  |         elif isinstance(self.verify, six.string_types): | ||||||
|  |             string_parts.append('--cacert "%s"' % self.verify) | ||||||
|  |  | ||||||
|  |         if method: | ||||||
|  |             string_parts.extend(['-X', method]) | ||||||
|  |  | ||||||
|  |         string_parts.append(url) | ||||||
|  |  | ||||||
|  |         if headers: | ||||||
|  |             for header in six.iteritems(headers): | ||||||
|  |                 string_parts.append('-H "%s: %s"' | ||||||
|  |                                     % self._process_header(header)) | ||||||
|  |  | ||||||
|  |         if data: | ||||||
|  |             string_parts.append("-d '%s'" % data) | ||||||
|  |         try: | ||||||
|  |             logger.debug(' '.join(string_parts)) | ||||||
|  |         except UnicodeDecodeError: | ||||||
|  |             logger.debug("Replaced characters that could not be decoded" | ||||||
|  |                          " in log output, original caused UnicodeDecodeError") | ||||||
|  |             string_parts = [ | ||||||
|  |                 encodeutils.safe_decode( | ||||||
|  |                     part, errors='replace') for part in string_parts] | ||||||
|  |             logger.debug(' '.join(string_parts)) | ||||||
|  |  | ||||||
|  |     def _http_log_response(self, response, logger=LOG): | ||||||
|  |         if not logger.isEnabledFor(logging.DEBUG): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         text = _remove_service_catalog(response.text) | ||||||
|  |  | ||||||
|  |         string_parts = [ | ||||||
|  |             'RESP:', | ||||||
|  |             '[%s]' % response.status_code | ||||||
|  |         ] | ||||||
|  |         for header in six.iteritems(response.headers): | ||||||
|  |             string_parts.append('%s: %s' % self._process_header(header)) | ||||||
|  |         if text: | ||||||
|  |             string_parts.append('\nRESP BODY: %s\n' % | ||||||
|  |                                 strutils.mask_password(text)) | ||||||
|  |  | ||||||
|  |         logger.debug(' '.join(string_parts)) | ||||||
							
								
								
									
										63
									
								
								cratonclient/tests/test_session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								cratonclient/tests/test_session.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """Tests for `cratonclient.session` module.""" | ||||||
|  | import uuid | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  |  | ||||||
|  | import cratonclient | ||||||
|  | from cratonclient import session | ||||||
|  | from cratonclient.tests import base | ||||||
|  |  | ||||||
|  |  | ||||||
|  | USERNAME = 'example' | ||||||
|  | TOKEN =  uuid.uuid4().hex | ||||||
|  | PROJECT_ID = 1 | ||||||
|  |  | ||||||
|  | class TestSession(base.TestCase): | ||||||
|  |     """Test our session class.""" | ||||||
|  |  | ||||||
|  |     def test_uses_provided_session(self): | ||||||
|  |         """Verify that cratonclient does not override the session parameter.""" | ||||||
|  |         requests_session = requests.Session() | ||||||
|  |         craton = session.Session(session=requests_session) | ||||||
|  |         self.assertIs(requests_session, craton._session) | ||||||
|  |  | ||||||
|  |     def test_creates_new_session(self): | ||||||
|  |         """Verify that cratonclient creates a new session.""" | ||||||
|  |         craton = session.Session() | ||||||
|  |         self.assertIsInstance(craton._session, requests.Session) | ||||||
|  |  | ||||||
|  |     def test_sets_authentication_parameters_as_headers(self): | ||||||
|  |         """Verify we set auth parameters as headers on the session.""" | ||||||
|  |         requests_session = requests.Session() | ||||||
|  |         craton = session.Session( | ||||||
|  |             username=USERNAME, | ||||||
|  |             token=TOKEN, | ||||||
|  |             project_id=PROJECT_ID, | ||||||
|  |         ) | ||||||
|  |         expected_headers = { | ||||||
|  |             'X-Auth-User': USERNAME, | ||||||
|  |             'X-Auth-Token': TOKEN, | ||||||
|  |             'X-Auth-Project': str(PROJECT_ID), | ||||||
|  |             'User-Agent': 'python-cratonclient/{0} {1}'.format( | ||||||
|  |                 cratonclient.__version__,  | ||||||
|  |                 requests_session.headers['User-Agent']), | ||||||
|  |             'Connection': 'keep-alive', | ||||||
|  |             'Accept-Encoding': 'gzip, deflate', | ||||||
|  |             'Accept': 'application/json', | ||||||
|  |         } | ||||||
|  |         self.assertItemsEqual(expected_headers, craton._session.headers) | ||||||
|  |  | ||||||
							
								
								
									
										0
									
								
								cratonclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cratonclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										10
									
								
								cratonclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cratonclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | """Top-level client for version 1 of Craton's API.""" | ||||||
|  | from cratonclient.v1 import regions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Client(object): | ||||||
|  |     def __init__(self, session, url): | ||||||
|  |         self._url = url | ||||||
|  |         self._session = session | ||||||
|  |  | ||||||
|  |         self.regions = regions.RegionManager(self._session, self._url) | ||||||
							
								
								
									
										15
									
								
								cratonclient/v1/regions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cratonclient/v1/regions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | """Regions manager code.""" | ||||||
|  | from cratonclient import crud | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Region(crud.Resource): | ||||||
|  |     """Representation of a Region.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RegionManager(crud.CRUDClient): | ||||||
|  |     """A manager for regions.""" | ||||||
|  |  | ||||||
|  |     key = 'region' | ||||||
|  |     base_path = '/regions' | ||||||
|  |     resource_class = Region | ||||||
| @@ -3,3 +3,4 @@ | |||||||
| # process, which may cause wedges in the gate later. | # process, which may cause wedges in the gate later. | ||||||
|  |  | ||||||
| pbr>=1.6 | pbr>=1.6 | ||||||
|  | requests>=2.10.0 # Apache-2.0 | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ | |||||||
| # of appearance. Changing the order has an impact on the overall integration | # of appearance. Changing the order has an impact on the overall integration | ||||||
| # process, which may cause wedges in the gate later. | # process, which may cause wedges in the gate later. | ||||||
|  |  | ||||||
| hacking<0.11,>=0.10.0 | hacking<0.12,>=0.10.0 | ||||||
|  | flake8_docstrings==0.2.1.post1 # MIT | ||||||
|  |  | ||||||
| coverage>=3.6 | coverage>=3.6 | ||||||
| python-subunit>=0.0.18 | python-subunit>=0.0.18 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Ian Cordasco
					Ian Cordasco