diff --git a/heat/common/grouputils.py b/heat/common/grouputils.py index bb33025ca2..e0fe72ce9f 100644 --- a/heat/common/grouputils.py +++ b/heat/common/grouputils.py @@ -67,6 +67,8 @@ def get_member_names(group): def get_resource(stack, resource_name, use_indices, key): nested_stack = stack.nested() + if not nested_stack: + return None try: if use_indices: return get_members(stack)[int(resource_name)] @@ -79,12 +81,14 @@ def get_resource(stack, resource_name, use_indices, key): def get_rsrc_attr(stack, key, use_indices, resource_name, *attr_path): resource = get_resource(stack, resource_name, use_indices, key) - return resource.FnGetAtt(*attr_path) + if resource: + return resource.FnGetAtt(*attr_path) def get_rsrc_id(stack, key, use_indices, resource_name): resource = get_resource(stack, resource_name, use_indices, key) - return resource.FnGetRefId() + if resource: + return resource.FnGetRefId() def get_nested_attrs(stack, key, use_indices, *path): diff --git a/heat/engine/api.py b/heat/engine/api.py index 8923c42d41..998cb99c51 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -176,9 +176,9 @@ def format_stack_resource(resource, detail=True, with_props=False, rpc_api.RES_REQUIRED_BY: resource.required_by(), } - if (hasattr(resource, 'nested') and callable(resource.nested) and - resource.nested() is not None): - res[rpc_api.RES_NESTED_STACK_ID] = dict(resource.nested().identifier()) + if resource.has_nested(): + res[rpc_api.RES_NESTED_STACK_ID] = dict( + resource.nested().identifier()) if resource.stack.parent_resource_name: res[rpc_api.RES_PARENT_RESOURCE] = resource.stack.parent_resource_name diff --git a/heat/engine/resource.py b/heat/engine/resource.py index f26b3856df..8ac15b75ef 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -294,6 +294,10 @@ class Resource(object): self.action, self.status, "Failure occured while waiting.") + def has_nested(self): + # common resources have not nested, StackResource overrides it + return False + def has_hook(self, hook): # Clear the cache to make sure the data is up to date: self._data = None diff --git a/heat/engine/resources/aws/cfn/stack.py b/heat/engine/resources/aws/cfn/stack.py index c1ec832318..f2c0349a7a 100644 --- a/heat/engine/resources/aws/cfn/stack.py +++ b/heat/engine/resources/aws/cfn/stack.py @@ -12,6 +12,7 @@ # under the License. from requests import exceptions +import six from heat.common import exception from heat.common.i18n import _ @@ -92,6 +93,9 @@ class NestedStack(stack_resource.StackResource): return attributes.select_from_attribute(attribute, path) def FnGetRefId(self): + if self.nested() is None: + return six.text_type(self.name) + return self.nested().identifier().arn() def handle_update(self, json_snippet, tmpl_diff, prop_diff): diff --git a/heat/engine/resources/openstack/heat/resource_group.py b/heat/engine/resources/openstack/heat/resource_group.py index e20dffaa75..ddb6dd91ff 100644 --- a/heat/engine/resources/openstack/heat/resource_group.py +++ b/heat/engine/resources/openstack/heat/resource_group.py @@ -214,6 +214,8 @@ class ResourceGroup(stack_resource.StackResource): def _name_blacklist(self): """Resolve the remove_policies to names for removal.""" + nested = self.nested() + # To avoid reusing names after removal, we store a comma-separated # blacklist in the resource data db_rsrc_names = self.data().get('name_blacklist') @@ -225,17 +227,18 @@ class ResourceGroup(stack_resource.StackResource): # Now we iterate over the removal policies, and update the blacklist # with any additional names rsrc_names = list(current_blacklist) - for r in self.properties[self.REMOVAL_POLICIES]: - if self.REMOVAL_RSRC_LIST in r: - # Tolerate string or int list values - for n in r[self.REMOVAL_RSRC_LIST]: - str_n = six.text_type(n) - if str_n in self.nested() and str_n not in rsrc_names: - rsrc_names.append(str_n) - continue - rsrc = self.nested().resource_by_refid(str_n) - if rsrc and str_n not in rsrc_names: - rsrc_names.append(rsrc.name) + if nested: + for r in self.properties[self.REMOVAL_POLICIES]: + if self.REMOVAL_RSRC_LIST in r: + # Tolerate string or int list values + for n in r[self.REMOVAL_RSRC_LIST]: + str_n = six.text_type(n) + if str_n in nested and str_n not in rsrc_names: + rsrc_names.append(str_n) + continue + rsrc = self.nested().resource_by_refid(str_n) + if rsrc and str_n not in rsrc_names: + rsrc_names.append(rsrc.name) # If the blacklist has changed, update the resource data if rsrc_names != current_blacklist: diff --git a/heat/engine/resources/stack_resource.py b/heat/engine/resources/stack_resource.py index 433b4622c7..591cc5a3c0 100644 --- a/heat/engine/resources/stack_resource.py +++ b/heat/engine/resources/stack_resource.py @@ -83,8 +83,15 @@ class StackResource(resource.Resource): # resources in it decide if they need updating. return True + def has_nested(self): + if self.nested() is not None: + return True + + return False + def nested(self, force_reload=False, show_deleted=False): '''Return a Stack object representing the nested (child) stack. + if we catch NotFound exception when loading, return None. :param force_reload: Forces reloading from the DB instead of returning the locally cached Stack object @@ -94,13 +101,13 @@ class StackResource(resource.Resource): self._nested = None if self._nested is None and self.resource_id is not None: - self._nested = parser.Stack.load(self.context, - self.resource_id, - show_deleted=show_deleted, - force_reload=force_reload) - - if self._nested is None: - raise exception.NotFound(_("Nested stack not found in DB")) + try: + self._nested = parser.Stack.load(self.context, + self.resource_id, + show_deleted=show_deleted, + force_reload=force_reload) + except exception.NotFound: + return None return self._nested @@ -282,18 +289,14 @@ class StackResource(resource.Resource): def _check_status_complete(self, action, show_deleted=False, cookie=None): - try: - nested = self.nested(force_reload=True, show_deleted=show_deleted) - except exception.NotFound: + nested = self.nested(force_reload=True, show_deleted=show_deleted) + if nested is None: if action == resource.Resource.DELETE: return True # It's possible the engine handling the create hasn't persisted # the stack to the DB when we first start polling for state return False - if nested is None: - return True - if nested.action != action: return False @@ -399,11 +402,7 @@ class StackResource(resource.Resource): ''' Delete the nested stack. ''' - try: - stack = self.nested() - except exception.NotFound: - return - + stack = self.nested() if stack is None: return @@ -462,7 +461,11 @@ class StackResource(resource.Resource): return self._check_status_complete(resource.Resource.CHECK) def prepare_abandon(self): - return self.nested().prepare_abandon() + nested_stack = self.nested() + if nested_stack: + return self.nested().prepare_abandon() + + return {} def get_output(self, op): ''' diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 0a69b53dfd..e0888f7600 100755 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -212,14 +212,10 @@ class Stack(collections.Mapping): for res in self.values(): yield res - get_nested = getattr(res, 'nested', None) - if not callable(get_nested) or nested_depth == 0: - continue - - nested_stack = get_nested() - if nested_stack is None: + if not res.has_nested() or nested_depth == 0: continue + nested_stack = res.nested() for nested_res in nested_stack.iter_resources(nested_depth - 1): yield nested_res @@ -791,7 +787,7 @@ class Stack(collections.Mapping): def supports_check_action(self): def is_supported(stack, res): - if hasattr(res, 'nested'): + if res.has_nested(): return res.nested().supports_check_action() else: return hasattr(res, 'handle_%s' % self.CHECK.lower()) diff --git a/heat/tests/autoscaling/test_heat_scaling_group.py b/heat/tests/autoscaling/test_heat_scaling_group.py index 7f016ba707..d7bdf38fc9 100644 --- a/heat/tests/autoscaling/test_heat_scaling_group.py +++ b/heat/tests/autoscaling/test_heat_scaling_group.py @@ -362,6 +362,7 @@ class HeatScalingGroupAttrTest(common.HeatTestCase): def test_index_dotted_attribute(self): mock_members = self.patchobject(grouputils, 'get_members') + self.group.nested = mock.Mock() members = [] output = [] for ip_ex in six.moves.range(0, 2): diff --git a/heat/tests/generic_resource.py b/heat/tests/generic_resource.py index 456e67dab3..e1e0ca42fa 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -20,6 +20,7 @@ from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine.resources import signal_responder +from heat.engine.resources import stack_resource from heat.engine.resources import stack_user LOG = logging.getLogger(__name__) @@ -162,3 +163,30 @@ class ResourceWithCustomConstraint(GenericResource): 'Foo': properties.Schema( properties.Schema.STRING, constraints=[constraints.CustomConstraint('neutron.network')])} + + +class StackResourceType(stack_resource.StackResource, GenericResource): + def physical_resource_name(self): + return "cb2f2b28-a663-4683-802c-4b40c916e1ff" + + def set_template(self, nested_template, params): + self.nested_template = nested_template + self.nested_params = params + + def handle_create(self): + return self.create_with_template(self.nested_template, + self.nested_params) + + def handle_adopt(self, resource_data): + return self.create_with_template(self.nested_template, + self.nested_params, + adopt_data=resource_data) + + def handle_delete(self): + self.delete_nested() + + def has_nested(self): + if self.nested() is not None: + return True + + return False diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index db0f5b28a0..06744b3424 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -19,6 +19,7 @@ import mock from oslo_utils import timeutils import six +from heat.common import exception from heat.common import identifier from heat.common import template_format from heat.engine import api @@ -44,11 +45,14 @@ class FormatTest(common.HeatTestCase): 'generic1': {'Type': 'GenericResourceType'}, 'generic2': { 'Type': 'GenericResourceType', - 'DependsOn': 'generic1'} + 'DependsOn': 'generic1'}, + 'generic4': {'Type': 'StackResourceType'} } }) resource._register_class('GenericResourceType', generic_rsrc.GenericResource) + resource._register_class('StackResourceType', + generic_rsrc.StackResourceType) resource._register_class('ResWithComplexPropsAndAttrs', generic_rsrc.ResWithComplexPropsAndAttrs) self.stack = parser.Stack(utils.dummy_context(), 'test_stack', @@ -179,7 +183,7 @@ class FormatTest(common.HeatTestCase): self.assertEqual('', props['a_string']) def test_format_stack_resource_with_nested_stack(self): - res = self.stack['generic1'] + res = self.stack['generic4'] nested_id = {'foo': 'bar'} res.nested = mock.Mock() res.nested.return_value.identifier.return_value = nested_id @@ -188,7 +192,7 @@ class FormatTest(common.HeatTestCase): self.assertEqual(nested_id, formatted[rpc_api.RES_NESTED_STACK_ID]) def test_format_stack_resource_with_nested_stack_none(self): - res = self.stack['generic1'] + res = self.stack['generic4'] res.nested = mock.Mock() res.nested.return_value = None @@ -206,10 +210,32 @@ class FormatTest(common.HeatTestCase): rpc_api.RES_REQUIRED_BY)) formatted = api.format_stack_resource(res, False) - self.assertEqual(resource_keys, set(formatted.keys())) + self.assertEqual(resource_keys, set(six.iterkeys(formatted))) + + def test_format_stack_resource_with_nested_stack_not_found(self): + res = self.stack['generic4'] + self.patchobject(parser.Stack, 'load', + side_effect=exception.NotFound()) + + resource_keys = set(( + rpc_api.RES_UPDATED_TIME, + rpc_api.RES_NAME, + rpc_api.RES_PHYSICAL_ID, + rpc_api.RES_ACTION, + rpc_api.RES_STATUS, + rpc_api.RES_STATUS_DATA, + rpc_api.RES_TYPE, + rpc_api.RES_ID, + rpc_api.RES_STACK_ID, + rpc_api.RES_STACK_NAME, + rpc_api.RES_REQUIRED_BY)) + + formatted = api.format_stack_resource(res, False) + # 'nested_stack_id' is not in formatted + self.assertEqual(resource_keys, set(six.iterkeys(formatted))) def test_format_stack_resource_with_nested_stack_empty(self): - res = self.stack['generic1'] + res = self.stack['generic4'] nested_id = {'foo': 'bar'} res.nested = mock.MagicMock() diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index afb18ed0b9..7993cc85e2 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -53,6 +53,8 @@ class StackTest(common.HeatTestCase): resource._register_class('GenericResourceType', generic_rsrc.GenericResource) + resource._register_class('StackResourceType', + generic_rsrc.StackResourceType) resource._register_class('ResourceWithPropsType', generic_rsrc.ResourceWithProps) resource._register_class('ResWithComplexPropsAndAttrs', @@ -197,7 +199,7 @@ class StackTest(common.HeatTestCase): def test_iter_resources(self): tpl = {'HeatTemplateFormatVersion': '2012-12-12', 'Resources': - {'A': {'Type': 'GenericResourceType'}, + {'A': {'Type': 'StackResourceType'}, 'B': {'Type': 'GenericResourceType'}}} self.stack = stack.Stack(self.ctx, 'test_stack', template.Template(tpl), diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index f30718defc..d8a2556bc6 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -198,6 +198,11 @@ class StackResourceTest(common.HeatTestCase): nest.return_value.prepare_abandon.assert_called_once_with() self.assertEqual({'X': 'Y'}, ret) + def test_nested_abandon_stack_not_found(self): + self.parent_resource.nested = mock.MagicMock(return_value=None) + ret = self.parent_resource.prepare_abandon() + self.assertEqual({}, ret) + def test_implementation_signature(self): self.parent_resource.child_template = mock.Mock( return_value=self.simple_template) @@ -460,10 +465,11 @@ class StackResourceTest(common.HeatTestCase): parser.Stack.load(self.parent_resource.context, self.parent_resource.resource_id, show_deleted=False, - force_reload=False).AndReturn(None) + force_reload=False).AndRaise( + exception.NotFound) self.m.ReplayAll() - self.assertRaises(exception.NotFound, self.parent_resource.nested) + self.assertIsNone(self.parent_resource.nested()) self.m.VerifyAll() def test_load_nested_cached(self): @@ -490,10 +496,10 @@ class StackResourceTest(common.HeatTestCase): parser.Stack.load(self.parent_resource.context, self.parent_resource.resource_id, show_deleted=False, - force_reload=True).AndReturn(None) + force_reload=True).AndRaise( + exception.NotFound) self.m.ReplayAll() - self.assertRaises(exception.NotFound, self.parent_resource.nested, - force_reload=True) + self.assertIsNone(self.parent_resource.nested(force_reload=True)) self.m.VerifyAll() def test_delete_nested_none_nested_stack(self):