summaryrefslogtreecommitdiff
path: root/heatclient/openstack
diff options
context:
space:
mode:
authorThomas Herve <thomas.herve@enovance.com>2014-04-03 12:40:35 +0200
committerThomas Herve <thomas.herve@enovance.com>2014-04-03 12:40:35 +0200
commit72017c566850da589f7dc7dc275871986aadbd54 (patch)
treeeaddd14fa7ac3a799bc27e4c2b152bfbb3a1d19d /heatclient/openstack
parent34097a70a346f2dde59f47b035cc3477b826591f (diff)
downloadpython-heatclient-72017c566850da589f7dc7dc275871986aadbd54.tar.gz
Sync oslo incubator
Synchronize with latest versions for used oslo incubator modules. Change-Id: I165d95cd0845780cffce7a8adff221dd383b52b8
Diffstat (limited to 'heatclient/openstack')
-rw-r--r--heatclient/openstack/common/__init__.py15
-rw-r--r--heatclient/openstack/common/apiclient/__init__.py14
-rw-r--r--heatclient/openstack/common/apiclient/auth.py221
-rw-r--r--heatclient/openstack/common/apiclient/base.py19
-rw-r--r--heatclient/openstack/common/apiclient/client.py358
-rw-r--r--heatclient/openstack/common/apiclient/exceptions.py24
-rw-r--r--heatclient/openstack/common/apiclient/fake_client.py173
-rw-r--r--heatclient/openstack/common/gettextutils.py44
-rw-r--r--heatclient/openstack/common/importutils.py7
9 files changed, 818 insertions, 57 deletions
diff --git a/heatclient/openstack/common/__init__.py b/heatclient/openstack/common/__init__.py
index 2a00f3b..d1223ea 100644
--- a/heatclient/openstack/common/__init__.py
+++ b/heatclient/openstack/common/__init__.py
@@ -1,2 +1,17 @@
+#
+# 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.
+
import six
+
+
six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))
diff --git a/heatclient/openstack/common/apiclient/__init__.py b/heatclient/openstack/common/apiclient/__init__.py
index f3d0cde..e69de29 100644
--- a/heatclient/openstack/common/apiclient/__init__.py
+++ b/heatclient/openstack/common/apiclient/__init__.py
@@ -1,14 +0,0 @@
-# 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.
diff --git a/heatclient/openstack/common/apiclient/auth.py b/heatclient/openstack/common/apiclient/auth.py
new file mode 100644
index 0000000..0535748
--- /dev/null
+++ b/heatclient/openstack/common/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 heatclient.openstack.common.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 = "heatclient.openstack.common.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/heatclient/openstack/common/apiclient/base.py b/heatclient/openstack/common/apiclient/base.py
index 5051965..14b5766 100644
--- a/heatclient/openstack/common/apiclient/base.py
+++ b/heatclient/openstack/common/apiclient/base.py
@@ -457,17 +457,22 @@ class Resource(object):
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()
+ 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
+ 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
@@ -485,9 +490,11 @@ class Resource(object):
return self.id == other.id
return self._info == other._info
- @property
def is_loaded(self):
return self._loaded
+ def set_loaded(self, val):
+ self._loaded = val
+
def to_dict(self):
return copy.deepcopy(self._info)
diff --git a/heatclient/openstack/common/apiclient/client.py b/heatclient/openstack/common/apiclient/client.py
new file mode 100644
index 0000000..8f67185
--- /dev/null
+++ b/heatclient/openstack/common/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 heatclient.openstack.common.apiclient import exceptions
+from heatclient.openstack.common import importutils
+
+
+_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 = "heatclient.openstack.common.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/heatclient/openstack/common/apiclient/exceptions.py b/heatclient/openstack/common/apiclient/exceptions.py
index 45a70e0..ada1344 100644
--- a/heatclient/openstack/common/apiclient/exceptions.py
+++ b/heatclient/openstack/common/apiclient/exceptions.py
@@ -60,6 +60,11 @@ class AuthorizationFailure(ClientException):
pass
+class ConnectionRefused(ClientException):
+ """Cannot connect to API service."""
+ pass
+
+
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
@@ -122,6 +127,11 @@ class HttpError(ClientException):
super(HttpError, self).__init__(formatted_string)
+class HTTPRedirection(HttpError):
+ """HTTP Redirection."""
+ message = "HTTP Redirection"
+
+
class HTTPClientError(HttpError):
"""Client-side HTTP error.
@@ -139,6 +149,16 @@ class HttpServerError(HttpError):
message = "HTTP Server Error"
+class MultipleChoices(HTTPRedirection):
+ """HTTP 300 - Multiple Choices.
+
+ Indicates multiple options for the resource that the client may follow.
+ """
+
+ http_status = 300
+ message = "Multiple Choices"
+
+
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
@@ -420,8 +440,8 @@ def from_response(response, method, url):
except ValueError:
pass
else:
- if hasattr(body, "keys"):
- error = body[body.keys()[0]]
+ if isinstance(body, dict):
+ error = list(body.values())[0]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif content_type.startswith("text/"):
diff --git a/heatclient/openstack/common/apiclient/fake_client.py b/heatclient/openstack/common/apiclient/fake_client.py
new file mode 100644
index 0000000..eb10e0f
--- /dev/null
+++ b/heatclient/openstack/common/apiclient/fake_client.py
@@ -0,0 +1,173 @@
+# 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.
+
+"""
+A fake server that "responds" to API methods with pre-canned responses.
+
+All of these responses come from the spec, so if for some reason the spec's
+wrong the tests might raise AssertionError. I've indicated in comments the
+places where actual behavior differs from the spec.
+"""
+
+# W0102: Dangerous default value %s as argument
+# pylint: disable=W0102
+
+import json
+
+import requests
+import six
+from six.moves.urllib import parse
+
+from heatclient.openstack.common.apiclient import client
+
+
+def assert_has_keys(dct, required=[], optional=[]):
+ for k in required:
+ try:
+ assert k in dct
+ except AssertionError:
+ extra_keys = set(dct.keys()).difference(set(required + optional))
+ raise AssertionError("found unexpected keys: %s" %
+ list(extra_keys))
+
+
+class TestResponse(requests.Response):
+ """Wrap requests.Response and provide a convenient initialization.
+ """
+
+ def __init__(self, data):
+ super(TestResponse, self).__init__()
+ self._content_consumed = True
+ if isinstance(data, dict):
+ self.status_code = data.get('status_code', 200)
+ # Fake the text attribute to streamline Response creation
+ text = data.get('text', "")
+ if isinstance(text, (dict, list)):
+ self._content = json.dumps(text)
+ default_headers = {
+ "Content-Type": "application/json",
+ }
+ else:
+ self._content = text
+ default_headers = {}
+ if six.PY3 and isinstance(self._content, six.string_types):
+ self._content = self._content.encode('utf-8', 'strict')
+ self.headers = data.get('headers') or default_headers
+ else:
+ self.status_code = data
+
+ def __eq__(self, other):
+ return (self.status_code == other.status_code and
+ self.headers == other.headers and
+ self._content == other._content)
+
+
+class FakeHTTPClient(client.HTTPClient):
+
+ def __init__(self, *args, **kwargs):
+ self.callstack = []
+ self.fixtures = kwargs.pop("fixtures", None) or {}
+ if not args and not "auth_plugin" in kwargs:
+ args = (None, )
+ super(FakeHTTPClient, self).__init__(*args, **kwargs)
+
+ def assert_called(self, method, url, body=None, pos=-1):
+ """Assert than an API method was just called.
+ """
+ expected = (method, url)
+ called = self.callstack[pos][0:2]
+ assert self.callstack, \
+ "Expected %s %s but no calls were made." % expected
+
+ assert expected == called, 'Expected %s %s; got %s %s' % \
+ (expected + called)
+
+ if body is not None:
+ if self.callstack[pos][3] != body:
+ raise AssertionError('%r != %r' %
+ (self.callstack[pos][3], body))
+
+ def assert_called_anytime(self, method, url, body=None):
+ """Assert than an API method was called anytime in the test.
+ """
+ expected = (method, url)
+
+ assert self.callstack, \
+ "Expected %s %s but no calls were made." % expected
+
+ found = False
+ entry = None
+ for entry in self.callstack:
+ if expected == entry[0:2]:
+ found = True
+ break
+
+ assert found, 'Expected %s %s; got %s' % \
+ (method, url, self.callstack)
+ if body is not None:
+ assert entry[3] == body, "%s != %s" % (entry[3], body)
+
+ self.callstack = []
+
+ def clear_callstack(self):
+ self.callstack = []
+
+ def authenticate(self):
+ pass
+
+ def client_request(self, client, method, url, **kwargs):
+ # Check that certain things are called correctly
+ if method in ["GET", "DELETE"]:
+ assert "json" not in kwargs
+
+ # Note the call
+ self.callstack.append(
+ (method,
+ url,
+ kwargs.get("headers") or {},
+ kwargs.get("json") or kwargs.get("data")))
+ try:
+ fixture = self.fixtures[url][method]
+ except KeyError:
+ pass
+ else:
+ return TestResponse({"headers": fixture[0],
+ "text": fixture[1]})
+
+ # Call the method
+ args = parse.parse_qsl(parse.urlparse(url)[4])
+ kwargs.update(args)
+ munged_url = url.rsplit('?', 1)[0]
+ munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
+ munged_url = munged_url.replace('-', '_')
+
+ callback = "%s_%s" % (method.lower(), munged_url)
+
+ if not hasattr(self, callback):
+ raise AssertionError('Called unknown API method: %s %s, '
+ 'expected fakes method name: %s' %
+ (method, url, callback))
+
+ resp = getattr(self, callback)(**kwargs)
+ if len(resp) == 3:
+ status, headers, body = resp
+ else:
+ status, body = resp
+ headers = {}
+ return TestResponse({
+ "status_code": status,
+ "text": body,
+ "headers": headers,
+ })
diff --git a/heatclient/openstack/common/gettextutils.py b/heatclient/openstack/common/gettextutils.py
index 1a49c9d..07805a4 100644
--- a/heatclient/openstack/common/gettextutils.py
+++ b/heatclient/openstack/common/gettextutils.py
@@ -28,7 +28,6 @@ import gettext
import locale
from logging import handlers
import os
-import re
from babel import localedata
import six
@@ -248,47 +247,22 @@ class Message(six.text_type):
if other is None:
params = (other,)
elif isinstance(other, dict):
- params = self._trim_dictionary_parameters(other)
- else:
- params = self._copy_param(other)
- return params
-
- def _trim_dictionary_parameters(self, dict_param):
- """Return a dict that only has matching entries in the msgid."""
- # NOTE(luisg): Here we trim down the dictionary passed as parameters
- # to avoid carrying a lot of unnecessary weight around in the message
- # object, for example if someone passes in Message() % locals() but
- # only some params are used, and additionally we prevent errors for
- # non-deepcopyable objects by unicoding() them.
-
- # Look for %(param) keys in msgid;
- # Skip %% and deal with the case where % is first character on the line
- keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
-
- # If we don't find any %(param) keys but have a %s
- if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
- # Apparently the full dictionary is the parameter
- params = self._copy_param(dict_param)
- else:
+ # Merge the dictionaries
+ # Copy each item in case one does not support deep copy.
params = {}
- # Save our existing parameters as defaults to protect
- # ourselves from losing values if we are called through an
- # (erroneous) chain that builds a valid Message with
- # arguments, and then does something like "msg % kwds"
- # where kwds is an empty dictionary.
- src = {}
if isinstance(self.params, dict):
- src.update(self.params)
- src.update(dict_param)
- for key in keys:
- params[key] = self._copy_param(src[key])
-
+ for key, val in self.params.items():
+ params[key] = self._copy_param(val)
+ for key, val in other.items():
+ params[key] = self._copy_param(val)
+ else:
+ params = self._copy_param(other)
return params
def _copy_param(self, param):
try:
return copy.deepcopy(param)
- except TypeError:
+ except Exception:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
diff --git a/heatclient/openstack/common/importutils.py b/heatclient/openstack/common/importutils.py
index 4fd9ae2..7b4b09a 100644
--- a/heatclient/openstack/common/importutils.py
+++ b/heatclient/openstack/common/importutils.py
@@ -58,6 +58,13 @@ def import_module(import_str):
return sys.modules[import_str]
+def import_versioned_module(version, submodule=None):
+ module = 'heatclient.v%s' % version
+ if submodule:
+ module = '.'.join((module, submodule))
+ return import_module(module)
+
+
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try: