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 def6d89567..3840ad63b1 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -17,7 +17,6 @@ from oslo_log import log as logging from oslo_utils import timeutils import six -from heat.common import exception from heat.common.i18n import _ from heat.common.i18n import _LE from heat.common import param_utils @@ -211,13 +210,9 @@ def format_stack_resource(resource, detail=True, with_props=False, rpc_api.RES_REQUIRED_BY: resource.required_by(), } - try: - 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()) - except exception.NotFound: - pass + 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 413dd834d9..a3df2f822d 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -393,6 +393,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 37cce2f6f5..fd51be3d57 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 _ @@ -89,6 +90,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 4da17704c4..cdcf190c9c 100644 --- a/heat/engine/resources/openstack/heat/resource_group.py +++ b/heat/engine/resources/openstack/heat/resource_group.py @@ -294,17 +294,19 @@ class ResourceGroup(stack_resource.StackResource): # Now we iterate over the removal policies, and update the blacklist # with any additional names rsrc_names = set(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 nested: - rsrc_names.add(str_n) - continue - rsrc = nested.resource_by_refid(str_n) - if rsrc: - rsrc_names.add(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: + rsrc_names.add(str_n) + continue + rsrc = nested.resource_by_refid(str_n) + if rsrc: + rsrc_names.add(rsrc.name) # If the blacklist has changed, update the resource data if rsrc_names != set(current_blacklist): diff --git a/heat/engine/resources/stack_resource.py b/heat/engine/resources/stack_resource.py index d23a05a1fe..fa991ec95b 100644 --- a/heat/engine/resources/stack_resource.py +++ b/heat/engine/resources/stack_resource.py @@ -114,8 +114,15 @@ class StackResource(resource.Resource): self.rpc_client().stack_cancel_update(self.context, stack_identity) + 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 @@ -125,13 +132,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 @@ -322,18 +329,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 @@ -439,11 +442,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 @@ -505,7 +504,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 b149254f00..b9fb2c1f43 100755 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -235,14 +235,10 @@ class Stack(collections.Mapping): for res in six.itervalues(self): 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 @@ -860,7 +856,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 2e82143747..a2df9baa0f 100644 --- a/heat/tests/autoscaling/test_heat_scaling_group.py +++ b/heat/tests/autoscaling/test_heat_scaling_group.py @@ -437,6 +437,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 ce0e0d0c2d..ed792fb8d3 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -252,6 +252,12 @@ class StackResourceType(stack_resource.StackResource, GenericResource): def handle_delete(self): self.delete_nested() + def has_nested(self): + if self.nested() is not None: + return True + + return False + class ResourceWithRestoreType(ResWithComplexPropsAndAttrs): diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index cfb7045c5e..d1e24a5c9e 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -45,7 +45,8 @@ class FormatTest(common.HeatTestCase): 'generic2': { 'Type': 'GenericResourceType', 'DependsOn': 'generic1'}, - 'generic3': {'Type': 'ResWithShowAttrType'} + 'generic3': {'Type': 'ResWithShowAttrType'}, + 'generic4': {'Type': 'StackResourceType'} } }) self.stack = parser.Stack(utils.dummy_context(), 'test_stack', @@ -171,7 +172,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 @@ -180,7 +181,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 @@ -202,9 +203,9 @@ class FormatTest(common.HeatTestCase): self.assertEqual(resource_keys, set(six.iterkeys(formatted))) def test_format_stack_resource_with_nested_stack_not_found(self): - res = self.stack['generic1'] - res.nested = mock.Mock() - res.nested.side_effect = exception.NotFound() + res = self.stack['generic4'] + self.patchobject(parser.Stack, 'load', + side_effect=exception.NotFound()) resource_keys = set(( rpc_api.RES_CREATION_TIME, @@ -221,10 +222,11 @@ class FormatTest(common.HeatTestCase): 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 5413c396a2..1f0e127ec0 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -195,7 +195,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), @@ -222,7 +222,7 @@ class StackTest(common.HeatTestCase): def test_iter_resources_cached(self, mock_drg): 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 c04d021445..32e40bd1f6 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -181,6 +181,11 @@ class StackResourceTest(StackResourceBaseTest): 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) + @testtools.skipIf(six.PY3, "needs a separate change") def test_implementation_signature(self): self.parent_resource.child_template = mock.Mock( @@ -450,10 +455,11 @@ class StackResourceTest(StackResourceBaseTest): 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): @@ -480,10 +486,10 @@ class StackResourceTest(StackResourceBaseTest): 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):