diff --git a/keystoneauth1/discover.py b/keystoneauth1/discover.py index ccf53c96..24030e8c 100644 --- a/keystoneauth1/discover.py +++ b/keystoneauth1/discover.py @@ -555,6 +555,37 @@ class Discover(object): versions.sort(key=lambda v: v['version'], reverse=reverse) return versions + def version_string_data(self, reverse=False, **kwargs): + """Get normalized version data with versions as strings. + + Return version data in a structured way. + + :param bool reverse: Reverse the list. reverse=true will mean the + returned list is sorted from newest to oldest + version. + :returns: A list of version data dictionaries sorted by version number. + Each data element in the returned list is a dictionary + consisting of: + + :version string: The normalized version of the endpoint. + :url str: The url for the endpoint. + :collection: The URL for the discovery document. May be None. + :min_microversion str: The minimum microversion supported by the + endpoint. May be None. + :max_microversion str: The maximum microversion supported by the + endpoint. May be None. + :status str: A canonicalized version of the status. Valid values + are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL + :raw_status str: The status as provided by the server + :rtype: list(dict) + """ + version_data = self.version_data(reverse=reverse, **kwargs) + for version in version_data: + for key in ('version', 'min_microversion', 'max_microversion'): + if version[key]: + version[key] = version_to_string(version[key]) + return version_data + def data_for(self, version, **kwargs): """Return endpoint data for a version. @@ -869,6 +900,85 @@ class EndpointData(object): max_version=max_version) return new_data + def get_all_version_string_data(self, session, project_id=None): + """Return version data for all versions discovery can find. + + :param string project_id: ID of the currently scoped project. Used for + removing project_id components of URLs from + the catalog. (optional) + :returns: A list of version data dictionaries sorted by version number. + Each data element in the returned list is a dictionary + consisting of: + + :version string: The normalized version of the endpoint. + :url str: The url for the endpoint. + :collection: The URL for the discovery document. May be None. + :min_microversion: The minimum microversion supported by the + endpoint. May be None. + :max_microversion: The maximum microversion supported by the + endpoint. May be None. + :status str: A canonicalized version of the status. Valid values + are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL + :raw_status str: The status as provided by the server + :rtype: list(dict) + """ + versions = [] + for vers_url in self._get_discovery_url_choices(project_id=project_id): + try: + d = get_discovery(session, vers_url) + except Exception as e: + # Ignore errors here - we're just searching for one of the + # URLs that will give us data. + _LOGGER.debug( + "Failed attempt at discovery on %s: %s", vers_url, str(e)) + continue + for version in d.version_string_data(): + versions.append(version) + break + return versions or self._infer_version_data(project_id) + + def _infer_version_data(self, project_id=None): + """Return version data dict for when discovery fails. + + :param string project_id: ID of the currently scoped project. Used for + removing project_id components of URLs from + the catalog. (optional) + :returns: A list of version data dictionaries sorted by version number. + Each data element in the returned list is a dictionary + consisting of: + :version string: The normalized version of the endpoint. + :url str: The url for the endpoint. + :collection: The URL for the discovery document. May be None. + :min_microversion: The minimum microversion supported by the + endpoint. May be None. + :max_microversion: The maximum microversion supported by the + endpoint. May be None. + :status str: A canonicalized version of the status. Valid values + are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL + :raw_status str: The status as provided by the server + :rtype: list(dict) + """ + version = self.api_version + if version: + version = version_to_string(self.api_version) + + url = self.url.rstrip("/") + if project_id and url.endswith(project_id): + url, _ = self.url.rsplit('/', 1) + url += "/" + + return [{ + 'version': version, + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'status': 'CURRENT', + 'raw_status': None, + 'url': url, + }] + def _set_version_info(self, session, allow=None, cache=None, allow_version_hack=True, project_id=None, discover_versions=False, diff --git a/keystoneauth1/identity/base.py b/keystoneauth1/identity/base.py index 9cb5a18b..53568365 100644 --- a/keystoneauth1/identity/base.py +++ b/keystoneauth1/identity/base.py @@ -507,6 +507,62 @@ class BaseIdentityPlugin(plugin.BaseAuthPlugin): return None return data.api_version + def get_all_version_data(self, session, interface='public', + region_name=None, **kwargs): + """Get version data for all services in the catalog. + + :param session: A session object that can be used for communication. + :type session: keystoneauth1.session.Session + :param interface: + Type of endpoint to get version data for. Can be a single value + or a list of values. A value of None indicates that all interfaces + should be queried. (optional, defaults to public) + :param string region_name: + Region of endpoints to get version data for. A valueof None + indicates that all regions should be queried. (optional, defaults + to None) + :returns: A dictionary keyed by region_name with values containing + dictionaries keyed by interface with values being a list of + version data dictionaries. Each version data dictionary consists + of: + + :version string: The normalized version of the endpoint. + :url str: The url for the endpoint. + :collection: The URL for the discovery document. May be None. + :min_microversion: The minimum microversion supported by the + endpoint. May be None. + :max_microversion: The maximum microversion supported by the + endpoint. May be None. + :status str: A canonicalized version of the status. Valid values + are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL + :raw_status str: The status as provided by the server + """ + service_types = discover._SERVICE_TYPES + catalog = self.get_access(session).service_catalog + version_data = {} + endpoints_data = catalog.get_endpoints_data( + interface=interface, region_name=region_name) + + for service_type, services in endpoints_data.items(): + if service_types.is_known(service_type): + service_type = service_types.get_service_type(service_type) + for service in services: + versions = service.get_all_version_string_data( + session=session, + project_id=self.get_project_id(session), + ) + + if service.region_name not in version_data: + version_data[service.region_name] = {} + regions = version_data[service.region_name] + + interface = service.interface.rstrip('URL') + if interface not in regions: + regions[interface] = {} + regions[interface][service_type] = versions + + return version_data + def get_user_id(self, session, **kwargs): return self.get_access(session).user_id diff --git a/keystoneauth1/session.py b/keystoneauth1/session.py index 8853fff4..d712e2b5 100644 --- a/keystoneauth1/session.py +++ b/keystoneauth1/session.py @@ -1048,6 +1048,42 @@ class Session(object): auth = self._auth_required(auth, 'determine endpoint URL') return auth.get_api_major_version(self, **kwargs) + def get_all_version_data(self, auth=None, interface=None, + region_name=None, **kwargs): + """Get version data for all services in the catalog. + + :param auth: + The auth plugin to use for token. Overrides the plugin on + the session. (optional) + :type auth: keystoneauth1.plugin.BaseAuthPlugin + :param interface: + Type of endpoint to get version data for. Can be a single value + or a list of values. A value of None indicates that all interfaces + should be queried. (optional, defaults to public) + :param string region_name: + Region of endpoints to get version data for. A valueof None + indicates that all regions should be queried. (optional, defaults + to None) + :returns: A dictionary keyed by region_name with values containing + dictionaries keyed by interface with values being a list of + version data dictionaries. Each version data dictionary consists + of: + + :version string: The normalized version of the endpoint. + :url str: The url for the endpoint. + :collection: The URL for the discovery document. May be None. + :min_microversion: The minimum microversion supported by the + endpoint. May be None. + :max_microversion: The maximum microversion supported by the + endpoint. May be None. + :status str: A canonicalized version of the status. Valid values + are CURRENT, SUPPORTED, DEPRECATED and EXPERIMENTAL + :raw_status str: The status as provided by the server + """ + auth = self._auth_required(auth, 'determine endpoint URL') + return auth.get_all_version_data( + self, interface=interface, region_name=region_name, **kwargs) + def get_auth_connection_params(self, auth=None, **kwargs): """Return auth connection params as provided by the auth plugin. @@ -1148,7 +1184,6 @@ class Session(object): auth = self._auth_required(auth, 'get project_id') return auth.get_project_id(self) - REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.')) diff --git a/keystoneauth1/tests/unit/identity/test_identity_common.py b/keystoneauth1/tests/unit/identity/test_identity_common.py index 9438abe8..0e4ba6a4 100644 --- a/keystoneauth1/tests/unit/identity/test_identity_common.py +++ b/keystoneauth1/tests/unit/identity/test_identity_common.py @@ -579,6 +579,292 @@ class CommonIdentityTests(object): # We should have gotten the version from the URL self.assertEqual((3, 0), data.api_version) + def test_get_all_version_data_all_interfaces(self): + + for interface in ('public', 'internal', 'admin'): + # The version discovery dict will not have a project_id + disc = fixture.DiscoveryList(v2=False, v3=False) + disc.add_nova_microversion( + href=getattr(self.TEST_VOLUME.versions['v3'].discovery, + interface), + 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=getattr(self.TEST_VOLUME.versions['v2'].discovery, + interface), + id='v2.0', status='SUPPORTED') + + self.stub_url( + 'GET', [], + base_url=getattr(self.TEST_VOLUME.unversioned, + interface) + '/', + json=disc) + + for url in ( + self.TEST_COMPUTE_PUBLIC, + self.TEST_COMPUTE_INTERNAL, + self.TEST_COMPUTE_ADMIN): + + disc = fixture.DiscoveryList(v2=False, v3=False) + disc.add_microversion( + href=url, id='v2') + disc.add_microversion( + href=url, id='v2.1', + min_version='2.1', max_version='2.35') + + self.stub_url('GET', [], base_url=url, json=disc) + + a = self.create_auth_plugin() + s = session.Session(auth=a) + + identity_endpoint = 'http://127.0.0.1:35357/{}/'.format(self.version) + data = s.get_all_version_data(interface=None) + self.assertEqual({ + 'RegionOne': { + 'admin': { + 'block-storage': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'SUPPORTED', + 'status': 'SUPPORTED', + 'url': 'https://block-storage.example.com/admin/v2', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '3.20', + 'min_microversion': '3.0', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'CURRENT', + 'status': 'CURRENT', + 'url': 'https://block-storage.example.com/admin/v3', + 'version': '3.0' + }], + 'compute': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/admin', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '2.35', + 'min_microversion': '2.1', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/admin', + 'version': '2.1'}], + 'identity': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': None, + 'status': 'CURRENT', + 'url': identity_endpoint, + 'version': self.discovery_version, + }] + }, + 'internal': { + 'baremetal': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': None, + 'status': 'CURRENT', + 'url': 'https://baremetal.example.com/internal/', + 'version': None + }], + 'block-storage': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'SUPPORTED', + 'status': 'SUPPORTED', + 'url': 'https://block-storage.example.com/internal/v2', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '3.20', + 'min_microversion': '3.0', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'CURRENT', + 'status': 'CURRENT', + 'url': 'https://block-storage.example.com/internal/v3', + 'version': '3.0' + }], + 'compute': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/internal', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '2.35', + 'min_microversion': '2.1', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/internal', + 'version': '2.1' + }] + }, + 'public': { + 'block-storage': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'SUPPORTED', + 'status': 'SUPPORTED', + 'url': 'https://block-storage.example.com/public/v2', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '3.20', + 'min_microversion': '3.0', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'CURRENT', + 'status': 'CURRENT', + 'url': 'https://block-storage.example.com/public/v3', + 'version': '3.0' + }], + 'compute': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/public', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '2.35', + 'min_microversion': '2.1', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/public', + 'version': '2.1', + }] + } + } + }, data) + + def test_get_all_version_data(self): + + cinder_disc = fixture.DiscoveryList(v2=False, v3=False) + + # The version discovery dict will not have a project_id + cinder_disc.add_nova_microversion( + href=self.TEST_VOLUME.versions['v3'].discovery.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. + cinder_disc.add_nova_microversion( + href=self.TEST_VOLUME.versions['v2'].discovery.public, + id='v2.0', status='SUPPORTED') + + self.stub_url( + 'GET', [], + base_url=self.TEST_VOLUME.unversioned.public + '/', + json=cinder_disc) + + nova_disc = fixture.DiscoveryList(v2=False, v3=False) + nova_disc.add_microversion( + href=self.TEST_COMPUTE_PUBLIC, id='v2') + nova_disc.add_microversion( + href=self.TEST_COMPUTE_PUBLIC, id='v2.1', + min_version='2.1', max_version='2.35') + + self.stub_url( + 'GET', [], base_url=self.TEST_COMPUTE_PUBLIC, json=nova_disc) + a = self.create_auth_plugin() + s = session.Session(auth=a) + + data = s.get_all_version_data(interface='public') + self.assertEqual({ + 'RegionOne': { + 'public': { + 'block-storage': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'SUPPORTED', + 'status': 'SUPPORTED', + 'url': 'https://block-storage.example.com/public/v2', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '3.20', + 'min_microversion': '3.0', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'CURRENT', + 'status': 'CURRENT', + 'url': 'https://block-storage.example.com/public/v3', + 'version': '3.0' + }], + 'compute': [{ + 'collection': None, + 'max_microversion': None, + 'min_microversion': None, + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/public', + 'version': '2.0' + }, { + 'collection': None, + 'max_microversion': '2.35', + 'min_microversion': '2.1', + 'next_min_version': None, + 'not_before': None, + 'raw_status': 'stable', + 'status': 'CURRENT', + 'url': 'https://compute.example.com/nova/public', + 'version': '2.1' + }], + } + } + }, data) + def test_endpoint_data_no_version_no_discovery(self): a = self.create_auth_plugin() s = session.Session(auth=a) @@ -1095,6 +1381,10 @@ class V3(CommonIdentityTests, utils.TestCase): def version(self): return 'v3' + @property + def discovery_version(self): + return '3.0' + def get_auth_data(self, **kwargs): kwargs.setdefault('project_id', self.PROJECT_ID) token = fixture.V3Token(**kwargs) @@ -1150,6 +1440,10 @@ class V2(CommonIdentityTests, utils.TestCase): def version(self): return 'v2.0' + @property + def discovery_version(self): + return '2.0' + def create_auth_plugin(self, **kwargs): kwargs.setdefault('auth_url', self.TEST_URL) kwargs.setdefault('username', self.TEST_USER) @@ -1173,7 +1467,8 @@ class V2(CommonIdentityTests, utils.TestCase): region = 'RegionOne' svc = token.add_service('identity') - svc.add_endpoint(self.TEST_ADMIN_URL, region=region) + svc.add_endpoint(admin=self.TEST_ADMIN_URL, region=region, + public=None, internal=None) svc = token.add_service('compute') svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC,