You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2014/08/13 13:45:59 UTC

[2/3] git commit: Refactor OpenStack identity (auth) code and classes, make it more flexible, re-usable and maintainableL

Refactor OpenStack identity (auth) code and classes, make it more flexible,
re-usable and maintainableL

* Add new module which handles identity related logic
* (libcloud.common.openstack_identity)
* Add new classes for each identity API version.
* Add additional functionality to class for Keystone API v3
* Add new OpenStackServiceCatalogEntry and OpenStackServiceCatalogEntryEndpoint
  class
* Modify OpenStackServiceCatalog class to store entries in a structured format
  (OpenStackServiceCatalogEntry instances) instead of storing it in an
  unstructured dictionary
* Update all the affected code to use the new classes and methods.
* Fix a bug with the CDN requests in the CloudFiles driver.

Also a add new class for each identity API version and add additional
functionality to class for Keystone API v3.

Backward incompatible changes:

* OpenStackAuthConnection class has been removed and replaced with version
  specific classes and "get_class_for_auth_version" class retrieval function.


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/cd167194
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/cd167194
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/cd167194

Branch: refs/heads/trunk
Commit: cd1671941951d262e901a098d32693302330166e
Parents: 6deb49e
Author: Tomaz Muraus <to...@apache.org>
Authored: Sun Aug 10 18:18:36 2014 +0200
Committer: Tomaz Muraus <to...@apache.org>
Committed: Wed Aug 13 13:27:45 2014 +0200

----------------------------------------------------------------------
 CHANGES.rst                                     |   13 +
 docs/upgrade_notes.rst                          |   36 +
 libcloud/common/openstack.py                    |  605 +--------
 libcloud/common/openstack_identity.py           | 1225 ++++++++++++++++++
 libcloud/compute/drivers/hpcloud.py             |    2 +-
 libcloud/compute/drivers/kili.py                |    2 +-
 libcloud/compute/drivers/rackspace.py           |    6 +-
 libcloud/dns/drivers/rackspace.py               |    2 +-
 libcloud/storage/drivers/cloudfiles.py          |   35 +-
 libcloud/storage/drivers/ktucloud.py            |   10 +-
 libcloud/test/common/test_openstack.py          |    1 -
 libcloud/test/common/test_openstack_identity.py |  504 +++++++
 .../compute/fixtures/openstack/_v3__auth.json   |  182 +++
 .../openstack_identity/v3_create_user.json      |   12 +
 .../fixtures/openstack_identity/v3_domains.json |   18 +
 .../openstack_identity/v3_domains_default.json  |   11 +
 .../v3_domains_default_users_a_roles.json       |   16 +
 .../openstack_identity/v3_projects.json         |   49 +
 .../fixtures/openstack_identity/v3_roles.json   |   25 +
 .../fixtures/openstack_identity/v3_users.json   |  131 ++
 .../openstack_identity/v3_users_a_projects.json |    8 +
 libcloud/test/compute/test_openstack.py         |  249 +---
 libcloud/test/storage/test_cloudfiles.py        |   10 -
 23 files changed, 2306 insertions(+), 846 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/CHANGES.rst
----------------------------------------------------------------------
diff --git a/CHANGES.rst b/CHANGES.rst
index 4ac10f0..0cf0d47 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,13 @@ Changelog
 Changes with Apache Libcloud in development
 -------------------------------------------
 
+General
+~~~~~~~
+
+- Add new ``OpenStackIdentity_3_0_Connection`` class for working with
+  OpenStack Identity (Keystone) service API v3.
+  [Tomaz Muraus]
+
 Compute
 ~~~~~~~
 
@@ -44,6 +51,12 @@ Compute
   Reported by Chris DeRamus.
   [Tomaz Muraus]
 
+Storage
+~~~~~~~
+
+- Fix a bug with CDN requests in the CloudFiles driver.
+  [Tomaz Muraus]
+
 Loadbalancer
 ~~~~~~~~~~~~
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/docs/upgrade_notes.rst
----------------------------------------------------------------------
diff --git a/docs/upgrade_notes.rst b/docs/upgrade_notes.rst
index 93d920d..ef54b38 100644
--- a/docs/upgrade_notes.rst
+++ b/docs/upgrade_notes.rst
@@ -5,6 +5,42 @@ This page describes how to upgrade from a previous version to a new version
 which contains backward incompatible or semi-incompatible changes and how to
 preserve the old behavior when this is possible.
 
