summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAmey Bhide <abhide@vmware.com>2015-11-05 17:54:18 -0800
committerBryan Jones <jonesbr@us.ibm.com>2016-02-12 15:22:01 +0000
commitb696c52554b54cbe4cb74c7c45195718fb56014e (patch)
tree1f22ff74e937d7ee175d3a48851b4537370c77d5
parent57bb6e508ef3db61ab9f932dd4aaccd9758c3311 (diff)
downloadpython-heatclient-b696c52554b54cbe4cb74c7c45195718fb56014e.tar.gz
OpenstackClient plugin for stack delete
This change implements the 'openstack stack delete' command. Blueprint: heat-support-python-openstackclient Change-Id: I95df1390a9daee7115ccda68b261e0a76530ade4
-rw-r--r--heatclient/common/utils.py25
-rw-r--r--heatclient/osc/v1/stack.py81
-rw-r--r--heatclient/tests/unit/osc/v1/test_stack.py96
-rw-r--r--heatclient/tests/unit/test_utils.py11
-rw-r--r--setup.cfg1
5 files changed, 213 insertions, 1 deletions
diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py
index a36ab38..b646e59 100644
--- a/heatclient/common/utils.py
+++ b/heatclient/common/utils.py
@@ -17,6 +17,7 @@ import base64
import logging
import os
import textwrap
+import time
import uuid
from oslo_serialization import jsonutils
@@ -115,6 +116,30 @@ def event_log_formatter(events):
return "\n".join(event_log)
+def wait_for_delete(status_f,
+ res_id,
+ status_field='status',
+ sleep_time=5,
+ timeout=300):
+ """Wait for resource deletion."""
+
+ total_time = 0
+ while total_time < timeout:
+ try:
+ res = status_f(res_id)
+ except exc.HTTPNotFound:
+ return True
+
+ status = res.get(status_field, '').lower()
+ if 'failed' in status:
+ return False
+
+ time.sleep(sleep_time)
+ total_time += sleep_time
+
+ return False
+
+
def print_update_list(lst, fields, formatters=None):
"""Print the stack-update --dry-run output as a table.
diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py
index d71f5f7..d0370a5 100644
--- a/heatclient/osc/v1/stack.py
+++ b/heatclient/osc/v1/stack.py
@@ -14,7 +14,9 @@
"""Orchestration v1 Stack action implementations"""
import logging
+import sys
+from cliff import command
from cliff import lister
from cliff import show
from openstackclient.common import exceptions as exc
@@ -30,6 +32,7 @@ from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from heatclient import exc as heat_exc
from heatclient.openstack.common._i18n import _
+from heatclient.openstack.common._i18n import _LI
def _authenticated_fetcher(client):
@@ -505,7 +508,6 @@ class ListStack(lister.Lister):
help=_('List additional fields in output, this is implied by '
'--all-projects')
)
-
return parser
def take_action(self, parsed_args):
@@ -572,6 +574,83 @@ def _list(client, args=None):
)
+class DeleteStack(command.Command):
+ """Delete stack(s)."""
+
+ log = logging.getLogger(__name__ + ".DeleteStack")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteStack, self).get_parser(prog_name)
+ parser.add_argument(
+ 'stack',
+ metavar='<stack>',
+ nargs='+',
+ help=_('Stack(s) to delete (name or ID)')
+ )
+ parser.add_argument(
+ '--yes',
+ action='store_true',
+ help=_('Skip yes/no prompt (assume yes)')
+ )
+ parser.add_argument(
+ '--wait',
+ action='store_true',
+ help=_('Wait for stack delete to complete')
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ self.log.debug("take_action(%s)", parsed_args)
+
+ heat_client = self.app.client_manager.orchestration
+
+ try:
+ if not parsed_args.yes and sys.stdin.isatty():
+ sys.stdout.write(
+ _("Are you sure you want to delete this stack(s) [y/N]? "))
+ prompt_response = sys.stdin.readline().lower()
+ if not prompt_response.startswith('y'):
+ self.log.info(_LI('User did not confirm stack delete so '
+ 'taking no action.'))
+ return
+ except KeyboardInterrupt: # ctrl-c
+ self.log.info(_LI('User did not confirm stack delete '
+ '(ctrl-c) so taking no action.'))
+ return
+ except EOFError: # ctrl-d
+ self.log.info(_LI('User did not confirm stack delete '
+ '(ctrl-d) so taking no action.'))
+ return
+
+ failure_count = 0
+ stacks_waiting = []
+ for sid in parsed_args.stack:
+ try:
+ heat_client.stacks.delete(sid)
+ stacks_waiting.append(sid)
+ except heat_exc.HTTPNotFound:
+ failure_count += 1
+ print(_('Stack not found: %s') % sid)
+
+ if parsed_args.wait:
+ for sid in stacks_waiting:
+ def status_f(id):
+ return heat_client.stacks.get(id).to_dict()
+
+ # TODO(jonesbr): switch to use openstack client wait_for_delete
+ # when version 2.1.0 is adopted.
+ if not heat_utils.wait_for_delete(status_f,
+ sid,
+ status_field='stack_status'):
+ failure_count += 1
+ print(_('Stack failed to delete: %s') % sid)
+
+ if failure_count:
+ msg = (_('Unable to delete %(count)d of the %(total)d stacks.') %
+ {'count': failure_count, 'total': len(parsed_args.stack)})
+ raise exc.CommandError(msg)
+
+
class AdoptStack(show.ShowOne):
"""Adopt a stack."""
diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py
index 161de58..7a1fd36 100644
--- a/heatclient/tests/unit/osc/v1/test_stack.py
+++ b/heatclient/tests/unit/osc/v1/test_stack.py
@@ -527,6 +527,102 @@ class TestStackList(TestStack):
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
+class TestStackDelete(TestStack):
+
+ def setUp(self):
+ super(TestStackDelete, self).setUp()
+ self.cmd = stack.DeleteStack(self.app, None)
+ self.stack_client.delete = mock.MagicMock()
+ self.stack_client.get = mock.MagicMock(
+ side_effect=heat_exc.HTTPNotFound)
+
+ def test_stack_delete(self):
+ arglist = ['stack1', 'stack2', 'stack3']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.delete.assert_any_call('stack1')
+ self.stack_client.delete.assert_any_call('stack2')
+ self.stack_client.delete.assert_any_call('stack3')
+
+ def test_stack_delete_not_found(self):
+ arglist = ['my_stack']
+ self.stack_client.delete.side_effect = heat_exc.HTTPNotFound
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
+
+ def test_stack_delete_one_found_one_not_found(self):
+ arglist = ['stack1', 'stack2']
+ self.stack_client.delete.side_effect = [None, heat_exc.HTTPNotFound]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ error = self.assertRaises(exc.CommandError,
+ self.cmd.take_action, parsed_args)
+
+ self.stack_client.delete.assert_any_call('stack1')
+ self.stack_client.delete.assert_any_call('stack2')
+ self.assertEqual('Unable to delete 1 of the 2 stacks.', str(error))
+
+ def test_stack_delete_wait(self):
+ arglist = ['stack1', 'stack2', 'stack3', '--wait']
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ self.stack_client.delete.assert_any_call('stack1')
+ self.stack_client.get.assert_any_call('stack1')
+ self.stack_client.delete.assert_any_call('stack2')
+ self.stack_client.get.assert_any_call('stack2')
+ self.stack_client.delete.assert_any_call('stack3')
+ self.stack_client.get.assert_any_call('stack3')
+
+ def test_stack_delete_wait_one_pass_one_fail(self):
+ arglist = ['stack1', 'stack2', 'stack3', '--wait']
+ self.stack_client.get.side_effect = [
+ stacks.Stack(None, {'stack_status': 'DELETE_FAILED'}),
+ heat_exc.HTTPNotFound,
+ stacks.Stack(None, {'stack_status': 'DELETE_FAILED'}),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ error = self.assertRaises(exc.CommandError,
+ self.cmd.take_action, parsed_args)
+
+ self.stack_client.delete.assert_any_call('stack1')
+ self.stack_client.get.assert_any_call('stack1')
+ self.stack_client.delete.assert_any_call('stack2')
+ self.stack_client.get.assert_any_call('stack2')
+ self.stack_client.delete.assert_any_call('stack3')
+ self.stack_client.get.assert_any_call('stack3')
+ self.assertEqual('Unable to delete 2 of the 3 stacks.', str(error))
+
+ @mock.patch('sys.stdin', spec=six.StringIO)
+ def test_stack_delete_prompt(self, mock_stdin):
+ arglist = ['my_stack']
+ mock_stdin.isatty.return_value = True
+ mock_stdin.readline.return_value = 'y'
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ mock_stdin.readline.assert_called_with()
+ self.stack_client.delete.assert_called_with('my_stack')
+
+ @mock.patch('sys.stdin', spec=six.StringIO)
+ def test_stack_delete_prompt_no(self, mock_stdin):
+ arglist = ['my_stack']
+ mock_stdin.isatty.return_value = True
+ mock_stdin.readline.return_value = 'n'
+ parsed_args = self.check_parser(self.cmd, arglist, [])
+
+ self.cmd.take_action(parsed_args)
+
+ mock_stdin.readline.assert_called_with()
+ self.stack_client.delete.assert_not_called()
+
+
class TestStackAdopt(TestStack):
adopt_file = 'heatclient/tests/test_templates/adopt.json'
diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py
index 8237b4f..c58f788 100644
--- a/heatclient/tests/unit/test_utils.py
+++ b/heatclient/tests/unit/test_utils.py
@@ -188,6 +188,17 @@ class ShellTest(testtools.TestCase):
self.assertEqual(expected, utils.event_log_formatter(events_list))
self.assertEqual('', utils.event_log_formatter([]))
+ def test_wait_for_delete(self):
+ def status_f(id):
+ raise exc.HTTPNotFound
+
+ def bad_status_f(id):
+ return {'status': 'failed'}
+
+ self.assertTrue(utils.wait_for_delete(status_f, 123))
+ self.assertFalse(utils.wait_for_delete(status_f, 123, timeout=0))
+ self.assertFalse(utils.wait_for_delete(bad_status_f, 123))
+
class ShellTestParameterFiles(testtools.TestCase):
diff --git a/setup.cfg b/setup.cfg
index b88d067..f6e7cad 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -43,6 +43,7 @@ openstack.orchestration.v1 =
stack_adopt = heatclient.osc.v1.stack:AdoptStack
stack_check = heatclient.osc.v1.stack:CheckStack
stack_create = heatclient.osc.v1.stack:CreateStack
+ stack_delete = heatclient.osc.v1.stack:DeleteStack
stack_list = heatclient.osc.v1.stack:ListStack
stack_output_list = heatclient.osc.v1.stack:OutputListStack
stack_output_show = heatclient.osc.v1.stack:OutputShowStack