diff --git a/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml b/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml new file mode 100644 index 000000000..29a155236 --- /dev/null +++ b/releasenotes/notes/stack-update-5886e91fd6e423bf.yaml @@ -0,0 +1,4 @@ +--- +features: + - Implement update_stack to perform the update action on existing + orchestration stacks. diff --git a/shade/_tasks.py b/shade/_tasks.py index 1ada75a21..bd487ba02 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -764,6 +764,11 @@ class StackCreate(task_manager.Task): return client.heat_client.stacks.create(**self.args) +class StackUpdate(task_manager.Task): + def main(self, client): + return client.heat_client.stacks.update(**self.args) + + class StackDelete(task_manager.Task): def main(self, client): return client.heat_client.stacks.delete(self.args['id']) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index d0a27aff8..9ebc1a220 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -877,6 +877,71 @@ class OpenStackCloud(object): action='CREATE') return self.get_stack(name) + def update_stack( + self, name_or_id, + template_file=None, template_url=None, + template_object=None, files=None, + rollback=True, + wait=False, timeout=3600, + environment_files=None, + **parameters): + """Update a Heat Stack. + + :param string name_or_id: Name or id of the stack to update. + :param string template_file: Path to the template. + :param string template_url: URL of template. + :param string template_object: URL to retrieve template object. + :param dict files: dict of additional file content to include. + :param boolean rollback: Enable rollback on update failure. + :param boolean wait: Whether to wait for the delete to finish. + :param int timeout: Stack update timeout in seconds. + :param list environment_files: Paths to environment files to apply. + + Other arguments will be passed as stack parameters which will take + precedence over any parameters specified in the environments. + + Only one of template_file, template_url, template_object should be + specified. + + :returns: a dict containing the stack description + + :raises: ``OpenStackCloudException`` if something goes wrong during + the openstack API calls + """ + envfiles, env = template_utils.process_multiple_environments_and_files( + env_paths=environment_files) + tpl_files, template = template_utils.get_template_contents( + template_file=template_file, + template_url=template_url, + template_object=template_object, + files=files) + params = dict( + stack_id=name_or_id, + disable_rollback=not rollback, + parameters=parameters, + template=template, + files=dict(list(tpl_files.items()) + list(envfiles.items())), + environment=env, + timeout_mins=timeout // 60, + ) + if wait: + # find the last event to use as the marker + events = event_utils.get_events(self.heat_client, + name_or_id, + event_args={'sort_dir': 'desc', + 'limit': 1}) + marker = events[0].id if events else None + + with _utils.heat_exceptions("Error updating stack {name}".format( + name=name_or_id)): + self.manager.submitTask(_tasks.StackUpdate(**params)) + if wait: + event_utils.poll_for_events(self.heat_client, + name_or_id, + action='UPDATE', + marker=marker) + return self.get_stack(name_or_id) + def delete_stack(self, name_or_id): """Delete a Heat Stack diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 2d08e3970..8f073b87d 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -120,6 +120,29 @@ class TestStack(base.TestCase): stack_ids = [s['id'] for s in stacks] self.assertIn(stack['id'], stack_ids) + # update with no changes + stack = self.cloud.update_stack(self.stack_name, + template_file=test_template.name, + wait=True) + + # assert no change in updated stack + self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) + rand = stack['outputs'][0]['output_value'] + self.assertEqual(rand, stack['outputs'][0]['output_value']) + + # update with changes + stack = self.cloud.update_stack(self.stack_name, + template_file=test_template.name, + wait=True, + length=12) + + # assert changed output in updated stack + stack = self.cloud.get_stack(self.stack_name) + self.assertEqual('UPDATE_COMPLETE', stack['stack_status']) + new_rand = stack['outputs'][0]['output_value'] + self.assertNotEqual(rand, new_rand) + self.assertEqual(12, len(new_rand)) + def test_stack_nested(self): test_template = tempfile.NamedTemporaryFile( diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index d8b8250a0..e796e46eb 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -148,6 +148,46 @@ class TestStack(base.TestCase): self.assertEqual(1, mock_poll.call_count) self.assertEqual(stack, ret) + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_update_stack(self, mock_heat, mock_template): + mock_template.return_value = ({}, {}) + self.cloud.update_stack('stack_name') + self.assertTrue(mock_template.called) + mock_heat.stacks.update.assert_called_once_with( + stack_id='stack_name', + disable_rollback=False, + environment={}, + parameters={}, + template={}, + files={}, + timeout_mins=60, + ) + + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(template_utils, 'get_template_contents') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_update_stack_wait(self, mock_heat, mock_get, mock_template, + mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_template.return_value = ({}, {}) + mock_get.return_value = stack + ret = self.cloud.update_stack('stack_name', wait=True) + self.assertTrue(mock_template.called) + mock_heat.stacks.update.assert_called_once_with( + stack_id='stack_name', + disable_rollback=False, + environment={}, + parameters={}, + template={}, + files={}, + timeout_mins=60, + ) + self.assertEqual(1, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + self.assertEqual(stack, ret) + @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_get_stack(self, mock_heat): stack = fakes.FakeStack('azerty', 'stack',)