def _do_create_plugin(self, session): plugin = None try: disc = self.get_discovery(session, self.auth_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): LOG.warning('Discovering versions from the identity service ' 'failed when creating the password plugin. ' 'Attempting to determine version from URL.') url_parts = urlparse.urlparse(self.auth_url) path = url_parts.path.lower() if path.startswith('/v2.0'): if self._has_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') plugin = self.create_plugin(session, (2, 0), self.auth_url) elif path.startswith('/v3'): plugin = self.create_plugin(session, (3, 0), self.auth_url) else: disc_data = disc.version_data() v2_with_domain_scope = False for data in disc_data: version = data['version'] if (discover.version_match((2,), version) and self._has_domain_scope): # NOTE(jamielennox): if there are domain parameters there # is no point even trying against v2 APIs. v2_with_domain_scope = True continue plugin = self.create_plugin(session, version, data['url'], raw_status=data['raw_status']) if plugin: break if not plugin and v2_with_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') if plugin: return plugin # so there were no URLs that i could use for auth of any version. raise exceptions.DiscoveryFailure('Could not determine a suitable URL ' 'for the plugin')
def get_version_data(session, url, authenticated=None): """Retrieve raw version data from a url.""" headers = {'Accept': 'application/json'} resp = session.get(url, headers=headers, authenticated=authenticated) try: body_resp = resp.json() except ValueError: pass else: # In the event of querying a root URL we will get back a list of # available versions. try: return body_resp['versions']['values'] except (KeyError, TypeError): pass # Most servers don't have a 'values' element so accept a simple # versions dict if available. try: return body_resp['versions'] except KeyError: pass # Otherwise if we query an endpoint like /v2.0 then we will get back # just the one available version. try: return [body_resp['version']] except KeyError: pass err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text raise exceptions.DiscoveryFailure('Invalid Response - Bad version data ' 'returned: %s' % err_text)
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( project_id=project_id, allow_version_hack=allow_version_hack, min_version=min_version, max_version=max_version): if self._catalog_matches_exactly and not discover_versions: # The version we started with is correct, and we don't want # new data return if vers_url in tried: continue tried.add(vers_url) try: self._disc = get_discovery( session, vers_url, cache=cache, authenticated=False) break except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): continue if not self._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 # NOTE(jamielennox): The logic here is required for backwards # compatibility. By itself it is not ideal. if allow_version_hack: # 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 # you should be able to rely on that version being returned or # the request failing. _LOGGER.warning( '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 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 exceptions.DiscoveryFailure( "Version requested but version discovery document was not" " found and allow_version_hack was False")
def test_discovery_failure(self, get): """Test failure when discovery for placement URL failed. Replicate in devstack: start devstack with placement engine, create valid placement service user and specify it in auth section of [placement] in nova.conf. Stop keystone service. """ get.side_effect = ks_exc.DiscoveryFailure() res = self.cmd._check_placement() self.assertEqual(status.UpgradeCheckCode.FAILURE, res.code) self.assertIn('Discovery for placement API URI failed.', res.details)
def test_run_discovery_cache(self, mock_url_choices, mock_get_disc): # get_discovery raises so we keep looping mock_get_disc.side_effect = exceptions.DiscoveryFailure() # Duplicate 'url1' in here to validate the cache behavior mock_url_choices.return_value = ('url1', 'url2', 'url1', 'url3') epd = discover.EndpointData() epd._run_discovery( 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' self.assertEqual(3, mock_get_disc.call_count) mock_get_disc.assert_has_calls( [mock.call('sess', url, cache='cache', authenticated=False) for url in ('url1', 'url2', 'url3')])
def test_init(self): from keystoneauth1 import exceptions as ks_exc actual_exc = ks_exc.ConnectionError("Something") exc = osclients.AuthenticationFailed(error=actual_exc, url="https://example.com", username="******", project="project") # only original exc should be used self.assertEqual("Something", exc.format_message()) actual_exc = Exception("Something") exc = osclients.AuthenticationFailed(error=actual_exc, url="https://example.com", username="******", project="project") # additional info should be added self.assertEqual( "Failed to authenticate to https://example.com for " "user 'user' in project 'project': " "[Exception] Something", exc.format_message()) # check cutting message actual_exc = ks_exc.DiscoveryFailure( "Could not find versioned identity endpoints when attempting to " "authenticate. Please check that your auth_url is correct. " "Unable to establish connection to https://example.com: " "HTTPConnectionPool(host='example.com', port=80): Max retries " "exceeded with url: / (Caused by NewConnectionError('" "<urllib3.connection.HTTPConnection object at 0x7f32ab9809d0>: " "Failed to establish a new connection: [Errno -2] Name or service" " not known',))") exc = osclients.AuthenticationFailed(error=actual_exc, url="https://example.com", username="******", project="project") # original message should be simplified self.assertEqual( "Could not find versioned identity endpoints when attempting to " "authenticate. Please check that your auth_url is correct. " "Unable to establish connection to https://example.com", exc.format_message())
def create_plugin(self, session, version, url, raw_status=None): if discover.version_match((2, ), version): if self._user_domain_id or self._user_domain_name: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') return v2.Password(auth_url=url, user_id=self._user_id, username=self._username, password=self._password, **self._v2_params) elif discover.version_match((3, ), version): return v3.Password(auth_url=url, user_id=self._user_id, username=self._username, user_domain_id=self._user_domain_id, user_domain_name=self._user_domain_name, password=self._password, **self._v3_params)
def _do_create_plugin(self, session): plugin = None try: disc = self.get_discovery(session, self.auth_url, authenticated=False) except (exceptions.DiscoveryFailure, exceptions.HttpError, exceptions.ConnectionError): LOG.warning('Discovering versions from the identity service ' 'failed when creating the password plugin. ' 'Attempting to determine version from URL.') url_parts = urlparse.urlparse(self.auth_url) path = url_parts.path.lower() if path.startswith('/v2.0'): if self._has_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') plugin = self.create_plugin(session, (2, 0), self.auth_url) elif path.startswith('/v3'): plugin = self.create_plugin(session, (3, 0), self.auth_url) else: # NOTE(jamielennox): version_data is always in oldest to newest # order. This is fine normally because we explicitly skip v2 below # if there is domain data present. With default_domain params # though we want a v3 plugin if available and fall back to v2 so we # have to process in reverse order. FIXME(jamielennox): if we ever # go for another version we should reverse this logic as we always # want to favour the newest available version. reverse = self._default_domain_id or self._default_domain_name disc_data = disc.version_data(reverse=bool(reverse)) v2_with_domain_scope = False for data in disc_data: version = data['version'] if (discover.version_match((2,), version) and self._has_domain_scope): # NOTE(jamielennox): if there are domain parameters there # is no point even trying against v2 APIs. v2_with_domain_scope = True continue plugin = self.create_plugin(session, version, data['url'], raw_status=data['raw_status']) if plugin: break if not plugin and v2_with_domain_scope: raise exceptions.DiscoveryFailure( 'Cannot use v2 authentication with domain scope') if plugin: return plugin # so there were no URLs that i could use for auth of any version. raise exceptions.DiscoveryFailure('Could not determine a suitable URL ' 'for the plugin')
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 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 # defaulting to the most recent version. return elif no_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.url if project_id: self.project_id = project_id discovered_data = None # Maybe we've run discovery in the past and have a document that can # satisfy the request without further work if self._disc: discovered_data = self._disc.versioned_data_for( min_version=min_version, max_version=max_version, url=match_url, **allow) if not discovered_data: self._run_discovery( session=session, cache=cache, 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( min_version=min_version, max_version=max_version, url=match_url, **allow) if not discovered_data: 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))) elif max_version and not min_version: raise exceptions.DiscoveryFailure( "Maximum version {max_version} was not found".format( max_version=version_to_string(max_version))) elif min_version and max_version: raise exceptions.DiscoveryFailure( "No version found between {min_version}" " and {max_version}".format( min_version=version_to_string(min_version), max_version=version_to_string(max_version))) self.min_microversion = discovered_data['min_microversion'] self.max_microversion = discovered_data['max_microversion'] self.next_min_version = discovered_data['next_min_version'] self.not_before = discovered_data['not_before'] self.api_version = discovered_data['version'] # TODO(mordred): these next two things should be done by Discover # in versioned_data_for. discovered_url = discovered_data['url'] # 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. url = urllib.parse.urljoin(self._disc._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_version_data(session, url, authenticated=None): """Retrieve raw version data from a url. The return is a list of dicts of the form:: [{ 'status': 'STABLE', 'id': 'v2.3', 'links': [ { 'href': 'http://network.example.com/v2.3', 'rel': 'self', }, { 'href': 'http://network.example.com/', 'rel': 'collection', }, ], 'min_version': '2.0', 'max_version': '2.7', }, ..., ] Note: The maximum microversion may be specified by `max_version` or `version`, the former superseding the latter. All `*version` keys are optional. Other keys and 'links' entries are permitted, but ignored. :param session: A Session object that can be used for communication. :type session: keystoneauth1.session.Session :param string url: Endpoint or discovery URL from which to retrieve data. :param bool authenticated: Include a token in the discovery call. (optional) Defaults to None. :return: A list of dicts containing version information. :rtype: list(dict) """ headers = {'Accept': 'application/json'} resp = session.get(url, headers=headers, authenticated=authenticated) try: body_resp = resp.json() except ValueError: pass else: # In the event of querying a root URL we will get back a list of # available versions. try: return body_resp['versions']['values'] except (KeyError, TypeError): pass # Most servers don't have a 'values' element so accept a simple # versions dict if available. try: return body_resp['versions'] except KeyError: pass # Otherwise if we query an endpoint like /v2.0 then we will get back # just the one available version. try: return [body_resp['version']] except KeyError: pass err_text = resp.text[:50] + '...' if len(resp.text) > 50 else resp.text raise exceptions.DiscoveryFailure('Invalid Response - Bad version data ' 'returned: %s' % err_text)
def get_discovery(self, session, url, authenticated=None): raise exceptions.DiscoveryFailure()