From 998f5158e17b3e9a3764d31cd108652da844cd3d Mon Sep 17 00:00:00 2001 From: Andras Kovi Date: Wed, 8 Jun 2016 17:26:19 +0200 Subject: [PATCH] Use client credentials to retrieve service list Credentials received in the request are used to retrieve endpoint list from keystone. This avoids the usage of the admin creds and opens the gate towards connecting to any clouds without previous configuration. Implements: blueprint mistral-multi-vim-support Change-Id: Ib5ae5911f2535f4f340af8f4bcb4817818747029 --- mistral/actions/openstack/actions.py | 15 ++-- mistral/context.py | 4 ++ .../tests/unit/utils/test_keystone_utils.py | 72 +++++++++++++++++++ mistral/utils/openstack/keystone.py | 53 ++++++++------ 4 files changed, 118 insertions(+), 26 deletions(-) diff --git a/mistral/actions/openstack/actions.py b/mistral/actions/openstack/actions.py index 0aa518a4..55326312 100644 --- a/mistral/actions/openstack/actions.py +++ b/mistral/actions/openstack/actions.py @@ -109,21 +109,24 @@ class KeystoneAction(base.OpenStackAction): LOG.debug("Keystone action security context: %s" % ctx) + # TODO(akovi) cacert is deprecated in favor of session + # TODO(akovi) this piece of code should be refactored + # TODO(akovi) to follow the new guide lines kwargs = { 'token': ctx.auth_token, - 'auth_url': CONF.keystone_authtoken.auth_uri, + 'auth_url': ctx.auth_uri, 'project_id': ctx.project_id, - 'cacert': CONF.keystone_authtoken.cafile, + 'cacert': ctx.auth_cacert, } # In case of trust-scoped token explicitly pass endpoint parameter. if (ctx.is_trust_scoped or keystone_utils.is_token_trust_scoped(ctx.auth_token)): - kwargs['endpoint'] = CONF.keystone_authtoken.auth_uri + kwargs['endpoint'] = ctx.auth_uri client = self._client_class(**kwargs) - client.management_url = CONF.keystone_authtoken.auth_uri + client.management_url = ctx.auth_uri return client @@ -214,7 +217,7 @@ class NeutronAction(base.OpenStackAction): endpoint_url=neutron_endpoint.url, region_name=neutron_endpoint.region, token=ctx.auth_token, - auth_url=CONF.keystone_authtoken.auth_uri + auth_url=ctx.auth_uri ) @@ -595,7 +598,7 @@ class DesignateAction(base.OpenStackAction): client = self._client_class( endpoint=designate_url, tenant_id=ctx.project_id, - auth_url=CONF.keystone_authtoken.auth_uri, + auth_url=ctx.auth_uri, region_name=designate_endpoint.region, service_type='dns' ) diff --git a/mistral/context.py b/mistral/context.py index 80d23641..8c642ee8 100644 --- a/mistral/context.py +++ b/mistral/context.py @@ -68,6 +68,8 @@ class BaseContext(object): class MistralContext(BaseContext): # Use set([...]) since set literals are not supported in Python 2.6. _elements = set([ + "auth_uri", + "auth_cacert", "user_id", "project_id", "auth_token", @@ -120,6 +122,8 @@ def spawn(thread_description, func, *args, **kwargs): def context_from_headers(headers): return MistralContext( + auth_uri=CONF.keystone_authtoken.auth_uri, + auth_cacert=CONF.keystone_authtoken.cafile, user_id=headers.get('X-User-Id'), project_id=headers.get('X-Project-Id'), auth_token=headers.get('X-Auth-Token'), diff --git a/mistral/tests/unit/utils/test_keystone_utils.py b/mistral/tests/unit/utils/test_keystone_utils.py index 28360f58..d34fef8d 100644 --- a/mistral/tests/unit/utils/test_keystone_utils.py +++ b/mistral/tests/unit/utils/test_keystone_utils.py @@ -15,6 +15,57 @@ from mistral.tests.unit import base from mistral.utils.openstack import keystone +SERVICES_CATALOG = [ + { + "type": "compute", + "name": "nova", + "endpoints": [ + { + "interface": "private", + "url": "https://example.com/nova/private", + "region": "RegionOne" + }, + { + "interface": "public", + "url": "https://example.com/nova/public", + "region": "RegionOne" + } + ] + }, + { + "type": "compute", + "name": "nova2", + "endpoints": [ + { + "interface": "public", + "url": "https://example.com/nova2/public/r1", + "region": "RegionOne" + }, + { + "interface": "public", + "url": "https://example.com/nova2/public/r2", + "region": "RegionTwo" + } + ] + }, + { + "type": "orchestration", + "name": "heat", + "endpoints": [ + { + "interface": "private", + "url": "https://example.com/heat/private", + "region": "RegionOne" + }, + { + "interface": "public", + "url": "https://example.com/heat/public", + "region": "RegionOne" + } + ] + } +] + class KeystoneUtilsTest(base.BaseTest): def setUp(self): @@ -41,3 +92,24 @@ class KeystoneUtilsTest(base.BaseTest): expected, keystone.format_url(url_template, self.values) ) + + def test_service_endpoints_select(self): + def find(name, typ=None, catalog=SERVICES_CATALOG): + return keystone.select_service_endpoints(name, typ, catalog) + + endpoints = find('nova', 'compute') + self.assertEqual('https://example.com/nova/public', endpoints[0].url, + message='public interface must be selected') + + endpoints = find('nova2') + self.assertEqual(2, len(endpoints), + message='public endpoints must be selected ' + 'in each region') + + endpoints = find('heat') + self.assertEqual('https://example.com/heat/public', endpoints[0].url, + message='selection should work without type set') + + endpoints = find('nova', None, []) + self.assertEqual([], endpoints, + message='empty catalog should be accepted') diff --git a/mistral/utils/openstack/keystone.py b/mistral/utils/openstack/keystone.py index ec10467f..db29f3d8 100644 --- a/mistral/utils/openstack/keystone.py +++ b/mistral/utils/openstack/keystone.py @@ -14,6 +14,7 @@ # limitations under the License. from keystoneclient.v3 import client as ks_client +from keystoneclient.v3.endpoints import Endpoint from oslo_config import cfg from mistral import context @@ -23,7 +24,7 @@ CONF = cfg.CONF def client(): ctx = context.ctx() - auth_url = CONF.keystone_authtoken.auth_uri + auth_url = ctx.auth_uri cl = ks_client.Client( username=ctx.user_name, @@ -62,37 +63,49 @@ def client_for_trusts(trust_id): def get_endpoint_for_project(service_name=None, service_type=None): - admin_project_name = CONF.keystone_authtoken.admin_tenant_name - keystone_client = _admin_client(project_name=admin_project_name) - service_list = keystone_client.services.list() - - if service_name: - service_ids = [s.id for s in service_list if s.name == service_name] - elif service_type: - service_ids = [s.id for s in service_list if s.type == service_type] - else: + if service_name is None and service_type is None: raise Exception( "Either 'service_name' or 'service_type' must be provided." ) - if not service_ids: - raise Exception("Either service '%s' or service type " - "'%s' doesn't exist!" % (service_name, service_type)) + ctx = context.ctx() - endpoints = keystone_client.endpoints.list( - service=service_ids[0], - interface='public' - ) + token = ctx.auth_token + response = client().tokens.get_token_data(token, include_catalog=True) + + endpoints = select_service_endpoints( + service_name, + service_type, + response["token"]["catalog"]) if not endpoints: raise Exception( "No endpoints found [service_name=%s, service_type=%s]" % (service_name, service_type) ) + else: + # TODO(rakhmerov): We may have more than one endpoint because + # TODO(rakhmerov): of regions and ideally we need a config option + # TODO(rakhmerov): for region + return endpoints[0] - # TODO(rakhmerov): We may have more than one endpoint because of regions - # TODO(rakhmerov): and ideally we need a config option for region - return endpoints[0] + +def select_service_endpoints(service_name, service_type, services): + endpoints = [] + + for catalog in services: + + if service_name and catalog["name"] != service_name: + continue + + if service_type and catalog["type"] != service_type: + continue + + for endpoint in catalog["endpoints"]: + if endpoint["interface"] == 'public': + endpoints.append(Endpoint(None, endpoint, loaded=True)) + + return endpoints def get_keystone_endpoint_v2():