Remove the template mapping from the env before creating child stacks

Instead of going back through the stack linage, just remove the template
resource "mapping" from the environment when we go to create the
child environment. This is a lot more efficient when creating stacks
with RPC. We don't want to reload every stack in each stack.

part of blueprint decouple-nested
Change-Id: Ic93d08e41d2214e51c836be306a500e5c3939f0f
This commit is contained in:
Angus Salkeld 2015-03-03 11:18:23 +10:00
parent a63187a9da
commit 8bc93e286b
6 changed files with 141 additions and 46 deletions

View File

@ -106,8 +106,7 @@ class ClassResourceInfo(ResourceInfo):
class TemplateResourceInfo(ResourceInfo):
"""Store the info needed to start a TemplateResource.
"""
"""Store the info needed to start a TemplateResource."""
description = 'Template'
def __init__(self, registry, path, value):
@ -128,6 +127,7 @@ class TemplateResourceInfo(ResourceInfo):
class MapResourceInfo(ResourceInfo):
"""Store the mapping of one resource type to another.
like: OS::Networking::FloatingIp -> OS::Neutron::FloatingIp
"""
description = 'Mapping'
@ -141,6 +141,7 @@ class MapResourceInfo(ResourceInfo):
class GlobResourceInfo(MapResourceInfo):
"""Store the mapping (with wild cards) of one resource type to another.
like: OS::Networking::* -> OS::Neutron::*
"""
description = 'Wildcard Mapping'
@ -231,6 +232,16 @@ class ResourceRegistry(object):
info.user_resource = (self.global_registry is not None)
registry[name] = info
def remove_item(self, info):
if not isinstance(info, TemplateResourceInfo):
return
registry = self._registry
for key in info.path[:-1]:
registry = registry[key]
if info.path[-1] in registry:
registry.pop(info.path[-1])
def iterable_by(self, resource_type, resource_name=None):
is_templ_type = resource_type.endswith(('.yaml', '.template'))
if self.global_registry is not None and is_templ_type:
@ -263,7 +274,7 @@ class ResourceRegistry(object):
yield self._registry[pattern]
def get_resource_info(self, resource_type, resource_name=None,
registry_type=None, accept_fn=None):
registry_type=None):
"""Find possible matches to the resource type and name.
chain the results from the global and user registry to find
a match.
@ -294,11 +305,10 @@ class ResourceRegistry(object):
for info in sorted(matches):
match = info.get_resource_info(resource_type,
resource_name)
if ((registry_type is None or isinstance(match, registry_type)) and
(accept_fn is None or accept_fn(info))):
if ((registry_type is None or isinstance(match, registry_type))):
return match
def get_class(self, resource_type, resource_name=None, accept_fn=None):
def get_class(self, resource_type, resource_name=None):
if resource_type == "":
msg = _('Resource "%s" has no type') % resource_name
raise exception.StackValidationFailed(message=msg)
@ -311,8 +321,7 @@ class ResourceRegistry(object):
raise exception.StackValidationFailed(message=msg)
info = self.get_resource_info(resource_type,
resource_name=resource_name,
accept_fn=accept_fn)
resource_name=resource_name)
if info is None:
msg = _("Unknown resource Type : %s") % resource_type
raise exception.StackValidationFailed(message=msg)
@ -436,25 +445,39 @@ class Environment(object):
return self.stack_lifecycle_plugins
def get_child_environment(parent_env, child_params):
def get_child_environment(parent_env, child_params, item_to_remove=None):
"""Build a child environment using the parent environment and params.
This is built from the child_params and the parent env so some
resources can use user-provided parameters as if they come from an
environment.
1. resource_registry must be merged (child env should be loaded after the
parent env to take presdence).
2. child parameters must overwrite the parent's as they won't be relevant
in the child template.
"""
def is_flat_params(env_or_param):
if env_or_param is None:
return False
for sect in env_fmt.SECTIONS:
if sect in env_or_param:
return False
return True
child_env = parent_env.user_env_as_dict()
child_env[env_fmt.PARAMETERS] = {}
flat_params = is_flat_params(child_params)
new_env = Environment()
new_env.registry = copy.copy(parent_env.registry)
new_env.environment = new_env
child_env = {
env_fmt.PARAMETERS: {},
env_fmt.PARAMETER_DEFAULTS: parent_env.param_defaults}
if child_params is not None:
if env_fmt.PARAMETERS not in child_params:
child_env[env_fmt.PARAMETERS] = child_params
else:
child_env.update(child_params)
if flat_params and child_params is not None:
child_env[env_fmt.PARAMETERS] = child_params
new_env.load(child_env)
if not flat_params and child_params is not None:
new_env.load(child_params)
if item_to_remove is not None:
new_env.registry.remove_item(item_to_remove)
return new_env

View File

@ -32,7 +32,6 @@ from heat.common import short_id
from heat.common import timeutils
from heat.db import api as db_api
from heat.engine import attributes
from heat.engine import environment
from heat.engine import event
from heat.engine import function
from heat.engine import properties
@ -125,33 +124,11 @@ class Resource(object):
ResourceClass = cls
else:
from heat.engine.resources import template_resource
# Select the correct subclass to instantiate.
# Note: If the current stack is an implementation of
# a resource type (a TemplateResource mapped in the environment)
# then don't infinitely recurse by creating a child stack
# of the same type. Instead get the next match which will get
# us closer to a concrete class.
def get_ancestor_template_resources():
"""Return an ancestry list (TemplateResources only)."""
parent = stack.parent_resource
while parent is not None:
if isinstance(parent, template_resource.TemplateResource):
yield parent.template_name
parent = parent.stack.parent_resource
ancestor_list = set(get_ancestor_template_resources())
def accept_class(res_info):
if not isinstance(res_info, environment.TemplateResourceInfo):
return True
return res_info.template_name not in ancestor_list
registry = stack.env.registry
try:
ResourceClass = registry.get_class(definition.resource_type,
resource_name=name,
accept_fn=accept_class)
resource_name=name)
except exception.NotFound:
ResourceClass = template_resource.TemplateResource
assert issubclass(ResourceClass, Resource)

View File

@ -70,6 +70,7 @@ class TemplateResource(stack_resource.StackResource):
self.properties_schema = {}
self.attributes_schema = {}
super(TemplateResource, self).__init__(name, json_snippet, stack)
self.resource_info = tri
if self.validation_exception is None:
self._generate_schema(self.t)

View File

@ -49,6 +49,7 @@ class StackResource(resource.Resource):
def __init__(self, name, json_snippet, stack):
super(StackResource, self).__init__(name, json_snippet, stack)
self._nested = None
self.resource_info = None
def validate(self):
super(StackResource, self).validate()
@ -177,8 +178,10 @@ class StackResource(resource.Resource):
if child_params is None:
child_params = self.child_params()
child_env = environment.get_child_environment(
self.stack.env, child_params)
self.stack.env, child_params,
item_to_remove=self.resource_info)
# Note we disable rollback for nested stacks, since they
# should be rolled back by the parent stack on failure

View File

@ -457,3 +457,94 @@ class GlobalEnvLoadingTest(common.HeatTestCase):
call_list.sort()
self.assertEqual(expected, call_list)
class ChildEnvTest(common.HeatTestCase):
def test_params_flat(self):
new_params = {'foo': 'bar', 'tester': 'Yes'}
penv = environment.Environment()
expected = {'parameters': new_params,
'parameter_defaults': {},
'resource_registry': {'resources': {}}}
cenv = environment.get_child_environment(penv, new_params)
self.assertEqual(expected, cenv.user_env_as_dict())
def test_params_normal(self):
new_params = {'parameters': {'foo': 'bar', 'tester': 'Yes'}}
penv = environment.Environment()
expected = {'parameter_defaults': {},
'resource_registry': {'resources': {}}}
expected.update(new_params)
cenv = environment.get_child_environment(penv, new_params)
self.assertEqual(expected, cenv.user_env_as_dict())
def test_params_parent_overwritten(self):
new_params = {'parameters': {'foo': 'bar', 'tester': 'Yes'}}
parent_params = {'parameters': {'gone': 'hopefully'}}
penv = environment.Environment(env=parent_params)
expected = {'parameter_defaults': {},
'resource_registry': {'resources': {}}}
expected.update(new_params)
cenv = environment.get_child_environment(penv, new_params)
self.assertEqual(expected, cenv.user_env_as_dict())
def test_registry_merge_simple(self):
env1 = {u'resource_registry': {u'OS::Food': u'fruity.yaml'}}
env2 = {u'resource_registry': {u'OS::Fruit': u'apples.yaml'}}
penv = environment.Environment(env=env1)
cenv = environment.get_child_environment(penv, env2)
rr = cenv.user_env_as_dict()['resource_registry']
self.assertIn('OS::Food', rr)
self.assertIn('OS::Fruit', rr)
def test_registry_merge_favor_child(self):
env1 = {u'resource_registry': {u'OS::Food': u'carrots.yaml'}}
env2 = {u'resource_registry': {u'OS::Food': u'apples.yaml'}}
penv = environment.Environment(env=env1)
cenv = environment.get_child_environment(penv, env2)
res = cenv.get_resource_info('OS::Food')
self.assertEqual('apples.yaml', res.value)
def test_item_to_remove_simple(self):
env = {u'resource_registry': {u'OS::Food': u'fruity.yaml'}}
penv = environment.Environment(env)
victim = penv.get_resource_info('OS::Food', resource_name='abc')
self.assertIsNotNone(victim)
cenv = environment.get_child_environment(penv, None,
item_to_remove=victim)
res = cenv.get_resource_info('OS::Food', resource_name='abc')
self.assertIsNone(res)
self.assertNotIn('OS::Food',
cenv.user_env_as_dict()['resource_registry'])
# make sure the parent env is uneffected
innocent = penv.get_resource_info('OS::Food', resource_name='abc')
self.assertIsNotNone(innocent)
def test_item_to_remove_complex(self):
env = {u'resource_registry': {u'OS::Food': u'fruity.yaml',
u'resources': {u'abc': {
u'OS::Food': u'nutty.yaml'}}}}
penv = environment.Environment(env)
# the victim we want is the most specific one.
victim = penv.get_resource_info('OS::Food', resource_name='abc')
self.assertEqual(['resources', 'abc', 'OS::Food'], victim.path)
cenv = environment.get_child_environment(penv, None,
item_to_remove=victim)
res = cenv.get_resource_info('OS::Food', resource_name='abc')
self.assertEqual(['OS::Food'], res.path)
rr = cenv.user_env_as_dict()['resource_registry']
self.assertIn('OS::Food', rr)
self.assertNotIn('OS::Food', rr['resources']['abc'])
# make sure the parent env is uneffected
innocent2 = penv.get_resource_info('OS::Food', resource_name='abc')
self.assertEqual(['resources', 'abc', 'OS::Food'], innocent2.path)
def test_item_to_remove_none(self):
env = {u'resource_registry': {u'OS::Food': u'fruity.yaml'}}
penv = environment.Environment(env)
victim = penv.get_resource_info('OS::Food', resource_name='abc')
self.assertIsNotNone(victim)
cenv = environment.get_child_environment(penv, None)
res = cenv.get_resource_info('OS::Food', resource_name='abc')
self.assertIsNotNone(res)

View File

@ -239,7 +239,7 @@ class StackResourceTest(common.HeatTestCase):
result = parent_resource.preview()
mock_env_class.assert_called_once_with(self.parent_stack.env,
params)
params, item_to_remove=None)
self.assertEqual('preview_nested_stack', result)
mock_stack_class.assert_called_once_with(
mock.ANY,
@ -279,7 +279,7 @@ class StackResourceTest(common.HeatTestCase):
result = parent_resource.preview()
mock_env_class.assert_called_once_with(self.parent_stack.env,
params)
params, item_to_remove=None)
self.assertEqual('preview_nested_stack', result)
mock_stack_class.assert_called_once_with(
mock.ANY,