Don't raise exception when get nested stack

1. Stack.load() won't return None, instead will raise
notFound exception if we can't get the stack from db.
So we should catch the notFound exception in
StackResource.nested() when loading.
2. Return None if we catch the notFound when get the
nested, due there are many places where we call the
nested(), and raise the notFound exception makes no sense.

(cherry-picked from b101190f32)
Closes-Bug: #1479579
Change-Id: I2540ca13572dfe628e6fc596815bee427b41290f
This commit is contained in:
Steve Baker 2015-08-25 13:38:07 +12:00 committed by Angus Salkeld
parent 7248ec4252
commit e889d66e07
12 changed files with 130 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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