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