diff --git a/cinderclient/openstack/common/apiclient/__init__.py b/cinderclient/openstack/common/apiclient/__init__.py
new file mode 100644
index 000000000..d5d002224
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/__init__.py
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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/cinderclient/openstack/common/apiclient/auth.py b/cinderclient/openstack/common/apiclient/auth.py
new file mode 100644
index 000000000..374d20b67
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/auth.py
@@ -0,0 +1,227 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 logging
+import os
+
+from stevedore import extension
+
+from cinderclient.openstack.common.apiclient import exceptions
+
+
+logger = logging.getLogger(__name__)
+
+
+_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 = "cinderclient.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 _discovered_plugins.iteritems():
+        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 requred 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: AuthorizationFailure
+    """
+    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(_discovered_plugins.iterkeys()):
+        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"])
+
+
+class BaseAuthPlugin(object):
+    """Base class for authentication plugins.
+
+    An authentication plugin needs to override at least the authenticate
+    method to be a valid plugin.
+    """
+
+    __metaclass__ = abc.ABCMeta
+
+    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/cinderclient/openstack/common/apiclient/base.py b/cinderclient/openstack/common/apiclient/base.py
new file mode 100644
index 000000000..1b3e7903a
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/base.py
@@ -0,0 +1,492 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC
+# 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 urllib
+
+from cinderclient.openstack.common.apiclient import exceptions
+from cinderclient.openstack.common import strutils
+
+
+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)
+
+
+class ManagerWithFind(BaseManager):
+    """Manager with additional `find()`/`findall()` methods."""
+
+    __metaclass__ = abc.ABCMeta
+
+    @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 kwargs.copy().iteritems():
+            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' % urllib.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' % urllib.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)
+        return "<%s %s>" % (self.__class__.__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 info.iteritems():
+            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.set_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
+
+    def is_loaded(self):
+        return self._loaded
+
+    def set_loaded(self, val):
+        self._loaded = val
diff --git a/cinderclient/openstack/common/apiclient/client.py b/cinderclient/openstack/common/apiclient/client.py
new file mode 100644
index 000000000..85837da14
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/client.py
@@ -0,0 +1,360 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC
+# 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 cinderclient.openstack.common.apiclient import exceptions
+from cinderclient.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 exeptions 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 = "cinderclient.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/cinderclient/openstack/common/apiclient/exceptions.py b/cinderclient/openstack/common/apiclient/exceptions.py
new file mode 100644
index 000000000..b03def77c
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/exceptions.py
@@ -0,0 +1,446 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 sys
+
+
+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):
+        self.missing = missing
+        msg = "Missing argument(s): %s" % ", ".join(missing)
+        super(MissingArgs, self).__init__(msg)
+
+
+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 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"
+
+
+# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
+# so we can do this:
+#     _code_map = dict((c.http_status, c)
+#                      for c in HttpError.__subclasses__())
+_code_map = {}
+for obj in sys.modules[__name__].__dict__.values():
+    if isinstance(obj, type):
+        try:
+            http_status = obj.http_status
+        except AttributeError:
+            pass
+        else:
+            if http_status:
+                _code_map[http_status] = obj
+
+
+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 hasattr(body, "keys"):
+                error = body[body.keys()[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)
diff --git a/cinderclient/openstack/common/apiclient/fake_client.py b/cinderclient/openstack/common/apiclient/fake_client.py
new file mode 100644
index 000000000..914cebdbc
--- /dev/null
+++ b/cinderclient/openstack/common/apiclient/fake_client.py
@@ -0,0 +1,172 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 urlparse
+
+import requests
+
+from cinderclient.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 = {}
+            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 = urlparse.parse_qsl(urlparse.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/cinderclient/openstack/common/gettextutils.py b/cinderclient/openstack/common/gettextutils.py
new file mode 100644
index 000000000..e88786967
--- /dev/null
+++ b/cinderclient/openstack/common/gettextutils.py
@@ -0,0 +1,305 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Red Hat, Inc.
+# Copyright 2013 IBM Corp.
+# 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.
+
+"""
+gettext for openstack-common modules.
+
+Usual usage in an openstack.common module:
+
+    from cinderclient.openstack.common.gettextutils import _
+"""
+
+import copy
+import gettext
+import logging.handlers
+import os
+import re
+import UserString
+
+from babel import localedata
+import six
+
+_localedir = os.environ.get('cinderclient'.upper() + '_LOCALEDIR')
+_t = gettext.translation('cinderclient', localedir=_localedir, fallback=True)
+
+_AVAILABLE_LANGUAGES = []
+
+
+def _(msg):
+    return _t.ugettext(msg)
+
+
+def install(domain, lazy=False):
+    """Install a _() function using the given translation domain.
+
+    Given a translation domain, install a _() function using gettext's
+    install() function.
+
+    The main difference from gettext.install() is that we allow
+    overriding the default localedir (e.g. /usr/share/locale) using
+    a translation-domain-specific environment variable (e.g.
+    NOVA_LOCALEDIR).
+
+    :param domain: the translation domain
+    :param lazy: indicates whether or not to install the lazy _() function.
+                 The lazy _() introduces a way to do deferred translation
+                 of messages by installing a _ that builds Message objects,
+                 instead of strings, which can then be lazily translated into
+                 any available locale.
+    """
+    if lazy:
+        # NOTE(mrodden): Lazy gettext functionality.
+        #
+        # The following introduces a deferred way to do translations on
+        # messages in OpenStack. We override the standard _() function
+        # and % (format string) operation to build Message objects that can
+        # later be translated when we have more information.
+        #
+        # Also included below is an example LocaleHandler that translates
+        # Messages to an associated locale, effectively allowing many logs,
+        # each with their own locale.
+
+        def _lazy_gettext(msg):
+            """Create and return a Message object.
+
+            Lazy gettext function for a given domain, it is a factory method
+            for a project/module to get a lazy gettext function for its own
+            translation domain (i.e. nova, glance, cinder, etc.)
+
+            Message encapsulates a string so that we can translate
+            it later when needed.
+            """
+            return Message(msg, domain)
+
+        import __builtin__
+        __builtin__.__dict__['_'] = _lazy_gettext
+    else:
+        localedir = '%s_LOCALEDIR' % domain.upper()
+        gettext.install(domain,
+                        localedir=os.environ.get(localedir),
+                        unicode=True)
+
+
+class Message(UserString.UserString, object):
+    """Class used to encapsulate translatable messages."""
+    def __init__(self, msg, domain):
+        # _msg is the gettext msgid and should never change
+        self._msg = msg
+        self._left_extra_msg = ''
+        self._right_extra_msg = ''
+        self.params = None
+        self.locale = None
+        self.domain = domain
+
+    @property
+    def data(self):
+        # NOTE(mrodden): this should always resolve to a unicode string
+        # that best represents the state of the message currently
+
+        localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
+        if self.locale:
+            lang = gettext.translation(self.domain,
+                                       localedir=localedir,
+                                       languages=[self.locale],
+                                       fallback=True)
+        else:
+            # use system locale for translations
+            lang = gettext.translation(self.domain,
+                                       localedir=localedir,
+                                       fallback=True)
+
+        full_msg = (self._left_extra_msg +
+                    lang.ugettext(self._msg) +
+                    self._right_extra_msg)
+
+        if self.params is not None:
+            full_msg = full_msg % self.params
+
+        return six.text_type(full_msg)
+
+    def _save_dictionary_parameter(self, dict_param):
+        full_msg = self.data
+        # look for %(blah) fields in string;
+        # ignore %% and deal with the
+        # case where % is first character on the line
+        keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
+
+        # if we don't find any %(blah) blocks but have a %s
+        if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
+            # apparently the full dictionary is the parameter
+            params = copy.deepcopy(dict_param)
+        else:
+            params = {}
+            for key in keys:
+                try:
+                    params[key] = copy.deepcopy(dict_param[key])
+                except TypeError:
+                    # cast uncopyable thing to unicode string
+                    params[key] = unicode(dict_param[key])
+
+        return params
+
+    def _save_parameters(self, other):
+        # we check for None later to see if
+        # we actually have parameters to inject,
+        # so encapsulate if our parameter is actually None
+        if other is None:
+            self.params = (other, )
+        elif isinstance(other, dict):
+            self.params = self._save_dictionary_parameter(other)
+        else:
+            # fallback to casting to unicode,
+            # this will handle the problematic python code-like
+            # objects that cannot be deep-copied
+            try:
+                self.params = copy.deepcopy(other)
+            except TypeError:
+                self.params = unicode(other)
+
+        return self
+
+    # overrides to be more string-like
+    def __unicode__(self):
+        return self.data
+
+    def __str__(self):
+        return self.data.encode('utf-8')
+
+    def __getstate__(self):
+        to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
+                   'domain', 'params', 'locale']
+        new_dict = self.__dict__.fromkeys(to_copy)
+        for attr in to_copy:
+            new_dict[attr] = copy.deepcopy(self.__dict__[attr])
+
+        return new_dict
+
+    def __setstate__(self, state):
+        for (k, v) in state.items():
+            setattr(self, k, v)
+
+    # operator overloads
+    def __add__(self, other):
+        copied = copy.deepcopy(self)
+        copied._right_extra_msg += other.__str__()
+        return copied
+
+    def __radd__(self, other):
+        copied = copy.deepcopy(self)
+        copied._left_extra_msg += other.__str__()
+        return copied
+
+    def __mod__(self, other):
+        # do a format string to catch and raise
+        # any possible KeyErrors from missing parameters
+        self.data % other
+        copied = copy.deepcopy(self)
+        return copied._save_parameters(other)
+
+    def __mul__(self, other):
+        return self.data * other
+
+    def __rmul__(self, other):
+        return other * self.data
+
+    def __getitem__(self, key):
+        return self.data[key]
+
+    def __getslice__(self, start, end):
+        return self.data.__getslice__(start, end)
+
+    def __getattribute__(self, name):
+        # NOTE(mrodden): handle lossy operations that we can't deal with yet
+        # These override the UserString implementation, since UserString
+        # uses our __class__ attribute to try and build a new message
+        # after running the inner data string through the operation.
+        # At that point, we have lost the gettext message id and can just
+        # safely resolve to a string instead.
+        ops = ['capitalize', 'center', 'decode', 'encode',
+               'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
+               'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
+        if name in ops:
+            return getattr(self.data, name)
+        else:
+            return UserString.UserString.__getattribute__(self, name)
+
+
+def get_available_languages(domain):
+    """Lists the available languages for the given translation domain.
+
+    :param domain: the domain to get languages for
+    """
+    if _AVAILABLE_LANGUAGES:
+        return _AVAILABLE_LANGUAGES
+
+    localedir = '%s_LOCALEDIR' % domain.upper()
+    find = lambda x: gettext.find(domain,
+                                  localedir=os.environ.get(localedir),
+                                  languages=[x])
+
+    # NOTE(mrodden): en_US should always be available (and first in case
+    # order matters) since our in-line message strings are en_US
+    _AVAILABLE_LANGUAGES.append('en_US')
+    # NOTE(luisg): Babel <1.0 used a function called list(), which was
+    # renamed to locale_identifiers() in >=1.0, the requirements master list
+    # requires >=0.9.6, uncapped, so defensively work with both. We can remove
+    # this check when the master list updates to >=1.0, and all projects udpate
+    list_identifiers = (getattr(localedata, 'list', None) or
+                        getattr(localedata, 'locale_identifiers'))
+    locale_identifiers = list_identifiers()
+    for i in locale_identifiers:
+        if find(i) is not None:
+            _AVAILABLE_LANGUAGES.append(i)
+    return _AVAILABLE_LANGUAGES
+
+
+def get_localized_message(message, user_locale):
+    """Gets a localized version of the given message in the given locale."""
+    if (isinstance(message, Message)):
+        if user_locale:
+            message.locale = user_locale
+        return unicode(message)
+    else:
+        return message
+
+
+class LocaleHandler(logging.Handler):
+    """Handler that can have a locale associated to translate Messages.
+
+    A quick example of how to utilize the Message class above.
+    LocaleHandler takes a locale and a target logging.Handler object
+    to forward LogRecord objects to after translating the internal Message.
+    """
+
+    def __init__(self, locale, target):
+        """Initialize a LocaleHandler
+
+        :param locale: locale to use for translating messages
+        :param target: logging.Handler object to forward
+                       LogRecord objects to after translation
+        """
+        logging.Handler.__init__(self)
+        self.locale = locale
+        self.target = target
+
+    def emit(self, record):
+        if isinstance(record.msg, Message):
+            # set the locale and resolve to a string
+            record.msg.locale = self.locale
+
+        self.target.emit(record)
diff --git a/cinderclient/openstack/common/importutils.py b/cinderclient/openstack/common/importutils.py
new file mode 100644
index 000000000..7a303f93f
--- /dev/null
+++ b/cinderclient/openstack/common/importutils.py
@@ -0,0 +1,68 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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.
+
+"""
+Import related utilities and helper functions.
+"""
+
+import sys
+import traceback
+
+
+def import_class(import_str):
+    """Returns a class from a string including module and class."""
+    mod_str, _sep, class_str = import_str.rpartition('.')
+    try:
+        __import__(mod_str)
+        return getattr(sys.modules[mod_str], class_str)
+    except (ValueError, AttributeError):
+        raise ImportError('Class %s cannot be found (%s)' %
+                          (class_str,
+                           traceback.format_exception(*sys.exc_info())))
+
+
+def import_object(import_str, *args, **kwargs):
+    """Import a class and return an instance of it."""
+    return import_class(import_str)(*args, **kwargs)
+
+
+def import_object_ns(name_space, import_str, *args, **kwargs):
+    """Tries to import object from default namespace.
+
+    Imports a class and return an instance of it, first by trying
+    to find the class in a default namespace, then failing back to
+    a full path if not found in the default namespace.
+    """
+    import_value = "%s.%s" % (name_space, import_str)
+    try:
+        return import_class(import_value)(*args, **kwargs)
+    except ImportError:
+        return import_class(import_str)(*args, **kwargs)
+
+
+def import_module(import_str):
+    """Import a module."""
+    __import__(import_str)
+    return sys.modules[import_str]
+
+
+def try_import(import_str, default=None):
+    """Try to import a module and if it fails return default."""
+    try:
+        return import_module(import_str)
+    except ImportError:
+        return default
diff --git a/cinderclient/openstack/common/strutils.py b/cinderclient/openstack/common/strutils.py
index 7813b6422..7c9fceca5 100644
--- a/cinderclient/openstack/common/strutils.py
+++ b/cinderclient/openstack/common/strutils.py
@@ -1,6 +1,6 @@
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 
-# Copyright 2011 OpenStack LLC.
+# Copyright 2011 OpenStack Foundation.
 # All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -19,15 +19,35 @@
 System-level utilities and helper functions.
 """
 
