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
This commit is contained in:
Stan Lagun 2014-09-04 15:44:44 +04:00
parent c8cbcaa37b
commit f40169327b
10 changed files with 403 additions and 199 deletions

View File

@ -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:

View File

@ -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?

View File

@ -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

101
murano/engine/auth_utils.py Normal file
View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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)