+Libcloud in development
+-----------------------
+
+Changes in the OpenStack authentication and service catalog classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note::
+    If you are only working with the driver classes and have never dorectly
+    touched the classes mentioned bellow, then you aren't affected and those
+    changes are fully backward compatible.
+
+
+To make OpenStack authentication and identity related classes more extensible,
+easier to main and easier to use, those classes have been refactored. All of
+the changes are described bellow.
+
+* New ``libcloud.common.openstack_identity`` module has been added. This module
+  contains code for working with OpenStack Identity (Keystone) service.
+* ``OpenStackAuthConnection`` class has been removed and replaced with one
+  connection class per Keystone API version
+  (``OpenStackIdentity_1_0_Connection``, ``OpenStackIdentity_2_0_Connection``,
+  ``OpenStackIdentity_3_0_Connection``).
+* New ``get_auth_class`` method has been added to ``OpenStackBaseConnection``
+  class. This method allows you to retrieve an instance of the authentication
+  class which is used with the current connection.
+* ``OpenStackServiceCatalog`` class has been refactored to store parsed catalog
+  entries in a structured format (``OpenStackServiceCatalogEntry`` and
+  ``OpenStackServiceCatalogEntryEndpoint`` class). Previously entries were
+  stored in an unstructured form in a dictionary. All the catalog entries can
+  be retrieved by using ``OpenStackServiceCatalog.get_entris`` method.
+* ``ex_force_auth_version`` argument in ``OpenStackServiceCatalog`` constructor
+  method has been renamed to ``auth_version``
+* ``get_regions``, ``get_service_types`` and ``get_service_names`` methods on
+  the ``OpenStackServiceCatalog`` class have been modified to always return the
+  result in the same order (result values are sorted beforehand).
+
 Libcloud 0.14.1
 ---------------
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/common/openstack.py
----------------------------------------------------------------------
diff --git a/libcloud/common/openstack.py b/libcloud/common/openstack.py
index 61d16c1..80ca08f 100644
--- a/libcloud/common/openstack.py
+++ b/libcloud/common/openstack.py
@@ -17,23 +17,23 @@
 Common utilities for OpenStack
 """
 
-import sys
-import datetime
-
 try:
     from lxml import etree as ET
 except ImportError:
     from xml.etree import ElementTree as ET
 
 from libcloud.utils.py3 import httplib
-from libcloud.utils.iso8601 import parse_date
 
 from libcloud.common.base import ConnectionUserAndKey, Response
 from libcloud.common.types import ProviderError
-from libcloud.compute.types import (LibcloudError, InvalidCredsError,
-                                    MalformedResponseError)
+from libcloud.compute.types import (LibcloudError, MalformedResponseError)
 from libcloud.compute.types import KeyPairDoesNotExistError
 
+# Imports for backward compatibility reasons
+from libcloud.common.openstack_identity import get_class_for_auth_version
+from libcloud.common.openstack_identity import OpenStackServiceCatalog
+
+
 try:
     import simplejson as json
 except ImportError:
@@ -58,8 +58,6 @@ AUTH_TOKEN_EXPIRES_GRACE_SECONDS = 5
 
 __all__ = [
     'OpenStackBaseConnection',
-    'OpenStackAuthConnection',
-    'OpenStackServiceCatalog',
     'OpenStackResponse',
     'OpenStackException',
     'OpenStackDriverMixin',
@@ -68,540 +66,6 @@ __all__ = [
 ]
 
 
-# @TODO: Refactor for re-use by other openstack drivers
-class OpenStackAuthResponse(Response):
-    def success(self):
-        return True
-
-    def parse_body(self):
-        if not self.body:
-            return None
-
-        if 'content-type' in self.headers:
-            key = 'content-type'
-        elif 'Content-Type' in self.headers:
-            key = 'Content-Type'
-        else:
-            raise LibcloudError('Missing content-type header',
-                                driver=OpenStackAuthConnection)
-
-        content_type = self.headers[key]
-        if content_type.find(';') != -1:
-            content_type = content_type.split(';')[0]
-
-        if content_type == 'application/json':
-            try:
-                data = json.loads(self.body)
-            except:
-                raise MalformedResponseError('Failed to parse JSON',
-                                             body=self.body,
-                                             driver=OpenStackAuthConnection)
-        elif content_type == 'text/plain':
-            data = self.body
-        else:
-            data = self.body
-
-        return data
-
-
-class OpenStackAuthConnection(ConnectionUserAndKey):
-
-    responseCls = OpenStackAuthResponse
-    name = 'OpenStack Auth'
-    timeout = None
-
-    def __init__(self, parent_conn, auth_url, auth_version, user_id, key,
-                 tenant_name=None, timeout=None):
-        self.parent_conn = parent_conn
-        # enable tests to use the same mock connection classes.
-        self.conn_classes = parent_conn.conn_classes
-
-        super(OpenStackAuthConnection, self).__init__(
-            user_id, key, url=auth_url, timeout=timeout)
-
-        self.auth_version = auth_version
-        self.auth_url = auth_url
-        self.driver = self.parent_conn.driver
-        self.tenant_name = tenant_name
-        self.timeout = timeout
-
-        self.urls = {}
-        self.auth_token = None
-        self.auth_token_expires = None
-        self.auth_user_info = None
-
-    def morph_action_hook(self, action):
-        (_, _, _, request_path) = self._tuple_from_url(self.auth_url)
-
-        if request_path == '':
-            # No path is provided in the auth_url, use action passed to this
-            # method.
-            return action
-
-        return request_path
-
-    def add_default_headers(self, headers):
-        headers['Accept'] = 'application/json'
-        headers['Content-Type'] = 'application/json; charset=UTF-8'
-        return headers
-
-    def authenticate(self, force=False):
-        """
-        Authenticate against the keystone api.
-
-        :param force: Forcefully update the token even if it's already cached
-                      and still valid.
-        :type force: ``bool``
-        """
-        if not force and self.auth_version in AUTH_VERSIONS_WITH_EXPIRES \
-           and self.is_token_valid():
-            # If token is still valid, there is no need to re-authenticate
-            return self
-
-        if self.auth_version == "1.0":
-            return self.authenticate_1_0()
-        elif self.auth_version == "1.1":
-            return self.authenticate_1_1()
-        elif self.auth_version == "2.0" or self.auth_version == "2.0_apikey":
-            return self.authenticate_2_0_with_apikey()
-        elif self.auth_version == "2.0_password":
-            return self.authenticate_2_0_with_password()
-        elif self.auth_version == '3.x_password':
-            return self.authenticate_3_x_with_password()
-        else:
-            raise LibcloudError('Unsupported Auth Version requested')
-
-    def authenticate_1_0(self):
-        headers = {
-            'X-Auth-User': self.user_id,
-            'X-Auth-Key': self.key,
-        }
-
-        resp = self.request('/v1.0', headers=headers, method='GET')
-
-        if resp.status == httplib.UNAUTHORIZED:
-            # HTTP UNAUTHORIZED (401): auth failed
-            raise InvalidCredsError()
-        elif resp.status not in [httplib.NO_CONTENT, httplib.OK]:
-            body = 'code: %s body:%s headers:%s' % (resp.status,
-                                                    resp.body,
-                                                    resp.headers)
-            raise MalformedResponseError('Malformed response', body=body,
-                                         driver=self.driver)
-        else:
-            headers = resp.headers
-            # emulate the auth 1.1 URL list
-            self.urls = {}
-            self.urls['cloudServers'] = \
-                [{'publicURL': headers.get('x-server-management-url', None)}]
-            self.urls['cloudFilesCDN'] = \
-                [{'publicURL': headers.get('x-cdn-management-url', None)}]
-            self.urls['cloudFiles'] = \
-                [{'publicURL': headers.get('x-storage-url', None)}]
-            self.auth_token = headers.get('x-auth-token', None)
-            self.auth_user_info = None
-
-            if not self.auth_token:
-                raise MalformedResponseError('Missing X-Auth-Token in \
-                                              response headers')
-
-        return self
-
-    def authenticate_1_1(self):
-        reqbody = json.dumps({'credentials': {'username': self.user_id,
-                                              'key': self.key}})
-        resp = self.request('/v1.1/auth', data=reqbody, headers={},
-                            method='POST')
-
-        if resp.status == httplib.UNAUTHORIZED:
-            # HTTP UNAUTHORIZED (401): auth failed
-            raise InvalidCredsError()
-        elif resp.status != httplib.OK:
-            body = 'code: %s body:%s' % (resp.status, resp.body)
-            raise MalformedResponseError('Malformed response', body=body,
-                                         driver=self.driver)
-        else:
-            try:
-                body = json.loads(resp.body)
-            except Exception:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Failed to parse JSON', e)
-
-            try:
-                expires = body['auth']['token']['expires']
-
-                self.auth_token = body['auth']['token']['id']
-                self.auth_token_expires = parse_date(expires)
-                self.urls = body['auth']['serviceCatalog']
-                self.auth_user_info = None
-            except KeyError:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Auth JSON response is \
-                                             missing required elements', e)
-
-        return self
-
-    def authenticate_2_0_with_apikey(self):
-        # API Key based authentication uses the RAX-KSKEY extension.
-        # http://s.apache.org/oAi
-        data = {'auth':
-                {'RAX-KSKEY:apiKeyCredentials':
-                 {'username': self.user_id, 'apiKey': self.key}}}
-        if self.tenant_name:
-            data['auth']['tenantName'] = self.tenant_name
-        reqbody = json.dumps(data)
-        return self.authenticate_2_0_with_body(reqbody)
-
-    def authenticate_2_0_with_password(self):
-        # Password based authentication is the only 'core' authentication
-        # method in Keystone at this time.
-        # 'keystone' - http://s.apache.org/e8h
-        data = {'auth':
-                {'passwordCredentials':
-                 {'username': self.user_id, 'password': self.key}}}
-        if self.tenant_name:
-            data['auth']['tenantName'] = self.tenant_name
-        reqbody = json.dumps(data)
-        return self.authenticate_2_0_with_body(reqbody)
-
-    def authenticate_2_0_with_body(self, reqbody):
-        resp = self.request('/v2.0/tokens', data=reqbody,
-                            headers={'Content-Type': 'application/json'},
-                            method='POST')
-        if resp.status == httplib.UNAUTHORIZED:
-            raise InvalidCredsError()
-        elif resp.status not in [httplib.OK,
-                                 httplib.NON_AUTHORITATIVE_INFORMATION]:
-            body = 'code: %s body: %s' % (resp.status, resp.body)
-            raise MalformedResponseError('Malformed response', body=body,
-                                         driver=self.driver)
-        else:
-            try:
-                body = json.loads(resp.body)
-            except Exception:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Failed to parse JSON', e)
-
-            try:
-                access = body['access']
-                expires = access['token']['expires']
-
-                self.auth_token = access['token']['id']
-                self.auth_token_expires = parse_date(expires)
-                self.urls = access['serviceCatalog']
-                self.auth_user_info = access.get('user', {})
-            except KeyError:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Auth JSON response is \
-                                             missing required elements', e)
-
-        return self
-
-    def authenticate_3_x_with_password(self):
-        # TODO: Support for custom domain
-        # TODO: Refactor and add a class per API version
-        domain = 'Default'
-
-        data = {
-            'auth': {
-                'identity': {
-                    'methods': ['password'],
-                    'password': {
-                        'user': {
-                            'domain': {
-                                'name': domain
-                            },
-                            'name': self.user_id,
-                            'password': self.key
-                        }
-                    }
-                },
-                'scope': {
-                    'project': {
-                        'domain': {
-                            'name': domain
-                        },
-                        'name': self.tenant_name
-                    }
-                }
-            }
-        }
-
-        if self.tenant_name:
-            data['auth']['scope'] = {
-                'project': {
-                    'domain': {
-                        'name': domain
-                    },
-                    'name': self.tenant_name
-                }
-            }
-
-        data = json.dumps(data)
-        response = self.request('/v3/auth/tokens', data=data,
-                                headers={'Content-Type': 'application/json'},
-                                method='POST')
-
-        if response.status == httplib.UNAUTHORIZED:
-            # Invalid credentials
-            raise InvalidCredsError()
-        elif response.status in [httplib.OK, httplib.CREATED]:
-            headers = response.headers
-
-            try:
-                body = json.loads(response.body)
-            except Exception:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Failed to parse JSON', e)
-
-            try:
-                expires = body['token']['expires_at']
-
-                self.auth_token = headers['x-subject-token']
-                self.auth_token_expires = parse_date(expires)
-                self.urls = body['token']['catalog']
-                self.auth_user_info = None
-            except KeyError:
-                e = sys.exc_info()[1]
-                raise MalformedResponseError('Auth JSON response is \
-                                             missing required elements', e)
-            body = 'code: %s body:%s' % (response.status, response.body)
-        else:
-            raise MalformedResponseError('Malformed response', body=body,
-                                         driver=self.driver)
-
-        return self
-
-    def is_token_valid(self):
-        """
-        Return True if the current auth token is already cached and hasn't
-        expired yet.
-
-        :return: ``True`` if the token is still valid, ``False`` otherwise.
-        :rtype: ``bool``
-        """
-        if not self.auth_token:
-            return False
-
-        if not self.auth_token_expires:
-            return False
-
-        expires = self.auth_token_expires - \
-            datetime.timedelta(seconds=AUTH_TOKEN_EXPIRES_GRACE_SECONDS)
-
-        time_tuple_expires = expires.utctimetuple()
-        time_tuple_now = datetime.datetime.utcnow().utctimetuple()
-
-        if time_tuple_now < time_tuple_expires:
-            return True
-
-        return False
-
-
-class OpenStackServiceCatalog(object):
-    """
-    http://docs.openstack.org/api/openstack-identity-service/2.0/content/
-
-    This class should be instanciated with the contents of the
-    'serviceCatalog' in the auth response. This will do the work of figuring
-    out which services actually exist in the catalog as well as split them up
-    by type, name, and region if available
-    """
-
-    _auth_version = None
-    _service_catalog = None
-
-    def __init__(self, service_catalog, ex_force_auth_version=None):
-        self._auth_version = ex_force_auth_version or AUTH_API_VERSION
-        self._service_catalog = {}
-
-        # Check this way because there are a couple of different 2.0_*
-        # auth types.
-        if '3.x' in self._auth_version:
-            self._parse_auth_v3(service_catalog)
-        elif '2.0' in self._auth_version:
-            self._parse_auth_v2(service_catalog)
-        elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version):
-            self._parse_auth_v1(service_catalog)
-        else:
-            raise LibcloudError('auth version "%s" not supported'
-                                % (self._auth_version))
-
-    def get_catalog(self):
-        return self._service_catalog
-
-    def get_public_urls(self, service_type=None, name=None):
-        endpoints = self.get_endpoints(service_type=service_type,
-                                       name=name)
-
-        result = []
-        for endpoint in endpoints:
-            if 'publicURL' in endpoint:
-                result.append(endpoint['publicURL'])
-
-        return result
-
-    def get_endpoints(self, service_type=None, name=None):
-        eps = []
-
-        if '2.0' in self._auth_version:
-            endpoints = self._service_catalog.get(service_type, {}) \
-                                             .get(name, {})
-        elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version):
-            endpoints = self._service_catalog.get(name, {})
-
-        for regionName, values in endpoints.items():
-            eps.append(values[0])
-
-        return eps
-
-    def get_endpoint(self, service_type=None, name=None, region=None):
-        if '3.x' in self._auth_version:
-            endpoints = self._service_catalog.get(service_type, {}) \
-                                             .get(region, [])
-
-            endpoint = []
-            for _endpoint in endpoints:
-                if _endpoint['type'] == 'public':
-                    endpoint = [_endpoint]
-                    break
-        elif '2.0' in self._auth_version:
-            endpoint = self._service_catalog.get(service_type, {}) \
-                                            .get(name, {}).get(region, [])
-        elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version):
-            endpoint = self._service_catalog.get(name, {}).get(region, [])
-
-        # ideally an endpoint either isn't found or only one match is found.
-        if len(endpoint) == 1:
-            return endpoint[0]
-        else:
-            return {}
-
-    def get_regions(self):
-        """
-        Retrieve a list of all the available regions.
-
-        :rtype: ``list`` of ``str``
-        """
-        regions = set()
-
-        catalog_items = self._service_catalog.items()
-
-        if '3.x' in self._auth_version:
-            for service_type, values in catalog_items:
-                for region in values.keys():
-                    regions.add(region)
-        elif '2.0' in self._auth_version:
-            for service_type, services_by_name in catalog_items:
-                items = services_by_name.items()
-                for service_name, endpoints_by_region in items:
-                    for region in endpoints_by_region.keys():
-                        if region:
-                            regions.add(region)
-        elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version):
-            for service_name, endpoints_by_region in catalog_items:
-                for region in endpoints_by_region.keys():
-                    if region:
-                        regions.add(region)
-
-        return list(regions)
-
-    def get_service_types(self, region=None):
-        """
-        Retrieve all the available service types.
-
-        :param region: Optional region to retrieve service types for.
-        :type region: ``str``
-
-        :rtype: ``list`` of ``str``
-        """
-        service_types = set()
-
-        for service_type, values in self._service_catalog.items():
-            regions = values.keys()
-            if not region or region in regions:
-                service_types.add(service_type)
-
-        return list(service_types)
-
-    def get_service_names(self, service_type, region=None):
-        """
-        Retrieve list of service names that match service type and region
-
-        :type service_type: ``str``
-        :type region: ``str``
-
-        :rtype: ``list`` of ``str``
-        """
-        names = set()
-
-        if '2.0' in self._auth_version:
-            named_entries = self._service_catalog.get(service_type, {})
-            for (name, region_entries) in named_entries.items():
-                # Support None for region to return the first found
-                if region is None or region in region_entries.keys():
-                    names.add(name)
-        else:
-            raise ValueError('Unsupported version: %s' % (self._auth_version))
-
-        return list(names)
-
-    def _parse_auth_v1(self, service_catalog):
-        for service, endpoints in service_catalog.items():
-
-            self._service_catalog[service] = {}
-
-            for endpoint in endpoints:
-                region = endpoint.get('region')
-
-                if region not in self._service_catalog[service]:
-                    self._service_catalog[service][region] = []
-
-                self._service_catalog[service][region].append(endpoint)
-
-    def _parse_auth_v2(self, service_catalog):
-        for service in service_catalog:
-            service_type = service['type']
-            service_name = service.get('name', None)
-
-            if service_type not in self._service_catalog:
-                self._service_catalog[service_type] = {}
-
-            if service_name not in self._service_catalog[service_type]:
-                self._service_catalog[service_type][service_name] = {}
-
-            for endpoint in service.get('endpoints', []):
-                region = endpoint.get('region', None)
-
-                catalog = self._service_catalog[service_type][service_name]
-                if region not in catalog:
-                    catalog[region] = []
-
-                catalog[region].append(endpoint)
-
-    def _parse_auth_v3(self, service_catalog):
-        for entry in service_catalog:
-            service_type = entry['type']
-
-            # TODO: use defaultdict
-            if service_type not in self._service_catalog:
-                self._service_catalog[service_type] = {}
-
-            for endpoint in entry['endpoints']:
-                region = endpoint.get('region', None)
-
-                # TODO: Normalize entries for each version
-                catalog = self._service_catalog[service_type]
-                if region not in catalog:
-                    catalog[region] = []
-
-                region_entry = {
-                    'url': endpoint['url'],
-                    'type': endpoint['interface']  # public / private
-                }
-                catalog[region].append(region_entry)
-
-
 class OpenStackBaseConnection(ConnectionUserAndKey):
 
     """
