Merge "Add post hooks"

This commit is contained in:
Jenkins 2016-02-22 03:49:43 +00:00 committed by Gerrit Code Review
commit 99f7c51dd3
7 changed files with 146 additions and 80 deletions

View File

@ -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
-----------------

View File

@ -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')

View File

@ -793,7 +793,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:
@ -812,6 +813,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 {
@ -1092,6 +1097,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.
@ -1389,6 +1397,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."""

View File

@ -384,6 +384,7 @@ class AccessKeyTest(common.HeatTestCase):
rsrc)
rsrc._secret = None
rsrc._data = None
self.assertEqual(self.fc.secret,
rsrc.FnGetAtt('SecretAccessKey'))

View File

@ -780,7 +780,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))
@ -861,18 +862,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,
},
},
}
@ -881,96 +887,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):

View File

@ -2891,6 +2891,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')
@ -2971,6 +2986,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):

View File

@ -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',