Add flags to turn discovery on and off

If a user does not specify a version, that means they want whatever is
in the catalog. However, they may still want discovery to be run for
things like microversion information. The new parameter
"discover_versions", if set to True, will run discovery even with no
version parameter. The inverse of this is "skip_discovery" which will
tell keystoneauth to not run discovery even if a version has been given.

Note: This adds some parameters to some methods that get removed by
change I54053336edf1b3c2bd35a77dbd78f56388b8e806 so we should not
actually land this one until we're ready to land the stack up to and
including that one.

Note: This adds two new methods that will have behavior changes in
patch I8ba948a712002775098b0a86c70f05e0c68250f5.

Change-Id: I897c39743089c5994b51336a4ad44eebed33ec35
This commit is contained in:
Monty Taylor 2017-05-29 17:34:16 -05:00
parent a4066a86b5
commit cdc10d8741
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
4 changed files with 294 additions and 45 deletions

View File

@ -286,6 +286,10 @@ class Discover(object):
def data_for(self, version, **kwargs):
"""Return endpoint data for a version.
NOTE: This method raises a TypeError if version is None. It is
kept for backwards compatability. New code should use
versioned_data_for instead.
:param tuple 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.
@ -306,6 +310,10 @@ class Discover(object):
def url_for(self, version, **kwargs):
"""Get the endpoint url for a version.
NOTE: This method raises a TypeError if version is None. It is
kept for backwards compatability. New code should use
versioned_url_for instead.
:param tuple 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.
@ -316,6 +324,47 @@ 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, **kwargs):
"""Return endpoint data that matches the version or url.
: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.
: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
"""
if version:
version = normalize_version_number(version)
if url:
url = url.rstrip('/') + '/'
for data in self.version_data(reverse=True, **kwargs):
if url and data['url'] and data['url'].rstrip('/') + '/' == url:
return data
if version and version_match(version, data['version']):
return data
return None
def versioned_url_for(self, version, **kwargs):
"""Get the endpoint url for a version.
:param tuple 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.
:returns: The url for the specified version or None if no match.
:rtype: str
"""
data = self.versioned_data_for(version, **kwargs)
return data['url'] if data else None
class EndpointData(object):
"""Normalized information about a discovered endpoint.
@ -383,7 +432,8 @@ class EndpointData(object):
@positional(3)
def get_versioned_data(self, session, version,
authenticated=False, allow=None, cache=None,
allow_version_hack=True, project_id=None):
allow_version_hack=True, project_id=None,
discover_versions=False):
"""Run version discovery for the service described.
Performs Version Discovery and returns a new EndpointData object with
@ -406,6 +456,10 @@ class EndpointData(object):
(optional)
:param bool authenticated: Include a token in the discovery call.
(optional) Defaults to False.
:param bool discover_versions: Whether to perform version discovery
even if a version string wasn't
requested. This is useful for getting
microversion information.
:raises keystoneauth1.exceptions.http.HttpError: An error from an
invalid HTTP response.
@ -416,7 +470,7 @@ class EndpointData(object):
# This method should always return a new EndpointData
new_data = copy.copy(self)
if not version:
if not 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
# defaulting to the most recent version.
@ -425,12 +479,25 @@ class EndpointData(object):
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)
project_id=project_id, discover_versions=discover_versions)
return new_data
def _set_version_info(self, session, version,
authenticated=False, allow=None, cache=None,
allow_version_hack=True, project_id=None):
allow_version_hack=True, project_id=None,
discover_versions=False):
match_url = None
if not 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
# defaulting to the most recent version.
return
elif not version and discover_versions:
# We want to run discovery, but we don't want to find different
# endpoints than what's in the catalog
allow_version_hack = False
match_url = self.catalog_url
if project_id:
self.project_id = project_id
@ -490,7 +557,8 @@ class EndpointData(object):
# 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)
discovered_data = disc.versioned_data_for(
version, url=match_url, **allow)
if not discovered_data:
raise exceptions.DiscoveryFailure(
"Version {version} requested, but was not found".format(

View File

@ -158,7 +158,9 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
def get_endpoint_data(self, session, service_type=None, interface=None,
region_name=None, service_name=None, version=None,
allow={}, allow_version_hack=True, **kwargs):
allow={}, allow_version_hack=True,
discover_versions=False, skip_discovery=False,
**kwargs):
"""Return a valid endpoint data for a service.
If a valid token is not present then a new one will be fetched using
@ -185,6 +187,15 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
URLS to support older schemes.
(optional, default True)
:param bool discover_versions: Whether to perform version discovery
even if a version string wasn't
requested. This is useful for getting
microversion information.
:param bool skip_discovery: Whether to skip version discovery even
if a version has been given. This is useful
if endpoint_override or similar has been
given and grabbing additional information
about the endpoint is not useful.
:raises keystoneauth1.exceptions.http.HttpError: An error from an
invalid HTTP response.
@ -226,21 +237,32 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
if not endpoint_data:
return None
if skip_discovery:
return endpoint_data
try:
return endpoint_data.get_versioned_data(
session, version,
project_id=project_id,
authenticated=False,
cache=self._discovery_cache,
discover_versions=discover_versions,
allow_version_hack=allow_version_hack, allow=allow)
except (exceptions.DiscoveryFailure,
exceptions.HttpError,
exceptions.ConnectionError):
return None
# If a version was requested, we didn't find it, return
# None.
if version:
return None
# If one wasn't, then the endpoint_data we already have
# should be fine
return endpoint_data
def get_endpoint(self, session, service_type=None, interface=None,
region_name=None, service_name=None, version=None,
allow={}, allow_version_hack=True, **kwargs):
allow={}, allow_version_hack=True,
discover_versions=False, skip_discovery=False, **kwargs):
"""Return a valid endpoint for a service.
If a valid token is not present then a new one will be fetched using
@ -267,6 +289,15 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
:param bool allow_version_hack: Allow keystoneauth to hack up catalog
URLS to support older schemes.
(optional, default True)
:param bool discover_versions: Whether to perform version discovery
even if a version string wasn't
requested. This is useful for getting
microversion information.
:param bool skip_discovery: Whether to skip version discovery even
if a version has been given. This is useful
if endpoint_override or similar has been
given and grabbing additional information
about the endpoint is not useful.
:raises keystoneauth1.exceptions.http.HttpError: An error from an
invalid HTTP response.
@ -278,6 +309,8 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin):
session, service_type=service_type, interface=interface,
region_name=region_name, service_name=service_name,
version=version, allow=allow,
discover_versions=discover_versions,
skip_discovery=skip_discovery,
allow_version_hack=allow_version_hack, **kwargs)
return endpoint_data.url if endpoint_data else None