-import logging
+import re
 import sys
+import unicodedata
 
-LOG = logging.getLogger(__name__)
+import six
+
+from cinderclient.openstack.common.gettextutils import _  # noqa
+
+
+# Used for looking up extensions of text
+# to their 'multiplied' byte amount
+BYTE_MULTIPLIERS = {
+    '': 1,
+    't': 1024 ** 4,
+    'g': 1024 ** 3,
+    'm': 1024 ** 2,
+    'k': 1024,
+}
+BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
+
+TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
+FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
+
+SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
+SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
 
 
 def int_from_bool_as_string(subject):
-    """
-    Interpret a string as a boolean and return either 1 or 0.
+    """Interpret a string as a boolean and return either 1 or 0.
 
     Any string value in:
 
@@ -40,42 +60,53 @@ def int_from_bool_as_string(subject):
     return bool_from_string(subject) and 1 or 0
 
 
-def bool_from_string(subject):
+def bool_from_string(subject, strict=False):
+    """Interpret a string as a boolean.
+
+    A case-insensitive match is performed such that strings matching 't',
+    'true', 'on', 'y', 'yes', or '1' are considered True and, when
+    `strict=False`, anything else is considered False.
+
+    Useful for JSON-decoded stuff and config file parsing.
+
+    If `strict=True`, unrecognized values, including None, will raise a
+    ValueError which is useful when parsing values passed in from an API call.
+    Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
     """
