From f40169327bf21a85848b36ed950657286e40c64f Mon Sep 17 00:00:00 2001 From: Stan Lagun Date: Thu, 4 Sep 2014 15:44:44 +0400 Subject: [PATCH] Use Keystone trusts to get fresh token Instead of using user's auth token (which can expire) for interactions with other services engine creates Keystone trust that impersonate user and create new tokens on demand. Heat stack is created on deployment start using token rather than trust so that Heat could establish trust of its own (trusts cannot be chained). New behavior is disabled by default and can be enabled using [engine]/use_trusts = True in murano.conf. With trusts enabled engine will not work with Heat prior to Juno. For Heat stacks with deferred actions or long deployment time to work it is also required to turn on trusts in Heat itself. This can be done via [DEFAULT]/deferred_auth_method=trusts in heat.conf and ensuring that current user has heat_stack_owner role (or any other that is in [DEFAULT]/trusts_delegated_roles=trusts in heat.conf) Change-Id: Ic9f3f956ddb6ff2a300a08056ee841cf3c0db870 Implements: blueprint auth-for-long-running-requests --- meta/io.murano/Classes/Environment.yaml | 18 +-- murano/common/config.py | 7 +- murano/common/engine.py | 78 +++++++++---- murano/engine/auth_utils.py | 101 +++++++++++++++++ murano/engine/client_manager.py | 142 ++++++++++++++++++++++++ murano/engine/environment.py | 3 + murano/engine/package_loader.py | 47 +------- murano/engine/system/heat_stack.py | 100 ++++++----------- murano/engine/system/net_explorer.py | 68 ++++-------- murano/tests/unit/test_heat_stack.py | 38 ++++--- 10 files changed, 403 insertions(+), 199 deletions(-) create mode 100644 murano/engine/auth_utils.py create mode 100644 murano/engine/client_manager.py diff --git a/meta/io.murano/Classes/Environment.yaml b/meta/io.murano/Classes/Environment.yaml index c8711eb56..acac1fdd4 100644 --- a/meta/io.murano/Classes/Environment.yaml +++ b/meta/io.murano/Classes/Environment.yaml @@ -59,15 +59,15 @@ Methods: deploy: Usage: Action Body: - Try: - - $.agentListener.start() - - If: len($.applications) = 0 - Then: - - $.stack.delete() - Else: - - $.applications.pselect($.deploy()) - Finally: - - $.agentListener.stop() + - $minimalStack: + resources: {} + - $.stack.updateTemplate($minimalStack) + - $.stack.push() + - Try: + - $.agentListener.start() + - $.applications.pselect($.deploy()) + Finally: + - $.agentListener.stop() destroy: Body: diff --git a/murano/common/config.py b/murano/common/config.py index d01daa90c..aec55300d 100644 --- a/murano/common/config.py +++ b/murano/common/config.py @@ -98,8 +98,6 @@ neutron_opts = [ ] keystone_opts = [ - cfg.StrOpt('auth_url', help='URL to access OpenStack Identity service.'), - cfg.BoolOpt('insecure', default=False, help='This option explicitly allows Murano to perform ' '"insecure" SSL connections and transfers with ' @@ -180,7 +178,10 @@ stats_opts = [ engine_opts = [ cfg.BoolOpt('disable_murano_agent', default=False, - help=_('Disallow the use of murano-agent')) + help=_('Disallow the use of murano-agent')), + cfg.BoolOpt('use_trusts', default=False, + help=_("Create resources using trust token rather " + "than user's token")) ] # TODO(sjmc7): move into engine opts? diff --git a/murano/common/engine.py b/murano/common/engine.py index b6bd7b584..c6714768d 100644 --- a/murano/common/engine.py +++ b/murano/common/engine.py @@ -26,6 +26,8 @@ from murano.common import rpc from murano.dsl import dsl_exception from murano.dsl import executor from murano.dsl import results_serializer +from murano.engine import auth_utils +from murano.engine import client_manager from murano.engine import environment from murano.engine import package_class_loader from murano.engine import package_loader @@ -103,32 +105,50 @@ class TaskExecutor(object): self._environment = environment.Environment() self._environment.token = task['token'] self._environment.tenant_id = task['tenant_id'] + self._environment.system_attributes = self._model.get('SystemData', {}) + self._environment.clients = client_manager.ClientManager() def execute(self): - token, tenant_id = self.environment.token, self.environment.tenant_id - with package_loader.ApiPackageLoader(token, tenant_id) as pkg_loader: - class_loader = package_class_loader.PackageClassLoader(pkg_loader) - system_objects.register(class_loader, pkg_loader) + self._create_trust() - exc = executor.MuranoDslExecutor(class_loader, self.environment) - obj = exc.load(self.model) + try: + # pkg_loader = package_loader.DirectoryPackageLoader('./meta') + # return self._execute(pkg_loader) - try: - # Skip execution of action in case of no action is provided. - # Model will be just loaded, cleaned-up and unloaded. - # Most of the time this is used for deletion of environments. - if self.action: - self._invoke(exc) - except Exception as e: - if isinstance(e, dsl_exception.MuranoPlException): - LOG.error('\n' + e.format(prefix=' ')) - else: - LOG.exception(e) - reporter = status_reporter.StatusReporter() - reporter.initialize(obj) - reporter.report_error(obj, str(e)) + murano_client_factory = lambda: \ + self._environment.clients.get_murano_client(self._environment) + with package_loader.ApiPackageLoader( + murano_client_factory) as pkg_loader: + return self._execute(pkg_loader) + finally: + if self._model['Objects'] is None: + self._delete_trust() - return results_serializer.serialize(obj, exc) + def _execute(self, pkg_loader): + class_loader = package_class_loader.PackageClassLoader(pkg_loader) + system_objects.register(class_loader, pkg_loader) + + exc = executor.MuranoDslExecutor(class_loader, self.environment) + obj = exc.load(self.model) + + try: + # Skip execution of action in case of no action is provided. + # Model will be just loaded, cleaned-up and unloaded. + # Most of the time this is used for deletion of environments. + if self.action: + self._invoke(exc) + except Exception as e: + if isinstance(e, dsl_exception.MuranoPlException): + LOG.error('\n' + e.format(prefix=' ')) + else: + LOG.exception(e) + reporter = status_reporter.StatusReporter() + reporter.initialize(obj) + reporter.report_error(obj, str(e)) + + result = results_serializer.serialize(obj, exc) + result['SystemData'] = self._environment.system_attributes + return result def _invoke(self, mpl_executor): obj = mpl_executor.object_store.get(self.action['object_id']) @@ -136,3 +156,19 @@ class TaskExecutor(object): if obj is not None: obj.type.invoke(method_name, mpl_executor, obj, args) + + def _create_trust(self): + if not config.CONF.engine.use_trusts: + return + trust_id = self._environment.system_attributes.get('TrustId') + if not trust_id: + trust_id = auth_utils.create_trust(self._environment) + self._environment.system_attributes['TrustId'] = trust_id + self._environment.trust_id = trust_id + + def _delete_trust(self): + trust_id = self._environment.trust_id + if trust_id: + auth_utils.delete_trust(self._environment) + self._environment.system_attributes['TrustId'] = None + self._environment.trust_id = None diff --git a/murano/engine/auth_utils.py b/murano/engine/auth_utils.py new file mode 100644 index 000000000..9f4935151 --- /dev/null +++ b/murano/engine/auth_utils.py @@ -0,0 +1,101 @@ +# Copyright (c) 2014 Mirantis, 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.v3 import client as ks_client +from oslo.config import cfg + +from murano.openstack.common import importutils + + +def get_client(environment): + settings = _get_keystone_settings() + kwargs = { + 'token': environment.token, + 'tenant_id': environment.tenant_id, + 'auth_url': settings['auth_url'] + } + kwargs.update(settings['ssl']) + keystone = ks_client.Client(**kwargs) + keystone.management_url = settings['auth_url'] + + return keystone + + +def get_client_for_admin(project_name): + return _admin_client(project_name=project_name) + + +def _admin_client(trust_id=None, project_name=None): + settings = _get_keystone_settings() + + kwargs = { + 'project_name': project_name, + 'trust_id': trust_id + } + for key in ('username', 'password', 'auth_url'): + kwargs[key] = settings[key] + kwargs.update(settings['ssl']) + + client = ks_client.Client(**kwargs) + + # without resetting this attributes keystone client cannot re-authenticate + client.project_id = None + client.project_name = None + + client.management_url = settings['auth_url'] + + return client + + +def get_client_for_trusts(environment): + return _admin_client(environment.trust_id) + + +def create_trust(environment): + client = get_client(environment) + + settings = _get_keystone_settings() + trustee_id = get_client_for_admin( + settings['project_name']).user_id + + roles = [t['name'] for t in client.auth_ref['roles']] + trust = client.trusts.create(trustor_user=client.user_id, + trustee_user=trustee_id, + impersonation=True, + role_names=roles, + project=environment.tenant_id) + + return trust.id + + +def delete_trust(environment): + keystone_client = get_client_for_trusts(environment) + keystone_client.trusts.delete(environment.trust_id) + + +def _get_keystone_settings(): + importutils.import_module('keystonemiddleware.auth_token') + return { + 'auth_url': cfg.CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3'), + 'username': cfg.CONF.keystone_authtoken.admin_user, + 'password': cfg.CONF.keystone_authtoken.admin_password, + 'project_name': cfg.CONF.keystone_authtoken.admin_tenant_name, + 'ssl': { + 'cacert': cfg.CONF.keystone.ca_file, + 'insecure': cfg.CONF.keystone.insecure, + 'cert': cfg.CONF.keystone.cert_file, + 'key': cfg.CONF.keystone.key_file + } + } diff --git a/murano/engine/client_manager.py b/murano/engine/client_manager.py new file mode 100644 index 000000000..7e5806e93 --- /dev/null +++ b/murano/engine/client_manager.py @@ -0,0 +1,142 @@ +# Copyright (c) 2014 Mirantis, 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 eventlet import semaphore +import heatclient.client as hclient +import muranoclient.v1.client as muranoclient +import neutronclient.v2_0.client as nclient + +from murano.common import config +from murano.dsl import helpers +from murano.engine import auth_utils +from murano.engine import environment + + +class ClientManager(object): + def __init__(self): + self._trusts_keystone_client = None + self._token_keystone_client = None + self._cache = {} + self._semaphore = semaphore.BoundedSemaphore() + + def _get_environment(self, context): + if isinstance(context, environment.Environment): + return context + return helpers.get_environment(context) + + def _get_client(self, context, name, use_trusts, client_factory): + if not config.CONF.engine.use_trusts: + use_trusts = False + + keystone_client = None if name == 'keystone' else \ + self.get_keystone_client(context, use_trusts) + + self._semaphore.acquire() + try: + client, used_token = self._cache.get( + (name, use_trusts), (None, None)) + fresh_token = None if keystone_client is None \ + else keystone_client.auth_token + if use_trusts and used_token != fresh_token: + client = None + if not client: + token = fresh_token + if not use_trusts: + env = self._get_environment(context) + token = env.token + client = client_factory(keystone_client, token) + self._cache[(name, use_trusts)] = (client, token) + return client + finally: + self._semaphore.release() + + def get_keystone_client(self, context, use_trusts=True): + if not config.CONF.engine.use_trusts: + use_trusts = False + env = self._get_environment(context) + factory = lambda _1, _2: auth_utils.get_client_for_trusts(env) \ + if use_trusts else auth_utils.get_client(env) + + return self._get_client(context, 'keystone', use_trusts, factory) + + def get_heat_client(self, context, use_trusts=True): + if not config.CONF.engine.use_trusts: + use_trusts = False + + def factory(keystone_client, auth_token): + heat_settings = config.CONF.heat + + heat_url = keystone_client.service_catalog.url_for( + service_type='orchestration', + endpoint_type=heat_settings.endpoint_type) + + kwargs = { + 'token': auth_token, + 'ca_file': heat_settings.ca_file or None, + 'cert_file': heat_settings.cert_file or None, + 'key_file': heat_settings.key_file or None, + 'insecure': heat_settings.insecure + } + + if not config.CONF.engine.use_trusts: + kwargs.update({ + 'username': 'badusername', + 'password': 'badpassword' + }) + return hclient.Client('1', heat_url, **kwargs) + + return self._get_client(context, 'heat', use_trusts, factory) + + def get_neutron_client(self, context, use_trusts=True): + if not config.CONF.engine.use_trusts: + use_trusts = False + + def factory(keystone_client, auth_token): + neutron_settings = config.CONF.neutron + + neutron_url = keystone_client.service_catalog.url_for( + service_type='network', + endpoint_type=neutron_settings.endpoint_type) + + return nclient.Client( + endpoint_url=neutron_url, + token=auth_token, + ca_cert=neutron_settings.ca_cert or None, + insecure=neutron_settings.insecure) + + return self._get_client(context, 'neutron', use_trusts, factory) + + def get_murano_client(self, context, use_trusts=True): + if not config.CONF.engine.use_trusts: + use_trusts = False + + def factory(keystone_client, auth_token): + murano_settings = config.CONF.murano + + murano_url = \ + murano_settings.url or keystone_client.service_catalog.url_for( + service_type='application_catalog', + endpoint_type=murano_settings.endpoint_type) + + return muranoclient.Client( + endpoint=murano_url, + key_file=murano_settings.key_file or None, + cacert=murano_settings.cacert or None, + cert_file=murano_settings.cert_file or None, + insecure=murano_settings.insecure, + auth_url=keystone_client.auth_url, + token=auth_token) + + return self._get_client(context, 'murano', use_trusts, factory) diff --git a/murano/engine/environment.py b/murano/engine/environment.py index e696a4677..df9ed561a 100644 --- a/murano/engine/environment.py +++ b/murano/engine/environment.py @@ -18,3 +18,6 @@ class Environment(object): def __init__(self): self.token = None self.tenant_id = None + self.trust_id = None + self.system_attributes = {} + self.clients = None diff --git a/murano/engine/package_loader.py b/murano/engine/package_loader.py index 0008ab37a..6af9cb544 100644 --- a/murano/engine/package_loader.py +++ b/murano/engine/package_loader.py @@ -20,9 +20,7 @@ import sys import tempfile import uuid -from keystoneclient.v2_0 import client as keystoneclient from muranoclient.common import exceptions as muranoclient_exc -from muranoclient.v1 import client as muranoclient import six from murano.common import config @@ -46,9 +44,9 @@ class PackageLoader(six.with_metaclass(abc.ABCMeta)): class ApiPackageLoader(PackageLoader): - def __init__(self, token_id, tenant_id): + def __init__(self, murano_client_factory): self._cache_directory = self._get_cache_directory() - self._client = self._get_murano_client(token_id, tenant_id) + self._murano_client_factory = murano_client_factory def get_package_by_class(self, name): filter_opts = {'class_name': name, 'limit': 1} @@ -81,44 +79,10 @@ class ApiPackageLoader(PackageLoader): LOG.debug('Cache for package loader is located at: %s' % directory) return directory - @staticmethod - def _get_murano_client(token_id, tenant_id): - murano_settings = config.CONF.murano - - endpoint_url = murano_settings.url - if endpoint_url is None: - keystone_settings = config.CONF.keystone - keystone_client = keystoneclient.Client( - endpoint=keystone_settings.auth_url, - cacert=keystone_settings.ca_file or None, - cert=keystone_settings.cert_file or None, - key=keystone_settings.key_file or None, - insecure=keystone_settings.insecure - ) - - if not keystone_client.authenticate( - auth_url=keystone_settings.auth_url, - tenant_id=tenant_id, - token=token_id): - raise muranoclient_exc.HTTPUnauthorized() - - endpoint_url = keystone_client.service_catalog.url_for( - service_type='application_catalog', - endpoint_type=murano_settings.endpoint_type - ) - - return muranoclient.Client( - endpoint=endpoint_url, - key_file=murano_settings.key_file or None, - cacert=murano_settings.cacert or None, - cert_file=murano_settings.cert_file or None, - insecure=murano_settings.insecure, - token=token_id - ) - def _get_definition(self, filter_opts): try: - packages = self._client.packages.filter(**filter_opts) + packages = self._murano_client_factory().packages.filter( + **filter_opts) try: return packages.next() except StopIteration: @@ -145,7 +109,8 @@ class ApiPackageLoader(PackageLoader): LOG.exception('Unable to load package from cache. Clean-up...') shutil.rmtree(package_directory, ignore_errors=True) try: - package_data = self._client.packages.download(package_id) + package_data = self._murano_client_factory().packages.download( + package_id) except muranoclient_exc.HTTPException as e: msg = 'Error loading package id {0}: {1}'.format( package_id, str(e) diff --git a/murano/engine/system/heat_stack.py b/murano/engine/system/heat_stack.py index 717fb3709..d0c4020f8 100644 --- a/murano/engine/system/heat_stack.py +++ b/murano/engine/system/heat_stack.py @@ -16,11 +16,8 @@ import copy import eventlet -import heatclient.client as hclient import heatclient.exc as heat_exc -import keystoneclient.v2_0.client as ksclient -import murano.common.config as config import murano.common.utils as utils import murano.dsl.helpers as helpers import murano.dsl.murano_class as murano_class @@ -44,45 +41,15 @@ class HeatStack(murano_object.MuranoObject): self._parameters = {} self._applied = True self._description = description - environment = helpers.get_environment(_context) - keystone_settings = config.CONF.keystone - heat_settings = config.CONF.heat + self._clients = helpers.get_environment(_context).clients - client = ksclient.Client( - endpoint=keystone_settings.auth_url, - cacert=keystone_settings.ca_file or None, - cert=keystone_settings.cert_file or None, - key=keystone_settings.key_file or None, - insecure=keystone_settings.insecure) - - if not client.authenticate( - auth_url=keystone_settings.auth_url, - tenant_id=environment.tenant_id, - token=environment.token): - raise heat_exc.HTTPUnauthorized() - - heat_url = client.service_catalog.url_for( - service_type='orchestration', - endpoint_type=heat_settings.endpoint_type) - - self._heat_client = hclient.Client( - '1', - heat_url, - username='badusername', - password='badpassword', - token_only=True, - token=client.auth_token, - ca_file=heat_settings.ca_file or None, - cert_file=heat_settings.cert_file or None, - key_file=heat_settings.key_file or None, - insecure=heat_settings.insecure) - - def current(self): + def current(self, _context): + client = self._clients.get_heat_client(_context) if self._template is not None: return self._template try: - stack_info = self._heat_client.stacks.get(stack_id=self._name) - template = self._heat_client.stacks.template( + stack_info = client.stacks.get(stack_id=self._name) + template = client.stacks.template( stack_id='{0}/{1}'.format( stack_info.stack_name, stack_info.id)) @@ -98,14 +65,14 @@ class HeatStack(murano_object.MuranoObject): self._parameters.clear() return {} - def parameters(self): - self.current() + def parameters(self, _context): + self.current(_context) return self._parameters.copy() - def reload(self): + def reload(self, _context): self._template = None self._parameters.clear() - return self.current() + return self.current(_context) def setTemplate(self, template): self._template = template @@ -116,14 +83,14 @@ class HeatStack(murano_object.MuranoObject): self._parameters = parameters self._applied = False - def updateTemplate(self, template): + def updateTemplate(self, _context, template): template_version = template.get('heat_template_version', HEAT_TEMPLATE_VERSION) if template_version != HEAT_TEMPLATE_VERSION: err_msg = ("Currently only heat_template_version %s " "is supported." % HEAT_TEMPLATE_VERSION) raise HeatStackError(err_msg) - self.current() + self.current(_context) self._template = helpers.merge_dicts(self._template, template) self._applied = False @@ -132,23 +99,24 @@ class HeatStack(murano_object.MuranoObject): return dict((k, v) for k, v in parameters.iteritems() if not k.startswith('OS::')) - def _get_status(self): + def _get_status(self, context): status = [None] def status_func(state_value): status[0] = state_value return True - self._wait_state(status_func) + self._wait_state(context, status_func) return status[0] - def _wait_state(self, status_func): + def _wait_state(self, context, status_func): tries = 4 delay = 1 while tries > 0: while True: + client = self._clients.get_heat_client(context) try: - stack_info = self._heat_client.stacks.get( + stack_info = client.stacks.get( stack_id=self._name) status = stack_info.stack_status tries = 4 @@ -164,7 +132,7 @@ class HeatStack(murano_object.MuranoObject): eventlet.sleep(delay) break - if 'IN_PROGRESS' in status: + if 'IN_PROGRESS' in status or status == '_': eventlet.sleep(2) continue if not status_func(status): @@ -180,10 +148,10 @@ class HeatStack(murano_object.MuranoObject): return {} return {} - def output(self): - return self._wait_state(lambda status: True) + def output(self, _context): + return self._wait_state(_context, lambda status: True) - def push(self): + def push(self, _context): if self._applied or self._template is None: return @@ -196,41 +164,47 @@ class HeatStack(murano_object.MuranoObject): template = copy.deepcopy(self._template) LOG.info('Pushing: {0}'.format(template)) - current_status = self._get_status() + current_status = self._get_status(_context) resources = template.get('Resources') or template.get('resources') if current_status == 'NOT_FOUND': - if resources: - self._heat_client.stacks.create( + if resources is not None: + token_client = self._clients.get_heat_client(_context, False) + token_client.stacks.create( stack_name=self._name, parameters=self._parameters, template=template, disable_rollback=True) self._wait_state( + _context, lambda status: status == 'CREATE_COMPLETE') else: - if resources: - self._heat_client.stacks.update( + if resources is not None: + trust_client = self._clients.get_heat_client(_context) + + trust_client.stacks.update( stack_id=self._name, parameters=self._parameters, template=template) self._wait_state( + _context, lambda status: status == 'UPDATE_COMPLETE') else: - self.delete() + self.delete(_context) self._applied = not utils.is_different(self._template, template) - def delete(self): + def delete(self, _context): + client = self._clients.get_heat_client(_context) try: - if not self.current(): + if not self.current(_context): return - self._heat_client.stacks.delete( - stack_id=self._name) + client.stacks.delete(stack_id=self._name) self._wait_state( + _context, lambda status: status in ('DELETE_COMPLETE', 'NOT_FOUND')) except heat_exc.NotFound: - LOG.warn("Stack {0} already deleted?".format(self._name)) + LOG.warn('Stack {0} already deleted?'.format(self._name)) self._template = {} self._applied = True diff --git a/murano/engine/system/net_explorer.py b/murano/engine/system/net_explorer.py index f1aa9b711..a0366df5a 100644 --- a/murano/engine/system/net_explorer.py +++ b/murano/engine/system/net_explorer.py @@ -14,11 +14,8 @@ # limitations under the License. import math -import keystoneclient.apiclient.exceptions as ks_exc -import keystoneclient.v2_0.client as ksclient import netaddr from netaddr.strategy import ipv4 -import neutronclient.v2_0.client as nclient import murano.common.config as config import murano.dsl.helpers as helpers @@ -36,43 +33,18 @@ class NetworkExplorer(murano_object.MuranoObject): # noinspection PyAttributeOutsideInit def initialize(self, _context): environment = helpers.get_environment(_context) + self._clients = environment.clients self._tenant_id = environment.tenant_id - keystone_settings = config.CONF.keystone - neutron_settings = config.CONF.neutron self._settings = config.CONF.networking - - keystone_client = ksclient.Client( - endpoint=keystone_settings.auth_url, - cacert=keystone_settings.ca_file or None, - cert=keystone_settings.cert_file or None, - key=keystone_settings.key_file or None, - insecure=keystone_settings.insecure) - - if not keystone_client.authenticate( - auth_url=keystone_settings.auth_url, - tenant_id=environment.tenant_id, - token=environment.token): - raise ks_exc.AuthorizationFailure() - - neutron_url = keystone_client.service_catalog.url_for( - service_type='network', - endpoint_type=neutron_settings.endpoint_type) - - self._neutron = \ - nclient.Client(endpoint_url=neutron_url, - token=environment.token, - ca_cert=neutron_settings.ca_cert or None, - insecure=neutron_settings.insecure) - self._available_cidrs = self._generate_possible_cidrs() # noinspection PyPep8Naming - def getDefaultRouter(self): + def getDefaultRouter(self, _context): + client = self._clients.get_neutron_client(_context) router_name = self._settings.router_name - routers = self._neutron.\ - list_routers(tenant_id=self._tenant_id, name=router_name).\ - get('routers') + routers = client.list_routers( + tenant_id=self._tenant_id, name=router_name).get('routers') if len(routers) == 0: LOG.debug('Router {0} not found'.format(router_name)) if self._settings.create_router: @@ -82,8 +54,7 @@ class NetworkExplorer(murano_object.MuranoObject): kwargs = {'id': external_network} \ if uuidutils.is_uuid_like(external_network) \ else {'name': external_network} - networks = self._neutron.list_networks(**kwargs). \ - get('networks') + networks = client.list_networks(**kwargs).get('networks') ext_nets = filter(lambda n: n['router:external'], networks) if len(ext_nets) == 0: raise KeyError('Router %s could not be created, ' @@ -99,8 +70,7 @@ class NetworkExplorer(murano_object.MuranoObject): 'admin_state_up': True, } } - router = self._neutron.create_router(body=body_data).\ - get('router') + router = client.create_router(body=body_data).get('router') LOG.debug('Created router: {0}'.format(router)) return router['id'] else: @@ -113,13 +83,13 @@ class NetworkExplorer(murano_object.MuranoObject): return router_id # noinspection PyPep8Naming - def getAvailableCidr(self, routerId, netId): + def getAvailableCidr(self, _context, routerId, netId): """Uses hash of network IDs to minimize the collisions: different nets will attempt to pick different cidrs out of available range. If the cidr is taken will pick another one """ - taken_cidrs = self._get_cidrs_taken_by_router(routerId) + taken_cidrs = self._get_cidrs_taken_by_router(_context, routerId) id_hash = hash(netId) num_fails = 0 while num_fails < len(self._available_cidrs): @@ -137,20 +107,22 @@ class NetworkExplorer(murano_object.MuranoObject): return self._settings.default_dns # noinspection PyPep8Naming - def getExternalNetworkIdForRouter(self, routerId): - router = self._neutron.show_router(routerId).get('router') + def getExternalNetworkIdForRouter(self, _context, routerId): + client = self._clients.get_neutron_client(_context) + router = client.show_router(routerId).get('router') if not router or 'external_gateway_info' not in router: return None return router['external_gateway_info'].get('network_id') # noinspection PyPep8Naming - def getExternalNetworkIdForNetwork(self, networkId): - network = self._neutron.show_network(networkId).get('network') + def getExternalNetworkIdForNetwork(self, _context, networkId): + client = self._clients.get_neutron_client(_context) + network = client.show_network(networkId).get('network') if network.get('router:external', False): return networkId # Get router interfaces of the network - router_ports = self._neutron.list_ports( + router_ports = client.list_ports( **{'device_owner': 'network:router_interface', 'network_id': networkId}).get('ports') @@ -158,21 +130,23 @@ class NetworkExplorer(murano_object.MuranoObject): # check if the router has external_gateway set for router_port in router_ports: ext_net_id = self.getExternalNetworkIdForRouter( + _context, router_port.get('device_id')) if ext_net_id: return ext_net_id return None - def _get_cidrs_taken_by_router(self, router_id): + def _get_cidrs_taken_by_router(self, _context, router_id): if not router_id: return [] - ports = self._neutron.list_ports(device_id=router_id)['ports'] + client = self._clients.get_neutron_client(_context) + ports = client.list_ports(device_id=router_id)['ports'] subnet_ids = [] for port in ports: for fixed_ip in port['fixed_ips']: subnet_ids.append(fixed_ip['subnet_id']) - all_subnets = self._neutron.list_subnets()['subnets'] + all_subnets = client.list_subnets()['subnets'] filtered_cidrs = [netaddr.IPNetwork(subnet['cidr']) for subnet in all_subnets if subnet['id'] in subnet_ids] diff --git a/murano/tests/unit/test_heat_stack.py b/murano/tests/unit/test_heat_stack.py index 04db94ba4..b46b383fa 100644 --- a/murano/tests/unit/test_heat_stack.py +++ b/murano/tests/unit/test_heat_stack.py @@ -13,11 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from heatclient.v1 import stacks import mock -from murano.tests.unit import base from murano.dsl import murano_object +from murano.engine import client_manager from murano.engine.system import heat_stack +from murano.tests.unit import base MOD_NAME = 'murano.engine.system.heat_stack' @@ -29,9 +31,15 @@ class TestHeatStack(base.MuranoTestCase): self.mock_murano_obj = mock.Mock(spec=murano_object.MuranoObject) self.mock_murano_obj.name = 'TestObj' self.mock_murano_obj.parents = [] + self.heat_client_mock = mock.MagicMock() + self.heat_client_mock.stacks = mock.MagicMock(spec=stacks.StackManager) + self.client_manager_mock = mock.Mock( + spec=client_manager.ClientManager) - @mock.patch('heatclient.client.Client') - def test_push_adds_version(self, mock_heat_client): + self.client_manager_mock.get_heat_client.return_value = \ + self.heat_client_mock + + def test_push_adds_version(self): """Assert that if heat_template_version is omitted, it's added.""" # Note that the 'with x as y, a as b:' syntax was introduced in # python 2.7, and contextlib.nested was deprecated in py2.7 @@ -43,20 +51,20 @@ class TestHeatStack(base.MuranoTestCase): hs = heat_stack.HeatStack(self.mock_murano_obj, None, None, None) - hs._heat_client = mock_heat_client hs._name = 'test-stack' hs._description = 'Generated by TestHeatStack' hs._template = {'resources': {'test': 1}} hs._parameters = {} hs._applied = False - hs.push() + hs._clients = self.client_manager_mock + hs.push(None) expected_template = { 'heat_template_version': '2013-05-23', 'description': 'Generated by TestHeatStack', 'resources': {'test': 1} } - mock_heat_client.stacks.create.assert_called_with( + self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, @@ -64,8 +72,7 @@ class TestHeatStack(base.MuranoTestCase): ) self.assertTrue(hs._applied) - @mock.patch('heatclient.client.Client') - def test_description_is_optional(self, mock_heat_client): + def test_description_is_optional(self): """Assert that if heat_template_version is omitted, it's added.""" # Note that the 'with x as y, a as b:' syntax was introduced in # python 2.7, and contextlib.nested was deprecated in py2.7 @@ -77,19 +84,19 @@ class TestHeatStack(base.MuranoTestCase): hs = heat_stack.HeatStack(self.mock_murano_obj, None, None, None) - hs._heat_client = mock_heat_client + hs._clients = self.client_manager_mock hs._name = 'test-stack' hs._description = None hs._template = {'resources': {'test': 1}} hs._parameters = {} hs._applied = False - hs.push() + hs.push(None) expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } - mock_heat_client.stacks.create.assert_called_with( + self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, @@ -107,7 +114,7 @@ class TestHeatStack(base.MuranoTestCase): hs._template = {'resources': {'test': 1}} hs.type.properties = {} - erroring_template = { + invalid_template = { 'heat_template_version': 'something else' } @@ -116,17 +123,18 @@ class TestHeatStack(base.MuranoTestCase): e = self.assertRaises(heat_stack.HeatStackError, hs.updateTemplate, - erroring_template) + None, + invalid_template) err_msg = "Currently only heat_template_version 2013-05-23 "\ "is supported." self.assertEqual(err_msg, str(e)) # Check it's ok without a version - hs.updateTemplate({}) + hs.updateTemplate(None, {}) expected = {'resources': {'test': 1}} self.assertEqual(expected, hs._template) # .. or with a good version - hs.updateTemplate({'heat_template_version': '2013-05-23'}) + hs.updateTemplate(None, {'heat_template_version': '2013-05-23'}) expected['heat_template_version'] = '2013-05-23' self.assertEqual(expected, hs._template)