Merge "Discover supported APIs"
This commit is contained in:
@@ -12,7 +12,30 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from keystoneclient import discover
|
||||
from keystoneclient import httpclient
|
||||
|
||||
# Using client.HTTPClient is deprecated. Use httpclient.HTTPClient instead.
|
||||
HTTPClient = httpclient.HTTPClient
|
||||
|
||||
|
||||
def Client(version=None, unstable=False, **kwargs):
|
||||
"""Factory function to create a new identity service client.
|
||||
|
||||
:param tuple version: The required version of the identity API. If
|
||||
specified the client will be selected such that the
|
||||
major version is equivalent and an endpoint provides
|
||||
at least the specified minor version. For example to
|
||||
specify the 3.1 API use (3, 1).
|
||||
:param bool unstable: Accept endpoints not marked as 'stable'. (optional)
|
||||
:param kwargs: Additional arguments are passed through to the client
|
||||
that is being created.
|
||||
:returns: New keystone client object
|
||||
(keystoneclient.v2_0.Client or keystoneclient.v3.Client).
|
||||
|
||||
:raises: DiscoveryFailure if the server's response is invalid
|
||||
:raises: VersionNotAvailable if a suitable client cannot be found.
|
||||
"""
|
||||
|
||||
return discover.Discover(**kwargs).create_client(version=version,
|
||||
unstable=unstable)
|
||||
|
421
keystoneclient/discover.py
Normal file
421
keystoneclient/discover.py
Normal file
@@ -0,0 +1,421 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import six
|
||||
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import httpclient
|
||||
from keystoneclient.v2_0 import client as v2_client
|
||||
from keystoneclient.v3 import client as v3_client
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _KeystoneVersion(object):
|
||||
"""A factory object that holds all the information to create a client.
|
||||
|
||||
Instances of this class are callable objects that hold all the kwargs that
|
||||
were passed to discovery so that a user may simply call it to create a new
|
||||
client object. Additional arguments passed to the call will override or
|
||||
add to those provided to the object.
|
||||
"""
|
||||
|
||||
_CLIENT_VERSIONS = {2: v2_client.Client,
|
||||
3: v3_client.Client}
|
||||
|
||||
def __init__(self, version, status, client_class=None, **kwargs):
|
||||
"""Create a new discovered version object.
|
||||
|
||||
:param tuple version: the version of the available API.
|
||||
:param string status: the stability of the API.
|
||||
:param Class client_class: the client class that should be used to
|
||||
instantiate against this version of the API.
|
||||
(optional, will be matched against known)
|
||||
:param dict **kwargs: Additional arguments that should be passed on to
|
||||
the client when it is constructed.
|
||||
"""
|
||||
self.version = version
|
||||
self.status = status
|
||||
self.client_class = client_class
|
||||
self.client_kwargs = kwargs
|
||||
|
||||
if not self.client_class:
|
||||
try:
|
||||
self.client_class = self._CLIENT_VERSIONS[self.version[0]]
|
||||
except KeyError:
|
||||
raise exceptions.DiscoveryFailure("No client available "
|
||||
"for version: %s" %
|
||||
self.version)
|
||||
|
||||
def __lt__(self, other):
|
||||
"""Version Ordering.
|
||||
|
||||
Versions are ordered by major, then minor version number, then
|
||||
'stable' is deemed the highest possible status, then they are just
|
||||
treated alphabetically (alpha < beta etc)
|
||||
"""
|
||||
if self.version == other.version:
|
||||
if self.status == 'stable':
|
||||
return False
|
||||
elif other.status == 'stable':
|
||||
return True
|
||||
else:
|
||||
return self.status < other.status
|
||||
|
||||
return self.version < other.version
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.version == other.version and self.status == other.status
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
if kwargs:
|
||||
client_kwargs = self.client_kwargs.copy()
|
||||
client_kwargs.update(kwargs)
|
||||
else:
|
||||
client_kwargs = self.client_kwargs
|
||||
return self.client_class(**client_kwargs)
|
||||
|
||||
@property
|
||||
def _str_ver(self):
|
||||
ver = ".".join([str(v) for v in self.version])
|
||||
|
||||
if self.status != 'stable':
|
||||
ver = "%s-%s" % (ver, self.status)
|
||||
|
||||
return ver
|
||||
|
||||
|
||||
def _normalize_version_number(version):
|
||||
"""Turn a version representation into a tuple."""
|
||||
|
||||
# trim the v from a 'v2.0' or similar
|
||||
try:
|
||||
version = version.lstrip('v')
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# if it's an integer or a numeric as a string then normalize it
|
||||
# to a string, this ensures 1 decimal point
|
||||
try:
|
||||
num = float(version)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
version = str(num)
|
||||
|
||||
# if it's a string (or an integer) from above break it on .
|
||||
try:
|
||||
return tuple(map(int, version.split(".")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# last attempt, maybe it's a list or iterable.
|
||||
try:
|
||||
return tuple(map(int, version))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise TypeError("Invalid version specified: %s" % version)
|
||||
|
||||
|
||||
def available_versions(url, **kwargs):
|
||||
headers = {'Accept': 'application/json'}
|
||||
|
||||
client = httpclient.HTTPClient(**kwargs)
|
||||
resp, body_resp = client.request(url, 'GET', headers=headers)
|
||||
|
||||
# In the event of querying a root URL we will get back a list of
|
||||
# available versions.
|
||||
try:
|
||||
return body_resp['versions']['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# Otherwise if we query an endpoint like /v2.0 then we will get back
|
||||
# just the one available version.
|
||||
try:
|
||||
return [body_resp['version']]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
raise exceptions.DiscoveryFailure("Invalid Response - Bad version"
|
||||
" data returned: %s" % body_resp)
|
||||
|
||||
|
||||
class Discover(object):
|
||||
"""A means to discover and create clients depending on the supported API
|
||||
versions on the server.
|
||||
|
||||
Querying the server is done on object creation and every subsequent method
|
||||
operates upon the data that was retrieved.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Construct a new discovery object.
|
||||
|
||||
The connection parameters associated with this method are the same
|
||||
format and name as those used by a client (see
|
||||
keystoneclient.v2_0.client.Client and keystoneclient.v3.client.Client).
|
||||
If not overridden in subsequent methods they will also be what is
|
||||
passed to the constructed client.
|
||||
|
||||
In the event that auth_url and endpoint is provided then auth_url will
|
||||
be used in accordance with how the client operates.
|
||||
|
||||
The initialization process also queries the server.
|
||||
|
||||
:param string auth_url: Identity service endpoint for authorization.
|
||||
(optional)
|
||||
:param string endpoint: A user-supplied endpoint URL for the identity
|
||||
service. (optional)
|
||||
:param string original_ip: The original IP of the requesting user
|
||||
which will be sent to identity service in a
|
||||
'Forwarded' header. (optional)
|
||||
:param boolean debug: Enables debug logging of all request and
|
||||
responses to the identity service.
|
||||
default False (optional)
|
||||
:param string cacert: Path to the Privacy Enhanced Mail (PEM) file
|
||||
which contains the trusted authority X.509
|
||||
certificates needed to established SSL connection
|
||||
with the identity service. (optional)
|
||||
:param string key: Path to the Privacy Enhanced Mail (PEM) file which
|
||||
contains the unencrypted client private key needed
|
||||
to established two-way SSL connection with the
|
||||
identity service. (optional)
|
||||
:param string cert: Path to the Privacy Enhanced Mail (PEM) file which
|
||||
contains the corresponding X.509 client certificate
|
||||
needed to established two-way SSL connection with
|
||||
the identity service. (optional)
|
||||
:param boolean insecure: Does not perform X.509 certificate validation
|
||||
when establishing SSL connection with identity
|
||||
service. default: False (optional)
|
||||
"""
|
||||
|
||||
url = kwargs.get('endpoint') or kwargs.get('auth_url')
|
||||
if not url:
|
||||
raise exceptions.DiscoveryFailure('Not enough information to '
|
||||
'determine URL. Provide either '
|
||||
'auth_url or endpoint')
|
||||
|
||||
self._client_kwargs = kwargs
|
||||
self._available_versions = available_versions(url, **kwargs)
|
||||
|
||||
def _get_client_constructor_kwargs(self, kwargs_dict={}, **kwargs):
|
||||
client_kwargs = self._client_kwargs.copy()
|
||||
|
||||
client_kwargs.update(kwargs_dict)
|
||||
client_kwargs.update(**kwargs)
|
||||
|
||||
return client_kwargs
|
||||
|
||||
def available_versions(self, unstable=False):
|
||||
"""Return a list of identity APIs available on the server and the data
|
||||
associated with them.
|
||||
|
||||
:param bool unstable: Accept endpoints not marked 'stable'. (optional)
|
||||
|
||||
:returns: A List of dictionaries as presented by the server. Each dict
|
||||
will contain the version and the URL to use for the version.
|
||||
It is a direct representation of the layout presented by the
|
||||
identity API.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from keystoneclient import discover
|
||||
>>> disc = discover.Discovery(auth_url='http://localhost:5000')
|
||||
>>> disc.available_versions()
|
||||
[{'id': 'v3.0',
|
||||
'links': [{'href': u'http://127.0.0.1:5000/v3/',
|
||||
'rel': u'self'}],
|
||||
'media-types': [
|
||||
{'base': 'application/json',
|
||||
'type': 'application/vnd.openstack.identity-v3+json'},
|
||||
{'base': 'application/xml',
|
||||
'type': 'application/vnd.openstack.identity-v3+xml'}],
|
||||
'status': 'stable',
|
||||
'updated': '2013-03-06T00:00:00Z'},
|
||||
{'id': 'v2.0',
|
||||
'links': [{'href': u'http://127.0.0.1:5000/v2.0/',
|
||||
'rel': u'self'},
|
||||
{'href': u'...',
|
||||
'rel': u'describedby',
|
||||
'type': u'application/pdf'}],
|
||||
'media-types': [
|
||||
{'base': 'application/json',
|
||||
'type': 'application/vnd.openstack.identity-v2.0+json'},
|
||||
{'base': 'application/xml',
|
||||
'type': 'application/vnd.openstack.identity-v2.0+xml'}],
|
||||
'status': 'stable',
|
||||
'updated': '2013-03-06T00:00:00Z'}]
|
||||
"""
|
||||
if unstable:
|
||||
# no need to determine the stable endpoints, just return everything
|
||||
return self._available_versions
|
||||
|
||||
versions = []
|
||||
|
||||
for v in self._available_versions:
|
||||
try:
|
||||
status = v['status']
|
||||
except KeyError:
|
||||
_logger.warning("Skipping over invalid version data. "
|
||||
"No stability status in version.")
|
||||
else:
|
||||
if status == 'stable':
|
||||
versions.append(v)
|
||||
|
||||
return versions
|
||||
|
||||
def _get_factory_from_response_entry(self, version_data, **kwargs):
|
||||
"""Create a _KeystoneVersion factory object from a version response
|
||||
entry returned from a server.
|
||||
"""
|
||||
try:
|
||||
version_str = version_data['id']
|
||||
status = version_data['status']
|
||||
|
||||
if not version_str.startswith('v'):
|
||||
raise exceptions.DiscoveryFailure('Skipping over invalid '
|
||||
'version string: %s. It '
|
||||
'should start with a v.' %
|
||||
version_str)
|
||||
|
||||
for link in version_data['links']:
|
||||
# NOTE(jamielennox): there are plenty of links like with
|
||||
# documentation and such, we only care about the self
|
||||
# which is a link to the URL we should use.
|
||||
if link['rel'].lower() == 'self':
|
||||
version_number = _normalize_version_number(version_str)
|
||||
version_url = link['href']
|
||||
break
|
||||
else:
|
||||
raise exceptions.DiscoveryFailure("Didn't find any links "
|
||||
"in version data.")
|
||||
|
||||
except (KeyError, TypeError, ValueError):
|
||||
raise exceptions.DiscoveryFailure('Skipping over invalid '
|
||||
'version data.')
|
||||
|
||||
# NOTE(jamielennox): the url might be the auth_url or the endpoint
|
||||
# depending on what was passed initially. Order is important, endpoint
|
||||
# needs to override auth_url.
|
||||
for url_type in ('auth_url', 'endpoint'):
|
||||
if self._client_kwargs.get(url_type, False):
|
||||
kwargs[url_type] = version_url
|
||||
else:
|
||||
kwargs[url_type] = None
|
||||
|
||||
return _KeystoneVersion(status=status,
|
||||
version=version_number,
|
||||
**kwargs)
|
||||
|
||||
def _available_clients(self, unstable=False, **kwargs):
|
||||
"""Return a dictionary of factory functions for available API versions.
|
||||
|
||||
:returns: A dictionary of available API endpoints with the version
|
||||
number as a tuple as the key, and a factory object that can
|
||||
be used to create an appropriate client as the value.
|
||||
|
||||
To use the returned factory you simply call it with the parameters to
|
||||
pass to keystoneclient. These parameters will override those saved in
|
||||
the factory.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from keystoneclient import client
|
||||
>>> available_clients = client._available_clients(auth_url=url,
|
||||
... **kwargs)
|
||||
>>> try:
|
||||
... v2_factory = available_clients[(2, 0)]
|
||||
... except KeyError:
|
||||
... print "Version 2.0 unavailable"
|
||||
... else:
|
||||
... v2_client = v2_factory(token='abcdef')
|
||||
... v2_client.tenants.list()
|
||||
|
||||
:raises: DiscoveryFailure if the response is invalid
|
||||
:raises: VersionNotAvailable if a suitable client cannot be found.
|
||||
"""
|
||||
|
||||
versions = dict()
|
||||
response_values = self.available_versions(unstable=unstable)
|
||||
client_kwargs = self._get_client_constructor_kwargs(kwargs_dict=kwargs)
|
||||
|
||||
for version_data in response_values:
|
||||
try:
|
||||
v = self._get_factory_from_response_entry(version_data,
|
||||
**client_kwargs)
|
||||
except exceptions.DiscoveryFailure, e:
|
||||
_logger.warning("Invalid entry: %s", e, exc_info=True)
|
||||
else:
|
||||
versions[v.version] = v
|
||||
|
||||
return versions
|
||||
|
||||
def create_client(self, version=None, **kwargs):
|
||||
"""Factory function to create a new identity service client.
|
||||
|
||||
:param tuple version: The required version of the identity API. If
|
||||
specified the client will be selected such that
|
||||
the major version is equivalent and an endpoint
|
||||
provides at least the specified minor version.
|
||||
For example to specify the 3.1 API use (3, 1).
|
||||
(optional)
|
||||
:param bool unstable: Accept endpoints not marked 'stable'. (optional)
|
||||
:param kwargs: Additional arguments will override those provided to
|
||||
this object's constructor.
|
||||
|
||||
:returns: An instantiated identity client object.
|
||||
|
||||
:raises: DiscoveryFailure if the server response is invalid
|
||||
:raises: VersionNotAvailable if a suitable client cannot be found.
|
||||
"""
|
||||
versions = self._available_clients(**kwargs)
|
||||
chosen = None
|
||||
|
||||
if version:
|
||||
version = _normalize_version_number(version)
|
||||
|
||||
for keystone_version in six.itervalues(versions):
|
||||
# major versions must be the same (eg even though v2 is a lower
|
||||
# version than v3 we can't use it if v2 was requested)
|
||||
if version[0] != keystone_version.version[0]:
|
||||
continue
|
||||
|
||||
# prevent selecting a minor version less than what is required
|
||||
if version <= keystone_version.version:
|
||||
chosen = keystone_version
|
||||
break
|
||||
|
||||
elif versions:
|
||||
# if no version specified pick the latest one
|
||||
chosen = max(six.iteritems(versions))[1]
|
||||
|
||||
if not chosen:
|
||||
msg = "Could not find a suitable endpoint"
|
||||
|
||||
if version:
|
||||
msg = "%s for client version: %s" % (msg, version)
|
||||
|
||||
if versions:
|
||||
available = ", ".join([v._str_ver
|
||||
for v in six.itervalues(versions)])
|
||||
msg = "%s. Available_versions are: %s" % (msg, available)
|
||||
else:
|
||||
msg = "%s. No versions reported available" % msg
|
||||
|
||||
raise exceptions.VersionNotAvailable(msg)
|
||||
|
||||
return chosen()
|
@@ -41,3 +41,11 @@ class SSLError(ConnectionError):
|
||||
|
||||
class Timeout(ClientException):
|
||||
"""The request timed out."""
|
||||
|
||||
|
||||
class DiscoveryFailure(ClientException):
|
||||
"""Discovery of client versions failed."""
|
||||
|
||||
|
||||
class VersionNotAvailable(DiscoveryFailure):
|
||||
"""Discovery failed as the version you requested is not available."""
|
||||
|
459
keystoneclient/tests/test_discovery.py
Normal file
459
keystoneclient/tests/test_discovery.py
Normal file
@@ -0,0 +1,459 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import httpretty
|
||||
from testtools import matchers
|
||||
|
||||
from keystoneclient import client
|
||||
from keystoneclient import discover
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient.openstack.common import jsonutils
|
||||
from keystoneclient.tests import utils
|
||||
from keystoneclient.v2_0 import client as v2_client
|
||||
from keystoneclient.v3 import client as v3_client
|
||||
|
||||
BASE_HOST = 'http://keystone.example.com'
|
||||
BASE_URL = "%s:5000/" % BASE_HOST
|
||||
UPDATED = '2013-03-06T00:00:00Z'
|
||||
|
||||
TEST_SERVICE_CATALOG = [{
|
||||
"endpoints": [{
|
||||
"adminURL": "%s:8774/v1.0" % BASE_HOST,
|
||||
"region": "RegionOne",
|
||||
"internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST,
|
||||
"publicURL": "%s:8774/v1.0/" % BASE_HOST
|
||||
}],
|
||||
"type": "nova_compat",
|
||||
"name": "nova_compat"
|
||||
}, {
|
||||
"endpoints": [{
|
||||
"adminURL": "http://nova/novapi/admin",
|
||||
"region": "RegionOne",
|
||||
"internalURL": "http://nova/novapi/internal",
|
||||
"publicURL": "http://nova/novapi/public"
|
||||
}],
|
||||
"type": "compute",
|
||||
"name": "nova"
|
||||
}, {
|
||||
"endpoints": [{
|
||||
"adminURL": "http://glance/glanceapi/admin",
|
||||
"region": "RegionOne",
|
||||
"internalURL": "http://glance/glanceapi/internal",
|
||||
"publicURL": "http://glance/glanceapi/public"
|
||||
}],
|
||||
"type": "image",
|
||||
"name": "glance"
|
||||
}, {
|
||||
"endpoints": [{
|
||||
"adminURL": "%s:35357/v2.0" % BASE_HOST,
|
||||
"region": "RegionOne",
|
||||
"internalURL": "%s:5000/v2.0" % BASE_HOST,
|
||||
"publicURL": "%s:5000/v2.0" % BASE_HOST
|
||||
}],
|
||||
"type": "identity",
|
||||
"name": "keystone"
|
||||
}, {
|
||||
"endpoints": [{
|
||||
"adminURL": "http://swift/swiftapi/admin",
|
||||
"region": "RegionOne",
|
||||
"internalURL": "http://swift/swiftapi/internal",
|
||||
"publicURL": "http://swift/swiftapi/public"
|
||||
}],
|
||||
"type": "object-store",
|
||||
"name": "swift"
|
||||
}]
|
||||
|
||||
V2_URL = "%sv2.0" % BASE_URL
|
||||
V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/'
|
||||
'openstack-identity-service/2.0/content/',
|
||||
'rel': 'describedby',
|
||||
'type': 'text/html'}
|
||||
V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident'
|
||||
'ity-service/2.0/identity-dev-guide-2.0.pdf',
|
||||
'rel': 'describedby',
|
||||
'type': 'application/pdf'}
|
||||
|
||||
V2_VERSION = {'id': 'v2.0',
|
||||
'links': [{'href': V2_URL, 'rel': 'self'},
|
||||
V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}
|
||||
|
||||
V2_AUTH_RESPONSE = jsonutils.dumps({
|
||||
"access": {
|
||||
"token": {
|
||||
"expires": "2020-01-01T00:00:10.000123Z",
|
||||
"id": 'fakeToken',
|
||||
"tenant": {
|
||||
"id": '1'
|
||||
},
|
||||
},
|
||||
"user": {
|
||||
"id": 'test'
|
||||
},
|
||||
"serviceCatalog": TEST_SERVICE_CATALOG,
|
||||
},
|
||||
})
|
||||
|
||||
V3_URL = "%sv3" % BASE_URL
|
||||
V3_MEDIA_TYPES = [{'base': 'application/json',
|
||||
'type': 'application/vnd.openstack.identity-v3+json'},
|
||||
{'base': 'application/xml',
|
||||
'type': 'application/vnd.openstack.identity-v3+xml'}]
|
||||
|
||||
V3_VERSION = {'id': 'v3.0',
|
||||
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}
|
||||
|
||||
V3_TOKEN = u'3e2813b7ba0b4006840c3825860b86ed',
|
||||
V3_AUTH_RESPONSE = jsonutils.dumps({
|
||||
"token": {
|
||||
"methods": [
|
||||
"token",
|
||||
"password"
|
||||
],
|
||||
|
||||
"expires_at": "2020-01-01T00:00:10.000123Z",
|
||||
"project": {
|
||||
"domain": {
|
||||
"id": '1',
|
||||
"name": 'test-domain'
|
||||
},
|
||||
"id": '1',
|
||||
"name": 'test-project'
|
||||
},
|
||||
"user": {
|
||||
"domain": {
|
||||
"id": '1',
|
||||
"name": 'test-domain'
|
||||
},
|
||||
"id": '1',
|
||||
"name": 'test-user'
|
||||
},
|
||||
"issued_at": "2013-05-29T16:55:21.468960Z",
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
def _create_version_list(versions):
|
||||
return jsonutils.dumps({'versions': {'values': versions}})
|
||||
|
||||
|
||||
def _create_single_version(version):
|
||||
return jsonutils.dumps({'version': version})
|
||||
|
||||
|
||||
V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
|
||||
V2_VERSION_LIST = _create_version_list([V2_VERSION])
|
||||
|
||||
V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
|
||||
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
class AvailableVersionsTests(utils.TestCase):
|
||||
|
||||
def test_available_versions(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
versions = discover.available_versions(BASE_URL)
|
||||
|
||||
for v in versions:
|
||||
self.assertIn('id', v)
|
||||
self.assertIn('status', v)
|
||||
self.assertIn('links', v)
|
||||
|
||||
def test_available_versions_individual(self):
|
||||
httpretty.register_uri(httpretty.GET, V3_URL, status=200,
|
||||
body=V3_VERSION_ENTRY)
|
||||
|
||||
versions = discover.available_versions(V3_URL)
|
||||
|
||||
for v in versions:
|
||||
self.assertEqual(v['id'], 'v3.0')
|
||||
self.assertEqual(v['status'], 'stable')
|
||||
self.assertIn('media-types', v)
|
||||
self.assertIn('links', v)
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
class ClientDiscoveryTests(utils.TestCase):
|
||||
|
||||
def assertCreatesV3(self, **kwargs):
|
||||
httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL,
|
||||
body=V3_AUTH_RESPONSE, X_Subject_Token=V3_TOKEN)
|
||||
|
||||
kwargs.setdefault('username', 'foo')
|
||||
kwargs.setdefault('password', 'bar')
|
||||
keystone = client.Client(**kwargs)
|
||||
self.assertIsInstance(keystone, v3_client.Client)
|
||||
return keystone
|
||||
|
||||
def assertCreatesV2(self, **kwargs):
|
||||
httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL,
|
||||
body=V2_AUTH_RESPONSE)
|
||||
|
||||
kwargs.setdefault('username', 'foo')
|
||||
kwargs.setdefault('password', 'bar')
|
||||
keystone = client.Client(**kwargs)
|
||||
self.assertIsInstance(keystone, v2_client.Client)
|
||||
return keystone
|
||||
|
||||
def assertVersionNotAvailable(self, **kwargs):
|
||||
kwargs.setdefault('username', 'foo')
|
||||
kwargs.setdefault('password', 'bar')
|
||||
|
||||
self.assertRaises(exceptions.VersionNotAvailable,
|
||||
client.Client, **kwargs)
|
||||
|
||||
def assertDiscoveryFailure(self, **kwargs):
|
||||
kwargs.setdefault('username', 'foo')
|
||||
kwargs.setdefault('password', 'bar')
|
||||
|
||||
self.assertRaises(exceptions.DiscoveryFailure,
|
||||
client.Client, **kwargs)
|
||||
|
||||
def test_discover_v3(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
self.assertCreatesV3(auth_url=BASE_URL)
|
||||
|
||||
def test_discover_v2(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V2_VERSION_LIST)
|
||||
httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL,
|
||||
body=V2_AUTH_RESPONSE)
|
||||
|
||||
self.assertCreatesV2(auth_url=BASE_URL)
|
||||
|
||||
def test_discover_endpoint_v2(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V2_VERSION_LIST)
|
||||
self.assertCreatesV2(endpoint=BASE_URL, token='fake-token')
|
||||
|
||||
def test_discover_endpoint_v3(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
self.assertCreatesV3(endpoint=BASE_URL, token='fake-token')
|
||||
|
||||
def test_discover_invalid_major_version(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
self.assertVersionNotAvailable(auth_url=BASE_URL, version=5)
|
||||
|
||||
def test_discover_200_response_fails(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body='ok')
|
||||
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
||||
|
||||
def test_discover_minor_greater_than_available_fails(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4)
|
||||
|
||||
def test_discover_individual_version_v2(self):
|
||||
httpretty.register_uri(httpretty.GET, V2_URL, status=200,
|
||||
body=V2_VERSION_ENTRY)
|
||||
|
||||
self.assertCreatesV2(auth_url=V2_URL)
|
||||
|
||||
def test_discover_individual_version_v3(self):
|
||||
httpretty.register_uri(httpretty.GET, V3_URL, status=200,
|
||||
body=V3_VERSION_ENTRY)
|
||||
|
||||
self.assertCreatesV3(auth_url=V3_URL)
|
||||
|
||||
def test_discover_individual_endpoint_v2(self):
|
||||
httpretty.register_uri(httpretty.GET, V2_URL, status=200,
|
||||
body=V2_VERSION_ENTRY)
|
||||
self.assertCreatesV2(endpoint=V2_URL, token='fake-token')
|
||||
|
||||
def test_discover_individual_endpoint_v3(self):
|
||||
httpretty.register_uri(httpretty.GET, V3_URL, status=200,
|
||||
body=V3_VERSION_ENTRY)
|
||||
self.assertCreatesV3(endpoint=V3_URL, token='fake-token')
|
||||
|
||||
def test_discover_fail_to_create_bad_individual_version(self):
|
||||
httpretty.register_uri(httpretty.GET, V2_URL, status=200,
|
||||
body=V2_VERSION_ENTRY)
|
||||
httpretty.register_uri(httpretty.GET, V3_URL, status=200,
|
||||
body=V3_VERSION_ENTRY)
|
||||
|
||||
self.assertVersionNotAvailable(auth_url=V2_URL, version=3)
|
||||
self.assertVersionNotAvailable(auth_url=V3_URL, version=2)
|
||||
|
||||
def test_discover_unstable_versions(self):
|
||||
v3_unstable_version = V3_VERSION.copy()
|
||||
v3_unstable_version['status'] = 'beta'
|
||||
version_list = _create_version_list([v3_unstable_version, V2_VERSION])
|
||||
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=version_list)
|
||||
|
||||
self.assertCreatesV2(auth_url=BASE_URL)
|
||||
self.assertVersionNotAvailable(auth_url=BASE_URL, version=3)
|
||||
self.assertCreatesV3(auth_url=BASE_URL, unstable=True)
|
||||
|
||||
def test_discover_forwards_original_ip(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
ip = '192.168.1.1'
|
||||
self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip)
|
||||
|
||||
self.assertThat(httpretty.httpretty.last_request.headers['forwarded'],
|
||||
matchers.Contains(ip))
|
||||
|
||||
def test_discover_bad_args(self):
|
||||
self.assertRaises(exceptions.DiscoveryFailure,
|
||||
client.Client)
|
||||
|
||||
def test_discover_bad_response(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=jsonutils.dumps({'FOO': 'BAR'}))
|
||||
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
||||
|
||||
def test_discovery_ignore_invalid(self):
|
||||
resp = [{'id': '3.99', # without a leading v
|
||||
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED},
|
||||
{'id': 'v3.0',
|
||||
'links': [1, 2, 3, 4], # invalid links
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}]
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=_create_version_list(resp))
|
||||
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
||||
|
||||
def test_ignore_entry_without_links(self):
|
||||
v3 = V3_VERSION.copy()
|
||||
v3['links'] = []
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=_create_version_list([v3, V2_VERSION]))
|
||||
self.assertCreatesV2(auth_url=BASE_URL)
|
||||
|
||||
def test_ignore_entry_without_status(self):
|
||||
v3 = V3_VERSION.copy()
|
||||
del v3['status']
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=_create_version_list([v3, V2_VERSION]))
|
||||
self.assertCreatesV2(auth_url=BASE_URL)
|
||||
|
||||
def test_greater_version_than_required(self):
|
||||
resp = [{'id': 'v3.6',
|
||||
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}]
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=200,
|
||||
body=_create_version_list(resp))
|
||||
self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4))
|
||||
|
||||
def test_lesser_version_than_required(self):
|
||||
resp = [{'id': 'v3.4',
|
||||
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}]
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=200,
|
||||
body=_create_version_list(resp))
|
||||
self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6))
|
||||
|
||||
def test_bad_response(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body="Ugly Duckling")
|
||||
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
||||
|
||||
def test_pass_client_arguments(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V2_VERSION_LIST)
|
||||
kwargs = {'original_ip': '100', 'use_keyring': False,
|
||||
'stale_duration': 15}
|
||||
|
||||
cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs)
|
||||
|
||||
self.assertEqual(cl.original_ip, '100')
|
||||
self.assertEqual(cl.stale_duration, 15)
|
||||
self.assertFalse(cl.use_keyring)
|
||||
|
||||
def test_overriding_stored_kwargs(self):
|
||||
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||
body=V3_VERSION_LIST)
|
||||
|
||||
httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL,
|
||||
body=V3_AUTH_RESPONSE, X_Subject_Token=V3_TOKEN)
|
||||
|
||||
disc = discover.Discover(auth_url=BASE_URL, debug=False,
|
||||
username='foo')
|
||||
client = disc.create_client(debug=True, password='bar')
|
||||
|
||||
self.assertIsInstance(client, v3_client.Client)
|
||||
self.assertTrue(client.debug_log)
|
||||
self.assertFalse(disc._client_kwargs['debug'])
|
||||
self.assertEqual(client.username, 'foo')
|
||||
self.assertEqual(client.password, 'bar')
|
||||
|
||||
|
||||
class DiscoverUtils(utils.TestCase):
|
||||
|
||||
def test_version_number(self):
|
||||
def assertVersion(inp, out):
|
||||
self.assertEqual(discover._normalize_version_number(inp), out)
|
||||
|
||||
def versionRaises(inp):
|
||||
self.assertRaises(TypeError,
|
||||
discover._normalize_version_number,
|
||||
inp)
|
||||
|
||||
assertVersion('v1.2', (1, 2))
|
||||
assertVersion('v11', (11, 0))
|
||||
assertVersion('1.2', (1, 2))
|
||||
assertVersion('1.5.1', (1, 5, 1))
|
||||
assertVersion('1', (1, 0))
|
||||
assertVersion(1, (1, 0))
|
||||
assertVersion(5.2, (5, 2))
|
||||
assertVersion((6, 1), (6, 1))
|
||||
assertVersion([1, 4], (1, 4))
|
||||
|
||||
versionRaises('hello')
|
||||
versionRaises('1.a')
|
||||
versionRaises('vaccuum')
|
||||
|
||||
def test_keystone_version_objects(self):
|
||||
v31s = discover._KeystoneVersion((3, 1), 'stable')
|
||||
v20s = discover._KeystoneVersion((2, 0), 'stable')
|
||||
v30s = discover._KeystoneVersion((3, 0), 'stable')
|
||||
|
||||
v31a = discover._KeystoneVersion((3, 1), 'alpha')
|
||||
v31b = discover._KeystoneVersion((3, 1), 'beta')
|
||||
|
||||
self.assertTrue(v31s > v30s)
|
||||
self.assertTrue(v30s > v20s)
|
||||
|
||||
self.assertTrue(v31s > v31a)
|
||||
self.assertFalse(v31s < v31a)
|
||||
self.assertTrue(v31b > v31a)
|
||||
self.assertTrue(v31a < v31b)
|
||||
self.assertTrue(v31b > v30s)
|
||||
|
||||
self.assertNotEqual(v31s, v31b)
|
||||
self.assertEqual(v31s, discover._KeystoneVersion((3, 1), 'stable'))
|
@@ -121,10 +121,11 @@ class Client(httpclient.HTTPClient):
|
||||
|
||||
"""
|
||||
|
||||
version = 'v2.0'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new client for the Keystone v2.0 API."""
|
||||
super(Client, self).__init__(**kwargs)
|
||||
self.version = 'v2.0'
|
||||
self.endpoints = endpoints.EndpointManager(self)
|
||||
self.roles = roles.RoleManager(self)
|
||||
self.services = services.ServiceManager(self)
|
||||
|
@@ -84,11 +84,12 @@ class Client(httpclient.HTTPClient):
|
||||
|
||||
"""
|
||||
|
||||
version = 'v3'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize a new client for the Keystone v3 API."""
|
||||
super(Client, self).__init__(**kwargs)
|
||||
|
||||
self.version = 'v3'
|
||||
self.credentials = credentials.CredentialManager(self)
|
||||
self.endpoints = endpoints.EndpointManager(self)
|
||||
self.domains = domains.DomainManager(self)
|
||||
|
Reference in New Issue
Block a user