Merge "Discovery URL querying functions"
This commit is contained in:
252
keystoneclient/_discover.py
Normal file
252
keystoneclient/_discover.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""The passive components to version discovery.
|
||||||
|
|
||||||
|
The Discover object in discover.py contains functions that can create objects
|
||||||
|
on your behalf. These functions are not usable from within the keystoneclient
|
||||||
|
library because you will get dependency resolution issues.
|
||||||
|
|
||||||
|
The Discover object in this file provides the querying components of Discovery.
|
||||||
|
This includes functions like url_for which allow you to retrieve URLs and the
|
||||||
|
raw data specified in version discovery responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_data(session, url):
|
||||||
|
"""Retrieve raw version data from a url."""
|
||||||
|
headers = {'Accept': 'application/json'}
|
||||||
|
|
||||||
|
resp = session.get(url, headers=headers)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_resp = resp.json()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Most servers don't have a 'values' element so accept a simple
|
||||||
|
# versions dict if available.
|
||||||
|
try:
|
||||||
|
return body_resp['versions']
|
||||||
|
except KeyError:
|
||||||
|
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:
|
||||||
|
pass
|
||||||
|
|
||||||
|
err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text
|
||||||
|
msg = 'Invalid Response - Bad version data returned: %s' % err_text
|
||||||
|
raise exceptions.DiscoveryFailure(msg)
|
||||||
|
|
||||||
|
|
||||||
|
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 version_match(required, candidate):
|
||||||
|
"""Test that an available version is a suitable match for a required
|
||||||
|
version.
|
||||||
|
|
||||||
|
To be suitable a version must be of the same major version as required
|
||||||
|
and be at least a match in minor/patch level.
|
||||||
|
|
||||||
|
eg. 3.3 is a match for a required 3.1 but 4.1 is not.
|
||||||
|
|
||||||
|
:param tuple required: the version that must be met.
|
||||||
|
:param tuple candidate: the version to test against required.
|
||||||
|
|
||||||
|
:returns bool: True if candidate is suitable False otherwise.
|
||||||
|
"""
|
||||||
|
# major versions must be the same (e.g. even though v2 is a lower
|
||||||
|
# version than v3 we can't use it if v2 was requested)
|
||||||
|
if candidate[0] != required[0]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# prevent selecting a minor version less than what is required
|
||||||
|
if candidate < required:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Discover(object):
|
||||||
|
|
||||||
|
CURRENT_STATUSES = ('stable', 'current', 'supported')
|
||||||
|
DEPRECATED_STATUSES = ('deprecated',)
|
||||||
|
EXPERIMENTAL_STATUSES = ('experimental',)
|
||||||
|
|
||||||
|
def __init__(self, session, url):
|
||||||
|
self._data = get_version_data(session, url)
|
||||||
|
|
||||||
|
def raw_version_data(self, allow_experimental=False,
|
||||||
|
allow_deprecated=True, allow_unknown=False):
|
||||||
|
"""Get raw version information from URL.
|
||||||
|
|
||||||
|
Raw data indicates that only minimal validation processing is performed
|
||||||
|
on the data, so what is returned here will be the data in the same
|
||||||
|
format it was received from the endpoint.
|
||||||
|
|
||||||
|
:param bool allow_experimental: Allow experimental version endpoints.
|
||||||
|
:param bool allow_deprecated: Allow deprecated version endpoints.
|
||||||
|
:param bool allow_unknown: Allow endpoints with an unrecognised status.
|
||||||
|
|
||||||
|
:returns list: The endpoints returned from the server that match the
|
||||||
|
criteria.
|
||||||
|
"""
|
||||||
|
versions = []
|
||||||
|
for v in self._data:
|
||||||
|
try:
|
||||||
|
status = v['status']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.warning('Skipping over invalid version data. '
|
||||||
|
'No stability status in version.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = status.lower()
|
||||||
|
|
||||||
|
if status in self.CURRENT_STATUSES:
|
||||||
|
versions.append(v)
|
||||||
|
elif status in self.DEPRECATED_STATUSES:
|
||||||
|
if allow_deprecated:
|
||||||
|
versions.append(v)
|
||||||
|
elif status in self.EXPERIMENTAL_STATUSES:
|
||||||
|
if allow_experimental:
|
||||||
|
versions.append(v)
|
||||||
|
elif allow_unknown:
|
||||||
|
versions.append(v)
|
||||||
|
|
||||||
|
return versions
|
||||||
|
|
||||||
|
def version_data(self, **kwargs):
|
||||||
|
"""Get normalized version data.
|
||||||
|
|
||||||
|
Return version data in a structured way.
|
||||||
|
|
||||||
|
:returns list(dict): A list of version data dictionaries sorted by
|
||||||
|
version number. Each data element in the returned
|
||||||
|
list is a dictionary consisting of at least:
|
||||||
|
|
||||||
|
:version tuple: The normalized version of the endpoint.
|
||||||
|
:url str: The url for the endpoint.
|
||||||
|
:raw_status str: The status as provided by the server
|
||||||
|
"""
|
||||||
|
data = self.raw_version_data(**kwargs)
|
||||||
|
versions = []
|
||||||
|
|
||||||
|
for v in data:
|
||||||
|
try:
|
||||||
|
version_str = v['id']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.info('Skipping invalid version data. Missing ID.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
links = v['links']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.info('Skipping invalid version data. Missing links')
|
||||||
|
continue
|
||||||
|
|
||||||
|
version_number = normalize_version_number(version_str)
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
try:
|
||||||
|
rel = link['rel']
|
||||||
|
url = link['href']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
_LOGGER.info('Skipping invalid version link. '
|
||||||
|
'Missing link URL or relationship.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rel.lower() == 'self':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
_LOGGER.info('Skipping invalid version data. '
|
||||||
|
'Missing link to endpoint.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
versions.append({'version': version_number,
|
||||||
|
'url': url,
|
||||||
|
'raw_status': v['status']})
|
||||||
|
|
||||||
|
versions.sort(key=lambda v: v['version'])
|
||||||
|
return versions
|
||||||
|
|
||||||
|
def data_for(self, version, **kwargs):
|
||||||
|
"""Return endpoint data for a specific version.
|
||||||
|
|
||||||
|
:returns dict: the endpoint data for a URL that matches the required
|
||||||
|
version (the format is described in version_data)
|
||||||
|
or None if no match.
|
||||||
|
"""
|
||||||
|
version = normalize_version_number(version)
|
||||||
|
version_data = self.version_data(**kwargs)
|
||||||
|
|
||||||
|
for data in reversed(version_data):
|
||||||
|
if version_match(version, data['version']):
|
||||||
|
return data
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def url_for(self, version, **kwargs):
|
||||||
|
"""Get the endpoint url for a required version.
|
||||||
|
|
||||||
|
:returns str: The url for the specified version or None if no match.
|
||||||
|
"""
|
||||||
|
data = self.data_for(version, **kwargs)
|
||||||
|
return data['url'] if data else None
|
@@ -14,6 +14,7 @@ import logging
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from keystoneclient import _discover
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
from keystoneclient import session as client_session
|
from keystoneclient import session as client_session
|
||||||
from keystoneclient.v2_0 import client as v2_client
|
from keystoneclient.v2_0 import client as v2_client
|
||||||
@@ -23,155 +24,19 @@ from keystoneclient.v3 import client as v3_client
|
|||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _KeystoneVersion(object):
|
_CLIENT_VERSIONS = {2: v2_client.Client,
|
||||||
"""A factory object that holds all the information to create a client.
|
3: v3_client.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 create_client(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)
|
|
||||||
|
|
||||||
def __call__(self, **kwargs):
|
|
||||||
return self.create_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, session=None, **kwargs):
|
def available_versions(url, session=None, **kwargs):
|
||||||
headers = {'Accept': 'application/json'}
|
"""Retrieve raw version data from a url."""
|
||||||
|
|
||||||
if not session:
|
if not session:
|
||||||
session = client_session.Session.construct(kwargs)
|
session = client_session.Session.construct(kwargs)
|
||||||
|
|
||||||
resp = session.get(url, headers=headers)
|
return _discover.get_version_data(session, url)
|
||||||
|
|
||||||
try:
|
|
||||||
body_resp = resp.json()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Most servers don't have a 'values' element so accept a simple
|
|
||||||
# versions dict if available.
|
|
||||||
try:
|
|
||||||
return body_resp['versions']
|
|
||||||
except KeyError:
|
|
||||||
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:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise exceptions.DiscoveryFailure("Invalid Response - Bad version"
|
|
||||||
" data returned: %s" % resp.text)
|
|
||||||
|
|
||||||
|
|
||||||
class Discover(object):
|
class Discover(_discover.Discover):
|
||||||
"""A means to discover and create clients depending on the supported API
|
"""A means to discover and create clients depending on the supported API
|
||||||
versions on the server.
|
versions on the server.
|
||||||
|
|
||||||
@@ -239,39 +104,66 @@ class Discover(object):
|
|||||||
session = client_session.Session.construct(kwargs)
|
session = client_session.Session.construct(kwargs)
|
||||||
kwargs['session'] = session
|
kwargs['session'] = session
|
||||||
|
|
||||||
url = kwargs.get('endpoint') or kwargs.get('auth_url')
|
url = None
|
||||||
|
endpoint = kwargs.pop('endpoint', None)
|
||||||
|
auth_url = kwargs.pop('auth_url', None)
|
||||||
|
|
||||||
|
if endpoint:
|
||||||
|
self._use_endpoint = True
|
||||||
|
url = endpoint
|
||||||
|
elif auth_url:
|
||||||
|
self._use_endpoint = False
|
||||||
|
url = auth_url
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
raise exceptions.DiscoveryFailure('Not enough information to '
|
raise exceptions.DiscoveryFailure('Not enough information to '
|
||||||
'determine URL. Provide either '
|
'determine URL. Provide either '
|
||||||
'auth_url or endpoint')
|
'auth_url or endpoint')
|
||||||
|
|
||||||
self._client_kwargs = kwargs
|
self._client_kwargs = kwargs
|
||||||
self._available_versions = available_versions(url, session=session)
|
super(Discover, self).__init__(session, url)
|
||||||
|
|
||||||
def _get_client_constructor_kwargs(self, kwargs_dict={}, **kwargs):
|
def available_versions(self, **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
|
"""Return a list of identity APIs available on the server and the data
|
||||||
associated with them.
|
associated with them.
|
||||||
|
|
||||||
|
DEPRECATED: use raw_version_data()
|
||||||
|
|
||||||
:param bool unstable: Accept endpoints not marked 'stable'. (optional)
|
:param bool unstable: Accept endpoints not marked 'stable'. (optional)
|
||||||
|
DEPRECTED. Equates to setting allow_experimental
|
||||||
|
and allow_unknown to True.
|
||||||
|
:param bool allow_experimental: Allow experimental version endpoints.
|
||||||
|
:param bool allow_deprecated: Allow deprecated version endpoints.
|
||||||
|
:param bool allow_unknown: Allow endpoints with an unrecognised status.
|
||||||
|
|
||||||
:returns: A List of dictionaries as presented by the server. Each dict
|
:returns: A List of dictionaries as presented by the server. Each dict
|
||||||
will contain the version and the URL to use for the version.
|
will contain the version and the URL to use for the version.
|
||||||
It is a direct representation of the layout presented by the
|
It is a direct representation of the layout presented by the
|
||||||
identity API.
|
identity API.
|
||||||
|
"""
|
||||||
|
return self.raw_version_data(**kwargs)
|
||||||
|
|
||||||
|
def raw_version_data(self, unstable=False, **kwargs):
|
||||||
|
"""Get raw version information from URL.
|
||||||
|
|
||||||
|
Raw data indicates that only minimal validation processing is performed
|
||||||
|
on the data, so what is returned here will be the data in the same
|
||||||
|
format it was received from the endpoint.
|
||||||
|
|
||||||
|
:param bool unstable: (deprecated) equates to setting
|
||||||
|
allow_experimental and allow_unknown.
|
||||||
|
:param bool allow_experimental: Allow experimental version endpoints.
|
||||||
|
:param bool allow_deprecated: Allow deprecated version endpoints.
|
||||||
|
:param bool allow_unknown: Allow endpoints with an unrecognised status.
|
||||||
|
|
||||||
|
:returns list: The endpoints returned from the server that match the
|
||||||
|
criteria.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
>>> from keystoneclient import discover
|
>>> from keystoneclient import discover
|
||||||
>>> disc = discover.Discovery(auth_url='http://localhost:5000')
|
>>> disc = discover.Discovery(auth_url='http://localhost:5000')
|
||||||
>>> disc.available_versions()
|
>>> disc.raw_version_data()
|
||||||
[{'id': 'v3.0',
|
[{'id': 'v3.0',
|
||||||
'links': [{'href': u'http://127.0.0.1:5000/v3/',
|
'links': [{'href': u'http://127.0.0.1:5000/v3/',
|
||||||
'rel': u'self'}],
|
'rel': u'self'}],
|
||||||
@@ -297,110 +189,57 @@ class Discover(object):
|
|||||||
'updated': '2013-03-06T00:00:00Z'}]
|
'updated': '2013-03-06T00:00:00Z'}]
|
||||||
"""
|
"""
|
||||||
if unstable:
|
if unstable:
|
||||||
# no need to determine the stable endpoints, just return everything
|
kwargs.setdefault('allow_experimental', True)
|
||||||
return self._available_versions
|
kwargs.setdefault('allow_unknown', True)
|
||||||
|
|
||||||
versions = []
|
return super(Discover, self).raw_version_data(**kwargs)
|
||||||
|
|
||||||
for v in self._available_versions:
|
def _calculate_version(self, version, unstable):
|
||||||
try:
|
version_data = None
|
||||||
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
|
if version:
|
||||||
|
version_data = self.data_for(version)
|
||||||
|
else:
|
||||||
|
# if no version specified pick the latest one
|
||||||
|
all_versions = self.version_data(unstable=unstable)
|
||||||
|
if all_versions:
|
||||||
|
version_data = all_versions[-1]
|
||||||
|
|
||||||
def _get_factory_from_response_entry(self, version_data, **kwargs):
|
if not version_data:
|
||||||
"""Create a _KeystoneVersion factory object from a version response
|
msg = 'Could not find a suitable endpoint'
|
||||||
entry returned from a server.
|
|
||||||
"""
|
if version:
|
||||||
|
msg += ' for client version: %s' % str(version)
|
||||||
|
|
||||||
|
raise exceptions.VersionNotAvailable(msg)
|
||||||
|
|
||||||
|
return version_data
|
||||||
|
|
||||||
|
def _create_client(self, version_data, **kwargs):
|
||||||
|
# Get the client for the version requested that was returned
|
||||||
try:
|
try:
|
||||||
version_str = version_data['id']
|
client_class = _CLIENT_VERSIONS[version_data['version'][0]]
|
||||||
status = version_data['status']
|
except KeyError:
|
||||||
|
version = '.'.join(str(v) for v in version_data['version'])
|
||||||
|
msg = 'No client available for version: %s' % version
|
||||||
|
raise exceptions.DiscoveryFailure(msg)
|
||||||
|
|
||||||
if not version_str.startswith('v'):
|
# kwargs should take priority over stored kwargs.
|
||||||
raise exceptions.DiscoveryFailure('Skipping over invalid '
|
for k, v in six.iteritems(self._client_kwargs):
|
||||||
'version string: %s. It '
|
kwargs.setdefault(k, v)
|
||||||
'should start with a v.' %
|
|
||||||
version_str)
|
|
||||||
|
|
||||||
for link in version_data['links']:
|
# restore the url to either auth_url or endpoint depending on what
|
||||||
# NOTE(jamielennox): there are plenty of links like with
|
# was initially given
|
||||||
# documentation and such, we only care about the self
|
if self._use_endpoint:
|
||||||
# which is a link to the URL we should use.
|
kwargs['auth_url'] = None
|
||||||
if link['rel'].lower() == 'self':
|
kwargs['endpoint'] = version_data['url']
|
||||||
version_number = _normalize_version_number(version_str)
|
else:
|
||||||
version_url = link['href']
|
kwargs['auth_url'] = version_data['url']
|
||||||
break
|
kwargs['endpoint'] = None
|
||||||
else:
|
|
||||||
raise exceptions.DiscoveryFailure("Didn't find any links "
|
|
||||||
"in version data.")
|
|
||||||
|
|
||||||
except (KeyError, TypeError, ValueError):
|
return client_class(**kwargs)
|
||||||
raise exceptions.DiscoveryFailure('Skipping over invalid '
|
|
||||||
'version data.')
|
|
||||||
|
|
||||||
# NOTE(jamielennox): the url might be the auth_url or the endpoint
|
def create_client(self, version=None, unstable=False, **kwargs):
|
||||||
# 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 as 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.
|
"""Factory function to create a new identity service client.
|
||||||
|
|
||||||
:param tuple version: The required version of the identity API. If
|
:param tuple version: The required version of the identity API. If
|
||||||
@@ -418,41 +257,5 @@ class Discover(object):
|
|||||||
:raises: DiscoveryFailure if the server response is invalid
|
:raises: DiscoveryFailure if the server response is invalid
|
||||||
:raises: VersionNotAvailable if a suitable client cannot be found.
|
:raises: VersionNotAvailable if a suitable client cannot be found.
|
||||||
"""
|
"""
|
||||||
versions = self._available_clients(**kwargs)
|
version_data = self._calculate_version(version, unstable)
|
||||||
chosen = None
|
return self._create_client(version_data, **kwargs)
|
||||||
|
|
||||||
if version:
|
|
||||||
version = _normalize_version_number(version)
|
|
||||||
|
|
||||||
for keystone_version in six.itervalues(versions):
|
|
||||||
# major versions must be the same (e.g. 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.create_client()
|
|
||||||
|
@@ -14,6 +14,7 @@ import httpretty
|
|||||||
import six
|
import six
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
|
|
||||||
|
from keystoneclient import _discover
|
||||||
from keystoneclient import client
|
from keystoneclient import client
|
||||||
from keystoneclient import discover
|
from keystoneclient import discover
|
||||||
from keystoneclient import exceptions
|
from keystoneclient import exceptions
|
||||||
@@ -464,12 +465,7 @@ class ClientDiscoveryTests(utils.TestCase):
|
|||||||
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
self.assertDiscoveryFailure(auth_url=BASE_URL)
|
||||||
|
|
||||||
def test_discovery_ignore_invalid(self):
|
def test_discovery_ignore_invalid(self):
|
||||||
resp = [{'id': '3.99', # without a leading v
|
resp = [{'id': 'v3.0',
|
||||||
'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
|
'links': [1, 2, 3, 4], # invalid links
|
||||||
'media-types': V3_MEDIA_TYPES,
|
'media-types': V3_MEDIA_TYPES,
|
||||||
'status': 'stable',
|
'status': 'stable',
|
||||||
@@ -546,16 +542,237 @@ class ClientDiscoveryTests(utils.TestCase):
|
|||||||
self.assertEqual(client.username, 'foo')
|
self.assertEqual(client.username, 'foo')
|
||||||
self.assertEqual(client.password, 'bar')
|
self.assertEqual(client.password, 'bar')
|
||||||
|
|
||||||
|
def test_available_versions(self):
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||||
|
body=V3_VERSION_ENTRY)
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
|
||||||
|
versions = disc.available_versions()
|
||||||
|
self.assertEqual(1, len(versions))
|
||||||
|
self.assertEqual(V3_VERSION, versions[0])
|
||||||
|
|
||||||
|
def test_unknown_client_version(self):
|
||||||
|
V4_VERSION = {'id': 'v4.0',
|
||||||
|
'links': [{'href': 'http://url', 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED}
|
||||||
|
body = _create_version_list([V4_VERSION, V3_VERSION, V2_VERSION])
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=300, body=body)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
self.assertRaises(exceptions.DiscoveryFailure,
|
||||||
|
disc.create_client, version=4)
|
||||||
|
|
||||||
|
|
||||||
|
@httpretty.activate
|
||||||
|
class DiscoverQueryTests(utils.TestCase):
|
||||||
|
|
||||||
|
def test_available_keystone_data(self):
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=300,
|
||||||
|
body=V3_VERSION_LIST)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
versions = disc.version_data()
|
||||||
|
|
||||||
|
self.assertEqual((2, 0), versions[0]['version'])
|
||||||
|
self.assertEqual('stable', versions[0]['raw_status'])
|
||||||
|
self.assertEqual(V2_URL, versions[0]['url'])
|
||||||
|
self.assertEqual((3, 0), versions[1]['version'])
|
||||||
|
self.assertEqual('stable', versions[1]['raw_status'])
|
||||||
|
self.assertEqual(V3_URL, versions[1]['url'])
|
||||||
|
|
||||||
|
version = disc.data_for('v3.0')
|
||||||
|
self.assertEqual((3, 0), version['version'])
|
||||||
|
self.assertEqual('stable', version['raw_status'])
|
||||||
|
self.assertEqual(V3_URL, version['url'])
|
||||||
|
|
||||||
|
version = disc.data_for(2)
|
||||||
|
self.assertEqual((2, 0), version['version'])
|
||||||
|
self.assertEqual('stable', version['raw_status'])
|
||||||
|
self.assertEqual(V2_URL, version['url'])
|
||||||
|
|
||||||
|
self.assertIsNone(disc.url_for('v4'))
|
||||||
|
self.assertEqual(V3_URL, disc.url_for('v3'))
|
||||||
|
self.assertEqual(V2_URL, disc.url_for('v2'))
|
||||||
|
|
||||||
|
def test_available_cinder_data(self):
|
||||||
|
body = jsonutils.dumps(CINDER_EXAMPLES)
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=300, body=body)
|
||||||
|
|
||||||
|
v1_url = "%sv1/" % BASE_URL
|
||||||
|
v2_url = "%sv2/" % BASE_URL
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
versions = disc.version_data()
|
||||||
|
|
||||||
|
self.assertEqual((1, 0), versions[0]['version'])
|
||||||
|
self.assertEqual('CURRENT', versions[0]['raw_status'])
|
||||||
|
self.assertEqual(v1_url, versions[0]['url'])
|
||||||
|
self.assertEqual((2, 0), versions[1]['version'])
|
||||||
|
self.assertEqual('CURRENT', versions[1]['raw_status'])
|
||||||
|
self.assertEqual(v2_url, versions[1]['url'])
|
||||||
|
|
||||||
|
version = disc.data_for('v2.0')
|
||||||
|
self.assertEqual((2, 0), version['version'])
|
||||||
|
self.assertEqual('CURRENT', version['raw_status'])
|
||||||
|
self.assertEqual(v2_url, version['url'])
|
||||||
|
|
||||||
|
version = disc.data_for(1)
|
||||||
|
self.assertEqual((1, 0), version['version'])
|
||||||
|
self.assertEqual('CURRENT', version['raw_status'])
|
||||||
|
self.assertEqual(v1_url, version['url'])
|
||||||
|
|
||||||
|
self.assertIsNone(disc.url_for('v3'))
|
||||||
|
self.assertEqual(v2_url, disc.url_for('v2'))
|
||||||
|
self.assertEqual(v1_url, disc.url_for('v1'))
|
||||||
|
|
||||||
|
def test_available_glance_data(self):
|
||||||
|
body = jsonutils.dumps(GLANCE_EXAMPLES)
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body)
|
||||||
|
|
||||||
|
v1_url = "%sv1/" % BASE_URL
|
||||||
|
v2_url = "%sv2/" % BASE_URL
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
versions = disc.version_data()
|
||||||
|
|
||||||
|
self.assertEqual((1, 0), versions[0]['version'])
|
||||||
|
self.assertEqual('SUPPORTED', versions[0]['raw_status'])
|
||||||
|
self.assertEqual(v1_url, versions[0]['url'])
|
||||||
|
self.assertEqual((1, 1), versions[1]['version'])
|
||||||
|
self.assertEqual('CURRENT', versions[1]['raw_status'])
|
||||||
|
self.assertEqual(v1_url, versions[1]['url'])
|
||||||
|
self.assertEqual((2, 0), versions[2]['version'])
|
||||||
|
self.assertEqual('SUPPORTED', versions[2]['raw_status'])
|
||||||
|
self.assertEqual(v2_url, versions[2]['url'])
|
||||||
|
self.assertEqual((2, 1), versions[3]['version'])
|
||||||
|
self.assertEqual('SUPPORTED', versions[3]['raw_status'])
|
||||||
|
self.assertEqual(v2_url, versions[3]['url'])
|
||||||
|
self.assertEqual((2, 2), versions[4]['version'])
|
||||||
|
self.assertEqual('CURRENT', versions[4]['raw_status'])
|
||||||
|
self.assertEqual(v2_url, versions[4]['url'])
|
||||||
|
|
||||||
|
for ver in (2, 2.1, 2.2):
|
||||||
|
version = disc.data_for(ver)
|
||||||
|
self.assertEqual((2, 2), version['version'])
|
||||||
|
self.assertEqual('CURRENT', version['raw_status'])
|
||||||
|
self.assertEqual(v2_url, version['url'])
|
||||||
|
self.assertEqual(v2_url, disc.url_for(ver))
|
||||||
|
|
||||||
|
for ver in (1, 1.1):
|
||||||
|
version = disc.data_for(ver)
|
||||||
|
self.assertEqual((1, 1), version['version'])
|
||||||
|
self.assertEqual('CURRENT', version['raw_status'])
|
||||||
|
self.assertEqual(v1_url, version['url'])
|
||||||
|
self.assertEqual(v1_url, disc.url_for(ver))
|
||||||
|
|
||||||
|
self.assertIsNone(disc.url_for('v3'))
|
||||||
|
self.assertIsNone(disc.url_for('v2.3'))
|
||||||
|
|
||||||
|
def test_allow_deprecated(self):
|
||||||
|
status = 'deprecated'
|
||||||
|
version_list = [{'id': 'v3.0',
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': status,
|
||||||
|
'updated': UPDATED}]
|
||||||
|
body = jsonutils.dumps({'versions': version_list})
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
|
||||||
|
# deprecated is allowed by default
|
||||||
|
versions = disc.version_data(allow_deprecated=False)
|
||||||
|
self.assertEqual(0, len(versions))
|
||||||
|
|
||||||
|
versions = disc.version_data(allow_deprecated=True)
|
||||||
|
self.assertEqual(1, len(versions))
|
||||||
|
self.assertEqual(status, versions[0]['raw_status'])
|
||||||
|
self.assertEqual(V3_URL, versions[0]['url'])
|
||||||
|
self.assertEqual((3, 0), versions[0]['version'])
|
||||||
|
|
||||||
|
def test_allow_experimental(self):
|
||||||
|
status = 'experimental'
|
||||||
|
version_list = [{'id': 'v3.0',
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': status,
|
||||||
|
'updated': UPDATED}]
|
||||||
|
body = jsonutils.dumps({'versions': version_list})
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
|
||||||
|
versions = disc.version_data()
|
||||||
|
self.assertEqual(0, len(versions))
|
||||||
|
|
||||||
|
versions = disc.version_data(allow_experimental=True)
|
||||||
|
self.assertEqual(1, len(versions))
|
||||||
|
self.assertEqual(status, versions[0]['raw_status'])
|
||||||
|
self.assertEqual(V3_URL, versions[0]['url'])
|
||||||
|
self.assertEqual((3, 0), versions[0]['version'])
|
||||||
|
|
||||||
|
def test_allow_unknown(self):
|
||||||
|
status = 'abcdef'
|
||||||
|
version_list = [{'id': 'v3.0',
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': status,
|
||||||
|
'updated': UPDATED}]
|
||||||
|
body = jsonutils.dumps({'versions': version_list})
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
|
||||||
|
versions = disc.version_data()
|
||||||
|
self.assertEqual(0, len(versions))
|
||||||
|
|
||||||
|
versions = disc.version_data(allow_unknown=True)
|
||||||
|
self.assertEqual(1, len(versions))
|
||||||
|
self.assertEqual(status, versions[0]['raw_status'])
|
||||||
|
self.assertEqual(V3_URL, versions[0]['url'])
|
||||||
|
self.assertEqual((3, 0), versions[0]['version'])
|
||||||
|
|
||||||
|
def test_ignoring_invalid_lnks(self):
|
||||||
|
version_list = [{'id': 'v3.0',
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED},
|
||||||
|
{'id': 'v3.1',
|
||||||
|
'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED},
|
||||||
|
{'media-types': V3_MEDIA_TYPES,
|
||||||
|
'status': 'stable',
|
||||||
|
'updated': UPDATED,
|
||||||
|
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||||
|
}]
|
||||||
|
|
||||||
|
body = jsonutils.dumps({'versions': version_list})
|
||||||
|
httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body=body)
|
||||||
|
|
||||||
|
disc = discover.Discover(auth_url=BASE_URL)
|
||||||
|
|
||||||
|
# raw_version_data will return all choices, even invalid ones
|
||||||
|
versions = disc.raw_version_data()
|
||||||
|
self.assertEqual(3, len(versions))
|
||||||
|
|
||||||
|
# only the version with both id and links will be actually returned
|
||||||
|
versions = disc.version_data()
|
||||||
|
self.assertEqual(1, len(versions))
|
||||||
|
|
||||||
|
|
||||||
class DiscoverUtils(utils.TestCase):
|
class DiscoverUtils(utils.TestCase):
|
||||||
|
|
||||||
def test_version_number(self):
|
def test_version_number(self):
|
||||||
def assertVersion(inp, out):
|
def assertVersion(inp, out):
|
||||||
self.assertEqual(discover._normalize_version_number(inp), out)
|
self.assertEqual(out, _discover.normalize_version_number(inp))
|
||||||
|
|
||||||
def versionRaises(inp):
|
def versionRaises(inp):
|
||||||
self.assertRaises(TypeError,
|
self.assertRaises(TypeError,
|
||||||
discover._normalize_version_number,
|
_discover.normalize_version_number,
|
||||||
inp)
|
inp)
|
||||||
|
|
||||||
assertVersion('v1.2', (1, 2))
|
assertVersion('v1.2', (1, 2))
|
||||||
@@ -571,23 +788,3 @@ class DiscoverUtils(utils.TestCase):
|
|||||||
versionRaises('hello')
|
versionRaises('hello')
|
||||||
versionRaises('1.a')
|
versionRaises('1.a')
|
||||||
versionRaises('vacuum')
|
versionRaises('vacuum')
|
||||||
|
|
||||||
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'))
|
|
||||||
|
Reference in New Issue
Block a user