From c31c34f8dfd0919bf46a975701c139073115debc Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 4 Nov 2014 14:46:39 +0800 Subject: [PATCH] Do static template validation for nested stacks Currently we don't fail fast, even if there is an error in the nested template which should fail validation. This change to recurse and do static template validation for nested stack resources. Change-Id: I572ded640582419e0888e4b9f8eed3a3432d6121 Closes-Bug: #1388140 Closes-Bug: #1389104 --- heat/engine/stack_resource.py | 12 ++ heat/tests/autoscaling/test_scaling_group.py | 3 + heat/tests/test_autoscaling.py | 4 +- heat/tests/test_instance_group.py | 5 +- heat/tests/test_nested_stack.py | 2 + heat/tests/test_resource_group.py | 1 + heat/tests/test_stack_resource.py | 113 +++++++++++++++++++ 7 files changed, 136 insertions(+), 4 deletions(-) diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index 9ed17c8ae9..87b3b46f62 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -48,6 +48,18 @@ class StackResource(resource.Resource): super(StackResource, self).__init__(name, json_snippet, stack) self._nested = None + def validate(self): + super(StackResource, self).validate() + try: + nested_stack = self._parse_nested_stack( + self.stack.name, + self.child_template(), + self.child_params()) + nested_stack.validate() + except Exception as ex: + msg = _("Failed to validate: %s") % ex + raise exception.StackValidationFailed(message=msg) + def _outputs_to_attribs(self, json_snippet): outputs = json_snippet.get('Outputs') if not self.attributes and outputs: diff --git a/heat/tests/autoscaling/test_scaling_group.py b/heat/tests/autoscaling/test_scaling_group.py index a2f56d9c25..eee8a9942d 100644 --- a/heat/tests/autoscaling/test_scaling_group.py +++ b/heat/tests/autoscaling/test_scaling_group.py @@ -179,6 +179,9 @@ class TestGroupAdjust(common.HeatTestCase): t = template_format.parse(as_template) stack = utils.parse_stack(t, params=inline_templates.as_params) self.group = stack['WebServerGroup'] + self.stub_ImageConstraint_validate() + self.stub_FlavorConstraint_validate() + self.stub_SnapshotConstraint_validate() self.assertIsNone(self.group.validate()) def test_scaling_policy_cooldown_toosoon(self): diff --git a/heat/tests/test_autoscaling.py b/heat/tests/test_autoscaling.py index a02feb7500..e97e78ed5f 100644 --- a/heat/tests/test_autoscaling.py +++ b/heat/tests/test_autoscaling.py @@ -508,7 +508,7 @@ class AutoScalingTest(common.HeatTestCase): u'AvailabilityZones': ['abc', 'xyz']}} self.m.StubOutWithMock(short_id, 'generate_id') - short_id.generate_id().AndReturn('aaaabbbbcccc') + short_id.generate_id().MultipleTimes().AndReturn('aaaabbbbcccc') now = timeutils.utcnow() self._stub_meta_expected(now, 'ExactCapacity : 1') @@ -556,7 +556,7 @@ class AutoScalingTest(common.HeatTestCase): } self.m.StubOutWithMock(short_id, 'generate_id') - short_id.generate_id().AndReturn('aaaabbbbcccc') + short_id.generate_id().MultipleTimes().AndReturn('aaaabbbbcccc') self.m.StubOutWithMock(neutron_lb.LoadBalancer, 'handle_update') neutron_lb.LoadBalancer.handle_update(expected, diff --git a/heat/tests/test_instance_group.py b/heat/tests/test_instance_group.py index 4da0cec3e4..2c63bedafc 100644 --- a/heat/tests/test_instance_group.py +++ b/heat/tests/test_instance_group.py @@ -75,7 +75,7 @@ class InstanceGroupTest(common.HeatTestCase): instead of instance.Instance. """ self.m.StubOutWithMock(parser.Stack, 'validate') - parser.Stack.validate() + parser.Stack.validate().MultipleTimes().AndReturn(None) self.stub_KeypairConstraint_validate() self.stub_ImageConstraint_validate() self.stub_FlavorConstraint_validate() @@ -244,7 +244,7 @@ class InstanceGroupTest(common.HeatTestCase): stack = utils.parse_stack(t) self.m.StubOutWithMock(parser.Stack, 'validate') - parser.Stack.validate() + parser.Stack.validate().MultipleTimes().AndReturn(None) self.stub_ImageConstraint_validate() self.stub_KeypairConstraint_validate() self.stub_FlavorConstraint_validate() @@ -327,6 +327,7 @@ class InstanceGroupTest(common.HeatTestCase): stack = utils.parse_stack(t) self._stub_create(2) + self.m.ReplayAll() self.create_resource(t, stack, 'JobServerConfig') rsrc = self.create_resource(t, stack, 'JobServerGroup') diff --git a/heat/tests/test_nested_stack.py b/heat/tests/test_nested_stack.py index d9c3bf97b3..2251edba5c 100644 --- a/heat/tests/test_nested_stack.py +++ b/heat/tests/test_nested_stack.py @@ -438,6 +438,8 @@ Resources: urlfetch.get( 'https://server.test/depth3.template').AndReturn( self.nested_template) + self.m.StubOutWithMock(parser.Stack, 'validate') + parser.Stack.validate().MultipleTimes().AndReturn(None) self.m.ReplayAll() self.create_stack(root_template) self.m.VerifyAll() diff --git a/heat/tests/test_resource_group.py b/heat/tests/test_resource_group.py index f28c147e99..54f5efcee5 100644 --- a/heat/tests/test_resource_group.py +++ b/heat/tests/test_resource_group.py @@ -161,6 +161,7 @@ class ResourceGroupTest(common.HeatTestCase): AttributeResource = generic_resource.ResourceWithComplexAttributes resource._register_class("dummyattr.resource", AttributeResource) + self.m.StubOutWithMock(stackm.Stack, 'validate') def test_build_resource_definition(self): stack = utils.parse_stack(template) diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index 875078012b..2ec79c3047 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -70,6 +70,63 @@ simple_template = ''' } ''' +main_template = ''' +heat_template_version: 2013-05-23 +resources: + volume_server: + type: nested.yaml +''' + +my_wrong_nested_template = ''' +heat_template_version: 2013-05-23 +resources: + server: + type: OS::Nova::Server + properties: + image: F17-x86_64-gold + flavor: m1.small + volume: + type: OS::Cinder::Volume + properties: + size: 10 + description: Volume for stack + volume_attachment: + type: OS::Cinder::VolumeAttachment + properties: + volume_id: { get_resource: volume } + instance_uuid: { get_resource: instance } +''' + +resource_group_template = ''' +heat_template_version: 2013-05-23 +resources: + my_resource_group: + type: OS::Heat::ResourceGroup + properties: + resource_def: + type: idontexist +''' + +heat_autoscaling_group_template = ''' +heat_template_version: 2013-05-23 +resources: + my_autoscaling_group: + type: OS::Heat::AutoScalingGroup + properties: + resource: + type: idontexist + desired_capacity: 2 + max_size: 4 + min_size: 1 +''' + +nova_server_template = ''' +heat_template_version: 2013-05-23 +resources: + group_server: + type: idontexist +''' + class MyStackResource(stack_resource.StackResource, generic_rsrc.GenericResource): @@ -288,6 +345,62 @@ class StackResourceTest(common.HeatTestCase): self.assertFalse(nested_stack.validate.called) self.assertTrue(nested_stack.preview_resources.called) + def test_validate_error_reference(self): + stack_name = 'validate_error_reference' + tmpl = template_format.parse(main_template) + files = {'nested.yaml': my_wrong_nested_template} + stack = parser.Stack(utils.dummy_context(), stack_name, + templatem.Template(tmpl, files=files)) + rsrc = stack['volume_server'] + raise_exc_msg = ('The specified reference "instance" (' + 'in volume_attachment.Properties.instance_uuid) ' + 'is incorrect') + exc = self.assertRaises(exception.StackValidationFailed, + rsrc.validate) + self.assertIn(raise_exc_msg, six.text_type(exc)) + + def _test_validate_unknown_resource_type(self, stack_name, + tmpl, resource_name): + raise_exc_msg = ('Unknown resource Type : idontexist') + stack = parser.Stack(utils.dummy_context(), stack_name, tmpl) + rsrc = stack[resource_name] + + exc = self.assertRaises(exception.StackValidationFailed, + rsrc.validate) + self.assertIn(raise_exc_msg, six.text_type(exc)) + + def test_validate_resource_group(self): + # test validate without nested template + stack_name = 'validate_resource_group_template' + t = template_format.parse(resource_group_template) + tmpl = templatem.Template(t) + self._test_validate_unknown_resource_type(stack_name, tmpl, + 'my_resource_group') + + # validate with nested template + res_prop = t['resources']['my_resource_group']['properties'] + res_prop['resource_def']['type'] = 'nova_server.yaml' + files = {'nova_server.yaml': nova_server_template} + tmpl = templatem.Template(t, files=files) + self._test_validate_unknown_resource_type(stack_name, tmpl, + 'my_resource_group') + + def test_validate_heat_autoscaling_group(self): + # test validate without nested template + stack_name = 'validate_heat_autoscaling_group_template' + t = template_format.parse(heat_autoscaling_group_template) + tmpl = templatem.Template(t) + self._test_validate_unknown_resource_type(stack_name, tmpl, + 'my_autoscaling_group') + + # validate with nested template + res_prop = t['resources']['my_autoscaling_group']['properties'] + res_prop['resource']['type'] = 'nova_server.yaml' + files = {'nova_server.yaml': nova_server_template} + tmpl = templatem.Template(t, files=files) + self._test_validate_unknown_resource_type(stack_name, tmpl, + 'my_autoscaling_group') + def test__validate_nested_resources_checks_num_of_resources(self): stack_resource.cfg.CONF.set_override('max_resources_per_stack', 2) tmpl = {'HeatTemplateFormatVersion': '2012-12-12',