@@ -687,6 +151,7 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
         self._ex_force_service_type = ex_force_service_type
         self._ex_force_service_name = ex_force_service_name
         self._ex_force_service_region = ex_force_service_region
+        self._osa = None
 
         if ex_force_auth_token and not ex_force_base_url:
             raise LibcloudError(
@@ -705,11 +170,24 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
             raise LibcloudError('OpenStack instance must ' +
                                 'have auth_url set')
 
-        osa = OpenStackAuthConnection(self, auth_url, self._auth_version,
-                                      self.user_id, self.key,
-                                      tenant_name=self._ex_tenant_name,
-                                      timeout=self.timeout)
-        self._osa = osa
+    def get_auth_class(self):
+        """
+        Retrieve identity / authentication class instance.
+
+        :rtype: :class:`OpenStackIdentityConnection`
+        """
+        if not self._osa:
+            auth_url = self._get_auth_url()
+
+            cls = get_class_for_auth_version(auth_version=self._auth_version)
+            self._osa = cls(auth_url=auth_url,
+                            user_id=self.user_id,
+                            key=self.key,
+                            tenant_name=self._ex_tenant_name,
+                            timeout=self.timeout,
+                            parent_conn=self)
+
+        return self._osa
 
     def request(self, action, params=None, data='', headers=None,
                 method='GET', raw=False):
@@ -756,6 +234,7 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
         service_type = self.service_type
         service_name = self.service_name
         service_region = self.service_region
+
         if self._ex_force_service_type:
             service_type = self._ex_force_service_type
         if self._ex_force_service_name:
@@ -763,18 +242,17 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
         if self._ex_force_service_region:
             service_region = self._ex_force_service_region
 
-        ep = self.service_catalog.get_endpoint(service_type=service_type,
-                                               name=service_name,
-                                               region=service_region)
+        endpoint = self.service_catalog.get_endpoint(service_type=service_type,
+                                                     name=service_name,
+                                                     region=service_region)
 
         # TODO: Normalize keys for different auth versions and use an object
-        if 'publicURL' in ep:
-            return ep['publicURL']
-        elif 'url' in ep:
-            # v3
-            return ep['url']
+        url = endpoint.url
 
-        raise LibcloudError('Could not find specified endpoint')
+        if not url:
+            raise LibcloudError('Could not find specified endpoint')
+
+        return url
 
     def add_default_headers(self, headers):
         headers['X-Auth-Token'] = self.auth_token
@@ -794,7 +272,7 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
         OpenStack uses a separate host for API calls which is only provided
         after an initial authentication request.
         """
-        osa = self._osa
+        osa = self.get_auth_class()
 
         if self._ex_force_auth_token:
             # If ex_force_auth_token is provided we always hit the api directly
@@ -808,15 +286,22 @@ class OpenStackBaseConnection(ConnectionUserAndKey):
         if not osa.is_token_valid():
             # Token is not available or it has expired. Need to retrieve a
             # new one.
-            osa.authenticate()  # may throw InvalidCreds
+            if self._auth_version == '2.0_apikey':
+                kwargs = {'auth_type': 'api_key'}
+            elif self._auth_version == '2.0_password':
+                kwargs = {'auth_type': 'password'}
+            else:
+                kwargs = {}
+
+            osa = osa.authenticate(**kwargs)  # may throw InvalidCreds
 
             self.auth_token = osa.auth_token
             self.auth_token_expires = osa.auth_token_expires
             self.auth_user_info = osa.auth_user_info
 
             # Pull out and parse the service catalog
-            osc = OpenStackServiceCatalog(
-                osa.urls, ex_force_auth_version=self._auth_version)
+            osc = OpenStackServiceCatalog(service_catalog=osa.urls,
+                                          auth_version=self._auth_version)
             self.service_catalog = osc
 
         url = self._ex_force_base_url or self.get_endpoint()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/common/openstack_identity.py
----------------------------------------------------------------------
diff --git a/libcloud/common/openstack_identity.py b/libcloud/common/openstack_identity.py
new file mode 100644
index 0000000..5fdd186
--- /dev/null
+++ b/libcloud/common/openstack_identity.py
@@ -0,0 +1,1225 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+"""
+Common / shared code for handling authentication against OpenStack identity
+service (Keystone).
+"""
+
+import sys
+import datetime
+
+from libcloud.utils.py3 import httplib
+from libcloud.utils.iso8601 import parse_date
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.compute.types import (LibcloudError, InvalidCredsError,
+                                    MalformedResponseError)
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+AUTH_API_VERSION = '1.1'
+
+# Auth versions which contain token expiration information.
+AUTH_VERSIONS_WITH_EXPIRES = [
+    '1.1',
+    '2.0',
+    '2.0_apikey',
+    '2.0_password',
+    '3.0',
+    '3.x_password'
+]
+
+# How many seconds to substract from the auth token expiration time before
+# testing if the token is still valid.
+# The time is subtracted to account for the HTTP request latency and prevent
+# user from getting "InvalidCredsError" if token is about to expire.
+AUTH_TOKEN_EXPIRES_GRACE_SECONDS = 5
+
+
+__all__ = [
+    'OpenStackIdentityDomain',
+    'OpenStackIdentityProject',
+    'OpenStackIdentityUser',
+    'OpenStackIdentityRole',
+
+    'OpenStackServiceCatalog',
+    'OpenStackServiceCatalogEntry',
+    'OpenStackServiceCatalogEntryEndpoint',
+
+    'OpenStackIdentityConnection',
+    'OpenStackIdentity_1_0_Connection',
+    'OpenStackIdentity_1_1_Connection',
+    'OpenStackIdentity_2_0_Connection',
+    'OpenStackIdentity_3_0_Connection',
+
+    'get_class_for_auth_version'
+]
+
+
+class OpenStackIdentityDomain(object):
+    def __init__(self, id, name, enabled):
+        self.id = id
+        self.name = name
+        self.enabled = enabled
+
+    def __repr__(self):
+        return (('<OpenStackIdentityDomain id=%s, name=%s, enabled=%s>' %
+                 (self.id, self.name, self.enabled)))
+
+
+class OpenStackIdentityProject(object):
+    def __init__(self, id, domain_id, name, description, enabled):
+        self.id = id
+        self.domain_id = domain_id
+        self.name = name
+        self.description = description
+        self.enabled = enabled
+
+    def __repr__(self):
+        return (('<OpenStackIdentityProject id=%s, domain_id=%s, name=%s, '
+                 'enabled=%s>' %
+                 (self.id, self.domain_id, self.name, self.enabled)))
+
+
+class OpenStackIdentityRole(object):
+    def __init__(self, id, name, description, enabled):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.enabled = enabled
+
+    def __repr__(self):
+        return (('<OpenStackIdentityRole id=%s, name=%s, description=%s, '
+                 'enabled=%s>' % (self.id, self.name, self.description,
+                                  self.enabled)))
+
+
+class OpenStackIdentityUser(object):
+    def __init__(self, id, domain_id, name, email, description, enabled):
+        self.id = id
+        self.domain_id = domain_id
+        self.name = name
+        self.email = email
+        self.description = description
+        self.enabled = enabled
+
+    def __repr__(self):
+        return (('<OpenStackIdentityUser id=%s, domain_id=%s, name=%s, '
+                 'email=%s, enabled=%s' % (self.id, self.domain_id, self.name,
+                                           self.email, self.enabled)))
+
+
+class OpenStackServiceCatalog(object):
+    """
+    http://docs.openstack.org/api/openstack-identity-service/2.0/content/
+
+    This class should be instanciated with the contents of the
+    'serviceCatalog' in the auth response. This will do the work of figuring
+    out which services actually exist in the catalog as well as split them up
+    by type, name, and region if available
+    """
+
+    _auth_version = None
+    _service_catalog = None
+
+    def __init__(self, service_catalog, auth_version=AUTH_API_VERSION):
+        self._auth_version = auth_version
+
+        # Check this way because there are a couple of different 2.0_*
+        # auth types.
+        if '3.x' in self._auth_version:
+            entries = self._parse_service_catalog_auth_v3(
+                service_catalog=service_catalog)
+        elif '2.0' in self._auth_version:
+            entries = self._parse_service_catalog_auth_v2(
+                service_catalog=service_catalog)
+        elif ('1.1' in self._auth_version) or ('1.0' in self._auth_version):
+            entries = self._parse_service_catalog_auth_v1(
+                service_catalog=service_catalog)
+        else:
+            raise LibcloudError('auth version "%s" not supported'
+                                % (self._auth_version))
+
+        # Force consistent ordering by sorting the entries
+        entries = sorted(entries,
+                         key=lambda x: x.service_type + (x.service_name or ''))
+        self._entries = entries  # stories all the service catalog entries
+
+    def get_entries(self):
+        """
+        Return all the entries for this service catalog.
+
+        :rtype: ``list`` of :class:`.OpenStackServiceCatalogEntry`
+        """
+        return self._entries
+
+    def get_catalog(self):
+        """
+        Deprecated in the favor of ``get_entries`` method.
+        """
+        return self.get_entries()
+
+    def get_public_urls(self, service_type=None, name=None):
+        """
+        Retrieve all the available public (external) URLs for the provided
+        service type and name.
+        """
+        endpoints = self.get_endpoints(service_type=service_type,
+                                       name=name)
+
+        result = []
+        for endpoint in endpoints:
+            if endpoint.endpoint_type == 'external':
+                result.append(endpoint.url)
+
+        return result
+
+    def get_endpoints(self, service_type=None, name=None):
+        """
+        Retrieve all the endpoints for the provided service type and name.
+
+        :rtype: ``list`` of :class:`.OpenStackServiceCatalogEntryEndpoint`
+        """
+        endpoints = []
+
+        for entry in self._entries:
+            if service_type and entry.service_type != service_type:
+                continue
+
+            if name and entry.service_name != name:
+                continue
+
+            for endpoint in entry.endpoints:
+                endpoints.append(endpoint)
+
+        return endpoints
+
+    def get_endpoint(self, service_type=None, name=None, region=None,
+                     endpoint_type='external'):
+        """
+        Retrieve a single endpoint using the provided criteria.
+
+        Note: If no or more than one matching endpoint is found, an exception
+        is thrown.
+        """
+        endpoints = []
+
+        for entry in self._entries:
+            if service_type and entry.service_type != service_type:
+                continue
+
+            if name and entry.service_name != name:
+                continue
+
+            for endpoint in entry.endpoints:
+                if region and endpoint.region != region:
+                    continue
+
+                if endpoint_type and endpoint.endpoint_type != endpoint_type:
+                    continue
+
+                endpoints.append(endpoint)
+
+        if len(endpoints) == 1:
+            return endpoints[0]
+        elif len(endpoints) > 1:
+            raise ValueError('Found more than 1 matching endpoint')
+        else:
+            raise LibcloudError('Could not find specified endpoint')
+
+    def get_regions(self, service_type=None):
+        """
+        Retrieve a list of all the available regions.
+
+        :param service_type: If specified, only return regions for this
+                             service type.
+        :type service_type: ``str``
+
+        :rtype: ``list`` of ``str``
+        """
+        regions = set()
+
+        for entry in self._entries:
+            if service_type and entry.service_type != service_type:
+                continue
+
+            for endpoint in entry.endpoints:
+                if endpoint.region:
+                    regions.add(endpoint.region)
+
+        return sorted(list(regions))
+
+    def get_service_types(self, region=None):
+        """
+        Retrieve all the available service types.
+
+        :param region: Optional region to retrieve service types for.
+        :type region: ``str``
+
+        :rtype: ``list`` of ``str``
+        """
+        service_types = set()
+
+        for entry in self._entries:
+            include = True
+
+            for endpoint in entry.endpoints:
+                if region and endpoint.region != region:
+                    include = False
+                    break
+
+            if include:
+                service_types.add(entry.service_type)
+
+        return sorted(list(service_types))
+
+    def get_service_names(self, service_type=None, region=None):
+        """
+        Retrieve list of service names that match service type and region.
+
+        :type service_type: ``str``
+        :type region: ``str``
+
+        :rtype: ``list`` of ``str``
+        """
+        names = set()
+
+        if '2.0' not in self._auth_version:
+            raise ValueError('Unsupported version: %s' % (self._auth_version))
+
+        for entry in self._entries:
+            if service_type and entry.service_type != service_type:
+                continue
+
+            include = True
+            for endpoint in entry.endpoints:
+                if region and endpoint.region != region:
+                    include = False
+                    break
+
+            if include and entry.service_name:
+                names.add(entry.service_name)
+
+        return sorted(list(names))
+
+    def _parse_service_catalog_auth_v1(self, service_catalog):
+        entries = []
+
+        for service, endpoints in service_catalog.items():
+            entry_endpoints = []
+            for endpoint in endpoints:
+                region = endpoint.get('region', None)
+
+                public_url = endpoint.get('publicURL', None)
+                private_url = endpoint.get('internalURL', None)
+
+                if public_url:
+                    entry_endpoint = OpenStackServiceCatalogEntryEndpoint(
+                        region=region, url=public_url,
+                        endpoint_type='external')
+                    entry_endpoints.append(entry_endpoint)
+
+                if private_url:
+                    entry_endpoint = OpenStackServiceCatalogEntryEndpoint(
+                        region=region, url=private_url,
+                        endpoint_type='internal')
+                    entry_endpoints.append(entry_endpoint)
+
+            entry = OpenStackServiceCatalogEntry(service_type=service,
+                                                 endpoints=entry_endpoints)
+            entries.append(entry)
+
+        return entries
+
+    def _parse_service_catalog_auth_v2(self, service_catalog):
+        entries = []
+
+        for service in service_catalog:
+            service_type = service['type']
+            service_name = service.get('name', None)
+
+            entry_endpoints = []
+            for endpoint in service.get('endpoints', []):
+                region = endpoint.get('region', None)
+
+                public_url = endpoint.get('publicURL', None)
+                private_url = endpoint.get('internalURL', None)
+
+                if public_url:
+                    entry_endpoint = OpenStackServiceCatalogEntryEndpoint(
+                        region=region, url=public_url,
+                        endpoint_type='external')
+                    entry_endpoints.append(entry_endpoint)
+
+                if private_url:
+                    entry_endpoint = OpenStackServiceCatalogEntryEndpoint(
+                        region=region, url=private_url,
+                        endpoint_type='internal')
+                    entry_endpoints.append(entry_endpoint)
+
+            entry = OpenStackServiceCatalogEntry(service_type=service_type,
+                                                 endpoints=entry_endpoints,
+                                                 service_name=service_name)
+            entries.append(entry)
+
+        return entries
+
+    def _parse_service_catalog_auth_v3(self, service_catalog):
+        entries = []
+
+        for item in service_catalog:
+            service_type = item['type']
+
+            entry_endpoints = []
+            for endpoint in item['endpoints']:
+                region = endpoint.get('region', None)
+                url = endpoint['url']
+                endpoint_type = endpoint['interface']
+
+                if endpoint_type == 'internal':
+                    endpoint_type = 'internal'
+                elif endpoint_type == 'public':
+                    endpoint_type = 'external'
+                elif endpoint_type == 'admin':
+                    endpoint_type = 'admin'
+
+                entry_endpoint = OpenStackServiceCatalogEntryEndpoint(
+                    region=region, url=url, endpoint_type=endpoint_type)
+                entry_endpoints.append(entry_endpoint)
+
+            entry = OpenStackServiceCatalogEntry(service_type=service_type,
+                                                 endpoints=entry_endpoints)
+            entries.append(entry)
+
+        return entries
+
+
+class OpenStackServiceCatalogEntry(object):
+    def __init__(self, service_type, endpoints=None, service_name=None):
+        """
+        :param service_type: Service type.
+        :type service_type: ``str``
+
+        :param endpoints: Endpoints belonging to this entry.
+        :type endpoints: ``list``
+
+        :param service_name: Optional service name.
+        :type service_name: ``str``
+        """
+        self.service_type = service_type
+        self.endpoints = endpoints or []
+        self.service_name = service_name
+
+        # For consistency, sort the endpoints
+        self.endpoints = sorted(self.endpoints, key=lambda x: x.url or '')
+
+    def __eq__(self, other):
+        return (self.service_type == other.service_type and
+                self.endpoints == other.endpoints and
+                other.service_name == self.service_name)
+
+    def __ne__(self, other):
+        return not self.__eq__(other=other)
+
+    def __repr__(self):
+        return (('<OpenStackServiceCatalogEntry service_type=%s, '
+                 'service_name=%s, endpoints=%s' %
+                 (self.service_type, self.service_name, repr(self.endpoints))))
+
+
+class OpenStackServiceCatalogEntryEndpoint(object):
+    def __init__(self, region, url, endpoint_type='external'):
+        """
+        :param region: Endpoint region.
+        :type region: ``str``
+
+        :param url: Endpoint URL.
+        :type url: ``str``
+
+        :param endpoint_type: Endpoint type (external / internal / admin).
+        :type endpoint_type: ``str``
+        """
+        if endpoint_type not in ['internal', 'external', 'admin']:
+            raise ValueError('Invalid type: %s' % (endpoint_type))
+
+        # TODO: Normalize / lowercase all the region names
+        self.region = region
+        self.url = url
+        self.endpoint_type = endpoint_type
+
+    def __eq__(self, other):
+        return (self.region == other.region and self.url == other.url and
+                self.endpoint_type == other.endpoint_type)
+
+    def __ne__(self, other):
+        return not self.__eq__(other=other)
+
+    def __repr__(self):
+        return (('<OpenStackServiceCatalogEntryEndpoint region=%s, url=%s, '
+                 'type=%s' % (self.region, self.url, self.endpoint_type)))
+
+
+class OpenStackAuthResponse(Response):
+    def success(self):
+        return self.status in [httplib.OK, httplib.CREATED,
+                               httplib.ACCEPTED, httplib.NO_CONTENT,
+                               httplib.UNAUTHORIZED,
+                               httplib.INTERNAL_SERVER_ERROR]
+
+    def parse_body(self):
+        if not self.body:
+            return None
+
+        if 'content-type' in self.headers:
+            key = 'content-type'
+        elif 'Content-Type' in self.headers:
+            key = 'Content-Type'
+        else:
+            raise LibcloudError('Missing content-type header',
+                                driver=OpenStackIdentityConnection)
+
+        content_type = self.headers[key]
+        if content_type.find(';') != -1:
+            content_type = content_type.split(';')[0]
+
+        if content_type == 'application/json':
+            try:
+                data = json.loads(self.body)
+            except:
+                driver = OpenStackIdentityConnection
+                raise MalformedResponseError('Failed to parse JSON',
+                                             body=self.body,
+                                             driver=driver)
+        elif content_type == 'text/plain':
+            data = self.body
+        else:
+            data = self.body
+
+        return data
+
+
+class OpenStackIdentityConnection(ConnectionUserAndKey):
+    """
+    Base identity connection class which contains common / shared logic.
+
+    Note: This class shouldn't be instantiated directly.
+    """
+    responseCls = OpenStackAuthResponse
+    timeout = None
+
+    def __init__(self, auth_url, user_id, key, tenant_name=None,
+                 timeout=None, parent_conn=None):
+        super(OpenStackIdentityConnection, self).__init__(user_id=user_id,
+                                                          key=key,
+                                                          url=auth_url,
+                                                          timeout=timeout)
+
+        self.auth_url = auth_url
+        self.tenant_name = tenant_name
+        self.parent_conn = parent_conn
+
+        # enable tests to use the same mock connection classes.
+        if parent_conn:
+            self.conn_classes = parent_conn.conn_classes
+            self.driver = parent_conn.driver
+        else:
+            self.driver = None
+
+        self.auth_url = auth_url
+        self.tenant_name = tenant_name
+        self.timeout = timeout
+
+        self.urls = {}
+        self.auth_token = None
+        self.auth_token_expires = None
+        self.auth_user_info = None
+
+    def authenticated_request(self, action, params=None, data=None,
+                              headers=None, method='GET', raw=False):
+        """
+        Perform an authenticated request against the identity API.
+        """
+        if not self.auth_token:
+            raise ValueError('Not to be authenticated to perform this request')
+
+        headers = headers or {}
+        headers['X-Auth-Token'] = self.auth_token
+
+        return self.request(action=action, params=params, data=data,
+                            headers=headers, method=method, raw=raw)
+
+    def morph_action_hook(self, action):
+        (_, _, _, request_path) = self._tuple_from_url(self.auth_url)
+
+        if request_path == '':
+            # No path is provided in the auth_url, use action passed to this
+            # method.
+            return action
+
+        return request_path
+
+    def add_default_headers(self, headers):
+        headers['Accept'] = 'application/json'
+        headers['Content-Type'] = 'application/json; charset=UTF-8'
+        return headers
+
+    def is_token_valid(self):
+        """
+        Return True if the current auth token is already cached and hasn't
+        expired yet.
+
+        :return: ``True`` if the token is still valid, ``False`` otherwise.
+        :rtype: ``bool``
+        """
+        if not self.auth_token:
+            return False
+
+        if not self.auth_token_expires:
+            return False
+
+        expires = self.auth_token_expires - \
+            datetime.timedelta(seconds=AUTH_TOKEN_EXPIRES_GRACE_SECONDS)
+
+        time_tuple_expires = expires.utctimetuple()
+        time_tuple_now = datetime.datetime.utcnow().utctimetuple()
+
+        if time_tuple_now < time_tuple_expires:
+            return True
+
+        return False
+
+    def authenticate(self, force=False):
+        """
+        Authenticate against the identity API.
+
+        :param force: Forcefully update the token even if it's already cached
+                      and still valid.
+        :type force: ``bool``
+        """
+        raise NotImplementedError('authenticate not implemented')
+
+    def _is_authentication_needed(self, force=False):
+        """
+        Determine if the authentication is needed or if the existing token (if
+        any exists) is still valid.
+        """
+        if force:
+            return True
+
+        if self.auth_version not in AUTH_VERSIONS_WITH_EXPIRES:
+            return True
+
+        if self.is_token_valid():
+            return False
+
+        return True
+
+
+class OpenStackAuthConnection(OpenStackIdentityConnection):
+    """
+    Note: This class is only here for backward compatibility reasons.
+    """
+    responseCls = OpenStackAuthResponse
+    name = 'OpenStack Auth'
+    timeout = None
+
+    def __init__(self, parent_conn, auth_url, auth_version, user_id, key,
+                 tenant_name=None, timeout=None):
+        super(OpenStackAuthConnection, self).__init__(auth_url=auth_url,
+                                                      user_id=user_id,
+                                                      key=key,
+                                                      tenant_name=tenant_name,
+                                                      timeout=timeout,
+                                                      parent_conn=parent_conn)
+        self.auth_version = auth_version
+        self._instance_cache = {}
+
+    def _get_cls_for_auth_version(self, auth_version):
+        if auth_version == '1.0':
+            cls = OpenStackIdentity_1_0_Connection
+        elif auth_version == '1.1':
+            cls = OpenStackIdentity_1_1_Connection
+        elif auth_version == '2.0' or auth_version == '2.0_apikey':
+            cls = OpenStackIdentity_2_0_Connection
+        elif auth_version == '2.0_password':
+            cls = OpenStackIdentity_2_0_Connection
+        elif auth_version == '3.x_password':
+            cls = OpenStackIdentity_3_0_Connection
+        else:
+            raise LibcloudError('Unsupported Auth Version requested')
+
+        return cls
+
+    def _get_instance_for_auth_version(self, auth_version):
+        """
+        Retrieve instance for the provided auth version for the local cache (if
+        exists).
+        """
+        # TODO: Just delegate to the new classes
+        kwargs = {'auth_url': self.auth_url, 'user_id': self.user_id,
+                  'key': self.key, 'tenant_name': self.tenant_name,
+                  'timeout': self.timeout, 'parent_conn': self.parent_conn}
+
+        cls = self._get_cls_for_auth_version(auth_version=auth_version)
+
+        if auth_version not in self._instance_cache:
+            obj = cls(**kwargs)
+            self._instance_cache[auth_version] = obj
+
+        return self._instance_cache[auth_version]
+
+    def authenticate(self, force=False):
+        """
+        Authenticate against the keystone api.
+
+        :param force: Forcefully update the token even if it's already cached
+                      and still valid.
+        :type force: ``bool``
+        """
+        if not self._is_authentication_needed(force=force):
+            return self
+
+        obj = self._get_instance_for_auth_version(
+            auth_version=self.auth_version)
+
+        try:
+            obj.authenticate()
+        finally:
+            self.action = obj.action
+
+        # For backward compatibility, re-assign attributes to this class
+        self.auth_token = obj.auth_token
+        self.auth_token_expires = obj.auth_token_expires
+        self.urls = obj.urls
+        self.auth_user_info = obj.auth_user_info
+
+        return self
+
+
+class OpenStackIdentity_1_0_Connection(OpenStackIdentityConnection):
+    """
+    Connection class for Keystone API v1.0.
+    """
+
+    responseCls = OpenStackAuthResponse
+    name = 'OpenStack Identity API v1.0'
+    auth_version = '1.0'
+
+    def authenticate(self, force=False):
+        if not self._is_authentication_needed(force=force):
+            return self
+
+        headers = {
+            'X-Auth-User': self.user_id,
+            'X-Auth-Key': self.key,
+        }
+
+        resp = self.request('/v1.0', headers=headers, method='GET')
+
+        if resp.status == httplib.UNAUTHORIZED:
+            # HTTP UNAUTHORIZED (401): auth failed
+            raise InvalidCredsError()
+        elif resp.status not in [httplib.NO_CONTENT, httplib.OK]:
+            body = 'code: %s body:%s headers:%s' % (resp.status,
+                                                    resp.body,
+                                                    resp.headers)
+            raise MalformedResponseError('Malformed response', body=body,
+                                         driver=self.driver)
+        else:
+            headers = resp.headers
+            # emulate the auth 1.1 URL list
+            self.urls = {}
+            self.urls['cloudServers'] = \
+                [{'publicURL': headers.get('x-server-management-url', None)}]
+            self.urls['cloudFilesCDN'] = \
+                [{'publicURL': headers.get('x-cdn-management-url', None)}]
+            self.urls['cloudFiles'] = \
+                [{'publicURL': headers.get('x-storage-url', None)}]
+            self.auth_token = headers.get('x-auth-token', None)
+            self.auth_user_info = None
+
+            if not self.auth_token:
+                raise MalformedResponseError('Missing X-Auth-Token in \
+                                              response headers')
+
+        return self
+
+
+class OpenStackIdentity_1_1_Connection(OpenStackIdentityConnection):
+    """
+    Connection class for Keystone API v1.1.
+    """
+
+    responseCls = OpenStackAuthResponse
+    name = 'OpenStack Identity API v1.1'
+    auth_version = '1.1'
+
+    def authenticate(self, force=False):
+        if not self._is_authentication_needed(force=force):
+            return self
+
+        reqbody = json.dumps({'credentials': {'username': self.user_id,
+                                              'key': self.key}})
+        resp = self.request('/v1.1/auth', data=reqbody, headers={},
+                            method='POST')
+
+        if resp.status == httplib.UNAUTHORIZED:
+            # HTTP UNAUTHORIZED (401): auth failed
+            raise InvalidCredsError()
+        elif resp.status != httplib.OK:
+            body = 'code: %s body:%s' % (resp.status, resp.body)
+            raise MalformedResponseError('Malformed response', body=body,
+                                         driver=self.driver)
+        else:
+            try:
+                body = json.loads(resp.body)
+            except Exception:
+                e = sys.exc_info()[1]
+                raise MalformedResponseError('Failed to parse JSON', e)
+
+            try:
+                expires = body['auth']['token']['expires']
+
+                self.auth_token = body['auth']['token']['id']
+                self.auth_token_expires = parse_date(expires)
+                self.urls = body['auth']['serviceCatalog']
+                self.auth_user_info = None
+            except KeyError:
+                e = sys.exc_info()[1]
+                raise MalformedResponseError('Auth JSON response is \
+                                             missing required elements', e)
+
+        return self
+
+
+class OpenStackIdentity_2_0_Connection(OpenStackIdentityConnection):
+    """
+    Connection class for Keystone API v2.0.
+    """
+
+    responseCls = OpenStackAuthResponse
+    name = 'OpenStack Identity API v1.0'
+    auth_version = '2.0'
+
+    def authenticate(self, auth_type='api_key', force=False):
+        if not self._is_authentication_needed(force=force):
+            return self
+
+        if auth_type == 'api_key':
+            return self._authenticate_2_0_with_api_key()
+        elif auth_type == 'password':
+            return self._authenticate_2_0_with_password()
+        else:
+            raise ValueError('Invalid value for auth_type argument')
+
+    def _authenticate_2_0_with_api_key(self):
+        # API Key based authentication uses the RAX-KSKEY extension.
+        # http://s.apache.org/oAi
+        data = {'auth':
+                {'RAX-KSKEY:apiKeyCredentials':
+                 {'username': self.user_id, 'apiKey': self.key}}}
+        if self.tenant_name:
+            data['auth']['tenantName'] = self.tenant_name
+        reqbody = json.dumps(data)
+        return self._authenticate_2_0_with_body(reqbody)
+
+    def _authenticate_2_0_with_password(self):
+        # Password based authentication is the only 'core' authentication
+        # method in Keystone at this time.
+        # 'keystone' - http://s.apache.org/e8h
+        data = {'auth':
+                {'passwordCredentials':
+                 {'username': self.user_id, 'password': self.key}}}
+        if self.tenant_name:
+            data['auth']['tenantName'] = self.tenant_name
+        reqbody = json.dumps(data)
+        return self._authenticate_2_0_with_body(reqbody)
+
+    def _authenticate_2_0_with_body(self, reqbody):
+        resp = self.request('/v2.0/tokens', data=reqbody,
+                            headers={'Content-Type': 'application/json'},
+                            method='POST')
+
+        if resp.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError()
+        elif resp.status not in [httplib.OK,
+                                 httplib.NON_AUTHORITATIVE_INFORMATION]:
+            body = 'code: %s body: %s' % (resp.status, resp.body)
+            raise MalformedResponseError('Malformed response', body=body,
+                                         driver=self.driver)
+        else:
+            body = resp.object
+
+            try:
+                access = body['access']
+                expires = access['token']['expires']
+
+                self.auth_token = access['token']['id']
+                self.auth_token_expires = parse_date(expires)
+                self.urls = access['serviceCatalog']
+                self.auth_user_info = access.get('user', {})
+            except KeyError:
+                e = sys.exc_info()[1]
+                raise MalformedResponseError('Auth JSON response is \
+                                             missing required elements', e)
+
+        return self
+
+
+class OpenStackIdentity_3_0_Connection(OpenStackIdentityConnection):
+    """
+    Connection class for Keystone API v3.x.
+    """
+
+    responseCls = OpenStackAuthResponse
+    name = 'OpenStack Identity API v3.x'
+    auth_version = '3.0'
+
+    def __init__(self, auth_url, user_id, key, tenant_name=None, timeout=None,
+                 parent_conn=None):
+        super(OpenStackIdentity_3_0_Connection,
+              self).__init__(auth_url=auth_url,
+                             user_id=user_id,
+                             key=key,
+                             tenant_name=tenant_name,
+                             timeout=timeout,
+                             parent_conn=parent_conn)
+
+    def authenticate(self, force=False):
+        """
+        Perform authentication.
+        """
+        if not self._is_authentication_needed(force=force):
+            return self
+
+        # TODO: Support for custom domain
+        domain = 'Default'
+
+        data = {
+            'auth': {
+                'identity': {
+                    'methods': ['password'],
+                    'password': {
+                        'user': {
+                            'domain': {
+                                'name': domain
+                            },
+                            'name': self.user_id,
+                            'password': self.key
+                        }
+                    }
+                },
+                'scope': {
+                    'project': {
+                        'domain': {
+                            'name': domain
+                        },
+                        'name': self.tenant_name
+                    }
+                }
+            }
+        }
+
+        if self.tenant_name:
+            data['auth']['scope'] = {
+                'project': {
+                    'domain': {
+                        'name': domain
+                    },
+                    'name': self.tenant_name
+                }
+            }
+
+        data = json.dumps(data)
+        response = self.request('/v3/auth/tokens', data=data,
+                                headers={'Content-Type': 'application/json'},
+                                method='POST')
+
+        if response.status == httplib.UNAUTHORIZED:
+            # Invalid credentials
+            raise InvalidCredsError()
+        elif response.status in [httplib.OK, httplib.CREATED]:
+            headers = response.headers
+
+            try:
+                body = json.loads(response.body)
+            except Exception:
+                e = sys.exc_info()[1]
+                raise MalformedResponseError('Failed to parse JSON', e)
+
+            try:
+                expires = body['token']['expires_at']
+
+                self.auth_token = headers['x-subject-token']
+                self.auth_token_expires = parse_date(expires)
+                self.urls = body['token']['catalog']
+                self.auth_user_info = None
+            except KeyError:
+                e = sys.exc_info()[1]
+                raise MalformedResponseError('Auth JSON response is \
+                                             missing required elements', e)
+            body = 'code: %s body:%s' % (response.status, response.body)
+        else:
+            raise MalformedResponseError('Malformed response', body=body,
+                                         driver=self.driver)
+
+        return self
+
+    def list_domains(self):
+        """
+        List the available domains.
+
+        :rtype: ``list`` of :class:`OpenStackIdentityDomain`
+        """
+        response = self.authenticated_request('/v3/domains', method='GET')
+        result = self._to_domains(data=response.object['domains'])
+        return result
+
+    def list_projects(self):
+        """
+        List the available projects.
+
+        Note: To perform this action, user you are currently authenticated with
+        needs to be an admin.
+
+        :rtype: ``list`` of :class:`OpenStackIdentityProject`
+        """
+        response = self.authenticated_request('/v3/projects', method='GET')
+        result = self._to_projects(data=response.object['projects'])
+        return result
+
+    def list_users(self):
+        """
+        List the available users.
+
+        :rtype: ``list`` of :class:`.OpenStackIdentityUser`
+        """
+        response = self.authenticated_request('/v3/users', method='GET')
+        result = self._to_users(data=response.object['users'])
+        return result
+
+    def list_roles(self):
+        """
+        List the available roles.
+
+        :rtype: ``list`` of :class:`.OpenStackIdentityRole`
+        """
+        response = self.authenticated_request('/v3/roles', method='GET')
+        result = self._to_roles(data=response.object['roles'])
+        return result
+
+    def get_domain(self, domain_id):
+        """
+        Retrieve information about a single domain.
+
+        :param domain_id: ID of domain to retrieve information for.
+        :type domain_id: ``str``
+
+        :rtyle: :class:`.OpenStackIdentityDomain`
+        """
+        response = self.authenticated_request('/v3/domains/%s' % (domain_id),
+                                              method='GET')
+        result = self._to_domain(data=response.object['domain'])
+        return result
+
+    def list_user_projects(self, user):
+        """
+        Retrieve all the projects user belongs to.
+
+        :rtype: ``list`` of :class:`.OpenStackIdentityProject`
+        """
+        path = '/v3/users/%s/projects' % (user.id)
+        response = self.authenticated_request(path, method='GET')
+        result = self._to_projects(data=response.object['projects'])
+        return result
+
+    def list_user_domain_roles(self, domain, user):
+        """
+        Retrieve all the roles for a particular user on a domain.
+
+        :rtype: ``list`` of :class:`.OpenStackIdentityRole`
+        """
+        # TODO: Also add "get users roles" and "get assginements" which are
+        # available in 3.1 and 3.3
+        path = '/v3/domains/%s/users/%s/roles' % (domain.id, user.id)
+        response = self.authenticated_request(path, method='GET')
+        result = self._to_roles(data=response.object['roles'])
+        return result
+
+    def grant_role_to_user(self, domain, role, user):
+        """
+        Grant role to the domain user.
+
+        Note: This function appeats to be idempodent.
+
+        :param role: Role to grant.
+        :type role: :class:`.OpenStackIdentityRole`
+
+        :param user: User to grant the role to.
+        :type user: :class:`.OpenStackIdentityUser`
+
+        :return: ``True`` on success.
+        :rtype: ``bool``
+        """
+        path = ('/v3/domains/%s/users/%s/roles/%s' %
+                (domain.id, user.id, role.id))
+        response = self.authenticated_request(path, method='PUT')
+        return response.status == httplib.NO_CONTENT
+
+    def revoke_role_from_user(self, domain, user, role):
+        """
+        Revoke role from a domain user.
+        """
+        path = ('/v3/domains/%s/users/%s/roles/%s' %
+                (domain.id, user.id, role.id))
+        response = self.authenticated_request(path, method='DELETE')
+        return response.status == httplib.NO_CONTENT
+
+    def create_domain(self):
+        pass
+
+    def create_user(self, email, password, name, description=None,
+                    domain_id=None, enabled=True):
+        """
+        Create a new user account.
+
+        :param email: User's mail address.
+        :type email: ``str``
+
+        :param password: User's password.
+        :type password: ``str``
+
+        :param name: User's name.
+        :type name: ``str``
+
+        :param description: Optional description.
+        :type description: ``str``
+
+        :param domain_id: ID of the domain to add the user to (optional).
+        :type domain_id: ``str``
+
+        :param enabled: True to enable user after creation.
+        :type enabled: ``bool``
+
+        :return: Created user.
+        :rtype: :class:`.OpenStackIdentityUser`
+        """
+        data = {
+            'email': email,
+            'password': password,
+            'name': name,
+            'enabled': enabled
+        }
+
+        if description:
+            data['description'] = description
+
+        if domain_id:
+            data['domain_id'] = domain_id
+
+        data = json.dumps({'user': data})
+        response = self.authenticated_request('/v3/users', data=data,
+                                              method='POST')
+
+        user = self._to_user(data=response.object['user'])
+        return user
+
+    def _to_domains(self, data):
+        result = []
+        for item in data:
+            domain = self._to_domain(data=item)
+            result.append(domain)
+
+        return result
+
+    def _to_domain(self, data):
+        domain = OpenStackIdentityDomain(id=data['id'],
+                                         name=data['name'],
+                                         enabled=data['enabled'])
+        return domain
+
+    def _to_projects(self, data):
+        result = []
+        for item in data:
+            project = self._to_project(data=item)
+            result.append(project)
+
+        return result
+
+    def _to_project(self, data):
+        project = OpenStackIdentityProject(id=data['id'],
+                                           domain_id=data['domain_id'],
+                                           name=data['name'],
+                                           description=data['description'],
+                                           enabled=data['enabled'])
+        return project
+
+    def _to_users(self, data):
+        result = []
+        for item in data:
+            user = self._to_user(data=item)
+            result.append(user)
+
+        return result
+
+    def _to_user(self, data):
+        user = OpenStackIdentityUser(id=data['id'],
+                                     domain_id=data['domain_id'],
+                                     name=data['name'],
+                                     email=data['email'],
+                                     description=data.get('description',
+                                                          None),
+                                     enabled=data['enabled'])
+        return user
+
+    def _to_roles(self, data):
+        result = []
+        for item in data:
+            user = self._to_role(data=item)
+            result.append(user)
+
+        return result
+
+    def _to_role(self, data):
+        role = OpenStackIdentityRole(id=data['id'],
+                                     name=data['name'],
+                                     description=data.get('description',
+                                                          None),
+                                     enabled=data.get('enabled', True))
+        return role
+
+
+def get_class_for_auth_version(auth_version):
+    """
+    Retrieve class for the provided auth version.
+    """
+    if auth_version == '1.0':
+        cls = OpenStackIdentity_1_0_Connection
+    elif auth_version == '1.1':
+        cls = OpenStackIdentity_1_1_Connection
+    elif auth_version == '2.0' or auth_version == '2.0_apikey':
+        cls = OpenStackIdentity_2_0_Connection
+    elif auth_version == '2.0_password':
+        cls = OpenStackIdentity_2_0_Connection
+    elif auth_version == '3.x_password':
+        cls = OpenStackIdentity_3_0_Connection
+    else:
+        raise LibcloudError('Unsupported Auth Version requested')
+
+    return cls

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/compute/drivers/hpcloud.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/hpcloud.py b/libcloud/compute/drivers/hpcloud.py
index 97de03e..3c4360a 100644
--- a/libcloud/compute/drivers/hpcloud.py
+++ b/libcloud/compute/drivers/hpcloud.py
@@ -61,7 +61,7 @@ class HPCloudConnection(OpenStack_1_1_Connection):
             raise LibcloudError(
                 'Auth version "%s" not supported' % (self._auth_version))
 
-        public_url = ep.get('publicURL', None)
+        public_url = ep.url
 
         if not public_url:
             raise LibcloudError('Could not find specified endpoint')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/compute/drivers/kili.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/kili.py b/libcloud/compute/drivers/kili.py
index de61067..f98e131 100644
--- a/libcloud/compute/drivers/kili.py
+++ b/libcloud/compute/drivers/kili.py
@@ -53,7 +53,7 @@ class KiliCloudConnection(OpenStack_1_1_Connection):
             raise LibcloudError(
                 'Auth version "%s" not supported' % (self._auth_version))
 
-        public_url = ep.get('publicURL', None)
+        public_url = ep.url
 
         if not public_url:
             raise LibcloudError('Could not find specified endpoint')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/compute/drivers/rackspace.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/rackspace.py b/libcloud/compute/drivers/rackspace.py
index 367facd..57559c8 100644
--- a/libcloud/compute/drivers/rackspace.py
+++ b/libcloud/compute/drivers/rackspace.py
@@ -62,8 +62,6 @@ class RackspaceFirstGenConnection(OpenStack_1_0_Connection):
         super(RackspaceFirstGenConnection, self).__init__(*args, **kwargs)
 
     def get_endpoint(self):
-        ep = {}
-
         if '2.0' in self._auth_version:
             ep = self.service_catalog.get_endpoint(service_type='compute',
                                                    name='cloudServers')
@@ -71,7 +69,7 @@ class RackspaceFirstGenConnection(OpenStack_1_0_Connection):
             raise LibcloudError(
                 'Auth version "%s" not supported' % (self._auth_version))
 
-        public_url = ep.get('publicURL', None)
+        public_url = ep.url
 
         if not public_url:
             raise LibcloudError('Could not find specified endpoint')
@@ -165,7 +163,7 @@ class RackspaceConnection(OpenStack_1_1_Connection):
             raise LibcloudError(
                 'Auth version "%s" not supported' % (self._auth_version))
 
-        public_url = ep.get('publicURL', None)
+        public_url = ep.url
 
         if not public_url:
             raise LibcloudError('Could not find specified endpoint')

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/dns/drivers/rackspace.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/rackspace.py b/libcloud/dns/drivers/rackspace.py
index d71f4c5..abdb273 100644
--- a/libcloud/dns/drivers/rackspace.py
+++ b/libcloud/dns/drivers/rackspace.py
@@ -115,7 +115,7 @@ class RackspaceDNSConnection(OpenStack_1_1_Connection, PollingConnection):
             raise LibcloudError("Auth version %s not supported" %
                                 (self._auth_version))
 
-        public_url = ep.get('publicURL', None)
+        public_url = ep.url
 
         # This is a nasty hack, but because of how global auth and old accounts
         # work, there is no way around it.

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/storage/drivers/cloudfiles.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/drivers/cloudfiles.py b/libcloud/storage/drivers/cloudfiles.py
index bce89a8..d1f88a3 100644
--- a/libcloud/storage/drivers/cloudfiles.py
+++ b/libcloud/storage/drivers/cloudfiles.py
@@ -183,30 +183,25 @@ class CloudFilesConnection(OpenStackSwiftConnection):
         self.cdn_request = False
         self.use_internal_url = use_internal_url
 
-    def _get_endpoint_key(self):
-        if self.use_internal_url:
-            endpoint_key = INTERNAL_ENDPOINT_KEY
-        else:
-            endpoint_key = PUBLIC_ENDPOINT_KEY
-
-        if self.cdn_request:
-            # cdn endpoints don't have internal urls
-            endpoint_key = PUBLIC_ENDPOINT_KEY
-
-        return endpoint_key
-
     def get_endpoint(self):
         region = self._ex_force_service_region.upper()
 
+        if self.use_internal_url:
+            endpoint_type = 'internal'
+        else:
+            endpoint_type = 'external'
+
         if '2.0' in self._auth_version:
             ep = self.service_catalog.get_endpoint(
                 service_type='object-store',
                 name='cloudFiles',
-                region=region)
+                region=region,
+                endpoint_type=endpoint_type)
             cdn_ep = self.service_catalog.get_endpoint(
                 service_type='rax:object-cdn',
                 name='cloudFilesCDN',
-                region=region)
+                region=region,
+                endpoint_type=endpoint_type)
         else:
             raise LibcloudError(
                 'Auth version "%s" not supported' % (self._auth_version))
@@ -215,16 +210,14 @@ class CloudFilesConnection(OpenStackSwiftConnection):
         if self.cdn_request:
             ep = cdn_ep
 
-        endpoint_key = self._get_endpoint_key()
-
         if not ep:
             raise LibcloudError('Could not find specified endpoint')
 
-        if endpoint_key in ep:
-            return ep[endpoint_key]
-        else:
+        if not ep.url:
             raise LibcloudError('Could not find specified endpoint')
 
+        return ep.url
+
     def request(self, action, params=None, data='', headers=None, method='GET',
                 raw=False, cdn_request=False):
         if not headers:
@@ -242,7 +235,7 @@ class CloudFilesConnection(OpenStackSwiftConnection):
             action=action,
             params=params, data=data,
             method=method, headers=headers,
-            raw=raw)
+            raw=raw, cdn_request=cdn_request)
 
 
 class CloudFilesStorageDriver(StorageDriver, OpenStackDriverMixin):
@@ -343,7 +336,7 @@ class CloudFilesStorageDriver(StorageDriver, OpenStackDriverMixin):
         :param ex_ttl: cache time to live
         :type ex_ttl: ``int``
         """
-        container_name = container.name
+        container_name = self._encode_container_name(container.name)
         headers = {'X-CDN-Enabled': 'True'}
 
         if ex_ttl:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/storage/drivers/ktucloud.py
----------------------------------------------------------------------
diff --git a/libcloud/storage/drivers/ktucloud.py b/libcloud/storage/drivers/ktucloud.py
index 9708a9b..385d445 100644
--- a/libcloud/storage/drivers/ktucloud.py
+++ b/libcloud/storage/drivers/ktucloud.py
@@ -33,14 +33,18 @@ class KTUCloudStorageConnection(CloudFilesConnection):
 
     def get_endpoint(self):
         eps = self.service_catalog.get_endpoints(name='cloudFiles')
+
         if len(eps) == 0:
             raise LibcloudError('Could not find specified endpoint')
+
         ep = eps[0]
-        if 'publicURL' in ep:
-            return ep['publicURL']
-        else:
+        public_url = ep.url
+
+        if not public_url:
             raise LibcloudError('Could not find specified endpoint')
 
+        return public_url
+
 
 class KTUCloudStorageDriver(CloudFilesStorageDriver):
     """

http://git-wip-us.apache.org/repos/asf/libcloud/blob/cd167194/libcloud/test/common/test_openstack.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_openstack.py b/libcloud/test/common/test_openstack.py
index 03f0542..79ec583 100644
--- a/libcloud/test/common/test_openstack.py
+++ b/libcloud/test/common/test_openstack.py
@@ -23,7 +23,6 @@ from libcloud.utils.py3 import PY25
 
 
 class OpenStackBaseConnectionTest(unittest.TestCase):
-
     def setUp(self):
         self.timeout = 10
         OpenStackBaseConnection.conn_classes = (None, Mock())