View File

@ -924,6 +924,122 @@ class CatalogHackTests(utils.TestCase):
self.assertEqual(self.V2_URL, endpoint)
def test_returns_original_skipping_discovery(self):
token = fixture.V2Token()
service = token.add_service(self.IDENTITY)
service.add_endpoint(public=self.V2_URL,
admin=self.V2_URL,
internal=self.V2_URL)
self.stub_url('POST',
['tokens'],
base_url=self.V2_URL,
json=token)
v2_auth = identity.V2Password(self.V2_URL,
username=uuid.uuid4().hex,
password=uuid.uuid4().hex)
sess = session.Session(auth=v2_auth)
endpoint = sess.get_endpoint(service_type=self.IDENTITY,
interface='public',
skip_discovery=True,
version=(3, 0))
self.assertEqual(self.V2_URL, endpoint)
def test_forcing_discovery(self):
v2_disc = fixture.V2Discovery(self.V2_URL)
common_disc = fixture.DiscoveryList(href=self.BASE_URL)
v2_m = self.stub_url('GET',
['v2.0'],
base_url=self.BASE_URL,
status_code=200,
json={'version': v2_disc})
common_m = self.stub_url('GET',
[],
base_url=self.BASE_URL,
status_code=300,
json=common_disc)
token = fixture.V2Token()
service = token.add_service(self.IDENTITY)
service.add_endpoint(public=self.V2_URL,
admin=self.V2_URL,
internal=self.V2_URL)
self.stub_url('POST',
['tokens'],
base_url=self.V2_URL,
json=token)
v2_auth = identity.V2Password(self.V2_URL,
username=uuid.uuid4().hex,
password=uuid.uuid4().hex)
sess = session.Session(auth=v2_auth)
# v2 auth with v2 url doesn't make any discovery calls.
self.assertFalse(v2_m.called)
self.assertFalse(common_m.called)
endpoint = sess.get_endpoint(service_type=self.IDENTITY,
discover_versions=True)
# We should get the v2 document, but not the unversioned
self.assertTrue(v2_m.called)
self.assertFalse(common_m.called)
# got v2 url
self.assertEqual(self.V2_URL, endpoint)
def test_forcing_discovery_list_returns_url(self):
common_disc = fixture.DiscoveryList(href=self.BASE_URL)
# 2.0 doesn't usually return a list. This is testing that if
# the catalog url returns an endpoint that has a discovery document
# with more than one URL and that a different url would be returned
# by "return the latest" rules, that we get the info of the url from
# the catalog if we don't provide a version but do provide
# discover_versions
v2_m = self.stub_url('GET',
['v2.0'],
base_url=self.BASE_URL,
status_code=200,
json=common_disc)
token = fixture.V2Token()
service = token.add_service(self.IDENTITY)
service.add_endpoint(public=self.V2_URL,
admin=self.V2_URL,
internal=self.V2_URL)
self.stub_url('POST',
['tokens'],
base_url=self.V2_URL,
json=token)
v2_auth = identity.V2Password(self.V2_URL,
username=uuid.uuid4().hex,
password=uuid.uuid4().hex)
sess = session.Session(auth=v2_auth)
# v2 auth with v2 url doesn't make any discovery calls.
self.assertFalse(v2_m.called)
endpoint = sess.get_endpoint(service_type=self.IDENTITY,
discover_versions=True)
# We should make the one call
self.assertTrue(v2_m.called)
# got v2 url
self.assertEqual(self.V2_URL, endpoint)
def test_getting_endpoints_on_auth_interface(self):
disc = fixture.DiscoveryList(href=self.BASE_URL)
self.stub_url('GET',

View File

@ -359,6 +359,32 @@ class VersionDataTests(utils.TestCase):
self.assertTrue(mock.called_once)
def test_data_for_url(self):
mock = self.requests_mock.get(V3_URL,
status_code=200,
json=V3_VERSION_ENTRY)
disc = discover.Discover(self.session, V3_URL)
for url in (V3_URL, V3_URL + '/'):
data = disc.versioned_data_for(url=url)
self.assertEqual(data['version'], (3, 0))
self.assertEqual(data['raw_status'], 'stable')
self.assertEqual(data['url'], V3_URL)
self.assertTrue(mock.called_once)
def test_data_for_no_version(self):
mock = self.requests_mock.get(V3_URL,
status_code=200,
json=V3_VERSION_ENTRY)
disc = discover.Discover(self.session, V3_URL)
self.assertIsNone(disc.versioned_data_for(version=None))
self.assertRaises(TypeError, disc.data_for, version=None)
self.assertTrue(mock.called_once)
def test_keystone_version_data(self):
mock = self.requests_mock.get(BASE_URL,
status_code=300,
@ -383,19 +409,21 @@ class VersionDataTests(utils.TestCase):
self.assertIn(v['version'], ((2, 0), (3, 0)))
self.assertEqual(v['raw_status'], 'stable')
version = disc.data_for('v3.0')
self.assertEqual((3, 0), version['version'])
self.assertEqual('stable', version['raw_status'])
self.assertEqual(V3_URL, version['url'])
for meth in (disc.data_for, disc.versioned_data_for):
version = meth('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'])
version = meth(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'))
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.assertTrue(mock.called_once)
@ -452,20 +480,22 @@ class VersionDataTests(utils.TestCase):
},
])
version = disc.data_for('v2.0')
self.assertEqual((2, 0), version['version'])
self.assertEqual('CURRENT', version['raw_status'])
self.assertEqual(v2_url, version['url'])
for meth in (disc.data_for, disc.versioned_data_for):
version = meth('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'])
version = meth(1)
self.assertEqual((1, 0), version['version'])
self.assertEqual('CURRENT', version['raw_status'])
self.assertEqual(v1_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'))
self.assertEqual(v1_url, disc.url_for('v1'))
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.assertTrue(mock.called_once)
@ -534,22 +564,24 @@ class VersionDataTests(utils.TestCase):
},
])
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 meth in (disc.data_for, disc.versioned_data_for):
for ver in (2, 2.1, 2.2):
version = meth(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))
for ver in (1, 1.1):
version = meth(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'))
for meth in (disc.url_for, disc.versioned_url_for):
self.assertIsNone(meth('v3'))
self.assertIsNone(meth('v2.3'))
self.assertTrue(mock.called_once)