From f3f9d68fc13d192650f1e4dca484da2efa635a38 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Mon, 12 Jan 2015 16:32:56 +0000 Subject: [PATCH] Make StackResource less strict on initial validation When doing the initial validate(), skip validating values by setting the stack strict_validate to False, otherwise we incorrectly fail validation when values are passed in via properties/parameters which refer to resources in the parent stack. Co-Authored-by: Angus Salkeld Change-Id: Ib75c2de6c32373de72901b9f7c5e3828bd9ee7d9 Closes-Bug: #1407100 Closes-Bug: #1407877 Closes-Bug: #1405446 --- heat/engine/stack_resource.py | 7 +- heat/tests/test_stack_resource.py | 15 +++ heat_integrationtests/common/test.py | 5 +- .../functional/test_validation.py | 95 +++++++++++++++++++ 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 heat_integrationtests/functional/test_validation.py diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index 0cb0718e5..65bac88a1 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -12,6 +12,7 @@ # under the License. import hashlib +import six from oslo.config import cfg from oslo.serialization import jsonutils @@ -50,14 +51,18 @@ class StackResource(resource.Resource): def validate(self): super(StackResource, self).validate() + self.validate_nested_stack() + + def validate_nested_stack(self): try: nested_stack = self._parse_nested_stack( self.stack.name, self.child_template(), self.child_params()) + nested_stack.strict_validate = False nested_stack.validate() except Exception as ex: - msg = _("Failed to validate: %s") % ex + msg = _("Failed to validate: %s") % six.text_type(ex) raise exception.StackValidationFailed(message=msg) def _outputs_to_attribs(self, json_snippet): diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index eb0381a92..b8bd52627 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -622,6 +622,21 @@ class StackResourceTest(common.HeatTestCase): self.m.VerifyAll() + def test_validate_nested_stack(self): + self.parent_resource.child_template = mock.Mock(return_value='foo') + self.parent_resource.child_params = mock.Mock(return_value={}) + nested = self.m.CreateMockAnything() + nested.validate().AndReturn(True) + self.m.StubOutWithMock(stack_resource.StackResource, + '_parse_nested_stack') + stack_resource.StackResource._parse_nested_stack( + self.parent_stack.name, 'foo', {}).AndReturn(nested) + + self.m.ReplayAll() + self.parent_resource.validate_nested_stack() + self.assertFalse(nested.strict_validate) + self.m.VerifyAll() + class StackResourceCheckCompleteTest(common.HeatTestCase): scenarios = [ diff --git a/heat_integrationtests/common/test.py b/heat_integrationtests/common/test.py index 3de44f598..450ed829d 100644 --- a/heat_integrationtests/common/test.py +++ b/heat_integrationtests/common/test.py @@ -331,7 +331,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios, return dict((r.resource_name, r.resource_type) for r in resources) def stack_create(self, stack_name=None, template=None, files=None, - parameters=None, environment=None): + parameters=None, environment=None, + expected_status='CREATE_COMPLETE'): name = stack_name or self._stack_rand_name() templ = template or self.template templ_files = files or {} @@ -349,5 +350,5 @@ class HeatIntegrationTest(testscenarios.WithScenarios, stack = self.client.stacks.get(name) stack_identifier = '%s/%s' % (name, stack.id) - self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE') + self._wait_for_stack_status(stack_identifier, expected_status) return stack_identifier diff --git a/heat_integrationtests/functional/test_validation.py b/heat_integrationtests/functional/test_validation.py new file mode 100644 index 000000000..52309cd1e --- /dev/null +++ b/heat_integrationtests/functional/test_validation.py @@ -0,0 +1,95 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from heat_integrationtests.common import test + + +LOG = logging.getLogger(__name__) + + +class StackValidationTest(test.HeatIntegrationTest): + + def setUp(self): + super(StackValidationTest, self).setUp() + self.client = self.orchestration_client + if not self.conf.minimal_image_ref: + raise self.skipException("No image configured to test") + + if not self.conf.instance_type: + raise self.skipException("No instance_type configured to test") + + if self.conf.keypair_name: + self.keypair_name = self.conf.keypair_name + else: + self.keypair = self.create_keypair() + self.keypair_name = self.keypair.id + + def test_stack_validate_provider_references_parent_resource(self): + template = ''' +heat_template_version: 2014-10-16 +parameters: + keyname: + type: string + flavor: + type: string + image: + type: string +resources: + config: + type: My::Config + properties: + server: {get_resource: server} + + server: + type: OS::Nova::Server + properties: + image: {get_param: image} + flavor: {get_param: flavor} + key_name: {get_param: keyname} + user_data_format: SOFTWARE_CONFIG +''' + config_template = ''' +heat_template_version: 2014-10-16 +parameters: + server: + type: string +resources: + config: + type: OS::Heat::SoftwareConfig + + deployment: + type: OS::Heat::SoftwareDeployment + properties: + config: + get_resource: config + server: + get_param: server +''' + files = {'provider.yaml': config_template} + env = {'resource_registry': + {'My::Config': 'provider.yaml'}} + parameters = {'keyname': self.keypair_name, + 'flavor': self.conf.instance_type, + 'image': self.conf.minimal_image_ref} + # Note we don't wait for CREATE_COMPLETE, because we're using a + # minimal image without the tools to apply the config. + # The point of the test is just to prove that validation won't + # falsely prevent stack creation starting, ref bug #1407100 + # Note that we can be sure My::Config will stay IN_PROGRESS as + # there's no signal sent to the deployment + self.stack_create(template=template, + files=files, + environment=env, + parameters=parameters, + expected_status='CREATE_IN_PROGRESS')