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())