Discourage 'version' and accept 'M.latest'

We're discouraging the use of the ambiguous and difficult-to-understand
'version' parameter in new discovery methods, instead encouraging the
use of min_version and max_version.

In order to make it possible to get the same functionality, though, we
need a way to say the same thing as version="M.m", which actually means,
"min version is M.m, and max version is the latest within major version
M".

Introducing 'latest' syntax, which can be used in various ways,
including:

min_version='2.3', max_version='2.latest'

...which is equivalent to the old school version='2.3'

Change-Id: Ife842333e25c33e54bbae4c1adb101014cb8e8db
This commit is contained in:
Eric Fried 2017-07-13 17:44:47 -05:00
parent 218adc333e
commit 699fac136f
6 changed files with 436 additions and 170 deletions

View File

@ -34,6 +34,28 @@ from keystoneauth1 import exceptions
_LOGGER = utils.get_logger(__name__)
LATEST = float('inf')
def _str_or_latest(val):
"""Convert val to a string, handling LATEST => 'latest'.
:param val: An int or the special value LATEST.
:return: A string representation of val. If val was LATEST, the return is
'latest'.
"""
return 'latest' if val == LATEST else str(val)
def _int_or_latest(val):
"""Convert val to an int or the special value LATEST.
:param val: An int()-able, or the string 'latest', or the special value
LATEST.
:return: An int, or the special value LATEST
"""
return LATEST if val == 'latest' or val == LATEST else int(val)
@positional()
def get_version_data(session, url, authenticated=None):
@ -122,17 +144,26 @@ def normalize_version_number(version):
'v1.20.3', '1.20.3', (1, 20, 3), ['1', '20', '3']
The following all produce a return value of (LATEST, LATEST)::
'latest', 'vlatest', ('latest', 'latest'), (LATEST, LATEST)
The following all produce a return value of (2, LATEST)::
'2.latest', 'v2.latest', (2, LATEST), ('2', 'latest')
:param version: A version specifier in any of the following forms:
String, possibly prefixed with 'v', containing one or more numbers
separated by periods. Examples: 'v1', 'v1.2', '1.2.3', '123'
*or* the string 'latest', separated by periods. Examples: 'v1',
'v1.2', '1.2.3', '123', 'latest', '1.latest', 'v1.latest'.
Integer. This will be assumed to be the major version, with a minor
version of 0.
Float. The integer part is assumed to be the major version; the
decimal part the minor version.
Non-string iterable comprising integers or integer strings.
Examples: (1,), [1, 2], ('12', '34', '56')
:return: A tuple of integers of len >= 2.
:rtype: tuple(int)
Non-string iterable comprising integers, integer strings, the string
'latest', or the special value LATEST.
Examples: (1,), [1, 2], ('12', '34', '56'), (LATEST,), (2, 'latest')
:return: A tuple of len >= 2 comprising integers and/or LATEST.
:raises TypeError: If the input version cannot be interpreted.
"""
# Copy the input var so the error presents the original value
@ -142,7 +173,7 @@ def normalize_version_number(version):
# processing. This ensures at least 1 decimal point if e.g. [1] is given.
if not isinstance(ver, six.string_types):
try:
ver = '.'.join(map(str, ver))
ver = '.'.join(map(_str_or_latest, ver))
except TypeError:
# Not an iterable
pass
@ -164,7 +195,7 @@ def normalize_version_number(version):
# If it's an int or float, turn it into a float string
elif isinstance(ver, (int, float)):
ver = str(float(ver))
ver = _str_or_latest(float(ver))
# At this point, we should either have a string that contains numbers with
# at least one decimal point, or something decidedly else.
@ -176,9 +207,13 @@ def normalize_version_number(version):
# Not a string
pass
# Handle special case variants of just 'latest'
if ver == 'latest' or tuple(ver) == ('latest',):
return LATEST, LATEST
# It's either an interable, or something else that makes us sad.
try:
return tuple(map(int, ver))
return tuple(map(_int_or_latest, ver))
except (TypeError, ValueError):
pass
@ -190,58 +225,97 @@ def _normalize_version_args(version, min_version, max_version):
raise ValueError(
"version is mutually exclusive with min_version and max_version")
if min_version == 'latest' and max_version not in (None, 'latest'):
raise ValueError(
"min_version is 'latest' and max_version is {max_version}"
" but is only allowed to be 'latest' or None".format(
max_version=max_version))
if version:
# Explode this into min_version and max_version
min_version = normalize_version_number(version)
max_version = (min_version[0], LATEST)
return min_version, max_version
if version and version != 'latest':
version = normalize_version_number(version)
if min_version == 'latest':
if max_version not in (None, 'latest'):
raise ValueError(
"min_version is 'latest' and max_version is {max_version}"
" but is only allowed to be 'latest' or None".format(
max_version=max_version))
max_version = 'latest'
# Normalize e.g. empty string to None
min_version = min_version or None
max_version = max_version or None
if min_version:
if min_version == 'latest':
min_version = None
max_version = 'latest'
else:
min_version = normalize_version_number(min_version)
min_version = normalize_version_number(min_version)
# If min_version was specified but max_version was not, max is latest.
max_version = normalize_version_number(max_version or 'latest')
if max_version and max_version != 'latest':
# NOTE(efried): We should be doing this instead:
# max_version = normalize_version_number(max_version or 'latest')
# However, see first NOTE(jamielennox) in EndpointData._set_version_info.
if max_version:
max_version = normalize_version_number(max_version)
return version, min_version, max_version
if None not in (min_version, max_version) and max_version < min_version:
raise ValueError("min_version cannot be greater than max_version")
return min_version, max_version
def version_to_string(version):
"""Turn a version tuple into a string.
:param tuple(int) version: A version represented as a tuple of ints.
:param tuple version: A version represented as a tuple of ints. As a
special case, a tuple member may be LATEST, which
translates to 'latest'.
:return: A version represented as a period-delimited string.
"""
return ".".join(map(str, version))
# Special case
if all(ver == LATEST for ver in version):
return 'latest'
return ".".join(map(_str_or_latest, version))
def version_between(min_version, max_version, candidate):
"""Determine whether a candidate version is within a specified range.
:param min_version: Normalized lower bound. May be None. May be
(LATEST, LATEST).
:param max_version: Normalized upper bound. May be None. May be
(LATEST, LATEST).
:param candidate: Normalized candidate version to test. May not be None.
:return: True if candidate is between min_version and max_version; False
otherwise.
:raises ValueError: If candidate is None or the input is not properly
normalized.
"""
def is_normalized(ver):
return normalize_version_number(ver) == ver
# A version can't be between a range that doesn't exist
if not min_version and not max_version:
return False
if candidate is None:
raise ValueError("candidate cannot be None.")
if min_version is not None and not is_normalized(min_version):
raise ValueError("min_version is not normalized.")
if max_version is not None and not is_normalized(max_version):
raise ValueError("max_version is not normalized.")
if not is_normalized(candidate):
raise ValueError("candidate is not normalized.")
# This is only possible if args weren't run through _normalize_version_args
if max_version is None and min_version is not None:
raise ValueError("Can't use None as an upper bound.")
# If the candidate is less than the min_version, it's
# not a match.
if min_version:
min_version = normalize_version_number(min_version)
if candidate < min_version:
return False
# Lack of max_version implies latest.
if max_version == 'latest' or not max_version:
return True
max_version = normalize_version_number(max_version)
if version_match(max_version, candidate):
return True
if max_version < candidate:
# not a match. None works here.
if min_version is not None and candidate < min_version:
return False
if max_version is not None and candidate > max_version:
return False
return True
@ -271,6 +345,24 @@ def version_match(required, candidate):
return True
def _latest_soft_match(required, candidate):
if not required:
return False
if LATEST not in required:
return False
if all(part == LATEST for part in required):
return True
if required[0] == candidate[0] and required[1] == LATEST:
return True
# TODO(efried): Do we need to handle >2-part version numbers here?
return False
def _combine_relative_url(discovery_url, version_url):
# NOTE(jamielennox): urllib.parse.urljoin allows the url to be relative
# or even protocol-less. The additional trailing '/' makes urljoin respect
@ -446,6 +538,10 @@ class Discover(object):
version = normalize_version_number(version)
for data in self.version_data(reverse=True, **kwargs):
# Since the data is reversed, the latest version is first. If
# latest was requested, return it.
if _latest_soft_match(version, data['version']):
return data
if version_match(version, data['version']):
return data
@ -468,43 +564,39 @@ class Discover(object):
data = self.data_for(version, **kwargs)
return data['url'] if data else None
def versioned_data_for(self, version=None, url=None,
def versioned_data_for(self, url=None,
min_version=None, max_version=None,
**kwargs):
"""Return endpoint data for the service at a url.
version, min_version and max_version can all be given either as a
string or a tuple.
min_version and max_version can be given either as strings or tuples.
:param version: The version is the minimum version in the
same major release as there should be no compatibility issues with
using a version newer than the one asked for. If version is not
given, the highest available version will be matched.
:param string url: If url is given, the data will be returned for the
endpoint data that has a self link matching the url.
:param min_version: The minimum version that is acceptable. Mutually
exclusive with version. If min_version is given with no max_version
it is as if max version is 'latest'. If min_version is 'latest',
max_version may only be 'latest' or None.
:param max_version: The maximum version that is acceptable. Mutually
exclusive with version. If min_version is given with no max_version
it is as if max version is 'latest'. If min_version is 'latest',
max_version may only be 'latest' or None.
:param min_version: The minimum endpoint version that is acceptable. If
min_version is given with no max_version it is as if max version is
'latest'. If min_version is 'latest', max_version may only be
'latest' or None.
:param max_version: The maximum endpoint version that is acceptable. If
min_version is given with no max_version it is as if max version is
'latest'. If min_version is 'latest', max_version may only be
'latest' or None.
:returns: the endpoint data for a URL that matches the required version
(the format is described in version_data) or None if no
match.
:rtype: dict
"""
version, min_version, max_version = _normalize_version_args(
version, min_version, max_version)
no_version = not version and not max_version and not min_version
min_version, max_version = _normalize_version_args(
None, min_version, max_version)
no_version = not max_version and not min_version
version_data = self.version_data(reverse=True, **kwargs)
# If we don't have to check a min_version, we can short
# circuit anything else
if 'latest' in (version, max_version) and not min_version:
if (max_version == (LATEST, LATEST) and
(not min_version or min_version == (LATEST, LATEST))):
# because we reverse we can just take the first entry
return version_data[0]
@ -520,7 +612,7 @@ class Discover(object):
for data in version_data:
if url and data['url'] and data['url'].rstrip('/') + '/' == url:
return data
if version and version_match(version, data['version']):
if _latest_soft_match(min_version, data['version']):
return data
if version_between(min_version, max_version, data['version']):
return data
@ -537,27 +629,22 @@ class Discover(object):
# We couldn't find a match.
return None
def versioned_url_for(self, version=None,
min_version=None, max_version=None, **kwargs):
def versioned_url_for(self, min_version=None, max_version=None, **kwargs):
"""Get the endpoint url for a version.
version, min_version and max_version can all be given either as a
string or a tuple.
min_version and max_version can be given either as strings or tuples.
:param version: The version is always a minimum version in the
same major release as there should be no compatibility issues with
using a version newer than the one asked for.
:param min_version: The minimum version that is acceptable. Mutually
exclusive with version. If min_version is given with no max_version
it is as if max version is 'latest'.
:param max_version: The maximum version that is acceptable. Mutually
exclusive with version. If min_version is given with no max_version
it is as if max version is 'latest'.
:param min_version: The minimum version that is acceptable. If
min_version is given with no max_version it is as if max version
is 'latest'.
:param max_version: The maximum version that is acceptable. If
min_version is given with no max_version it is as if max version is
'latest'.
:returns: The url for the specified version or None if no match.
:rtype: str
"""
data = self.versioned_data_for(version, min_version=min_version,
data = self.versioned_data_for(min_version=min_version,
max_version=max_version, **kwargs)
return data['url'] if data else None
@ -639,22 +726,19 @@ class EndpointData(object):
return self.service_url or self.catalog_url
@positional(3)
def get_versioned_data(self, session, version=None,
allow=None, cache=None, allow_version_hack=True,
project_id=None, discover_versions=True,
def get_versioned_data(self, session, allow=None, cache=None,
allow_version_hack=True, project_id=None,
discover_versions=True,
min_version=None, max_version=None):
"""Run version discovery for the service described.
Performs Version Discovery and returns a new EndpointData object with
information found.
version, min_version and max_version can all be given either as a
string or a tuple.
min_version and max_version can be given either as strings or tuples.
:param session: A session object that can be used for communication.
:type session: keystoneauth1.session.Session
:param version: The minimum major version required for this endpoint.
Mutually exclusive with min_version and max_version.
:param dict allow: Extra filters to pass when discovering API
versions. (optional)
:param dict cache: A dict to be used for caching results in
@ -671,14 +755,12 @@ class EndpointData(object):
if it's not neccessary to fulfill the
major version request. (optional,
defaults to True)
:param min_version: The minimum version that is acceptable. Mutually
exclusive with version. If min_version is given
with no max_version it is as if max version is
'latest'.
:param max_version: The maximum version that is acceptable. Mutually
exclusive with version. If min_version is given
with no max_version it is as if max version is
'latest'.
:param min_version: The minimum version that is acceptable. If
min_version is given with no max_version it is as
if max version is 'latest'.
:param max_version: The maximum version that is acceptable. If
min_version is given with no max_version it is as
if max version is 'latest'.
:returns: A new EndpointData with the requested versioned data.
:rtype: :py:class:`keystoneauth1.discover.EndpointData`
@ -686,8 +768,8 @@ class EndpointData(object):
appropriate versioned data
could not be discovered.
"""
version, min_version, max_version = _normalize_version_args(
version, min_version, max_version)
min_version, max_version = _normalize_version_args(
None, min_version, max_version)
if not allow:
allow = {}
@ -696,19 +778,19 @@ class EndpointData(object):
new_data = copy.copy(self)
new_data._set_version_info(
session=session, version=version, allow=allow, cache=cache,
session=session, allow=allow, cache=cache,
allow_version_hack=allow_version_hack, project_id=project_id,
discover_versions=discover_versions, min_version=min_version,
max_version=max_version)
return new_data
def _set_version_info(self, session, version, allow=None, cache=None,
def _set_version_info(self, session, allow=None, cache=None,
allow_version_hack=True, project_id=None,
discover_versions=False,
min_version=None, max_version=None):
match_url = None
no_version = not version and not max_version and not min_version
no_version = not max_version and not min_version
if no_version and not discover_versions:
# NOTE(jamielennox): This may not be the best thing to default to
# but is here for backwards compatibility. It may be worth
@ -727,27 +809,22 @@ class EndpointData(object):
# satisfy the request without further work
if self._disc:
discovered_data = self._disc.versioned_data_for(
version, min_version=min_version, max_version=max_version,
min_version=min_version, max_version=max_version,
url=match_url, **allow)
if not discovered_data:
self._run_discovery(
session=session, cache=cache,
version=version, min_version=min_version,
max_version=max_version, project_id=project_id,
allow_version_hack=allow_version_hack,
min_version=min_version, max_version=max_version,
project_id=project_id, allow_version_hack=allow_version_hack,
discover_versions=discover_versions)
if not self._disc:
return
discovered_data = self._disc.versioned_data_for(
version, min_version=min_version, max_version=max_version,
min_version=min_version, max_version=max_version,
url=match_url, **allow)
if not discovered_data:
if version:
raise exceptions.DiscoveryFailure(
"Version {version} requested, but was not found".format(
version=version_to_string(version)))
elif min_version and not max_version:
if min_version and not max_version:
raise exceptions.DiscoveryFailure(
"Minimum version {min_version} was not found".format(
min_version=version_to_string(min_version)))
@ -787,13 +864,12 @@ class EndpointData(object):
self.service_url = url
@positional(1)
def _run_discovery(self, session, cache, version, min_version,
max_version, project_id,
allow_version_hack, discover_versions):
def _run_discovery(self, session, cache, min_version, max_version,
project_id, allow_version_hack, discover_versions):
tried = set()
for vers_url in self._get_discovery_url_choices(
version=version, project_id=project_id,
project_id=project_id,
allow_version_hack=allow_version_hack,
min_version=min_version,
max_version=max_version):
@ -850,12 +926,12 @@ class EndpointData(object):
" found and allow_version_hack was False")
def _get_discovery_url_choices(
self, version=None, project_id=None, allow_version_hack=True,
self, project_id=None, allow_version_hack=True,
min_version=None, max_version=None):
"""Find potential locations for version discovery URLs.
version, min_version and max_version are already normalized, so will
either be None, 'latest' or a tuple.
min_version and max_version are already normalized, so will either be
None or a tuple.
"""
url = urllib.parse.urlparse(self.url)
url_parts = url.path.split('/')
@ -891,14 +967,12 @@ class EndpointData(object):
except TypeError:
pass
else:
is_between = version_between(
min_version, max_version, url_version)
exact_match = (version and version != 'latest' and
version_match(version, url_version))
is_between = version_between(min_version, max_version, url_version)
exact_match = (is_between and max_version and
max_version[0] == url_version[0])
high_match = (is_between and max_version and
max_version != 'latest' and
max_version[1] != LATEST and
version_match(max_version, url_version))
if exact_match or is_between:
self._catalog_matches_version = True
self._catalog_matches_exactly = exact_match

View File

@ -157,12 +157,10 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
return False
def get_endpoint_data(self, session, service_type=None, interface=None,
region_name=None, service_name=None, version=None,
allow={}, allow_version_hack=True,
discover_versions=True, skip_discovery=False,
min_version=None, max_version=None,
endpoint_override=None,
**kwargs):
region_name=None, service_name=None, allow={},
allow_version_hack=True, discover_versions=True,
skip_discovery=False, min_version=None,
max_version=None, endpoint_override=None, **kwargs):
"""Return a valid endpoint data for a service.
If a valid token is not present then a new one will be fetched using
@ -190,8 +188,6 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
(optional)
:param string service_name: The name of the service in the catalog.
(optional)
:param version: The minimum version number required for this
endpoint. (optional)
:param dict allow: Extra filters to pass when discovering API
versions. (optional)
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
@ -220,6 +216,7 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
but version discovery will be run.
Sets allow_version_hack to False
(optional)
:param kwargs: Ignored.
:raises keystoneauth1.exceptions.http.HttpError: An error from an
invalid HTTP response.
@ -227,6 +224,9 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
:return: Valid EndpointData or None if not available.
:rtype: `keystoneauth1.discover.EndpointData` or None
"""
min_version, max_version = discover._normalize_version_args(
None, min_version, max_version)
# NOTE(jamielennox): if you specifically ask for requests to be sent to
# the auth url then we can ignore many of the checks. Typically if you
# are asking for the auth endpoint it means that there is no catalog to
@ -283,7 +283,7 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
try:
return endpoint_data.get_versioned_data(
session, version,
session,
project_id=project_id,
min_version=min_version,
max_version=max_version,
@ -295,7 +295,7 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
exceptions.ConnectionError):
# If a version was requested, we didn't find it, return
# None.
if version or max_version or min_version:
if max_version or min_version:
return None
# If one wasn't, then the endpoint_data we already have
# should be fine
@ -361,6 +361,10 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
:return: A valid endpoint URL or None if not available.
:rtype: string or None
"""
# Explode `version` into min_version and max_version - everything below
# here uses the latter rather than the former.
min_version, max_version = discover._normalize_version_args(
version, min_version, max_version)
# Set discover_versions to False since we're only going to return
# a URL. Fetching the microversion data would be needlessly
# expensive in the common case. However, discover_versions=False
@ -369,11 +373,8 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
endpoint_data = self.get_endpoint_data(
session, service_type=service_type, interface=interface,
region_name=region_name, service_name=service_name,
version=version, allow=allow,
min_version=min_version,
max_version=max_version,
discover_versions=False,
skip_discovery=skip_discovery,
allow=allow, min_version=min_version, max_version=max_version,
discover_versions=False, skip_discovery=skip_discovery,
allow_version_hack=allow_version_hack, **kwargs)
return endpoint_data.url if endpoint_data else None

