From 9bc1f314692c2849de0767fc18e7c8605efa2d94 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Fri, 23 Jun 2017 10:40:26 +0100 Subject: [PATCH] Remove the dependancy on Mistral The keystone utils for getting project endpoints is the last reason that we import directly from Mistral. This change copies over the relevant code that we need. Removing the dep also discovered two dependancies that we had but didn't explicitly state previously. Change-Id: If4cb4ac4ca75ea0f165196b4a0a08ea3d3a16e25 --- requirements.txt | 3 +- tripleo_common/actions/base.py | 25 ++-- tripleo_common/exception.py | 4 + tripleo_common/tests/actions/test_base.py | 4 +- .../tests/actions/test_parameters.py | 6 +- tripleo_common/utils/keystone.py | 122 ++++++++++++++++++ 6 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 tripleo_common/utils/keystone.py diff --git a/requirements.txt b/requirements.txt index 8a65656f7..cf06782b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,9 +14,10 @@ oslo.utils>=3.20.0 # Apache-2.0 python-glanceclient>=2.7.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0 six>=1.9.0 # MIT -mistral!=2015.1.0,>=3.0.0 # Apache-2.0 mistral-lib>=0.2.0 # Apache-2.0 +oslo.concurrency>=3.8.0 # Apache-2.0 python-ironic-inspector-client>=1.5.0 # Apache-2.0 +python-mistralclient>=3.1.0 # Apache-2.0 Jinja2!=2.9.0,!=2.9.1,!=2.9.2,!=2.9.3,!=2.9.4,>=2.8 # BSD License (3 clause) python-novaclient>=9.0.0 # Apache-2.0 passlib>=1.7.0 # BSD diff --git a/tripleo_common/actions/base.py b/tripleo_common/actions/base.py index c2edb75c4..ced6421fa 100644 --- a/tripleo_common/actions/base.py +++ b/tripleo_common/actions/base.py @@ -19,13 +19,14 @@ from glanceclient.v2 import client as glanceclient from heatclient.v1 import client as heatclient import ironic_inspector_client from ironicclient.v1 import client as ironicclient -from mistral.utils.openstack import keystone as keystone_utils from mistral_lib import actions from mistralclient.api import client as mistral_client from novaclient.client import Client as nova_client from swiftclient import client as swift_client from swiftclient import exceptions as swiftexceptions + from tripleo_common import constants +from tripleo_common.utils import keystone as keystone_utils class TripleOAction(actions.Action): @@ -34,7 +35,7 @@ class TripleOAction(actions.Action): super(TripleOAction, self).__init__() def get_object_client(self, context): - obj_ep = keystone_utils.get_endpoint_for_project('swift') + obj_ep = keystone_utils.get_endpoint_for_project(context, 'swift') kwargs = { 'preauthurl': obj_ep.url % {'tenant_id': context.project_id}, @@ -47,7 +48,8 @@ class TripleOAction(actions.Action): return swift_client.Connection(**kwargs) def get_baremetal_client(self, context): - ironic_endpoint = keystone_utils.get_endpoint_for_project('ironic') + ironic_endpoint = keystone_utils.get_endpoint_for_project( + context, 'ironic') # FIXME(lucasagomes): Use ironicclient.get_client() instead # of ironicclient.Client(). Client() might cause errors since @@ -69,7 +71,7 @@ class TripleOAction(actions.Action): def get_baremetal_introspection_client(self, context): bmi_endpoint = keystone_utils.get_endpoint_for_project( - 'ironic-inspector') + context, 'ironic-inspector') return ironic_inspector_client.ClientV1( api_version='1.2', @@ -79,7 +81,8 @@ class TripleOAction(actions.Action): ) def get_image_client(self, context): - glance_endpoint = keystone_utils.get_endpoint_for_project('glance') + glance_endpoint = keystone_utils.get_endpoint_for_project( + context, 'glance') return glanceclient.Client( glance_endpoint.url, token=context.auth_token, @@ -87,7 +90,8 @@ class TripleOAction(actions.Action): ) def get_orchestration_client(self, context): - heat_endpoint = keystone_utils.get_endpoint_for_project('heat') + heat_endpoint = keystone_utils.get_endpoint_for_project( + context, 'heat') endpoint_url = keystone_utils.format_url( heat_endpoint.url, @@ -102,7 +106,8 @@ class TripleOAction(actions.Action): ) def get_workflow_client(self, context): - mistral_endpoint = keystone_utils.get_endpoint_for_project('mistral') + mistral_endpoint = keystone_utils.get_endpoint_for_project( + context, 'mistral') mc = mistral_client.client(auth_token=context.auth_token, mistral_url=mistral_endpoint.url) @@ -110,8 +115,10 @@ class TripleOAction(actions.Action): return mc def get_compute_client(self, context): - keystone_endpoint = keystone_utils.get_endpoint_for_project('keystone') - nova_endpoint = keystone_utils.get_endpoint_for_project('nova') + keystone_endpoint = keystone_utils.get_endpoint_for_project( + context, 'keystone') + nova_endpoint = keystone_utils.get_endpoint_for_project( + context, 'nova') client = nova_client( 2, diff --git a/tripleo_common/exception.py b/tripleo_common/exception.py index 73dc6905d..d79ecb081 100644 --- a/tripleo_common/exception.py +++ b/tripleo_common/exception.py @@ -118,3 +118,7 @@ class NotFound(Exception): class RoleMetadataError(Exception): """Role metadata is invalid""" + + +class UnauthorizedException(Exception): + """Authorization failed""" diff --git a/tripleo_common/tests/actions/test_base.py b/tripleo_common/tests/actions/test_base.py index 349350389..1bd745acc 100644 --- a/tripleo_common/tests/actions/test_base.py +++ b/tripleo_common/tests/actions/test_base.py @@ -17,10 +17,10 @@ import zlib import mock from ironicclient.v1 import client as ironicclient -from mistral.utils.openstack import keystone as keystone_utils from tripleo_common.actions import base from tripleo_common.tests import base as tests_base +from tripleo_common.utils import keystone as keystone_utils from swiftclient.exceptions import ClientException @@ -41,7 +41,7 @@ class TestActionsBase(tests_base.TestCase): mock_client.assert_called_once_with( 'http://ironic/v1', max_retries=12, os_ironic_api_version='1.15', region_name='ironic-region', retry_interval=5, token=mock.ANY) - mock_endpoint.assert_called_once_with('ironic') + mock_endpoint.assert_called_once_with(mock_cxt, 'ironic') mock_cxt.assert_not_called() def test_cache_key(self, mock_endpoint): diff --git a/tripleo_common/tests/actions/test_parameters.py b/tripleo_common/tests/actions/test_parameters.py index 835d441c3..27ecb835a 100644 --- a/tripleo_common/tests/actions/test_parameters.py +++ b/tripleo_common/tests/actions/test_parameters.py @@ -857,8 +857,7 @@ class GetProfileOfFlavorActionTest(base.TestCase): @mock.patch('tripleo_common.utils.parameters.get_profile_of_flavor') @mock.patch('tripleo_common.actions.base.TripleOAction.' 'get_compute_client') - @mock.patch('mistral.context.ctx') - def test_profile_found(self, mock_ctx, mock_get_compute_client, + def test_profile_found(self, mock_get_compute_client, mock_get_profile_of_flavor): mock_ctx = mock.MagicMock() mock_get_profile_of_flavor.return_value = 'compute' @@ -870,8 +869,7 @@ class GetProfileOfFlavorActionTest(base.TestCase): @mock.patch('tripleo_common.utils.parameters.get_profile_of_flavor') @mock.patch('tripleo_common.actions.base.TripleOAction.' 'get_compute_client') - @mock.patch('mistral.context.ctx') - def test_profile_not_found(self, mock_ctx, mock_get_compute_client, + def test_profile_not_found(self, mock_get_compute_client, mock_get_profile_of_flavor): mock_ctx = mock.MagicMock() profile = (exception.DeriveParamsError, ) diff --git a/tripleo_common/utils/keystone.py b/tripleo_common/utils/keystone.py new file mode 100644 index 000000000..697906454 --- /dev/null +++ b/tripleo_common/utils/keystone.py @@ -0,0 +1,122 @@ +# Copyright (c) 2013 Mirantis Inc. +# Copyright (c) 2017 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keystoneclient import service_catalog as ks_service_catalog +from keystoneclient.v3 import client as ks_client +from keystoneclient.v3 import endpoints as ks_endpoints +import six + +from tripleo_common import exception + + +def client(ctx): + auth_url = ctx.auth_uri + + cl = ks_client.Client( + user_id=ctx.user_id, + token=ctx.auth_token, + tenant_id=ctx.project_id, + auth_url=auth_url + ) + + cl.management_url = auth_url + + return cl + + +def get_endpoint_for_project(ctx, service_name=None, service_type=None, + region_name=None): + if service_name is None and service_type is None: + raise ValueError( + "Either 'service_name' or 'service_type' must be provided." + ) + + service_catalog = obtain_service_catalog(ctx) + + # When region_name is not passed, first get from context as region_name + # could be passed to rest api in http header ('X-Region-Name'). Otherwise, + # just get region from mistral configuration. + region = (region_name or ctx.region_name) + + service_endpoints = service_catalog.get_endpoints( + service_name=service_name, + service_type=service_type, + region_name=region + ) + + endpoint = None + os_actions_endpoint_type = 'public' + + for endpoints in six.itervalues(service_endpoints): + for ep in endpoints: + # is V3 interface? + if 'interface' in ep: + interface_type = ep['interface'] + if os_actions_endpoint_type in interface_type: + endpoint = ks_endpoints.Endpoint( + None, + ep, + loaded=True + ) + break + # is V2 interface? + if 'publicURL' in ep: + endpoint_data = { + 'url': ep['publicURL'], + 'region': ep['region'] + } + endpoint = ks_endpoints.Endpoint( + None, + endpoint_data, + loaded=True + ) + break + + if not endpoint: + raise RuntimeError( + "No endpoints found [service_name=%s, service_type=%s," + " region_name=%s]" + % (service_name, service_type, region) + ) + else: + return endpoint + + +def obtain_service_catalog(ctx): + token = ctx.auth_token + + 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'] + + if not response: + raise exception.UnauthorizedException() + + service_catalog = ks_service_catalog.ServiceCatalog.factory(response) + + return service_catalog + + +def format_url(url_template, values): + # Since we can't use keystone module, we can do similar thing: + # see https://github.com/openstack/keystone/blob/master/keystone/ + # catalog/core.py#L42-L60 + return url_template.replace('$(', '%(') % values