diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 4edfa6b13..068eef542 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -921,6 +921,15 @@ class Resource(object): except ValueError: return True + def _check_for_convergence_replace(self, restricted_actions): + if 'replace' in restricted_actions: + ex = exception.ResourceActionRestricted(action='replace') + failure = exception.ResourceFailure(ex, self, self.UPDATE) + self._add_event(self.UPDATE, self.FAILED, six.text_type(ex)) + raise failure + else: + raise exception.UpdateReplace(self.name) + def update_convergence(self, template_id, resource_data, engine_id, timeout, new_stack): """Update the resource synchronously. @@ -938,12 +947,16 @@ class Resource(object): ) with self.lock(engine_id): + registry = new_stack.env.registry new_res_def = new_stack.t.resource_definitions( new_stack)[self.name] - new_res_type = new_stack.env.registry.get_class_to_instantiate( + new_res_type = registry.get_class_to_instantiate( new_res_def.resource_type, resource_name=self.name) + restricted_actions = registry.get_rsrc_restricted_actions( + self.name) + if type(self) is not new_res_type: - raise exception.UpdateReplace(self.name) + self._check_for_convergence_replace(restricted_actions) action_rollback = self.stack.action == self.stack.ROLLBACK status_in_progress = self.stack.status == self.stack.IN_PROGRESS diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 1017f62cf..59e256a26 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -3576,6 +3576,7 @@ class ResourceUpdateRestrictionTest(common.HeatTestCase): } } } + self.dummy_timeout = 10 def create_resource(self): self.stack = parser.Stack(utils.dummy_context(), 'test_stack', @@ -3585,6 +3586,17 @@ class ResourceUpdateRestrictionTest(common.HeatTestCase): scheduler.TaskRunner(res.create)() return res + def create_convergence_resource(self): + self.stack = parser.Stack(utils.dummy_context(), 'test_stack', + template.Template(self.tmpl, env=self.env), + stack_id=str(uuid.uuid4())) + res_data = {} + res = self.stack['bar'] + self.patchobject(res, 'lock') + scheduler.TaskRunner(res.create_convergence, self.stack.t.id, + res_data, 'engine-007', self.dummy_timeout)() + return res + def test_update_restricted(self): self.env_snippet = {u'resource_registry': { u'resources': { @@ -3679,3 +3691,59 @@ class ResourceUpdateRestrictionTest(common.HeatTestCase): self.assertIn('requires replacement', six.text_type(error)) self.assertEqual(1, prep_replace.call_count) ev.assert_not_called() + + def test_replace_restricted_type_change_with_convergence(self): + self.env_snippet = {u'resource_registry': { + u'resources': { + 'bar': {'restricted_actions': 'replace'} + } + } + } + self.env = environment.Environment() + self.env.load(self.env_snippet) + res = self.create_convergence_resource() + ev = self.patchobject(res, '_add_event') + bar = self.tmpl['resources']['bar'] + bar['type'] = 'OS::Heat::None' + self.new_stack = parser.Stack(utils.dummy_context(), 'test_stack', + template.Template(self.tmpl, + env=self.env)) + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(res.update_convergence, + self.stack.t.id, + {}, + 'engine-007', + self.dummy_timeout, + self.new_stack)) + self.assertEqual('ResourceActionRestricted: resources.bar: ' + 'replace is restricted for resource.', + six.text_type(error)) + self.assertEqual((res.CREATE, res.COMPLETE), res.state) + ev.assert_called_with(res.UPDATE, res.FAILED, + 'replace is restricted for resource.') + + def test_update_restricted_type_change_with_convergence(self): + self.env_snippet = {u'resource_registry': { + u'resources': { + 'bar': {'restricted_actions': 'update'} + } + } + } + self.env = environment.Environment() + self.env.load(self.env_snippet) + res = self.create_convergence_resource() + ev = self.patchobject(res, '_add_event') + bar = self.tmpl['resources']['bar'] + bar['type'] = 'OS::Heat::None' + self.new_stack = parser.Stack(utils.dummy_context(), 'test_stack', + template.Template(self.tmpl, + env=self.env)) + error = self.assertRaises(exception.UpdateReplace, + scheduler.TaskRunner(res.update_convergence, + self.stack.t.id, + {}, + 'engine-007', + self.dummy_timeout, + self.new_stack)) + self.assertIn('requires replacement', six.text_type(error)) + ev.assert_not_called()