View File

@ -434,6 +434,11 @@ class Session(object):
# TODO(mordred) Validate that the requested microversion works
# with the microversion range we found in discovery.
microversion = discover.normalize_version_number(microversion)
# Can't specify a M.latest microversion
if (microversion[0] != discover.LATEST and
discover.LATEST in microversion[1:]):
raise TypeError(
"Specifying a '{major}.latest' microversion is not allowed.")
microversion = discover.version_to_string(microversion)
if not service_type:
if endpoint_filter and 'service_type' in endpoint_filter:
@ -942,6 +947,7 @@ class Session(object):
return kwargs['endpoint_override']
auth = self._auth_required(auth, 'determine endpoint URL')
return auth.get_endpoint(self, **kwargs)
def get_endpoint_data(self, auth=None, **kwargs):

View File

@ -535,11 +535,13 @@ class CommonIdentityTests(object):
data_v2 = a.get_endpoint_data(session=s,
service_type='compute',
interface='admin',
version=(2, 0))
min_version=(2, 0),
max_version=(2, discover.LATEST))
data_v3 = a.get_endpoint_data(session=s,
service_type='compute',
interface='admin',
version=(3, 0))
min_version=(3, 0),
max_version=(3, discover.LATEST))
self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v2.0', data_v2.url)
self.assertEqual(self.TEST_COMPUTE_ADMIN + '/v3', data_v3.url)
@ -566,15 +568,26 @@ class CommonIdentityTests(object):
interface='admin')
self.assertEqual(v3_compute, data.url)
v2_data = data.get_versioned_data(s, version='2.0')
v2_data = data.get_versioned_data(s, min_version='2.0',
max_version='2.latest')
self.assertEqual(v2_compute, v2_data.url)
self.assertEqual(v2_compute, v2_data.service_url)
self.assertEqual(self.TEST_COMPUTE_ADMIN, v2_data.catalog_url)
v3_data = data.get_versioned_data(s, version='3.0')
self.assertEqual(v3_compute, v3_data.url)
self.assertEqual(v3_compute, v3_data.service_url)
self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url)
# Variants that all return v3 data
for vkwargs in (dict(min_version='3.0', max_version='3.latest'),
# min/max spans major versions
dict(min_version='2.0', max_version='3.latest'),
# latest major max
dict(min_version='2.0', max_version='latest'),
# implicit max
dict(min_version='2.0'),
# implicit min/max
dict()):
v3_data = data.get_versioned_data(s, **vkwargs)
self.assertEqual(v3_compute, v3_data.url)
self.assertEqual(v3_compute, v3_data.service_url)
self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url)
def test_interface_list(self):
@ -637,7 +650,8 @@ class CommonIdentityTests(object):
data.url)
v3_data = data.get_versioned_data(
s, version='3.0', project_id=self.project_id)
s, min_version='3.0', max_version='3.latest',
project_id=self.project_id)
self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public,
v3_data.url)
@ -651,7 +665,8 @@ class CommonIdentityTests(object):
# find the unversioned endpoint
self.requests_mock.get(self.TEST_VOLUME.unversioned.public, resps)
v2_data = data.get_versioned_data(
s, version='2.0', project_id=self.project_id)
s, min_version='2.0', max_version='2.latest',
project_id=self.project_id)
# Even though we never requested volumev2 from the catalog, we should
# wind up re-constructing it via version discovery and re-appending
@ -700,7 +715,8 @@ class CommonIdentityTests(object):
# it should fetch the unversioned endpoint
v2_data = s.get_endpoint_data(service_type='volumev3',
interface='public',
version='2.0',
min_version='2.0',
max_version='2.latest',
project_id=self.project_id)
# Even though we never requested volumev2 from the catalog, we should
@ -718,7 +734,8 @@ class CommonIdentityTests(object):
# request for v2, we should have all the relevant data cached in the
# discovery object - and should not fetch anything new.
v3_data = v2_data.get_versioned_data(
s, version='3.0', project_id=self.project_id)
s, min_version='3.0', max_version='3.latest',
project_id=self.project_id)
self.assertEqual(self.TEST_VOLUME.versions['v3'].service.public,
v3_data.url)
@ -1104,7 +1121,8 @@ class CatalogHackTests(utils.TestCase):
data = sess.get_endpoint_data(endpoint_override=self.OTHER_URL,
service_type=self.IDENTITY,
interface='public',
version=(2, 0))
min_version=(2, 0),
max_version=(2, discover.LATEST))
self.assertTrue(common_m.called)
self.assertEqual(self.OTHER_URL, data.url)
@ -1308,9 +1326,8 @@ class CatalogHackTests(utils.TestCase):
endpoint = sess.get_endpoint(service_type=self.IDENTITY,
min_version='1', max_version='2')
# We should make one more calls
# TODO(mordred) optimize this - we can peek in the cache
self.assertTrue(v2_m.called)
# We should make no calls - we peek in the cache
self.assertFalse(v2_m.called)
self.assertFalse(common_m.called)
# And get the v2 url

