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:
Amey Bhide
2015-11-05 17:54:18 -08:00
committed by Bryan Jones
parent 57bb6e508e
commit b696c52554
5 changed files with 213 additions and 1 deletions

View File

@@ -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.

View File

@@ -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."""

View File

@@ -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'

View File

@@ -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):

View File

@@ -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