Add url manipulation and microversion collection

From the API-WG spec, there are two common patterns for service URLs
that can be interpreted. Trailing project_id and a string that starts
with v. If the project_id is in the URL, it needs to be removed before
discovery can happen, but it needs to be put back on to the url found
via discovery. If the endpointin the catalog has a version, and it
matches the version we're asking for, then we don't need to go hunting
for the unversioned doc.

Also, in the EndpointData we're collecting, we want to grab microversion
info, since we're already there in the discovery doc.

There is one behavior change that can be seen in the tests. If the
attempt at an unversioned discovery endpoint fails, we fall back to the
url from the catalog ... but we attempt to get a discovery document from
it because we need the metadata for microversions. The catalog URL should be
returned as the endpoint even if the second discovery call attempt
succeeds, so the user-facing interface is the same - there will just be,
in some cases, an additional URL fetch behind the scenes.

Change-Id: I2a036d65e4f7dba6f50daf6a0ce4589ee59ae95f
This commit is contained in:
Monty Taylor 2017-05-29 16:50:50 -05:00
parent e89e354335
commit a4066a86b5
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
6 changed files with 518 additions and 57 deletions

View File

@ -25,6 +25,7 @@ import copy
import re
from positional import positional
import six
from six.moves import urllib
from keystoneauth1 import _utils as utils
@ -74,31 +75,39 @@ def get_version_data(session, url, authenticated=None):
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 float as a string, don't do that, the split/map below
# will do what we want. (Otherwise, we wind up with 3.20 -> (3, 2)
if isinstance(version, six.string_types):
# trim the v from a 'v2.0' or similar
version = version.lstrip('v')
try:
# If version is a pure int, like '1' or '200' this will produce
# a stringified version with a .0 added. If it's any other number,
# such as '1.1' - int(version) raises an Exception
version = str(float(int(version)))
except ValueError:
pass
# if it's a string (or an integer) from above break it on .
try:
return tuple(map(int, version.split('.')))
except Exception:
pass
# If it's an int, turn it into a float
elif isinstance(version, int):
version = str(float(version))
# last attempt, maybe it's a list or iterable.
elif isinstance(version, float):
version = str(version)
# At this point, we should either have a string that contains a number
# or something decidedly else.
# if it's a string from above break it on .
if hasattr(version, 'split'):
version = version.split('.')
# It's either an interable, or something else that makes us sad.
try:
return tuple(map(int, version))
except Exception:
except (TypeError, ValueError):
pass
raise TypeError('Invalid version specified: %s' % version)
@ -236,6 +245,16 @@ class Discover(object):
version_number = normalize_version_number(version_str)
# collect microversion information
min_microversion = v.get('min_version') or None
if min_microversion:
min_microversion = normalize_version_number(min_microversion)
max_microversion = v.get('max_version', v.get('version')) or None
if max_microversion:
max_microversion = normalize_version_number(max_microversion)
self_url = None
collection_url = None
for link in links:
try:
rel = link['rel']
@ -246,14 +265,19 @@ class Discover(object):
continue
if rel.lower() == 'self':
break
else:
self_url = url
elif rel.lower() == 'collection':
collection_url = url
if not self_url:
_LOGGER.info('Skipping invalid version data. '
'Missing link to endpoint.')
continue
versions.append({'version': version_number,
'url': url,
'url': self_url,
'collection': collection_url,
'min_microversion': min_microversion,
'max_microversion': max_microversion,
'raw_status': v['status']})
versions.sort(key=lambda v: v['version'], reverse=reverse)
@ -332,6 +356,8 @@ class EndpointData(object):
self.major_version = major_version
self.min_microversion = min_microversion
self.max_microversion = max_microversion
self._saved_project_id = None
self._catalog_matches_version = False
def __copy__(self):
"""Return a new EndpointData based on this one."""
@ -357,7 +383,7 @@ class EndpointData(object):
@positional(3)
def get_versioned_data(self, session, version,
authenticated=False, allow=None, cache=None,
allow_version_hack=True):
allow_version_hack=True, project_id=None):
"""Run version discovery for the service described.
Performs Version Discovery and returns a new EndpointData object with
@ -367,6 +393,9 @@ class EndpointData(object):
:type session: keystoneauth1.session.Session
:param tuple version: The minimum major version required for this
endpoint.
:param string project_id: ID of the currently scoped project. Used for
removing project_id components of URLs from
the catalog. (optional)
:param dict allow: Extra filters to pass when discovering API
versions. (optional)
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
@ -393,25 +422,47 @@ class EndpointData(object):
# defaulting to the most recent version.
return new_data
# NOTE(jamielennox): For backwards compatibility people might have a
# versioned endpoint in their catalog even though they want to use
# other endpoint versions. So we support a list of client defined
# situations where we can strip the version component from a URL before
# doing discovery.
vers_url = new_data._get_catalog_discover_hack(allow_version_hack)
new_data._set_version_info(
session=session, version=version, authenticated=authenticated,
allow=allow, cache=cache, allow_version_hack=allow_version_hack,
project_id=project_id)
return new_data
def _set_version_info(self, session, version,
authenticated=False, allow=None, cache=None,
allow_version_hack=True, project_id=None):
if project_id:
self.project_id = project_id
disc = None
vers_url = None
tried = set()
for vers_url in self._get_url_choices(version, project_id,
allow_version_hack):
if vers_url in tried:
continue
tried.update(vers_url)
try:
disc = get_discovery(session, vers_url,
cache=cache,
authenticated=False)
break
except (exceptions.DiscoveryFailure,
exceptions.HttpError,
exceptions.ConnectionError):
continue
if not disc:
# We couldn't find a version discovery document anywhere.
if self._catalog_matches_version:
# But - the version in the catalog is fine.
self.service_url = self.catalog_url
return
try:
disc = get_discovery(session, vers_url,
cache=cache,
authenticated=False)
except (exceptions.DiscoveryFailure,
exceptions.HttpError,
exceptions.ConnectionError):
# NOTE(jamielennox): The logic here is required for backwards
# compatibility. By itself it is not ideal.
if allow_version_hack:
# NOTE(jamielennox): Again if we can't contact the server we
# NOTE(jamielennox): If we can't contact the server we
# fall back to just returning the URL from the catalog. This
# is backwards compatible behaviour and used when there is no
# other choice. Realistically if you have provided a version
@ -421,25 +472,96 @@ class EndpointData(object):
'Failed to contact the endpoint at %s for '
'discovery. Fallback to using that endpoint as '
'the base url.', self.url)
return
else:
# NOTE(jamielennox): If you've said no to allow_version_hack
# and you can't determine the actual URL this is a failure
# and we can't determine the actual URL this is a failure
# because we are specifying that the deployment must be up to
# date enough to properly specify a version and keystoneauth
# can't deliver.
raise
else:
url = disc.url_for(version, **allow)
if not url:
raise exceptions.DiscoveryFailure(
"Version {version} requested, but was not found".format(
version=version_to_string(version)))
new_data.service_url = url
return new_data
"Version requested but version discovery document was not"
" found and allow_version_hack was False")
def _get_catalog_discover_hack(self, allow_version_hack=True):
# NOTE(jamielennox): urljoin allows the url to be relative or even
# protocol-less. The additional trailing '/' make urljoin respect
# the current path as canonical even if the url doesn't include it.
# for example a "v2" path from http://host/admin should resolve as
# http://host/admin/v2 where it would otherwise be host/v2.
# This has no effect on absolute urls returned from url_for.
discovered_data = disc.data_for(version, **allow)
if not discovered_data:
raise exceptions.DiscoveryFailure(
"Version {version} requested, but was not found".format(
version=version_to_string(version)))
self.min_microversion = discovered_data['min_microversion']
self.max_microversion = discovered_data['max_microversion']
discovered_url = discovered_data['url']
url = urllib.parse.urljoin(vers_url.rstrip('/') + '/', discovered_url)
# If we had to pop a project_id from the catalog_url, put it back on
if self._saved_project_id:
url = urllib.parse.urljoin(url.rstrip('/') + '/',
self._saved_project_id)
self.service_url = url
def _get_url_choices(self, version, project_id, allow_version_hack=True):
if allow_version_hack:
url = urllib.parse.urlparse(self.url)
url_parts = url.path.split('/')
# First, check to see if the catalog url ends with a project id
# We need to remove it and save it for later if it does
if project_id and url_parts[-1].endswith(project_id):
self._saved_project_id = url_parts.pop()
# Next, check to see if the url indicates a version and if that
# version matches our request. If so, we can start by trying
# the given url as it has a high potential for success
url_version = None
if url_parts[-1].startswith('v'):
try:
url_version = normalize_version_number(url_parts[-1])
except TypeError:
pass
if url_version:
if version_match(version, url_version):
self._catalog_matches_version = True
# This endpoint matches the version request, try it first
yield urllib.parse.ParseResult(
url.scheme,
url.netloc,
'/'.join(url_parts),
url.params,
url.query,
url.fragment).geturl()
url_parts.pop()
# If there were projects or versions in the url they are now gone.
# That means we're left with the unversioned url
yield urllib.parse.ParseResult(
url.scheme,
url.netloc,
'/'.join(url_parts),
url.params,
url.query,
url.fragment).geturl()
# NOTE(mordred): For backwards compatibility people might have
# added version hacks using the version hack system. The logic
# above should handle most cases, so by the time we get here it's
# most likely to be a no-op
yield self._get_catalog_discover_hack()
# As a final fallthrough case, add the url from the catalog. If hacks
# are turned off, this will be the only choice.
yield self.catalog_url
def _get_catalog_discover_hack(self):
"""Apply the catalog hacks and figure out an unversioned endpoint.
This function is internal to keystoneauth1.
@ -447,12 +569,9 @@ class EndpointData(object):
:param bool allow_version_hack: Whether or not to allow version hacks
to be applied. (defaults to True)
:returns: Either the unversioned url or the one from the catalog to try
:returns: A potential unversioned url
"""
if allow_version_hack:
return _VERSION_HACKS.get_discover_hack(self.service_type,
self.url)
return self.url
return _VERSION_HACKS.get_discover_hack(self.service_type, self.url)
@positional()

View File

@ -37,4 +37,5 @@ __all__ = ('DiscoveryList',
'V2Token',
'V3Token',
'V3FederationToken',
'VersionDiscovery',
)

View File

@ -17,6 +17,7 @@ from keystoneauth1 import _utils as utils
__all__ = ('DiscoveryList',
'V2Discovery',
'V3Discovery',
'VersionDiscovery',
)
_DEFAULT_DAYS_AGO = 30
@ -95,6 +96,103 @@ class DiscoveryBase(dict):
return mt
class VersionDiscovery(DiscoveryBase):
"""A Version element for non-keystone services without microversions.
Provides some default values and helper methods for creating a microversion
endpoint version structure. Clients should use this instead of creating
their own structures.
:param string href: The url that this entry should point to.
:param string id: The version id that should be reported.
"""
def __init__(self, href, id, **kwargs):
super(VersionDiscovery, self).__init__(id, **kwargs)
self.add_link(href)
class MicroversionDiscovery(DiscoveryBase):
"""A Version element for that has microversions.
Provides some default values and helper methods for creating a microversion
endpoint version structure. Clients should use this instead of creating
their own structures.
:param string href: The url that this entry should point to.
:param string id: The version id that should be reported.
:param string min_version: The minimum supported microversion. (optional)
:param string max_version: The maximum supported microversion. (optional)
"""
@positional()
def __init__(self, href, id, min_version='', max_version='', **kwargs):
super(MicroversionDiscovery, self).__init__(id, **kwargs)
self.add_link(href)
self.min_version = min_version
self.max_version = max_version
@property
def min_version(self):
return self.get('min_version')
@min_version.setter
def min_version(self, value):
self['min_version'] = value
@property
def max_version(self):
return self.get('max_version')
@max_version.setter
def max_version(self, value):
self['max_version'] = value
class NovaMicroversionDiscovery(DiscoveryBase):
"""A Version element with nova-style microversions.
Provides some default values and helper methods for creating a microversion
endpoint version structure. Clients should use this instead of creating
their own structures.
:param href: The url that this entry should point to.
:param string id: The version id that should be reported.
:param string min_version: The minimum microversion supported. (optional)
:param string version: The maximum microversion supported. (optional)
"""
@positional()
def __init__(self, href, id, min_version=None, version=None, **kwargs):
super(NovaMicroversionDiscovery, self).__init__(id, **kwargs)
self.add_link(href)
self.min_version = min_version
self.version = version
@property
def min_version(self):
return self.get('min_version')
@min_version.setter
def min_version(self, value):
if value:
self['min_version'] = value
@property
def version(self):
return self.get('version')
@version.setter
def version(self, value):
if value:
self['version'] = value
class V2Discovery(DiscoveryBase):
"""A Version element for a V2 identity service endpoint.
@ -255,3 +353,21 @@ class DiscoveryList(dict):
obj = V3Discovery(href, **kwargs)
self.add_version(obj)
return obj
def add_microversion(self, href, id, **kwargs):
"""Add a microversion version to the list.
The parameters are the same as MicroversionDiscovery.
"""
obj = MicroversionDiscovery(href=href, id=id, **kwargs)
self.add_version(obj)
return obj
def add_nova_microversion(self, href, id, **kwargs):
"""Add a nova microversion version to the list.
The parameters are the same as NovaMicroversionDiscovery.
"""
obj = NovaMicroversionDiscovery(href=href, id=id, **kwargs)
self.add_version(obj)
return obj

