diff --git a/doc/source/template_guide/environment.rst b/doc/source/template_guide/environment.rst index a34b951c54..6a8e60b42e 100644 --- a/doc/source/template_guide/environment.rst +++ b/doc/source/template_guide/environment.rst @@ -137,15 +137,16 @@ template:: my_db_server: "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml -Pause stack creation or update on a given resource --------------------------------------------------- -If you want to debug your stack as it's being created or updated, or if you want -to run it in phases, you can set ``pre-create`` and ``pre-update`` hooks in the -``resources`` section of ``resource_registry``. +Pause stack creation, update or deletion on a given resource +------------------------------------------------------------ +If you want to debug your stack as it's being created, updated or deleted, or +if you want to run it in phases, you can set ``pre-create``, ``pre-update``, +``pre-delete``, ``post-create``, ``post-update`` and ``post-delete`` hooks in +the ``resources`` section of ``resource_registry``. -To set a hook, add either ``hooks: pre-create`` or ``hooks: pre-update`` to the -resource's dictionary. You can also use ``[pre-create, pre-update]`` to stop -on both actions. +To set a hook, add either ``hooks: $hook_name`` (for example ``hooks: +pre-update``) to the resource's dictionary. You can also use a list (``hooks: +[pre-create, pre-update]``) to stop on several actions. You can combine hooks with other ``resources`` properties such as provider templates or type mapping:: @@ -175,8 +176,8 @@ resource name. For example, the following entry pauses while creating "*_server": hooks: pre-create -Clear hooks by signaling the resource with ``{unset_hook: pre-create}`` -or ``{unset_hook: pre-update}``. +Clear hooks by signaling the resource with ``{unset_hook: $hook_name}`` (for +example ``{unset_hook: pre-update}``). Retrieving events ----------------- diff --git a/heat/engine/environment.py b/heat/engine/environment.py index 86b13bb242..adb9246a29 100644 --- a/heat/engine/environment.py +++ b/heat/engine/environment.py @@ -35,9 +35,11 @@ LOG = log.getLogger(__name__) HOOK_TYPES = ( - HOOK_PRE_CREATE, HOOK_PRE_UPDATE, HOOK_PRE_DELETE + HOOK_PRE_CREATE, HOOK_PRE_UPDATE, HOOK_PRE_DELETE, HOOK_POST_CREATE, + HOOK_POST_UPDATE, HOOK_POST_DELETE ) = ( - 'pre-create', 'pre-update', 'pre-delete' + 'pre-create', 'pre-update', 'pre-delete', 'post-create', + 'post-update', 'post-delete' ) RESTRICTED_ACTIONS = (UPDATE, REPLACE) = ('update', 'replace') diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 2b72cac6ec..94e0896024 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -790,7 +790,8 @@ class Resource(object): try: yield self._do_action(action, self.properties.validate) if action == self.CREATE: - return + first_failure = None + break else: action = self.CREATE except exception.ResourceFailure as failure: @@ -809,6 +810,10 @@ class Resource(object): if first_failure: raise first_failure + if self.stack.action == self.stack.CREATE: + yield self._break_if_required( + self.CREATE, environment.HOOK_POST_CREATE) + def prepare_abandon(self): self.abandon_in_progress = True return { @@ -1084,6 +1089,9 @@ class Resource(object): raise failure raise + yield self._break_if_required( + self.UPDATE, environment.HOOK_POST_UPDATE) + def prepare_for_replace(self): """Prepare resource for replacing. @@ -1381,6 +1389,10 @@ class Resource(object): action_args = [] yield self.action_handler_task(action, *action_args) + if self.stack.action == self.stack.DELETE: + yield self._break_if_required( + self.DELETE, environment.HOOK_POST_DELETE) + @scheduler.wrappertask def destroy(self): """A task to delete the resource and remove it from the database.""" diff --git a/heat/tests/aws/test_user.py b/heat/tests/aws/test_user.py index fdb7ebc077..78d75ffe19 100644 --- a/heat/tests/aws/test_user.py +++ b/heat/tests/aws/test_user.py @@ -384,6 +384,7 @@ class AccessKeyTest(common.HeatTestCase): rsrc) rsrc._secret = None + rsrc._data = None self.assertEqual(self.fc.secret, rsrc.FnGetAtt('SecretAccessKey')) diff --git a/heat/tests/test_environment.py b/heat/tests/test_environment.py index 094a13d06e..fd38c7a6a3 100644 --- a/heat/tests/test_environment.py +++ b/heat/tests/test_environment.py @@ -779,7 +779,8 @@ class ResourceRegistryTest(common.HeatTestCase): registry = environment.ResourceRegistry(None, {}) msg = ('Invalid hook type "invalid-type" for resource breakpoint, ' 'acceptable hook types are: (\'pre-create\', \'pre-update\', ' - '\'pre-delete\')') + '\'pre-delete\', \'post-create\', \'post-update\', ' + '\'post-delete\')') ex = self.assertRaises(exception.InvalidBreakPointHook, registry.load, {'resources': resources}) self.assertEqual(msg, six.text_type(ex)) @@ -851,18 +852,23 @@ class ResourceRegistryTest(common.HeatTestCase): class HookMatchTest(common.HeatTestCase): + scenarios = [(hook_type, {'hook': hook_type}) for hook_type in + environment.HOOK_TYPES] + def test_plain_matches(self): + other_hook = next(hook for hook in environment.HOOK_TYPES + if hook != self.hook) resources = { u'a': { u'OS::Fruit': u'apples.yaml', - u'hooks': [u'pre-create', u'pre-update'], + u'hooks': [self.hook, other_hook] }, u'b': { u'OS::Food': u'fruity.yaml', }, u'nested': { u'res': { - u'hooks': 'pre-create', + u'hooks': self.hook, }, }, } @@ -871,96 +877,59 @@ class HookMatchTest(common.HeatTestCase): u'OS::Fruit': u'apples.yaml', 'resources': resources}) - self.assertTrue(registry.matches_hook( - 'a', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'b', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'OS::Fruit', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'res', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'unknown', environment.HOOK_PRE_CREATE)) + self.assertTrue(registry.matches_hook('a', self.hook)) + self.assertFalse(registry.matches_hook('b', self.hook)) + self.assertFalse(registry.matches_hook('OS::Fruit', self.hook)) + self.assertFalse(registry.matches_hook('res', self.hook)) + self.assertFalse(registry.matches_hook('unknown', self.hook)) def test_wildcard_matches(self): + other_hook = next(hook for hook in environment.HOOK_TYPES + if hook != self.hook) resources = { u'prefix_*': { - u'hooks': 'pre-create', + u'hooks': self.hook }, u'*_suffix': { - u'hooks': 'pre-create', + u'hooks': self.hook }, u'*': { - u'hooks': 'pre-update', + u'hooks': other_hook }, } registry = environment.ResourceRegistry(None, {}) registry.load({'resources': resources}) - self.assertTrue(registry.matches_hook( - 'prefix_', environment.HOOK_PRE_CREATE)) - self.assertTrue(registry.matches_hook( - 'prefix_some', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'some_prefix', environment.HOOK_PRE_CREATE)) + self.assertTrue(registry.matches_hook('prefix_', self.hook)) + self.assertTrue(registry.matches_hook('prefix_some', self.hook)) + self.assertFalse(registry.matches_hook('some_prefix', self.hook)) - self.assertTrue(registry.matches_hook( - '_suffix', environment.HOOK_PRE_CREATE)) - self.assertTrue(registry.matches_hook( - 'some_suffix', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - '_suffix_blah', environment.HOOK_PRE_CREATE)) + self.assertTrue(registry.matches_hook('_suffix', self.hook)) + self.assertTrue(registry.matches_hook('some_suffix', self.hook)) + self.assertFalse(registry.matches_hook('_suffix_blah', self.hook)) - self.assertTrue(registry.matches_hook( - 'some_prefix', environment.HOOK_PRE_UPDATE)) - self.assertTrue(registry.matches_hook( - '_suffix_blah', environment.HOOK_PRE_UPDATE)) + self.assertTrue(registry.matches_hook('some_prefix', other_hook)) + self.assertTrue(registry.matches_hook('_suffix_blah', other_hook)) def test_hook_types(self): resources = { - u'pre_create': { - u'hooks': 'pre-create', + u'hook': { + u'hooks': self.hook }, - u'pre_update': { - u'hooks': 'pre-update', - }, - u'pre_delete': { - u'hooks': 'pre-delete', + u'not-hook': { + u'hooks': [hook for hook in environment.HOOK_TYPES if hook != + self.hook] }, u'all': { - u'hooks': ['pre-create', 'pre-update', 'pre-delete'], + u'hooks': environment.HOOK_TYPES }, } registry = environment.ResourceRegistry(None, {}) registry.load({'resources': resources}) - self.assertTrue(registry.matches_hook( - 'pre_create', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'pre_create', environment.HOOK_PRE_UPDATE)) - self.assertFalse(registry.matches_hook( - 'pre_create', environment.HOOK_PRE_DELETE)) - - self.assertTrue(registry.matches_hook( - 'pre_update', environment.HOOK_PRE_UPDATE)) - self.assertFalse(registry.matches_hook( - 'pre_update', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'pre_update', environment.HOOK_PRE_DELETE)) - - self.assertTrue(registry.matches_hook( - 'pre_delete', environment.HOOK_PRE_DELETE)) - self.assertFalse(registry.matches_hook( - 'pre_delete', environment.HOOK_PRE_CREATE)) - self.assertFalse(registry.matches_hook( - 'pre_delete', environment.HOOK_PRE_UPDATE)) - - self.assertTrue(registry.matches_hook( - 'all', environment.HOOK_PRE_CREATE)) - self.assertTrue(registry.matches_hook( - 'all', environment.HOOK_PRE_UPDATE)) - self.assertTrue(registry.matches_hook( - 'all', environment.HOOK_PRE_DELETE)) + self.assertTrue(registry.matches_hook('hook', self.hook)) + self.assertFalse(registry.matches_hook('not-hook', self.hook)) + self.assertTrue(registry.matches_hook('all', self.hook)) class ActionRestrictedTest(common.HeatTestCase): diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 6f5303db85..8edc09d3ab 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -2838,6 +2838,21 @@ class ResourceHookTest(common.HeatTestCase): self.assertFalse(res.has_hook('pre-update')) self.assertTrue(res.has_hook('pre-delete')) + res.data = mock.Mock(return_value={'post-create': 'True'}) + self.assertFalse(res.has_hook('post-delete')) + self.assertFalse(res.has_hook('post-update')) + self.assertTrue(res.has_hook('post-create')) + + res.data = mock.Mock(return_value={'post-update': 'True'}) + self.assertFalse(res.has_hook('post-create')) + self.assertFalse(res.has_hook('post-delete')) + self.assertTrue(res.has_hook('post-update')) + + res.data = mock.Mock(return_value={'post-delete': 'True'}) + self.assertFalse(res.has_hook('post-create')) + self.assertFalse(res.has_hook('post-update')) + self.assertTrue(res.has_hook('post-delete')) + def test_set_hook(self): snippet = rsrc_defn.ResourceDefinition('res', 'GenericResourceType') @@ -2918,6 +2933,38 @@ class ResourceHookTest(common.HeatTestCase): task.run_to_completion() self.assertEqual((res.DELETE, res.COMPLETE), res.state) + def test_post_create_hook_call(self): + self.stack.env.registry.load( + {'resources': {'res': {'hooks': 'post-create'}}}) + snippet = rsrc_defn.ResourceDefinition('res', + 'GenericResourceType') + res = resource.Resource('res', snippet, self.stack) + res.id = '1234' + task = scheduler.TaskRunner(res.create) + task.start() + task.step() + self.assertTrue(res.has_hook('post-create')) + res.clear_hook('post-create') + task.run_to_completion() + self.assertEqual((res.CREATE, res.COMPLETE), res.state) + + def test_post_delete_hook_call(self): + self.stack.env.registry.load( + {'resources': {'res': {'hooks': 'post-delete'}}}) + snippet = rsrc_defn.ResourceDefinition('res', + 'GenericResourceType') + res = resource.Resource('res', snippet, self.stack) + res.id = '1234' + res.action = 'CREATE' + self.stack.action = 'DELETE' + task = scheduler.TaskRunner(res.delete) + task.start() + task.step() + self.assertTrue(res.has_hook('post-delete')) + res.clear_hook('post-delete') + task.run_to_completion() + self.assertEqual((res.DELETE, res.COMPLETE), res.state) + class ResourceAvailabilityTest(common.HeatTestCase): def _mock_client_plugin(self, service_types=None, is_available=True): diff --git a/heat/tests/test_stack_update.py b/heat/tests/test_stack_update.py index fd7c67af51..0350726d2d 100644 --- a/heat/tests/test_stack_update.py +++ b/heat/tests/test_stack_update.py @@ -484,6 +484,40 @@ class StackUpdateTest(common.HeatTestCase): self.stack.state) self.assertEqual('xyz', self.stack['AResource'].properties['Foo']) + def test_update_replace_post_hook(self): + tmpl = { + 'HeatTemplateFormatVersion': '2012-12-12', + 'Parameters': { + 'foo': {'Type': 'String'} + }, + 'Resources': { + 'AResource': { + 'Type': 'ResWithComplexPropsAndAttrs', + 'Properties': {'an_int': {'Ref': 'foo'}} + } + } + } + self.stack = stack.Stack( + self.ctx, 'update_test_stack', + template.Template(tmpl, env=environment.Environment({'foo': 1}))) + self.stack.store() + self.stack.create() + self.assertEqual((stack.Stack.CREATE, stack.Stack.COMPLETE), + self.stack.state) + + env2 = environment.Environment({'foo': 2}) + env2.registry.load( + {'resources': {'AResource': {'hooks': 'post-update'}}}) + updated_stack = stack.Stack(self.ctx, 'updated_stack', + template.Template(tmpl, env=env2)) + mock_hook = self.patchobject(self.stack['AResource'], 'trigger_hook') + + self.stack.update(updated_stack) + mock_hook.assert_called_once_with('post-update') + self.assertEqual((stack.Stack.UPDATE, stack.Stack.COMPLETE), + self.stack.state) + self.assertEqual(2, self.stack['AResource'].properties['an_int']) + def test_update_modify_update_failed(self): tmpl = {'HeatTemplateFormatVersion': '2012-12-12', 'Resources': {'AResource': {'Type': 'ResourceWithPropsType',