View File

@ -303,13 +303,114 @@ class DiscoverUtils(utils.TestCase):
assertVersion([1, 40], (1, 40))
assertVersion((1,), (1, 0))
assertVersion(['1'], (1, 0))
assertVersion('latest', (discover.LATEST, discover.LATEST))
assertVersion(['latest'], (discover.LATEST, discover.LATEST))
assertVersion(discover.LATEST, (discover.LATEST, discover.LATEST))
assertVersion((discover.LATEST,), (discover.LATEST, discover.LATEST))
assertVersion('10.latest', (10, discover.LATEST))
assertVersion((10, 'latest'), (10, discover.LATEST))
assertVersion((10, discover.LATEST), (10, discover.LATEST))
versionRaises(None)
versionRaises('hello')
versionRaises('1.a')
versionRaises('vacuum')
versionRaises('')
versionRaises(('1', 'a'))
def test_version_args(self):
"""Validate _normalize_version_args."""
def assert_min_max(in_ver, in_min, in_max, out_min, out_max):
self.assertEqual(
(out_min, out_max),
discover._normalize_version_args(in_ver, in_min, in_max))
def normalize_raises(ver, min, max):
self.assertRaises(ValueError,
discover._normalize_version_args, ver, min, max)
assert_min_max(None, None, None,
None, None)
assert_min_max(None, None, 'v1.2',
None, (1, 2))
assert_min_max(None, 'v1.2', 'latest',
(1, 2), (discover.LATEST, discover.LATEST))
assert_min_max(None, 'v1.2', '1.6',
(1, 2), (1, 6))
assert_min_max(None, 'v1.2', '1.latest',
(1, 2), (1, discover.LATEST))
assert_min_max(None, 'latest', 'latest',
(discover.LATEST, discover.LATEST),
(discover.LATEST, discover.LATEST))
assert_min_max(None, 'latest', None,
(discover.LATEST, discover.LATEST),
(discover.LATEST, discover.LATEST))
assert_min_max(None, (1, 2), None,
(1, 2), (discover.LATEST, discover.LATEST))
assert_min_max('', ('1', '2'), (1, 6),
(1, 2), (1, 6))
assert_min_max(None, ('1', '2'), (1, discover.LATEST),
(1, 2), (1, discover.LATEST))
assert_min_max('v1.2', '', None,
(1, 2), (1, discover.LATEST))
assert_min_max('1.latest', None, '',
(1, discover.LATEST), (1, discover.LATEST))
assert_min_max('v1', None, None,
(1, 0), (1, discover.LATEST))
assert_min_max('latest', None, None,
(discover.LATEST, discover.LATEST),
(discover.LATEST, discover.LATEST))
normalize_raises('v1', 'v2', None)
normalize_raises('v1', None, 'v2')
normalize_raises(None, 'latest', 'v1')
normalize_raises(None, 'v1.2', 'v1.1')
normalize_raises(None, 'v1.2', 1)
def test_version_to_string(self):
def assert_string(inp, out):
self.assertEqual(out, discover.version_to_string(inp))
assert_string((discover.LATEST,), 'latest')
assert_string((discover.LATEST, discover.LATEST), 'latest')
assert_string((discover.LATEST, discover.LATEST, discover.LATEST),
'latest')
assert_string((1,), '1')
assert_string((1, 2), '1.2')
assert_string((1, discover.LATEST), '1.latest')
def test_version_between(self):
def good(minver, maxver, cand):
self.assertTrue(discover.version_between(minver, maxver, cand))
def bad(minver, maxver, cand):
self.assertFalse(discover.version_between(minver, maxver, cand))
def exc(minver, maxver, cand):
self.assertRaises(ValueError,
discover.version_between, minver, maxver, cand)
good((1, 0), (1, 0), (1, 0))
good((1, 0), (1, 10), (1, 2))
good(None, (1, 10), (1, 2))
good((1, 20), (2, 0), (1, 21))
good((1, 0), (2, discover.LATEST), (1, 21))
good((1, 0), (2, discover.LATEST), (1, discover.LATEST))
good((1, 50), (2, discover.LATEST), (2, discover.LATEST))
bad((discover.LATEST, discover.LATEST),
(discover.LATEST, discover.LATEST), (1, 0))
bad(None, None, (1, 0))
bad((1, 50), (2, discover.LATEST), (3, 0))
bad((1, 50), (2, discover.LATEST), (3, discover.LATEST))
bad((1, 50), (2, 5), (2, discover.LATEST))
exc((1, 0), (1, 0), None)
exc('v1.0', (1, 0), (1, 0))
exc((1, 0), 'v1.0', (1, 0))
exc((1, 0), (1, 0), 'v1.0')
exc((1, 0), None, (1, 0))
class VersionDataTests(utils.TestCase):
@ -476,7 +577,7 @@ class VersionDataTests(utils.TestCase):
disc = discover.Discover(self.session, V3_URL)
data = disc.versioned_data_for(version=None)
data = disc.versioned_data_for()
self.assertEqual(data['version'], (3, 0))
self.assertEqual(data['raw_status'], 'stable')
self.assertEqual(data['url'], V3_URL)
@ -508,21 +609,43 @@ class VersionDataTests(utils.TestCase):
self.assertIn(v['version'], ((2, 0), (3, 0)))
self.assertEqual(v['raw_status'], 'stable')
for meth in (disc.data_for, disc.versioned_data_for):
version = meth('v3.0')
for version in (disc.data_for('v3.0'),
disc.data_for('3.latest'),
disc.data_for('latest'),
disc.versioned_data_for(
min_version='v3.0', max_version='v3.latest'),
disc.versioned_data_for(min_version='3'),
disc.versioned_data_for(min_version='3.latest'),
disc.versioned_data_for(min_version='latest'),
disc.versioned_data_for(min_version='3.latest',
max_version='latest'),
disc.versioned_data_for(min_version='latest',
max_version='latest'),
disc.versioned_data_for(min_version=2),
disc.versioned_data_for(min_version='2.latest')):
self.assertEqual((3, 0), version['version'])
self.assertEqual('stable', version['raw_status'])
self.assertEqual(V3_URL, version['url'])
version = meth(2)
for version in (disc.data_for(2),
disc.data_for('2.latest'),
disc.versioned_data_for(
min_version=2, max_version=(2, discover.LATEST)),
disc.versioned_data_for(
min_version='2.latest', max_version='2.latest')):
self.assertEqual((2, 0), version['version'])
self.assertEqual('stable', version['raw_status'])
self.assertEqual(V2_URL, version['url'])
for meth in (disc.url_for, disc.versioned_url_for):
self.assertIsNone(meth('v4'))
self.assertEqual(V3_URL, meth('v3'))
self.assertEqual(V2_URL, meth('v2'))
self.assertIsNone(disc.url_for('v4'))
self.assertIsNone(disc.versioned_url_for(
min_version='v4', max_version='v4.latest'))
self.assertEqual(V3_URL, disc.url_for('v3'))
self.assertEqual(V3_URL, disc.versioned_url_for(
min_version='v3', max_version='v3.latest'))
self.assertEqual(V2_URL, disc.url_for('v2'))
self.assertEqual(V2_URL, disc.versioned_url_for(
min_version='v2', max_version='v2.latest'))
self.assertTrue(mock.called_once)
@ -585,22 +708,33 @@ class VersionDataTests(utils.TestCase):
},
])
for meth in (disc.data_for, disc.versioned_data_for):
version = meth('v2.0')
for version in (disc.data_for('v2.0'),
disc.versioned_data_for(min_version='v2.0',
max_version='v2.latest')):
self.assertEqual((2, 0), version['version'])
self.assertEqual('CURRENT', version['raw_status'])
self.assertEqual(v2_url, version['url'])
version = meth(1)
for version in (disc.data_for(1),
disc.versioned_data_for(
min_version=(1,),
max_version=(1, discover.LATEST))):
self.assertEqual((1, 0), version['version'])
self.assertEqual('CURRENT', version['raw_status'])
self.assertEqual(v1_url, version['url'])
for meth in (disc.url_for, disc.versioned_url_for):
self.assertIsNone(meth('v4'))
self.assertEqual(v3_url, meth('v3'))
self.assertEqual(v2_url, meth('v2'))
self.assertEqual(v1_url, meth('v1'))
self.assertIsNone(disc.url_for('v4'))
self.assertIsNone(disc.versioned_url_for(min_version='v4',
max_version='v4.latest'))
self.assertEqual(v3_url, disc.url_for('v3'))
self.assertEqual(v3_url, disc.versioned_url_for(
min_version='v3', max_version='v3.latest'))
self.assertEqual(v2_url, disc.url_for('v2'))
self.assertEqual(v2_url, disc.versioned_url_for(
min_version='v2', max_version='v2.latest'))
self.assertEqual(v1_url, disc.url_for('v1'))
self.assertEqual(v1_url, disc.versioned_url_for(
min_version='v1', max_version='v1.latest'))
self.assertTrue(mock.called_once)
@ -679,24 +813,32 @@ class VersionDataTests(utils.TestCase):
},
])
for meth in (disc.data_for, disc.versioned_data_for):
for ver in (2, 2.1, 2.2):
version = meth(ver)
for ver in (2, 2.1, 2.2):
for version in (disc.data_for(ver),
disc.versioned_data_for(
min_version=ver,
max_version=(2, discover.LATEST))):
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 = meth(ver)
for ver in (1, 1.1):
for version in (disc.data_for(ver),
disc.versioned_data_for(
min_version=ver,
max_version=(1, discover.LATEST))):
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))
for meth in (disc.url_for, disc.versioned_url_for):
self.assertIsNone(meth('v3'))
self.assertIsNone(meth('v2.3'))
self.assertIsNone(disc.url_for('v3'))
self.assertIsNone(disc.versioned_url_for(min_version='v3',
max_version='v3.latest'))
self.assertIsNone(disc.url_for('v2.3'))
self.assertIsNone(disc.versioned_url_for(min_version='v2.3',
max_version='v2.latest'))
self.assertTrue(mock.called_once)
@ -799,7 +941,7 @@ class EndpointDataTests(utils.TestCase):
mock_url_choices.return_value = ('url1', 'url2', 'url1', 'url3')
epd = discover.EndpointData()
epd._run_discovery(
session='sess', cache='cache', version='vers', min_version='min',
session='sess', cache='cache', min_version='min',
max_version='max', project_id='projid',
allow_version_hack='allow_hack', discover_versions='disc_vers')
# Only one call with 'url1'

