From: Clint Byrum Date: Tue, 27 Aug 2013 16:10:54 +0000 (-0700) Subject: Provide user control for maximum nesting depth X-Git-Tag: 2014.1~110^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=b1b31f170b2256cb73b330341b143f1299e424dd;p=openstack-build%2Fheat-build.git Provide user control for maximum nesting depth In the case of an infinitely recursing stack, Heat will continue to keep nesting stacks until python's maximum stack recursion depth is reached. By this point a lot of memory and time may have been spent parsing/loading/etc. The default of 3 is enough to deploy a stack of stacks of stacks. It can be raised by deployers if there is a desire to do larger and more complicated stacks, at the cost of more resource utilization. Fixes bug #1214239 Change-Id: Ic492ef84b94b1f715c49eef7e1794a486fb8182f --- diff --git a/etc/heat/heat.conf.sample b/etc/heat/heat.conf.sample index 5bf734ed..df2feb5d 100644 --- a/etc/heat/heat.conf.sample +++ b/etc/heat/heat.conf.sample @@ -54,6 +54,10 @@ # Maximum raw byte size of any template. (integer value) #max_template_size=524288 +# Maximum depth allowed when using nested stacks. (integer +# value) +#max_nested_stack_depth=3 + # # Options defined in heat.common.crypt diff --git a/heat/common/config.py b/heat/common/config.py index 3a902843..c428bb63 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -63,7 +63,10 @@ service_opts = [ help='Keystone role for heat template-defined users'), cfg.IntOpt('max_template_size', default=524288, - help='Maximum raw byte size of any template.')] + help='Maximum raw byte size of any template.'), + cfg.IntOpt('max_nested_stack_depth', + default=3, + help='Maximum depth allowed when using nested stacks.')] db_opts = [ cfg.StrOpt('sql_connection', diff --git a/heat/common/exception.py b/heat/common/exception.py index 52972522..1f92baf4 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -350,3 +350,11 @@ class NotFound(Error): class InvalidContentType(HeatException): message = "Invalid content type %(content_type)s" + + +class StackRecursionLimitReached(HeatException): + message = _("Recursion depth exceeds %d.") + + def __init__(self, recursion_depth): + self.message = self.message % recursion_depth + super(StackRecursionLimitReached, self).__init__() diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index b4ff15ad..6743ff2b 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo.config import cfg + from heat.common import exception from heat.engine import attributes from heat.engine import environment @@ -34,6 +36,11 @@ class StackResource(resource.Resource): def __init__(self, name, json_snippet, stack): super(StackResource, self).__init__(name, json_snippet, stack) self._nested = None + if self.stack.parent_resource: + self.recursion_depth = ( + self.stack.parent_resource.recursion_depth + 1) + else: + self.recursion_depth = 0 def _outputs_to_attribs(self, json_snippet): if not self.attributes and 'Outputs' in json_snippet: @@ -63,6 +70,9 @@ class StackResource(resource.Resource): ''' Handle the creation of the nested stack from a given JSON template. ''' + if self.recursion_depth >= cfg.CONF.max_nested_stack_depth: + raise exception.StackRecursionLimitReached( + cfg.CONF.max_nested_stack_depth) template = parser.Template(child_template) self._outputs_to_attribs(child_template) diff --git a/heat/tests/test_nested_stack.py b/heat/tests/test_nested_stack.py index dc752564..574d406f 100644 --- a/heat/tests/test_nested_stack.py +++ b/heat/tests/test_nested_stack.py @@ -161,6 +161,166 @@ Outputs: rsrc.delete() self.m.VerifyAll() + def test_nested_stack_three_deep(self): + root_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth1.template' +''' + depth1_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth2.template' +''' + depth2_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth3.template' + Parameters: + KeyName: foo +''' + urlfetch.get( + 'https://server.test/depth1.template').AndReturn( + depth1_template) + urlfetch.get( + 'https://server.test/depth2.template').AndReturn( + depth2_template) + urlfetch.get( + 'https://server.test/depth3.template').AndReturn( + self.nested_template) + self.m.ReplayAll() + self.create_stack(root_template) + self.m.VerifyAll() + + def test_nested_stack_four_deep(self): + root_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth1.template' +''' + depth1_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth2.template' +''' + depth2_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth3.template' +''' + depth3_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth4.template' + Parameters: + KeyName: foo +''' + urlfetch.get( + 'https://server.test/depth1.template').AndReturn( + depth1_template) + urlfetch.get( + 'https://server.test/depth2.template').AndReturn( + depth2_template) + urlfetch.get( + 'https://server.test/depth3.template').AndReturn( + depth3_template) + urlfetch.get( + 'https://server.test/depth4.template').AndReturn( + self.nested_template) + self.m.ReplayAll() + t = template_format.parse(root_template) + stack = self.parse_stack(t) + stack.create() + self.assertEquals((stack.CREATE, stack.FAILED), stack.state) + self.assertIn('Recursion depth exceeds', stack.status_reason) + self.m.VerifyAll() + + def test_nested_stack_four_wide(self): + root_template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth1.template' + Parameters: + KeyName: foo + Nested2: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth2.template' + Parameters: + KeyName: foo + Nested3: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth3.template' + Parameters: + KeyName: foo + Nested4: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/depth4.template' + Parameters: + KeyName: foo +''' + urlfetch.get( + 'https://server.test/depth1.template').InAnyOrder().AndReturn( + self.nested_template) + urlfetch.get( + 'https://server.test/depth2.template').InAnyOrder().AndReturn( + self.nested_template) + urlfetch.get( + 'https://server.test/depth3.template').InAnyOrder().AndReturn( + self.nested_template) + urlfetch.get( + 'https://server.test/depth4.template').InAnyOrder().AndReturn( + self.nested_template) + self.m.ReplayAll() + self.create_stack(root_template) + self.m.VerifyAll() + + def test_nested_stack_infinite_recursion(self): + template = ''' +HeatTemplateFormat: 2012-12-12 +Resources: + Nested: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: 'https://server.test/the.template' +''' + urlfetch.get( + 'https://server.test/the.template').MultipleTimes().AndReturn( + template) + self.m.ReplayAll() + t = template_format.parse(template) + stack = self.parse_stack(t) + stack.create() + self.assertEqual(stack.state, (stack.CREATE, stack.FAILED)) + self.assertIn('Recursion depth exceeds', stack.status_reason) + self.m.VerifyAll() + class ResDataResource(generic_rsrc.GenericResource): def handle_create(self):