diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index a4da5b026..a9c98b150 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -967,10 +967,11 @@ class OpenStackCloud(object): marker=marker) return self.get_stack(name_or_id) - def delete_stack(self, name_or_id): + def delete_stack(self, name_or_id, wait=False): """Delete a Heat Stack :param string name_or_id: Stack name or id. + :param boolean wait: Whether to wait for the delete to finish :returns: True if delete succeeded, False if the stack was not found. @@ -982,9 +983,33 @@ class OpenStackCloud(object): self.log.debug("Stack %s not found for deleting" % name_or_id) return False + 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("Failed to delete stack {id}".format( id=name_or_id)): self.manager.submitTask(_tasks.StackDelete(id=stack['id'])) + if wait: + try: + event_utils.poll_for_events(self.heat_client, + stack_name=name_or_id, + action='DELETE', + marker=marker) + except (heat_exceptions.NotFound, heat_exceptions.CommandError): + # heatclient might raise NotFound or CommandError on + # not found during poll_for_events + pass + stack = self.get_stack(name_or_id) + if stack and stack['stack_status'] == 'DELETE_FAILED': + raise OpenStackCloudException( + "Failed to delete stack {id}: {reason}".format( + id=name_or_id, reason=stack['stack_status_reason'])) + return True def get_name(self): diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py index 8f073b87d..a0e649b86 100644 --- a/shade/tests/functional/test_stack.py +++ b/shade/tests/functional/test_stack.py @@ -83,7 +83,8 @@ class TestStack(base.TestCase): self.skipTest('Orchestration service not supported by cloud') def _cleanup_stack(self): - self.cloud.delete_stack(self.stack_name) + self.cloud.delete_stack(self.stack_name, wait=True) + self.assertIsNone(self.cloud.get_stack(self.stack_name)) def test_stack_validation(self): test_template = tempfile.NamedTemporaryFile(delete=False) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index e796e46eb..5e1280a28 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -108,6 +108,35 @@ class TestStack(base.TestCase): ): self.cloud.delete_stack('stack_name') + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_wait(self, mock_heat, mock_get, mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + mock_get.side_effect = (stack, None) + self.assertTrue(self.cloud.delete_stack('stack_name', wait=True)) + mock_heat.stacks.delete.assert_called_once_with(stack['id']) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + + @mock.patch.object(event_utils, 'poll_for_events') + @mock.patch.object(shade.OpenStackCloud, 'get_stack') + @mock.patch.object(shade.OpenStackCloud, 'heat_client') + def test_delete_stack_wait_failed(self, mock_heat, mock_get, mock_poll): + stack = {'id': 'stack_id', 'name': 'stack_name'} + stack_failed = {'id': 'stack_id', 'name': 'stack_name', + 'stack_status': 'DELETE_FAILED', + 'stack_status_reason': 'ouch'} + mock_get.side_effect = (stack, stack_failed) + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to delete stack stack_name: ouch" + ): + self.cloud.delete_stack('stack_name', wait=True) + mock_heat.stacks.delete.assert_called_once_with(stack['id']) + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) + @mock.patch.object(template_utils, 'get_template_contents') @mock.patch.object(shade.OpenStackCloud, 'heat_client') def test_create_stack(self, mock_heat, mock_template):