View File

@ -23,6 +23,7 @@ import six
from testtools import matchers
from keystoneauth1 import adapter
from keystoneauth1 import discover
from keystoneauth1 import exceptions
from keystoneauth1 import plugin
from keystoneauth1 import session as client_session
@ -123,6 +124,22 @@ class SessionTests(utils.TestCase):
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30')
self.assertEqual(len(headers.keys()), 2)
# 'latest' (string) microversion
headers = {}
client_session.Session._set_microversion_headers(
headers, 'latest', 'compute', None)
self.assertEqual(headers['OpenStack-API-Version'], 'compute latest')
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], 'latest')
self.assertEqual(len(headers.keys()), 2)
# LATEST (tuple) microversion
headers = {}
client_session.Session._set_microversion_headers(
headers, (discover.LATEST, discover.LATEST), 'compute', None)
self.assertEqual(headers['OpenStack-API-Version'], 'compute latest')
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], 'latest')
self.assertEqual(len(headers.keys()), 2)
# ironic microversion, specified service type
headers = {}
client_session.Session._set_microversion_headers(
@ -154,10 +171,19 @@ class SessionTests(utils.TestCase):
self.assertEqual(headers['OpenStack-API-Version'], 'compute 2.30')
self.assertEqual(headers['X-OpenStack-Nova-API-Version'], '2.30')
# Can't specify a 'M.latest' microversion
self.assertRaises(TypeError,
client_session.Session._set_microversion_headers,
{}, '2.latest', 'service_type', None)
self.assertRaises(TypeError,
client_session.Session._set_microversion_headers,
{}, (2, discover.LATEST), 'service_type', None)
# Normalization error
self.assertRaises(TypeError,
client_session.Session._set_microversion_headers,
{}, 'bogus', 'service_type', None)
# No service type in param or endpoint filter
self.assertRaises(TypeError,
client_session.Session._set_microversion_headers,