From bffb2476e7166f337fa3244825a5ff477fb88379 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Tue, 25 Oct 2016 22:52:42 +1300 Subject: [PATCH] Use service catalog from authentication response When using Mistral in multi openstack deployments, user can pass 'X-Target-Auth-Uri' in the header to let Mistral run openstack service actions in different openstack deployment. 'X-Target-Service-Catalog' can also be provided but it's optional. This patch adds 'is_target' attribute to Mistral context, if it's true, Mistral will talk to another openstack deployment, 'service_catalog' in the context can be empty or contain target service catalog provided by user, Mistral will get service catalog dynamically if it's empty; if it's false, the 'service_catalog' in context can also be empty(when auth_enable=False) or the content that get from keystone authentication response. This patch also fix the tempest failure introduced by: https://review.openstack.org/#/c/387883/ Related-Bug: #1634090 Change-Id: Iec3ed0333cd08831f0a15f77e3880f07dd89e1e8 --- mistral/context.py | 25 +++++++---- .../tests/unit/utils/test_keystone_utils.py | 18 +++++--- mistral/utils/openstack/keystone.py | 41 +++++++++++-------- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/mistral/context.py b/mistral/context.py index b281d4d8..42a778e7 100644 --- a/mistral/context.py +++ b/mistral/context.py @@ -76,7 +76,6 @@ class MistralContext(BaseContext): "project_id", "auth_token", "service_catalog", - "target_service_catalog", "user_name", "project_name", "roles", @@ -85,6 +84,7 @@ class MistralContext(BaseContext): "redelivered", "expires_at", "trust_id", + "is_target", ]) def __repr__(self): @@ -108,18 +108,19 @@ def set_ctx(new_ctx): def context_from_headers_and_env(headers, env): params = _extract_auth_params_from_headers(headers) + auth_cacert = params['auth_cacert'] auth_token = params['auth_token'] auth_uri = params['auth_uri'] project_id = params['project_id'] user_id = params['user_id'] user_name = params['user_name'] - target_service_catalog = params['target_service_catalog'] + is_target = params['is_target'] - token_info = env.get('keystone.token_info') + token_info = env.get('keystone.token_info', {}) - if token_info and target_service_catalog is None: - target_service_catalog = token_info['token'] + service_catalog = (params['service_catalog'] if is_target + else token_info.get('token', {})) return MistralContext( auth_uri=auth_uri, @@ -127,7 +128,8 @@ def context_from_headers_and_env(headers, env): user_id=user_id, project_id=project_id, auth_token=auth_token, - target_service_catalog=target_service_catalog, + is_target=is_target, + service_catalog=service_catalog, user_name=user_name, project_name=headers.get('X-Project-Name'), roles=headers.get('X-Roles', "").split(","), @@ -137,7 +139,7 @@ def context_from_headers_and_env(headers, env): def _extract_auth_params_from_headers(headers): - target_service_catalog = None + service_catalog = None if headers.get("X-Target-Auth-Uri"): params = { @@ -148,13 +150,17 @@ def _extract_auth_params_from_headers(headers): 'project_id': headers.get('X-Target-Project-Id'), 'user_id': headers.get('X-Target-User-Id'), 'user_name': headers.get('X-Target-User-Name'), + 'is_target': True } if not params['auth_token']: raise (exc.MistralException( 'Target auth URI (X-Target-Auth-Uri) target auth token ' '(X-Target-Auth-Token) must be present')) - target_service_catalog = _extract_service_catalog_from_headers( + # It's possible that target service catalog is not provided, in this + # case, Mistral needs to get target service catalog dynamically when + # talking to target openstack deployment later on. + service_catalog = _extract_service_catalog_from_headers( headers ) else: @@ -165,9 +171,10 @@ def _extract_auth_params_from_headers(headers): 'project_id': headers.get('X-Project-Id'), 'user_id': headers.get('X-User-Id'), 'user_name': headers.get('X-User-Name'), + 'is_target': False } - params['target_service_catalog'] = target_service_catalog + params['service_catalog'] = service_catalog return params diff --git a/mistral/tests/unit/utils/test_keystone_utils.py b/mistral/tests/unit/utils/test_keystone_utils.py index 2d666948..502c7be9 100644 --- a/mistral/tests/unit/utils/test_keystone_utils.py +++ b/mistral/tests/unit/utils/test_keystone_utils.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mistral import config +from mistral import context as auth_context +from mistral import exceptions from mistral.tests.unit import base from mistral.utils.openstack import keystone @@ -23,10 +24,6 @@ class KeystoneUtilsTest(base.BaseTest): self.values = {'id': 'my_id'} - def override_config(self, name, override, group=None): - config.CONF.set_override(name, override, group) - self.addCleanup(config.CONF.clear_override, name, group) - def test_format_url_dollar_sign(self): url_template = "http://host:port/v1/$(id)s" @@ -46,3 +43,14 @@ class KeystoneUtilsTest(base.BaseTest): expected, keystone.format_url(url_template, self.values) ) + + def test_get_endpoint_for_project_noauth(self): + # service_catalog is not set by default. + auth_context.set_ctx(base.get_context()) + self.addCleanup(auth_context.set_ctx, None) + + self.assertRaises( + exceptions.UnauthorizedException, + keystone.get_endpoint_for_project, + 'keystone' + ) diff --git a/mistral/utils/openstack/keystone.py b/mistral/utils/openstack/keystone.py index 165ac05b..c8bf3ac6 100644 --- a/mistral/utils/openstack/keystone.py +++ b/mistral/utils/openstack/keystone.py @@ -20,8 +20,10 @@ from keystoneclient.v3 import client as ks_client from keystoneclient.v3 import endpoints as ks_endpoints from oslo_config import cfg from oslo_utils import timeutils +import six from mistral import context +from mistral import exceptions CONF = cfg.CONF @@ -76,30 +78,29 @@ def get_endpoint_for_project(service_name=None, service_type=None): service_catalog = obtain_service_catalog(ctx) - catalog = service_catalog.get_endpoints( + service_endpoints = service_catalog.get_endpoints( service_name=service_name, service_type=service_type ) endpoint = None - for service_type in catalog: - service = catalog.get(service_type) - for interface in service: + for endpoints in six.itervalues(service_endpoints): + for ep in endpoints: # is V3 interface? - if 'interface' in interface: - interface_type = interface['interface'] + if 'interface' in ep: + interface_type = ep['interface'] if CONF.os_actions_endpoint_type in interface_type: endpoint = ks_endpoints.Endpoint( None, - interface, + ep, loaded=True ) break # is V2 interface? - if 'publicURL' in interface: + if 'publicURL' in ep: endpoint_data = { - 'url': interface['publicURL'], - 'region': interface['region'] + 'url': ep['publicURL'], + 'region': ep['region'] } endpoint = ks_endpoints.Endpoint( None, @@ -122,6 +123,7 @@ def get_endpoint_for_project(service_name=None, service_type=None): def obtain_service_catalog(ctx): token = ctx.auth_token + if ctx.is_trust_scoped and is_token_trust_scoped(token): if ctx.trust_id is None: raise Exception( @@ -134,22 +136,29 @@ def obtain_service_catalog(ctx): include_catalog=True )['token'] else: - if not ctx.target_service_catalog: + response = ctx.service_catalog + + # Target service catalog may not be passed via API. + if not response and ctx.is_target: response = client().tokens.get_token_data( token, - include_catalog=True)['token'] - else: - response = ctx.target_service_catalog + include_catalog=True + )['token'] + + if not response: + raise exceptions.UnauthorizedException() + service_catalog = ks_service_catalog.ServiceCatalog.factory(response) + return service_catalog def get_keystone_endpoint_v2(): - return get_endpoint_for_project('keystone') + return get_endpoint_for_project('keystone', service_type='identity') def get_keystone_url_v2(): - return get_endpoint_for_project('keystone').url + return get_endpoint_for_project('keystone', service_type='identity').url def format_url(url_template, values):