Add a functional test for hooks/breakpoints
Adds initial tests for pre-create and pre-update hooks.
Tests for in-place updates, nested stacks and wildcards are still
todo.
Co-Authored-By: Tomas Sedovic <tsedovic@redhat.com>
Change-Id: I980ed9d3b3cce239ea7f588db2abc05d090849f5
diff --git a/common/test.py b/common/test.py
index 30b10a8..b88c72b 100644
--- a/common/test.py
+++ b/common/test.py
@@ -275,7 +275,8 @@
success_on_not_found=True)
def update_stack(self, stack_identifier, template, environment=None,
- files=None, parameters=None):
+ files=None, parameters=None,
+ expected_status='UPDATE_COMPLETE'):
env = environment or {}
env_files = files or {}
parameters = parameters or {}
@@ -289,9 +290,29 @@
parameters=parameters,
environment=env
)
- self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, expected_status)
- def assert_resource_is_a_stack(self, stack_identifier, res_name):
+ def assert_resource_is_a_stack(self, stack_identifier, res_name,
+ wait=False):
+ build_timeout = self.conf.build_timeout
+ build_interval = self.conf.build_interval
+ start = timeutils.utcnow()
+ while timeutils.delta_seconds(start,
+ timeutils.utcnow()) < build_timeout:
+ time.sleep(build_interval)
+ try:
+ nested_identifier = self._get_nested_identifier(
+ stack_identifier, res_name)
+ except Exception:
+ # We may have to wait, if the create is in-progress
+ if wait:
+ time.sleep(build_interval)
+ else:
+ raise
+ else:
+ return nested_identifier
+
+ def _get_nested_identifier(self, stack_identifier, res_name):
rsrc = self.client.resources.get(stack_identifier, res_name)
nested_link = [l for l in rsrc.links if l['rel'] == 'nested']
nested_href = nested_link[0]['href']
@@ -375,3 +396,22 @@
stack_name = stack_identifier.split('/')[0]
self.client.actions.resume(stack_name)
self._wait_for_stack_status(stack_identifier, 'RESUME_COMPLETE')
+
+ def wait_for_event_with_reason(self, stack_identifier, reason,
+ rsrc_name=None, num_expected=1):
+ build_timeout = self.conf.build_timeout
+ build_interval = self.conf.build_interval
+ start = timeutils.utcnow()
+ while timeutils.delta_seconds(start,
+ timeutils.utcnow()) < build_timeout:
+ try:
+ rsrc_events = self.client.events.list(stack_identifier,
+ resource_name=rsrc_name)
+ except heat_exceptions.HTTPNotFound:
+ LOG.debug("No events yet found for %s" % rsrc_name)
+ else:
+ matched = [e for e in rsrc_events
+ if e.resource_status_reason == reason]
+ if len(matched) == num_expected:
+ return matched
+ time.sleep(build_interval)
diff --git a/functional/test_hooks.py b/functional/test_hooks.py
new file mode 100644
index 0000000..f7d455a
--- /dev/null
+++ b/functional/test_hooks.py
@@ -0,0 +1,289 @@
+# 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
+
+import yaml
+
+from heat_integrationtests.common import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class HooksTest(test.HeatIntegrationTest):
+
+ def setUp(self):
+ super(HooksTest, self).setUp()
+ self.client = self.orchestration_client
+ self.template = {'heat_template_version': '2014-10-16',
+ 'resources': {
+ 'foo_step1': {'type': 'OS::Heat::RandomString'},
+ 'foo_step2': {'type': 'OS::Heat::RandomString',
+ 'depends_on': 'foo_step1'},
+ 'foo_step3': {'type': 'OS::Heat::RandomString',
+ 'depends_on': 'foo_step2'}}}
+
+ def test_hook_pre_create(self):
+ env = {'resource_registry':
+ {'resources':
+ {'foo_step2':
+ {'hooks': 'pre-create'}}}}
+ # Note we don't wait for CREATE_COMPLETE, because we need to
+ # signal to clear the hook before create will complete
+ stack_identifier = self.stack_create(
+ template=self.template,
+ environment=env,
+ expected_status='CREATE_IN_PROGRESS')
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step1', 'CREATE_COMPLETE')
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'INIT_COMPLETE')
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='CREATE paused until Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('INIT_COMPLETE', ev[0].resource_status)
+ self.client.resources.signal(stack_identifier, 'foo_step2',
+ data={'unset_hook': 'pre-create'})
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('INIT_COMPLETE', ev[0].resource_status)
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+
+ def test_hook_pre_update_nochange(self):
+ env = {'resource_registry':
+ {'resources':
+ {'foo_step2':
+ {'hooks': 'pre-update'}}}}
+ stack_identifier = self.stack_create(
+ template=self.template,
+ environment=env)
+ res_before = self.client.resources.get(stack_identifier, 'foo_step2')
+ # Note we don't wait for UPDATE_COMPLETE, because we need to
+ # signal to clear the hook before update will complete
+ self.update_stack(
+ stack_identifier,
+ template=self.template,
+ environment=env,
+ expected_status='UPDATE_IN_PROGRESS')
+
+ # Note when a hook is specified, the resource status doesn't change
+ # when we hit the hook, so we look for the event, then assert the
+ # state is unchanged.
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='UPDATE paused until Hook pre-update is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self.client.resources.signal(stack_identifier, 'foo_step2',
+ data={'unset_hook': 'pre-update'})
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-update is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+ res_after = self.client.resources.get(stack_identifier, 'foo_step2')
+ self.assertEqual(res_before.physical_resource_id,
+ res_after.physical_resource_id)
+
+ def test_hook_pre_update_replace(self):
+ env = {'resource_registry':
+ {'resources':
+ {'foo_step2':
+ {'hooks': 'pre-update'}}}}
+ stack_identifier = self.stack_create(
+ template=self.template,
+ environment=env)
+ res_before = self.client.resources.get(stack_identifier, 'foo_step2')
+ # Note we don't wait for UPDATE_COMPLETE, because we need to
+ # signal to clear the hook before update will complete
+ self.template['resources']['foo_step2']['properties'] = {'length': 10}
+ self.update_stack(
+ stack_identifier,
+ template=self.template,
+ environment=env,
+ expected_status='UPDATE_IN_PROGRESS')
+
+ # Note when a hook is specified, the resource status doesn't change
+ # when we hit the hook, so we look for the event, then assert the
+ # state is unchanged.
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='UPDATE paused until Hook pre-update is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self.client.resources.signal(stack_identifier, 'foo_step2',
+ data={'unset_hook': 'pre-update'})
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-update is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+ res_after = self.client.resources.get(stack_identifier, 'foo_step2')
+ self.assertNotEqual(res_before.physical_resource_id,
+ res_after.physical_resource_id)
+
+ def test_hook_pre_update_in_place(self):
+ env = {'resource_registry':
+ {'resources':
+ {'rg':
+ {'hooks': 'pre-update'}}}}
+ template = {'heat_template_version': '2014-10-16',
+ 'resources': {
+ 'rg': {
+ 'type': 'OS::Heat::ResourceGroup',
+ 'properties': {
+ 'count': 1,
+ 'resource_def': {
+ 'type': 'OS::Heat::RandomString'}}}}}
+ # Note we don't wait for CREATE_COMPLETE, because we need to
+ # signal to clear the hook before create will complete
+ stack_identifier = self.stack_create(
+ template=template,
+ environment=env)
+ res_before = self.client.resources.get(stack_identifier, 'rg')
+ template['resources']['rg']['properties']['count'] = 2
+ self.update_stack(
+ stack_identifier,
+ template=template,
+ environment=env,
+ expected_status='UPDATE_IN_PROGRESS')
+
+ # Note when a hook is specified, the resource status doesn't change
+ # when we hit the hook, so we look for the event, then assert the
+ # state is unchanged.
+ self._wait_for_resource_status(
+ stack_identifier, 'rg', 'CREATE_COMPLETE')
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='UPDATE paused until Hook pre-update is cleared',
+ rsrc_name='rg')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self.client.resources.signal(stack_identifier, 'rg',
+ data={'unset_hook': 'pre-update'})
+
+ ev = self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-update is cleared',
+ rsrc_name='rg')
+ self.assertEqual('CREATE_COMPLETE', ev[0].resource_status)
+ self._wait_for_resource_status(
+ stack_identifier, 'rg', 'CREATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+ res_after = self.client.resources.get(stack_identifier, 'rg')
+ self.assertEqual(res_before.physical_resource_id,
+ res_after.physical_resource_id)
+
+ def test_hook_pre_create_nested(self):
+ files = {'nested.yaml': yaml.dump(self.template)}
+ env = {'resource_registry':
+ {'resources':
+ {'nested':
+ {'foo_step2':
+ {'hooks': 'pre-create'}}}}}
+ template = {'heat_template_version': '2014-10-16',
+ 'resources': {
+ 'nested': {'type': 'nested.yaml'}}}
+ # Note we don't wait for CREATE_COMPLETE, because we need to
+ # signal to clear the hook before create will complete
+ stack_identifier = self.stack_create(
+ template=template,
+ environment=env,
+ files=files,
+ expected_status='CREATE_IN_PROGRESS')
+ self._wait_for_resource_status(stack_identifier, 'nested',
+ 'CREATE_IN_PROGRESS')
+ nested_identifier = self.assert_resource_is_a_stack(
+ stack_identifier, 'nested', wait=True)
+ self._wait_for_resource_status(
+ nested_identifier, 'foo_step1', 'CREATE_COMPLETE')
+ self._wait_for_resource_status(
+ nested_identifier, 'foo_step2', 'INIT_COMPLETE')
+ ev = self.wait_for_event_with_reason(
+ nested_identifier,
+ reason='CREATE paused until Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('INIT_COMPLETE', ev[0].resource_status)
+ self.client.resources.signal(nested_identifier, 'foo_step2',
+ data={'unset_hook': 'pre-create'})
+ ev = self.wait_for_event_with_reason(
+ nested_identifier,
+ reason='Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self.assertEqual('INIT_COMPLETE', ev[0].resource_status)
+ self._wait_for_resource_status(
+ nested_identifier, 'foo_step2', 'CREATE_COMPLETE')
+ self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+
+ def test_hook_pre_create_wildcard(self):
+ env = {'resource_registry':
+ {'resources':
+ {'foo_*':
+ {'hooks': 'pre-create'}}}}
+ # Note we don't wait for CREATE_COMPLETE, because we need to
+ # signal to clear the hook before create will complete
+ stack_identifier = self.stack_create(
+ template=self.template,
+ environment=env,
+ expected_status='CREATE_IN_PROGRESS')
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step1', 'INIT_COMPLETE')
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='CREATE paused until Hook pre-create is cleared',
+ rsrc_name='foo_step1')
+ self.client.resources.signal(stack_identifier, 'foo_step1',
+ data={'unset_hook': 'pre-create'})
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-create is cleared',
+ rsrc_name='foo_step1')
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step2', 'INIT_COMPLETE')
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='CREATE paused until Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self.client.resources.signal(stack_identifier, 'foo_step2',
+ data={'unset_hook': 'pre-create'})
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-create is cleared',
+ rsrc_name='foo_step2')
+ self._wait_for_resource_status(
+ stack_identifier, 'foo_step3', 'INIT_COMPLETE')
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='CREATE paused until Hook pre-create is cleared',
+ rsrc_name='foo_step3')
+ self.client.resources.signal(stack_identifier, 'foo_step3',
+ data={'unset_hook': 'pre-create'})
+ self.wait_for_event_with_reason(
+ stack_identifier,
+ reason='Hook pre-create is cleared',
+ rsrc_name='foo_step3')
+ self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')