OpenstackClient plugin for stack delete
This change implements the 'openstack stack delete' command. Blueprint: heat-support-python-openstackclient Change-Id: I95df1390a9daee7115ccda68b261e0a76530ade4
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user