summaryrefslogtreecommitdiff
path: root/troveclient/apiclient
diff options
context:
space:
mode:
authorSteve Martinelli <s.martinelli@gmail.com>2016-11-10 10:00:24 -0500
committerSteve Martinelli <s.martinelli@gmail.com>2016-11-10 10:02:08 -0500
commiteed19c835fc5706099ce2cee872fc66d99836cf8 (patch)
treec529014e6b3c472c63ea0e76b69e8cb28bcdc5ef /troveclient/apiclient
parentc4d9ebb66d28fa6ab4ac0ece3803184108893e94 (diff)
downloadpython-troveclient-eed19c835fc5706099ce2cee872fc66d99836cf8.tar.gz
move old apiclient code out of openstack/common
As part of the first community-wide goal [1], teams were asked to remove the openstack/common package of their projects if one existed. This was a byproduct of the old oslo-incubator form of syncing common functionality. The package, apiclient, was moved to a top level location and cliutils was moved to the common module. There are no oslo specific libraries, the recommended solution is to move it in tree and maintain it there. Specifically in this patch, I also rearranged some of the imports to make them match up with the hacking guidelines while I was re-ordering them for the refactor anyway. Lastly, the apiclient/fake_client.py was deleted since it was not used. [1] http://governance.openstack.org/goals/ocata/remove-incubated-oslo-code.html Change-Id: Ib7b691240b819c9c91b100ca47ffaba75b281c78
Diffstat (limited to 'troveclient/apiclient')
-rw-r--r--troveclient/apiclient/__init__.py0
-rw-r--r--troveclient/apiclient/auth.py221
-rw-r--r--troveclient/apiclient/base.py496
-rw-r--r--troveclient/apiclient/client.py358
-rw-r--r--troveclient/apiclient/exceptions.py445
5 files changed, 1520 insertions, 0 deletions
diff --git a/troveclient/apiclient/__init__.py b/troveclient/apiclient/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/troveclient/apiclient/__init__.py
diff --git a/troveclient/apiclient/auth.py b/troveclient/apiclient/auth.py
new file mode 100644
index 0000000..48a096c
--- /dev/null
+++ b/troveclient/apiclient/auth.py
@@ -0,0 +1,221 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright 2013 Spanish National Research Council.
+# 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.
+
+# E0202: An attribute inherited from %s hide this method
+# pylint: disable=E0202
+
+import abc
+import argparse
+import os
+
+import six
+from stevedore import extension
+
+from troveclient.apiclient import exceptions
+
+
+_discovered_plugins = {}
+
+
+def discover_auth_systems():
+ """Discover the available auth-systems.
+
+ This won't take into account the old style auth-systems.
+ """
+ global _discovered_plugins
+ _discovered_plugins = {}
+
+ def add_plugin(ext):
+ _discovered_plugins[ext.name] = ext.plugin
+
+ ep_namespace = "troveclient.apiclient.auth"
+ mgr = extension.ExtensionManager(ep_namespace)
+ mgr.map(add_plugin)
+
+
+def load_auth_system_opts(parser):
+ """Load options needed by the available auth-systems into a parser.
+
+ This function will try to populate the parser with options from the
+ available plugins.
+ """
+ group = parser.add_argument_group("Common auth options")
+ BaseAuthPlugin.add_common_opts(group)
+ for name, auth_plugin in six.iteritems(_discovered_plugins):
+ group = parser.add_argument_group(
+ "Auth-system '%s' options" % name,
+ conflict_handler="resolve")
+ auth_plugin.add_opts(group)
+
+
+def load_plugin(auth_system):
+ try:
+ plugin_class = _discovered_plugins[auth_system]
+ except KeyError:
+ raise exceptions.AuthSystemNotFound(auth_system)
+ return plugin_class(auth_system=auth_system)
+
+
+def load_plugin_from_args(args):
+ """Load required plugin and populate it with options.
+
+ Try to guess auth system if it is not specified. Systems are tried in
+ alphabetical order.
+
+ :type args: argparse.Namespace
+ :raises: AuthPluginOptionsMissing
+ """
+ auth_system = args.os_auth_system
+ if auth_system:
+ plugin = load_plugin(auth_system)
+ plugin.parse_opts(args)
+ plugin.sufficient_options()
+ return plugin
+
+ for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
+ plugin_class = _discovered_plugins[plugin_auth_system]
+ plugin = plugin_class()
+ plugin.parse_opts(args)
+ try:
+ plugin.sufficient_options()
+ except exceptions.AuthPluginOptionsMissing:
+ continue
+ return plugin
+ raise exceptions.AuthPluginOptionsMissing(["auth_system"])
+
+
+@six.add_metaclass(abc.ABCMeta)
+class BaseAuthPlugin(object):
+ """Base class for authentication plugins.
+
+ An authentication plugin needs to override at least the authenticate
+ method to be a valid plugin.
+ """
+
+ auth_system = None
+ opt_names = []
+ common_opt_names = [
+ "auth_system",
+ "username",
+ "password",
+ "tenant_name",
+ "token",
+ "auth_url",
+ ]
+
+ def __init__(self, auth_system=None, **kwargs):
+ self.auth_system = auth_system or self.auth_system
+ self.opts = dict((name, kwargs.get(name))
+ for name in self.opt_names)
+
+ @staticmethod
+ def _parser_add_opt(parser, opt):
+ """Add an option to parser in two variants.
+
+ :param opt: option name (with underscores)
+ """
+ dashed_opt = opt.replace("_", "-")
+ env_var = "OS_%s" % opt.upper()
+ arg_default = os.environ.get(env_var, "")
+ arg_help = "Defaults to env[%s]." % env_var
+ parser.add_argument(
+ "--os-%s" % dashed_opt,
+ metavar="<%s>" % dashed_opt,
+ default=arg_default,
+ help=arg_help)
+ parser.add_argument(
+ "--os_%s" % opt,
+ metavar="<%s>" % dashed_opt,
+ help=argparse.SUPPRESS)
+
+ @classmethod
+ def add_opts(cls, parser):
+ """Populate the parser with the options for this plugin.
+ """
+ for opt in cls.opt_names:
+ # use `BaseAuthPlugin.common_opt_names` since it is never
+ # changed in child classes
+ if opt not in BaseAuthPlugin.common_opt_names:
+ cls._parser_add_opt(parser, opt)
+
+ @classmethod
+ def add_common_opts(cls, parser):
+ """Add options that are common for several plugins.
+ """
+ for opt in cls.common_opt_names:
+ cls._parser_add_opt(parser, opt)
+
+ @staticmethod
+ def get_opt(opt_name, args):
+ """Return option name and value.
+
+ :param opt_name: name of the option, e.g., "username"
+ :param args: parsed arguments
+ """
+ return (opt_name, getattr(args, "os_%s" % opt_name, None))
+
+ def parse_opts(self, args):
+ """Parse the actual auth-system options if any.
+
+ This method is expected to populate the attribute `self.opts` with a
+ dict containing the options and values needed to make authentication.
+ """
+ self.opts.update(dict(self.get_opt(opt_name, args)
+ for opt_name in self.opt_names))
+
+ def authenticate(self, http_client):
+ """Authenticate using plugin defined method.
+
+ The method usually analyses `self.opts` and performs
+ a request to authentication server.
+
+ :param http_client: client object that needs authentication
+ :type http_client: HTTPClient
+ :raises: AuthorizationFailure
+ """
+ self.sufficient_options()
+ self._do_authenticate(http_client)
+
+ @abc.abstractmethod
+ def _do_authenticate(self, http_client):
+ """Protected method for authentication.
+ """
+
+ def sufficient_options(self):
+ """Check if all required options are present.
+
+ :raises: AuthPluginOptionsMissing
+ """
+ missing = [opt
+ for opt in self.opt_names
+ if not self.opts.get(opt)]
+ if missing:
+ raise exceptions.AuthPluginOptionsMissing(missing)
+
+ @abc.abstractmethod
+ def token_and_endpoint(self, endpoint_type, service_type):
+ """Return token and endpoint.
+
+ :param service_type: Service type of the endpoint
+ :type service_type: string
+ :param endpoint_type: Type of endpoint.
+ Possible values: public or publicURL,
+ internal or internalURL,
+ admin or adminURL
+ :type endpoint_type: string
+ :returns: tuple of token and endpoint strings
+ :raises: EndpointException
+ """
diff --git a/troveclient/apiclient/base.py b/troveclient/apiclient/base.py
new file mode 100644
index 0000000..72ec0c7
--- /dev/null
+++ b/troveclient/apiclient/base.py
@@ -0,0 +1,496 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack Foundation
+# Copyright 2012 Grid Dynamics
+# 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.
+"""
+
+# E1102: %s is not callable
+# pylint: disable=E1102
+
+import abc
+import copy
+
+from oslo_utils import reflection
+from oslo_utils import strutils
+import six
+from six.moves.urllib import parse
+
+from troveclient.apiclient import exceptions
+
+
+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:
+ pass
+ try:
+ return obj.id
+ except AttributeError:
+ return obj
+
+
+# TODO(aababilov): call run_hooks() in HookableMixin's child classes
+class HookableMixin(object):
+ """Mixin so classes can register and run hooks."""
+ _hooks_map = {}
+
+ @classmethod
+ def add_hook(cls, hook_type, hook_func):
+ """Add a new hook of specified type.
+
+ :param cls: class that registers hooks
+ :param hook_type: hook type, e.g., '__pre_parse_args__'
+ :param hook_func: hook function
+ """
+ if hook_type not in cls._hooks_map:
+ cls._hooks_map[hook_type] = []
+
+ cls._hooks_map[hook_type].append(hook_func)
+
+ @classmethod
+ def run_hooks(cls, hook_type, *args, **kwargs):
+ """Run all hooks of specified type.
+
+ :param cls: class that registers hooks
+ :param hook_type: hook type, e.g., '__pre_parse_args__'
+ :param **args: args to be passed to every hook function
+ :param **kwargs: kwargs to be passed to every hook function
+ """
+ hook_funcs = cls._hooks_map.get(hook_type) or []
+ for hook_func in hook_funcs:
+ hook_func(*args, **kwargs)
+
+
+class BaseManager(HookableMixin):
+ """Basic manager type providing common operations.
+
+ Managers interact with a particular type of API (servers, flavors, images,
+ etc.) and provide CRUD operations for them.
+ """
+ resource_class = None
+
+ def __init__(self, client):
+ """Initializes BaseManager with `client`.
+
+ :param client: instance of BaseClient descendant for HTTP requests
+ """
+ super(BaseManager, self).__init__()
+ self.client = client
+
+ def _list(self, url, response_key, obj_class=None, json=None):
+ """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 json: data that will be encoded as JSON and passed in POST
+ request (GET will be sent by default)
+ """
+ if json:
+ body = self.client.post(url, json=json).json()
+ else:
+ body = self.client.get(url).json()
+
+ 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):
+ pass
+
+ return [obj_class(self, res, loaded=True) for res in data if res]
+
+ def _get(self, url, response_key):
+ """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'
+ """
+ body = self.client.get(url).json()
+ return self.resource_class(self, body[response_key], loaded=True)
+
+ def _head(self, url):
+ """Retrieve request headers for an object.
+
+ :param url: a partial URL, e.g., '/servers'
+ """
+ resp = self.client.head(url)
+ return resp.status_code == 204
+
+ def _post(self, url, json, response_key, return_raw=False):
+ """Create an object.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: 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
+ """
+ body = self.client.post(url, json=json).json()
+ if return_raw:
+ return body[response_key]
+ return self.resource_class(self, body[response_key])
+
+ def _put(self, url, json=None, response_key=None):
+ """Update an object with PUT method.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: 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'
+ """
+ resp = self.client.put(url, json=json)
+ # PUT requests may not return a body
+ if resp.content:
+ body = resp.json()
+ 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, json=None, response_key=None):
+ """Update an object with PATCH method.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: 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'
+ """
+ body = self.client.patch(url, json=json).json()
+ 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):
+ """Delete an object.
+
+ :param url: a partial URL, e.g., '/servers/my-server'
+ """
+ return self.client.delete(url)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ManagerWithFind(BaseManager):
+ """Manager with additional `find()`/`findall()` methods."""
+
+ @abc.abstractmethod
+ def list(self):
+ pass
+
+ 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.
+ """
+ matches = self.findall(**kwargs)
+ num_matches = len(matches)
+ if num_matches == 0:
+ msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+ raise exceptions.NotFound(msg)
+ elif num_matches > 1:
+ raise exceptions.NoUniqueMatch()
+ else:
+ return matches[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(BaseManager):
+ """Base manager class for manipulating 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
+
+ def build_url(self, base_url=None, **kwargs):
+ """Builds 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}
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ url = base_url if base_url is not None else ''
+
+ url += '/%s' % self.collection_key
+
+ # do we have a specific entity?
+ entity_id = kwargs.get('%s_id' % self.key)
+ if entity_id is not None:
+ url += '/%s' % entity_id
+
+ return url
+
+ def _filter_kwargs(self, kwargs):
+ """Drop null values and handle ids."""
+ for key, ref in six.iteritems(kwargs.copy()):
+ if ref is None:
+ kwargs.pop(key)
+ else:
+ if isinstance(ref, Resource):
+ kwargs.pop(key)
+ kwargs['%s_id' % key] = getid(ref)
+ return kwargs
+
+ def create(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._post(
+ self.build_url(**kwargs),
+ {self.key: kwargs},
+ self.key)
+
+ def get(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._get(
+ self.build_url(**kwargs),
+ self.key)
+
+ def head(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._head(self.build_url(**kwargs))
+
+ def list(self, base_url=None, **kwargs):
+ """List the collection.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._list(
+ '%(base_url)s%(query)s' % {
+ 'base_url': self.build_url(base_url=base_url, **kwargs),
+ 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ },
+ self.collection_key)
+
+ def put(self, base_url=None, **kwargs):
+ """Update an element.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._put(self.build_url(base_url=base_url, **kwargs))
+
+ def update(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ params = kwargs.copy()
+ params.pop('%s_id' % self.key)
+
+ return self._patch(
+ self.build_url(**kwargs),
+ {self.key: params},
+ self.key)
+
+ def delete(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._delete(
+ self.build_url(**kwargs))
+
+ def find(self, base_url=None, **kwargs):
+ """Find a single item with attributes matching ``**kwargs``.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ rl = self._list(
+ '%(base_url)s%(query)s' % {
+ 'base_url': self.build_url(base_url=base_url, **kwargs),
+ 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ },
+ self.collection_key)
+ num = len(rl)
+
+ if num == 0:
+ msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+ raise exceptions.NotFound(404, msg)
+ elif num > 1:
+ raise exceptions.NoUniqueMatch
+ else:
+ return rl[0]
+
+
+class Extension(HookableMixin):
+ """Extension descriptor."""
+
+ SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+ manager_class = None
+
+ def __init__(self, name, module):
+ super(Extension, self).__init__()
+ self.name = name
+ self.module = module
+ self._parse_extension_module()
+
+ def _parse_extension_module(self):
+ self.manager_class = None
+ for attr_name, attr_value in self.module.__dict__.items():
+ if attr_name in self.SUPPORTED_HOOKS:
+ self.add_hook(attr_name, attr_value)
+ else:
+ try:
+ if issubclass(attr_value, BaseManager):
+ self.manager_class = attr_value
+ except TypeError:
+ pass
+
+ def __repr__(self):
+ return "<Extension '%s'>" % self.name
+
+
+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):
+ 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)
+ self_cls_name = reflection.get_class_name(self,
+ fully_qualified=False)
+ return "<%s %s>" % (self_cls_name, info)
+
+ @property
+ def human_id(self):
+ """Human-readable ID which can be used for bash completion.
+ """
+ if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
+ return strutils.to_slug(getattr(self, self.NAME_ATTR))
+ 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:
+ # In this case we already defined the attribute on the class
+ pass
+
+ def __getattr__(self, k):
+ 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):
+ # set _loaded first ... so if we have to bail, we know we tried.
+ self._loaded = True
+ if not hasattr(self.manager, 'get'):
+ return
+
+ new = self.manager.get(self.id)
+ if new:
+ self._add_details(new._info)
+
+ def __eq__(self, other):
+ if not isinstance(other, Resource):
+ return NotImplemented
+ # two resources of different types are not equal
+ if not isinstance(other, self.__class__):
+ return False
+ if hasattr(self, 'id') and hasattr(other, 'id'):
+ return self.id == other.id
+ return self._info == other._info
+
+ @property
+ def is_loaded(self):
+ return self._loaded
+
+ def to_dict(self):
+ return copy.deepcopy(self._info)
diff --git a/troveclient/apiclient/client.py b/troveclient/apiclient/client.py
new file mode 100644
index 0000000..a94f46d
--- /dev/null
+++ b/troveclient/apiclient/client.py
@@ -0,0 +1,358 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# Copyright 2013 Alessio Ababilov
+# Copyright 2013 Grid Dynamics
+# 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.
+
+"""
+OpenStack Client interface. Handles the REST calls and responses.
+"""
+
+# E0202: An attribute inherited from %s hide this method
+# pylint: disable=E0202
+
+import logging
+import time
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import requests
+
+from oslo_utils import importutils
+from troveclient.apiclient import exceptions
+
+
+_logger = logging.getLogger(__name__)
+
+
+class HTTPClient(object):
+ """This client handles sending HTTP requests to OpenStack servers.
+
+ Features:
+ - share authentication information between several clients to different
+ services (e.g., for compute and image clients);
+ - reissue authentication request for expired tokens;
+ - encode/decode JSON bodies;
+ - raise exceptions on HTTP errors;
+ - pluggable authentication;
+ - store authentication information in a keyring;
+ - store time spent for requests;
+ - register clients for particular services, so one can use
+ `http_client.identity` or `http_client.compute`;
+ - log requests and responses in a format that is easy to copy-and-paste
+ into terminal and send the same request with curl.
+ """
+
+ user_agent = "troveclient.apiclient"
+
+ def __init__(self,
+ auth_plugin,
+ region_name=None,
+ endpoint_type="publicURL",
+ original_ip=None,
+ verify=True,
+ cert=None,
+ timeout=None,
+ timings=False,
+ keyring_saver=None,
+ debug=False,
+ user_agent=None,
+ http=None):
+ self.auth_plugin = auth_plugin
+
+ self.endpoint_type = endpoint_type
+ self.region_name = region_name
+
+ self.original_ip = original_ip
+ self.timeout = timeout
+ self.verify = verify
+ self.cert = cert
+
+ self.keyring_saver = keyring_saver
+ self.debug = debug
+ self.user_agent = user_agent or self.user_agent
+
+ self.times = [] # [("item", starttime, endtime), ...]
+ self.timings = timings
+
+ # requests within the same session can reuse TCP connections from pool
+ self.http = http or requests.Session()
+
+ self.cached_token = None
+
+ def _http_log_req(self, method, url, kwargs):
+ if not self.debug:
+ return
+
+ 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 _http_log_resp(self, resp):
+ if not self.debug:
+ return
+ _logger.debug(
+ "RESP: [%s] %s\n",
+ resp.status_code,
+ resp.headers)
+ if resp._content_consumed:
+ _logger.debug(
+ "RESP BODY: %s\n",
+ resp.text)
+
+ def serialize(self, kwargs):
+ if kwargs.get('json') is not None:
+ kwargs['headers']['Content-Type'] = 'application/json'
+ kwargs['data'] = json.dumps(kwargs['json'])
+ try:
+ del kwargs['json']
+ except KeyError:
+ pass
+
+ def get_timings(self):
+ return self.times
+
+ def reset_timings(self):
+ self.times = []
+
+ def request(self, method, url, **kwargs):
+ """Send an http request with the specified characteristics.
+
+ Wrapper around `requests.Session.request` to handle tasks such as
+ setting headers, JSON encoding/decoding, and error handling.
+
+ :param method: method of HTTP request
+ :param url: URL of HTTP request
+ :param kwargs: any other parameter that can be passed to
+' requests.Session.request (such as `headers`) or `json`
+ that will be encoded as JSON and used as `data` argument
+ """
+ kwargs.setdefault("headers", kwargs.get("headers", {}))
+ kwargs["headers"]["User-Agent"] = self.user_agent
+ if self.original_ip:
+ kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
+ self.original_ip, self.user_agent)
+ if self.timeout is not None:
+ kwargs.setdefault("timeout", self.timeout)
+ kwargs.setdefault("verify", self.verify)
+ if self.cert is not None:
+ kwargs.setdefault("cert", self.cert)
+ self.serialize(kwargs)
+
+ self._http_log_req(method, url, kwargs)
+ if self.timings:
+ start_time = time.time()
+ resp = self.http.request(method, url, **kwargs)
+ if self.timings:
+ self.times.append(("%s %s" % (method, url),
+ start_time, time.time()))
+ self._http_log_resp(resp)
+
+ if resp.status_code >= 400:
+ _logger.debug(
+ "Request returned failure status: %s",
+ resp.status_code)
+ raise exceptions.from_response(resp, method, url)
+
+ return resp
+
+ @staticmethod
+ def concat_url(endpoint, url):
+ """Concatenate endpoint and final URL.
+
+ E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
+ "http://keystone/v2.0/tokens".
+
+ :param endpoint: the base URL
+ :param url: the final URL
+ """
+ return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
+
+ def client_request(self, client, method, url, **kwargs):
+ """Send an http request using `client`'s endpoint and specified `url`.
+
+ If request was rejected as unauthorized (possibly because the token is
+ expired), issue one authorization attempt and send the request once
+ again.
+
+ :param client: instance of BaseClient descendant
+ :param method: method of HTTP request
+ :param url: URL of HTTP request
+ :param kwargs: any other parameter that can be passed to
+' `HTTPClient.request`
+ """
+
+ filter_args = {
+ "endpoint_type": client.endpoint_type or self.endpoint_type,
+ "service_type": client.service_type,
+ }
+ token, endpoint = (self.cached_token, client.cached_endpoint)
+ just_authenticated = False
+ if not (token and endpoint):
+ try:
+ token, endpoint = self.auth_plugin.token_and_endpoint(
+ **filter_args)
+ except exceptions.EndpointException:
+ pass
+ if not (token and endpoint):
+ self.authenticate()
+ just_authenticated = True
+ token, endpoint = self.auth_plugin.token_and_endpoint(
+ **filter_args)
+ if not (token and endpoint):
+ raise exceptions.AuthorizationFailure(
+ "Cannot find endpoint or token for request")
+
+ old_token_endpoint = (token, endpoint)
+ kwargs.setdefault("headers", {})["X-Auth-Token"] = token
+ self.cached_token = token
+ client.cached_endpoint = endpoint
+ # Perform the request once. If we get Unauthorized, then it
+ # might be because the auth token expired, so try to
+ # re-authenticate and try again. If it still fails, bail.
+ try:
+ return self.request(
+ method, self.concat_url(endpoint, url), **kwargs)
+ except exceptions.Unauthorized as unauth_ex:
+ if just_authenticated:
+ raise
+ self.cached_token = None
+ client.cached_endpoint = None
+ self.authenticate()
+ try:
+ token, endpoint = self.auth_plugin.token_and_endpoint(
+ **filter_args)
+ except exceptions.EndpointException:
+ raise unauth_ex
+ if (not (token and endpoint) or
+ old_token_endpoint == (token, endpoint)):
+ raise unauth_ex
+ self.cached_token = token
+ client.cached_endpoint = endpoint
+ kwargs["headers"]["X-Auth-Token"] = token
+ return self.request(
+ method, self.concat_url(endpoint, url), **kwargs)
+
+ def add_client(self, base_client_instance):
+ """Add a new instance of :class:`BaseClient` descendant.
+
+ `self` will store a reference to `base_client_instance`.
+
+ Example:
+
+ >>> def test_clients():
+ ... from keystoneclient.auth import keystone
+ ... from openstack.common.apiclient import client
+ ... auth = keystone.KeystoneAuthPlugin(
+ ... username="user", password="pass", tenant_name="tenant",
+ ... auth_url="http://auth:5000/v2.0")
+ ... openstack_client = client.HTTPClient(auth)
+ ... # create nova client
+ ... from novaclient.v1_1 import client
+ ... client.Client(openstack_client)
+ ... # create keystone client
+ ... from keystoneclient.v2_0 import client
+ ... client.Client(openstack_client)
+ ... # use them
+ ... openstack_client.identity.tenants.list()
+ ... openstack_client.compute.servers.list()
+ """
+ service_type = base_client_instance.service_type
+ if service_type and not hasattr(self, service_type):
+ setattr(self, service_type, base_client_instance)
+
+ def authenticate(self):
+ self.auth_plugin.authenticate(self)
+ # Store the authentication results in the keyring for later requests
+ if self.keyring_saver:
+ self.keyring_saver.save(self)
+
+
+class BaseClient(object):
+ """Top-level object to access the OpenStack API.
+
+ This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
+ will handle a bunch of issues such as authentication.
+ """
+
+ service_type = None
+ endpoint_type = None # "publicURL" will be used
+ cached_endpoint = None
+
+ def __init__(self, http_client, extensions=None):
+ self.http_client = http_client
+ http_client.add_client(self)
+
+ # Add in any extensions...
+ if extensions:
+ for extension in extensions:
+ if extension.manager_class:
+ setattr(self, extension.name,
+ extension.manager_class(self))
+
+ def client_request(self, method, url, **kwargs):
+ return self.http_client.client_request(
+ self, method, url, **kwargs)
+
+ def head(self, url, **kwargs):
+ return self.client_request("HEAD", url, **kwargs)
+
+ def get(self, url, **kwargs):
+ return self.client_request("GET", url, **kwargs)
+
+ def post(self, url, **kwargs):
+ return self.client_request("POST", url, **kwargs)
+
+ def put(self, url, **kwargs):
+ return self.client_request("PUT", url, **kwargs)
+
+ def delete(self, url, **kwargs):
+ return self.client_request("DELETE", url, **kwargs)
+
+ def patch(self, url, **kwargs):
+ return self.client_request("PATCH", url, **kwargs)
+
+ @staticmethod
+ def get_class(api_name, version, version_map):
+ """Returns the client class for the requested API version
+
+ :param api_name: the name of the API, e.g. 'compute', 'image', etc
+ :param version: the requested API version
+ :param version_map: a dict of client classes keyed by version
+ :rtype: a client class for the requested API version
+ """
+ try:
+ client_path = version_map[str(version)]
+ except (KeyError, ValueError):
+ msg = "Invalid %s client version '%s'. must be one of: %s" % (
+ (api_name, version, ', '.join(version_map.keys())))
+ raise exceptions.UnsupportedVersion(msg)
+
+ return importutils.import_class(client_path)
diff --git a/troveclient/apiclient/exceptions.py b/troveclient/apiclient/exceptions.py
new file mode 100644
index 0000000..c48df3a
--- /dev/null
+++ b/troveclient/apiclient/exceptions.py
@@ -0,0 +1,445 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 Nebula, Inc.
+# Copyright 2013 Alessio Ababilov
+# 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.
+
+"""
+Exception definitions.
+"""
+
+import inspect
+import sys
+
+import six
+
+
+class ClientException(Exception):
+ """The base exception class for all exceptions this library raises.
+ """
+ pass
+
+
+class MissingArgs(ClientException):
+ """Supplied arguments are not sufficient for calling a function."""
+ def __init__(self, missing, message=None):
+ self.missing = missing
+ self.message = message or "Missing argument(s): %s"
+ self.message %= ", ".join(missing)
+ super(MissingArgs, self).__init__(self.message)
+
+
+class ValidationError(ClientException):
+ """Error in validation on API client side."""
+ pass
+
+
+class UnsupportedVersion(ClientException):
+ """User is trying to use an unsupported version of the API."""
+ pass
+
+
+class CommandError(ClientException):
+ """Error in CLI tool."""
+ pass
+
+
+class AuthorizationFailure(ClientException):
+ """Cannot authorize API client."""
+ pass
+
+
+class ConnectionRefused(ClientException):
+ """Cannot connect to API service."""
+ pass
+
+
+class AuthPluginOptionsMissing(AuthorizationFailure):
+ """Auth plugin misses some options."""
+ def __init__(self, opt_names):
+ super(AuthPluginOptionsMissing, self).__init__(
+ "Authentication failed. Missing options: %s" %
+ ", ".join(opt_names))
+ self.opt_names = opt_names
+
+
+class AuthSystemNotFound(AuthorizationFailure):
+ """User has specified a AuthSystem that is not installed."""
+ def __init__(self, auth_system):
+ super(AuthSystemNotFound, self).__init__(
+ "AuthSystemNotFound: %s" % repr(auth_system))
+ self.auth_system = auth_system
+
+
+class NoUniqueMatch(ClientException):
+ """Multiple entities found instead of one."""
+ pass
+
+
+class EndpointException(ClientException):
+ """Something is rotten in Service Catalog."""
+ pass
+
+
+class EndpointNotFound(EndpointException):
+ """Could not find requested endpoint in Service Catalog."""
+ pass
+
+
+class AmbiguousEndpoints(EndpointException):
+ """Found more than one matching endpoint in Service Catalog."""
+ def __init__(self, endpoints=None):
+ super(AmbiguousEndpoints, self).__init__(
+ "AmbiguousEndpoints: %s" % repr(endpoints))
+ self.endpoints = endpoints
+
+
+class HttpError(ClientException):
+ """The base exception class for all HTTP exceptions.
+ """
+ http_status = 0
+ message = "HTTP Error"
+
+ def __init__(self, message=None, details=None,
+ response=None, request_id=None,
+ url=None, method=None, http_status=None):
+ self.http_status = http_status or self.http_status
+ self.message = message or self.message
+ self.details = details
+ self.request_id = request_id
+ self.response = response
+ self.url = url
+ self.method = method
+ formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
+ if request_id:
+ formatted_string += " (Request-ID: %s)" % request_id
+ super(HttpError, self).__init__(formatted_string)
+
+
+class HTTPClientError(HttpError):
+ """Client-side HTTP error.
+
+ Exception for cases in which the client seems to have erred.
+ """
+ message = "HTTP Client Error"
+
+
+class HttpServerError(HttpError):
+ """Server-side HTTP error.
+
+ Exception for cases in which the server is aware that it has
+ erred or is incapable of performing the request.
+ """
+ message = "HTTP Server Error"
+
+
+class BadRequest(HTTPClientError):
+ """HTTP 400 - Bad Request.
+
+ The request cannot be fulfilled due to bad syntax.
+ """
+ http_status = 400
+ message = "Bad Request"
+
+
+class Unauthorized(HTTPClientError):
+ """HTTP 401 - Unauthorized.
+
+ Similar to 403 Forbidden, but specifically for use when authentication
+ is required and has failed or has not yet been provided.
+ """
+ http_status = 401
+ message = "Unauthorized"
+
+
+class PaymentRequired(HTTPClientError):
+ """HTTP 402 - Payment Required.
+
+ Reserved for future use.
+ """
+ http_status = 402
+ message = "Payment Required"
+
+
+class Forbidden(HTTPClientError):
+ """HTTP 403 - Forbidden.
+
+ The request was a valid request, but the server is refusing to respond
+ to it.
+ """
+ http_status = 403
+ message = "Forbidden"
+
+
+class NotFound(HTTPClientError):
+ """HTTP 404 - Not Found.
+
+ The requested resource could not be found but may be available again
+ in the future.
+ """
+ http_status = 404
+ message = "Not Found"
+
+
+class MethodNotAllowed(HTTPClientError):
+ """HTTP 405 - Method Not Allowed.
+
+ A request was made of a resource using a request method not supported
+ by that resource.
+ """
+ http_status = 405
+ message = "Method Not Allowed"
+
+
+class NotAcceptable(HTTPClientError):
+ """HTTP 406 - Not Acceptable.
+
+ The requested resource is only capable of generating content not
+ acceptable according to the Accept headers sent in the request.
+ """
+ http_status = 406
+ message = "Not Acceptable"
+
+
+class ProxyAuthenticationRequired(HTTPClientError):
+ """HTTP 407 - Proxy Authentication Required.
+
+ The client must first authenticate itself with the proxy.
+ """
+ http_status = 407
+ message = "Proxy Authentication Required"
+
+
+class RequestTimeout(HTTPClientError):
+ """HTTP 408 - Request Timeout.
+
+ The server timed out waiting for the request.
+ """
+ http_status = 408
+ message = "Request Timeout"
+
+
+class Conflict(HTTPClientError):
+ """HTTP 409 - Conflict.
+
+ Indicates that the request could not be processed because of conflict
+ in the request, such as an edit conflict.
+ """
+ http_status = 409
+ message = "Conflict"
+
+
+class Gone(HTTPClientError):
+ """HTTP 410 - Gone.
+
+ Indicates that the resource requested is no longer available and will
+ not be available again.
+ """
+ http_status = 410
+ message = "Gone"
+
+
+class LengthRequired(HTTPClientError):
+ """HTTP 411 - Length Required.
+
+ The request did not specify the length of its content, which is
+ required by the requested resource.
+ """
+ http_status = 411
+ message = "Length Required"
+
+
+class PreconditionFailed(HTTPClientError):
+ """HTTP 412 - Precondition Failed.
+
+ The server does not meet one of the preconditions that the requester
+ put on the request.
+ """
+ http_status = 412
+ message = "Precondition Failed"
+
+
+class RequestEntityTooLarge(HTTPClientError):
+ """HTTP 413 - Request Entity Too Large.
+
+ The request is larger than the server is willing or able to process.
+ """
+ http_status = 413
+ message = "Request Entity Too Large"
+
+ def __init__(self, *args, **kwargs):
+ try:
+ self.retry_after = int(kwargs.pop('retry_after'))
+ except (KeyError, ValueError):
+ self.retry_after = 0
+
+ super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
+
+
+class RequestUriTooLong(HTTPClientError):
+ """HTTP 414 - Request-URI Too Long.
+
+ The URI provided was too long for the server to process.
+ """
+ http_status = 414
+ message = "Request-URI Too Long"
+
+
+class UnsupportedMediaType(HTTPClientError):
+ """HTTP 415 - Unsupported Media Type.
+
+ The request entity has a media type which the server or resource does
+ not support.
+ """
+ http_status = 415
+ message = "Unsupported Media Type"
+
+
+class RequestedRangeNotSatisfiable(HTTPClientError):
+ """HTTP 416 - Requested Range Not Satisfiable.
+
+ The client has asked for a portion of the file, but the server cannot
+ supply that portion.
+ """
+ http_status = 416
+ message = "Requested Range Not Satisfiable"
+
+
+class ExpectationFailed(HTTPClientError):
+ """HTTP 417 - Expectation Failed.
+
+ The server cannot meet the requirements of the Expect request-header field.
+ """
+ http_status = 417
+ message = "Expectation Failed"
+
+
+class UnprocessableEntity(HTTPClientError):
+ """HTTP 422 - Unprocessable Entity.
+
+ The request was well-formed but was unable to be followed due to semantic
+ errors.
+ """
+ http_status = 422
+ message = "Unprocessable Entity"
+
+
+class InternalServerError(HttpServerError):
+ """HTTP 500 - Internal Server Error.
+
+ A generic error message, given when no more specific message is suitable.
+ """
+ http_status = 500
+ message = "Internal Server Error"
+
+
+# NotImplemented is a python keyword.
+class HttpNotImplemented(HttpServerError):
+ """HTTP 501 - Not Implemented.
+
+ The server either does not recognize the request method, or it lacks
+ the ability to fulfill the request.
+ """
+ http_status = 501
+ message = "Not Implemented"
+
+
+class BadGateway(HttpServerError):
+ """HTTP 502 - Bad Gateway.
+
+ The server was acting as a gateway or proxy and received an invalid
+ response from the upstream server.
+ """
+ http_status = 502
+ message = "Bad Gateway"
+
+
+class ServiceUnavailable(HttpServerError):
+ """HTTP 503 - Service Unavailable.
+
+ The server is currently unavailable.
+ """
+ http_status = 503
+ message = "Service Unavailable"
+
+
+class GatewayTimeout(HttpServerError):
+ """HTTP 504 - Gateway Timeout.
+
+ The server was acting as a gateway or proxy and did not receive a timely
+ response from the upstream server.
+ """
+ http_status = 504
+ message = "Gateway Timeout"
+
+
+class HttpVersionNotSupported(HttpServerError):
+ """HTTP 505 - HttpVersion Not Supported.
+
+ The server does not support the HTTP protocol version used in the request.
+ """
+ http_status = 505
+ message = "HTTP Version Not Supported"
+
+
+# _code_map contains all the classes that have http_status attribute.
+_code_map = dict(
+ (getattr(obj, 'http_status', None), obj)
+ for name, obj in six.iteritems(vars(sys.modules[__name__]))
+ if inspect.isclass(obj) and getattr(obj, 'http_status', False)
+)
+
+
+def from_response(response, method, url):
+ """Returns an instance of :class:`HttpError` or subclass based on response.
+
+ :param response: instance of `requests.Response` class
+ :param method: HTTP method used for request
+ :param url: URL used for request
+ """
+ kwargs = {
+ "http_status": response.status_code,
+ "response": response,
+ "method": method,
+ "url": url,
+ "request_id": response.headers.get("x-compute-request-id"),
+ }
+ if "retry-after" in response.headers:
+ kwargs["retry_after"] = response.headers["retry-after"]
+
+ content_type = response.headers.get("Content-Type", "")
+ if content_type.startswith("application/json"):
+ try:
+ body = response.json()
+ except ValueError:
+ pass
+ else:
+ if isinstance(body, dict):
+ error = list(body.values())[0]
+ kwargs["message"] = error.get("message", None)
+ kwargs["details"] = error.get("details", None)
+ elif content_type.startswith("text/"):
+ kwargs["details"] = response.text
+
+ try:
+ cls = _code_map[response.status_code]
+ except KeyError:
+ if 500 <= response.status_code < 600:
+ cls = HttpServerError
+ elif 400 <= response.status_code < 500:
+ cls = HTTPClientError
+ else:
+ cls = HttpError
+ return cls(**kwargs)