]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Provide user control for maximum nesting depth
authorClint Byrum <clint@fewbar.com>
Tue, 27 Aug 2013 16:10:54 +0000 (09:10 -0700)
committerClint Byrum <clint@fewbar.com>
Wed, 28 Aug 2013 00:26:17 +0000 (17:26 -0700)
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

etc/heat/heat.conf.sample
heat/common/config.py
heat/common/exception.py
heat/engine/stack_resource.py
heat/tests/test_nested_stack.py

index 5bf734ed39ca954c40574f20631b5ca0afbcadb8..df2feb5d3b40998e61a6e48916c5d62a0cf66041 100644 (file)
 # 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
index 3a90284310e3d1a0d29ad19e80690d0e608639cd..c428bb63757f8e369e33d78c07ddf8a50e5e36ea 100644 (file)
@@ -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',
index 52972522398606a851cfe6b80d89c43a528219b0..1f92baf4043af629e9201c02a41fb157b1e93a35 100644 (file)
@@ -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__()
index b4ff15ad40727da1824a38b13c6330c72b2679d1..6743ff2bc55093d66ed55dd3523f7409fb33e4d8 100644 (file)
@@ -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)
 
index dc752564c5548ed0e7b20bca5f1e50c22ebe19d2..574d406feb95e9ac0a0d90bdeb2446ad281b391a 100644 (file)
@@ -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):