View File

@ -201,6 +201,7 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
endpoint_data = discover.EndpointData(
service_url=self.auth_url,
service_type=service_type or 'identity')
project_id = None
else:
if not service_type:
LOG.warning('Plugin cannot return an endpoint without '
@ -214,6 +215,9 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
interface = 'public'
service_catalog = self.get_access(session).service_catalog
project_id = self.get_project_id(session)
# NOTE(mordred): service_catalog.url_data_for raises if it can't
# find a match, so this will always be a valid object.
endpoint_data = service_catalog.endpoint_data_for(
service_type=service_type,
interface=interface,
@ -224,7 +228,9 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
try:
return endpoint_data.get_versioned_data(
session, version, authenticated=False,
session, version,
project_id=project_id,
authenticated=False,
cache=self._discovery_cache,
allow_version_hack=allow_version_hack, allow=allow)
except (exceptions.DiscoveryFailure,

View File

@ -38,6 +38,30 @@ class CommonIdentityTests(object):
TEST_COMPUTE_INTERNAL = TEST_COMPUTE_BASE + '/novapi/internal'
TEST_COMPUTE_ADMIN = TEST_COMPUTE_BASE + '/novapi/admin'
TEST_VOLUME_V2_BASE = 'http://cinder'
TEST_VOLUME_V2_SERVICE_PUBLIC = TEST_VOLUME_V2_BASE + '/public/volumev2'
TEST_VOLUME_V2_SERVICE_INTERNAL = (
TEST_VOLUME_V2_BASE + '/internal/volumev2')
TEST_VOLUME_V2_SERVICE_ADMIN = TEST_VOLUME_V2_BASE + '/admin/volumev2'
TEST_VOLUME_V2_CATALOG_PUBLIC = (
TEST_VOLUME_V2_SERVICE_PUBLIC + '/{project_id}')
TEST_VOLUME_V2_CATALOG_INTERNAL = (
TEST_VOLUME_V2_SERVICE_INTERNAL + '/{project_id}')
TEST_VOLUME_V2_CATALOG_ADMIN = (
TEST_VOLUME_V2_SERVICE_ADMIN + '/{project_id}')
TEST_VOLUME_V3_BASE = 'http://cinder'
TEST_VOLUME_V3_SERVICE_PUBLIC = TEST_VOLUME_V3_BASE + '/public/volumev3'
TEST_VOLUME_V3_SERVICE_INTERNAL = (
TEST_VOLUME_V3_BASE + '/internal/volumev3')
TEST_VOLUME_V3_SERVICE_ADMIN = TEST_VOLUME_V3_BASE + '/admin/volumev3'
TEST_VOLUME_V3_CATALOG_PUBLIC = (
TEST_VOLUME_V3_SERVICE_PUBLIC + '/{project_id}')
TEST_VOLUME_V3_CATALOG_INTERNAL = (
TEST_VOLUME_V3_SERVICE_INTERNAL + '/{project_id}')
TEST_VOLUME_V3_CATALOG_ADMIN = (
TEST_VOLUME_V3_SERVICE_ADMIN + '/{project_id}')
TEST_PASS = uuid.uuid4().hex
def setUp(self):
@ -498,6 +522,118 @@ class CommonIdentityTests(object):
self.assertEqual(v3_compute, v3_data.service_url)
self.assertEqual(self.TEST_COMPUTE_ADMIN, v3_data.catalog_url)
def test_get_versioned_data_project_id(self):
# need to construct list this way for relative
disc = fixture.DiscoveryList(v2=False, v3=False)
# The version discovery dict will not have a project_id
disc.add_microversion(
href=self.TEST_VOLUME_V3_SERVICE_PUBLIC,
id='v3.0', status='CURRENT',
min_version='3.0', max_version='3.20')
# Adding a v2 version to a service named volumev3 is not
# an error. The service itself is cinder and has more than
# one major version.
disc.add_microversion(
href=self.TEST_VOLUME_V2_SERVICE_PUBLIC,
id='v2.0', status='SUPPORTED')
# We should only try to fetch the non-project_id url and only
# once
resps = [{'json': disc}, {'status_code': 500}]
self.requests_mock.get(self.TEST_VOLUME_V3_SERVICE_PUBLIC, resps)
body = 'SUCCESS'
self.stub_url('GET', ['path'], text=body)
a = self.create_auth_plugin()
s = session.Session(auth=a)
v2_catalog_url = self.TEST_VOLUME_V2_CATALOG_PUBLIC.format(
project_id=self.project_id)
v3_catalog_url = self.TEST_VOLUME_V3_CATALOG_PUBLIC.format(
project_id=self.project_id)
data = a.get_endpoint_data(session=s,
service_type='volumev3',
interface='public')
self.assertEqual(v3_catalog_url, data.url)
v3_data = data.get_versioned_data(
s, version='3.0', project_id=self.project_id)
self.assertEqual(v3_catalog_url, v3_data.url)
self.assertEqual(v3_catalog_url, v3_data.service_url)
self.assertEqual(v3_catalog_url, v3_data.catalog_url)
self.assertEqual((3, 0), v3_data.min_microversion)
self.assertEqual((3, 20), v3_data.max_microversion)
v2_data = data.get_versioned_data(
s, version='2.0', 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
# the project_id to the URL
self.assertEqual(v2_catalog_url, v2_data.url)
self.assertEqual(v2_catalog_url, v2_data.service_url)
self.assertEqual(v3_catalog_url, v2_data.catalog_url)
self.assertEqual(None, v2_data.min_microversion)
self.assertEqual(None, v2_data.max_microversion)
def test_get_versioned_data_compute_project_id(self):
# need to construct list this way for relative
disc = fixture.DiscoveryList(v2=False, v3=False)
# The version discovery dict will not have a project_id
disc.add_nova_microversion(
href=self.TEST_VOLUME_V3_SERVICE_PUBLIC,
id='v3.0', status='CURRENT',
min_version='3.0', version='3.20')
# Adding a v2 version to a service named volumev3 is not
# an error. The service itself is cinder and has more than
# one major version.
disc.add_nova_microversion(
href=self.TEST_VOLUME_V2_SERVICE_PUBLIC,
id='v2.0', status='SUPPORTED')
# We should only try to fetch the non-project_id url and only
# once
resps = [{'json': disc}, {'status_code': 500}]
self.requests_mock.get(self.TEST_VOLUME_V3_SERVICE_PUBLIC, resps)
body = 'SUCCESS'
self.stub_url('GET', ['path'], text=body)
a = self.create_auth_plugin()
s = session.Session(auth=a)
v2_catalog_url = self.TEST_VOLUME_V2_CATALOG_PUBLIC.format(
project_id=self.project_id)
v3_catalog_url = self.TEST_VOLUME_V3_CATALOG_PUBLIC.format(
project_id=self.project_id)
data = a.get_endpoint_data(session=s,
service_type='volumev3',
interface='public')
self.assertEqual(v3_catalog_url, data.url)
v3_data = data.get_versioned_data(
s, version='3.0', project_id=self.project_id)
self.assertEqual(v3_catalog_url, v3_data.url)
self.assertEqual(v3_catalog_url, v3_data.service_url)
self.assertEqual(v3_catalog_url, v3_data.catalog_url)
self.assertEqual((3, 0), v3_data.min_microversion)
self.assertEqual((3, 20), v3_data.max_microversion)
v2_data = data.get_versioned_data(
s, version='2.0', 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
# the project_id to the URL
self.assertEqual(v2_catalog_url, v2_data.url)
self.assertEqual(v2_catalog_url, v2_data.service_url)
self.assertEqual(v3_catalog_url, v2_data.catalog_url)
self.assertEqual(None, v2_data.min_microversion)
self.assertEqual(None, v2_data.max_microversion)
def test_asking_for_auth_endpoint_ignores_checks(self):
a = self.create_auth_plugin()
s = session.Session(auth=a)
@ -619,6 +755,7 @@ class V3(CommonIdentityTests, utils.TestCase):
return 'v3'
def get_auth_data(self, **kwargs):
kwargs.setdefault('project_id', uuid.uuid4().hex)
token = fixture.V3Token(**kwargs)
region = 'RegionOne'
@ -631,6 +768,20 @@ class V3(CommonIdentityTests, utils.TestCase):
internal=self.TEST_COMPUTE_INTERNAL,
region=region)
svc = token.add_service('volumev2')
svc.add_standard_endpoints(
admin=self.TEST_VOLUME_V2_CATALOG_ADMIN.format(**kwargs),
public=self.TEST_VOLUME_V2_CATALOG_PUBLIC.format(**kwargs),
internal=self.TEST_VOLUME_V2_CATALOG_INTERNAL.format(**kwargs),
region=region)
svc = token.add_service('volumev3')
svc.add_standard_endpoints(
admin=self.TEST_VOLUME_V3_CATALOG_ADMIN.format(**kwargs),
public=self.TEST_VOLUME_V3_CATALOG_PUBLIC.format(**kwargs),
internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs),
region=region)
return token
def stub_auth(self, subject_token=None, **kwargs):
@ -671,6 +822,7 @@ class V2(CommonIdentityTests, utils.TestCase):
return identity.V2Password(**kwargs)
def get_auth_data(self, **kwargs):
kwargs.setdefault('tenant_id', uuid.uuid4().hex)
token = fixture.V2Token(**kwargs)
region = 'RegionOne'
@ -683,6 +835,21 @@ class V2(CommonIdentityTests, utils.TestCase):
admin=self.TEST_COMPUTE_ADMIN,
region=region)
kwargs['project_id'] = kwargs['tenant_id']
svc = token.add_service('volumev2')
svc.add_endpoint(
admin=self.TEST_VOLUME_V2_CATALOG_ADMIN.format(**kwargs),
public=self.TEST_VOLUME_V2_CATALOG_PUBLIC.format(**kwargs),
internal=self.TEST_VOLUME_V2_CATALOG_INTERNAL.format(**kwargs),
region=region)
svc = token.add_service('volumev3')
svc.add_endpoint(
admin=self.TEST_VOLUME_V3_CATALOG_ADMIN.format(**kwargs),
public=self.TEST_VOLUME_V3_CATALOG_PUBLIC.format(**kwargs),
internal=self.TEST_VOLUME_V3_CATALOG_INTERNAL.format(**kwargs),
region=region)
return token
def stub_auth(self, **kwargs):
@ -743,6 +910,7 @@ class CatalogHackTests(utils.TestCase):
json=token)
self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404)
self.stub_url('GET', [], base_url=self.V2_URL, status_code=404)
v2_auth = identity.V2Password(self.V2_URL,
username=uuid.uuid4().hex,

View File

@ -148,6 +148,23 @@ CINDER_EXAMPLES = {
"rel": "self"
}
]
},
{
"status": "CURRENT",
"updated": "2012-11-21T11:33:21Z",
"id": "v3.0",
"version": "3.27",
"min_version": "3.0",
"links": [
{
"href": BASE_URL,
"rel": "collection"
},
{
"href": "%sv3/" % BASE_URL,
"rel": "self"
}
]
}
]
}
@ -277,6 +294,7 @@ class DiscoverUtils(utils.TestCase):
assertVersion('1', (1, 0))
assertVersion(1, (1, 0))
assertVersion(5.2, (5, 2))
assertVersion('3.20', (3, 20))
assertVersion((6, 1), (6, 1))
assertVersion([1, 4], (1, 4))
@ -390,7 +408,7 @@ class VersionDataTests(utils.TestCase):
raw_data = disc.raw_version_data()
clean_data = disc.version_data()
self.assertEqual(2, len(raw_data))
self.assertEqual(3, len(raw_data))
for v in raw_data:
self.assertEqual(v['status'], 'CURRENT')
@ -398,23 +416,40 @@ class VersionDataTests(utils.TestCase):
self.assertEqual(v['updated'], '2012-01-04T11:33:21Z')
elif v['id'] == 'v2.0':
self.assertEqual(v['updated'], '2012-11-21T11:33:21Z')
elif v['id'] == 'v3.0':
self.assertEqual(v['updated'], '2012-11-21T11:33:21Z')
else:
self.fail("Invalid version found")
v1_url = "%sv1/" % BASE_URL
v2_url = "%sv2/" % BASE_URL
v3_url = "%sv3/" % BASE_URL
self.assertEqual(clean_data, [
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (1, 0),
'url': v1_url,
'raw_status': 'CURRENT',
},
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (2, 0),
'url': v2_url,
'raw_status': 'CURRENT',
},
{
'collection': BASE_URL,
'max_microversion': (3, 27),
'min_microversion': (3, 0),
'version': (3, 0),
'url': v3_url,
'raw_status': 'CURRENT',
},
])
version = disc.data_for('v2.0')
@ -427,7 +462,8 @@ class VersionDataTests(utils.TestCase):
self.assertEqual('CURRENT', version['raw_status'])
self.assertEqual(v1_url, version['url'])
self.assertIsNone(disc.url_for('v3'))
self.assertIsNone(disc.url_for('v4'))
self.assertEqual(v3_url, disc.url_for('v3'))
self.assertEqual(v2_url, disc.url_for('v2'))
self.assertEqual(v1_url, disc.url_for('v1'))
@ -457,26 +493,41 @@ class VersionDataTests(utils.TestCase):
self.assertEqual(clean_data, [
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (1, 0),
'url': v1_url,
'raw_status': 'SUPPORTED',
},
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (1, 1),
'url': v1_url,
'raw_status': 'CURRENT',
},
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (2, 0),
'url': v2_url,
'raw_status': 'SUPPORTED',
},
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (2, 1),
'url': v2_url,
'raw_status': 'SUPPORTED',
},
{
'collection': None,
'max_microversion': None,
'min_microversion': None,
'version': (2, 2),
'url': v2_url,
'raw_status': 'CURRENT',