-    Interpret a string as a boolean.
+    if not isinstance(subject, six.string_types):
+        subject = str(subject)
 
-    Any string value in:
+    lowered = subject.strip().lower()
 
-        ('True', 'true', 'On', 'on', 'Yes', 'yes', '1')
-
-    is interpreted as a boolean True.
-
-    Useful for JSON-decoded stuff and config file parsing
-    """
-    if isinstance(subject, bool):
-        return subject
-    if isinstance(subject, basestring):
-        if subject.strip().lower() in ('true', 'on', 'yes', '1'):
-            return True
-    return False
+    if lowered in TRUE_STRINGS:
+        return True
+    elif lowered in FALSE_STRINGS:
+        return False
+    elif strict:
+        acceptable = ', '.join(
+            "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
+        msg = _("Unrecognized value '%(val)s', acceptable values are:"
+                " %(acceptable)s") % {'val': subject,
+                                      'acceptable': acceptable}
+        raise ValueError(msg)
+    else:
+        return False
 
 
 def safe_decode(text, incoming=None, errors='strict'):
-    """
-    Decodes incoming str using `incoming` if they're
-    not already unicode.
+    """Decodes incoming str using `incoming` if they're not already unicode.
 
     :param incoming: Text's current encoding
     :param errors: Errors handling policy. See here for valid
         values http://docs.python.org/2/library/codecs.html
     :returns: text or a unicode `incoming` encoded
                 representation of it.
-    :raises TypeError: If text is not an isntance of basestring
+    :raises TypeError: If text is not an isntance of str
     """
-    if not isinstance(text, basestring):
+    if not isinstance(text, six.string_types):
         raise TypeError("%s can't be decoded" % type(text))
 
-    if isinstance(text, unicode):
+    if isinstance(text, six.text_type):
         return text
 
     if not incoming:
@@ -102,11 +133,10 @@ def safe_decode(text, incoming=None, errors='strict'):
 
 def safe_encode(text, incoming=None,
                 encoding='utf-8', errors='strict'):
-    """
-    Encodes incoming str/unicode using `encoding`. If
-    incoming is not specified, text is expected to
-    be encoded with current python's default encoding.
-    (`sys.getdefaultencoding`)
+    """Encodes incoming str/unicode using `encoding`.
+
+    If incoming is not specified, text is expected to be encoded with
+    current python's default encoding. (`sys.getdefaultencoding`)
 
     :param incoming: Text's current encoding
     :param encoding: Expected encoding for text (Default UTF-8)
@@ -114,16 +144,16 @@ def safe_encode(text, incoming=None,
         values http://docs.python.org/2/library/codecs.html
     :returns: text or a bytestring `encoding` encoded
                 representation of it.
-    :raises TypeError: If text is not an isntance of basestring
+    :raises TypeError: If text is not an isntance of str
     """
-    if not isinstance(text, basestring):
+    if not isinstance(text, six.string_types):
         raise TypeError("%s can't be encoded" % type(text))
 
     if not incoming:
         incoming = (sys.stdin.encoding or
                     sys.getdefaultencoding())
 
-    if isinstance(text, unicode):
+    if isinstance(text, six.text_type):
         return text.encode(encoding, errors)
     elif text and encoding != incoming:
         # Decode text before encoding it with `encoding`
@@ -131,3 +161,58 @@ def safe_encode(text, incoming=None,
         return text.encode(encoding, errors)
 
     return text
+
+
+def to_bytes(text, default=0):
+    """Converts a string into an integer of bytes.
+
+    Looks at the last characters of the text to determine
+    what conversion is needed to turn the input text into a byte number.
+    Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
+
+    :param text: String input for bytes size conversion.
+    :param default: Default return value when text is blank.
+
+    """
+    match = BYTE_REGEX.search(text)
+    if match:
+        magnitude = int(match.group(1))
+        mult_key_org = match.group(2)
+        if not mult_key_org:
+            return magnitude
+    elif text:
+        msg = _('Invalid string format: %s') % text
+        raise TypeError(msg)
+    else:
+        return default
+    mult_key = mult_key_org.lower().replace('b', '', 1)
+    multiplier = BYTE_MULTIPLIERS.get(mult_key)
+    if multiplier is None:
+        msg = _('Unknown byte multiplier: %s') % mult_key_org
+        raise TypeError(msg)
+    return magnitude * multiplier
+
+
+def to_slug(value, incoming=None, errors="strict"):
+    """Normalize string.
+
+    Convert to lowercase, remove non-word characters, and convert spaces
+    to hyphens.
+
+    Inspired by Django's `slugify` filter.
+
+    :param value: Text to slugify
+    :param incoming: Text's current encoding
+    :param errors: Errors handling policy. See here for valid
+        values http://docs.python.org/2/library/codecs.html
+    :returns: slugified unicode representation of `value`
+    :raises TypeError: If text is not an instance of str
+    """
+    value = safe_decode(value, incoming, errors)
+    # NOTE(aababilov): no need to use safe_(encode|decode) here:
+    # encodings are always "ascii", error handling is always "ignore"
+    # and types are always known (first: unicode; second: str)
+    value = unicodedata.normalize("NFKD", value).encode(
+        "ascii", "ignore").decode("ascii")
+    value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
+    return SLUGIFY_HYPHENATE_RE.sub("-", value)
diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py
index f428c1e02..6ce5d0066 100644
--- a/tools/install_venv_common.py
+++ b/tools/install_venv_common.py
@@ -114,9 +114,10 @@ class InstallVenv(object):
         print('Installing dependencies with pip (this can take a while)...')
 
         # First things first, make sure our venv has the latest pip and
-        # setuptools.
-        self.pip_install('pip>=1.3')
+        # setuptools and pbr
+        self.pip_install('pip>=1.4')
         self.pip_install('setuptools')
+        self.pip_install('pbr')
 
         self.pip_install('-r', self.requirements)
         self.pip_install('-r', self.test_requirements)