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 logging
import os import os
import textwrap import textwrap
import time
import uuid import uuid
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
@@ -115,6 +116,30 @@ def event_log_formatter(events):
return "\n".join(event_log) 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): def print_update_list(lst, fields, formatters=None):
"""Print the stack-update --dry-run output as a table. """Print the stack-update --dry-run output as a table.

View File

@@ -14,7 +14,9 @@
"""Orchestration v1 Stack action implementations""" """Orchestration v1 Stack action implementations"""
import logging import logging
import sys
from cliff import command
from cliff import lister from cliff import lister
from cliff import show from cliff import show
from openstackclient.common import exceptions as exc 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.common import utils as heat_utils
from heatclient import exc as heat_exc from heatclient import exc as heat_exc
from heatclient.openstack.common._i18n import _ from heatclient.openstack.common._i18n import _
from heatclient.openstack.common._i18n import _LI
def _authenticated_fetcher(client): def _authenticated_fetcher(client):
@@ -505,7 +508,6 @@ class ListStack(lister.Lister):
help=_('List additional fields in output, this is implied by ' help=_('List additional fields in output, this is implied by '
'--all-projects') '--all-projects')
) )
return parser return parser
def take_action(self, parsed_args): 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): class AdoptStack(show.ShowOne):
"""Adopt a stack.""" """Adopt a stack."""

View File

@@ -527,6 +527,102 @@ class TestStackList(TestStack):
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) 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): class TestStackAdopt(TestStack):
adopt_file = 'heatclient/tests/test_templates/adopt.json' 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(expected, utils.event_log_formatter(events_list))
self.assertEqual('', utils.event_log_formatter([])) 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): class ShellTestParameterFiles(testtools.TestCase):

View File

@@ -43,6 +43,7 @@ openstack.orchestration.v1 =
stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_adopt = heatclient.osc.v1.stack:AdoptStack
stack_check = heatclient.osc.v1.stack:CheckStack stack_check = heatclient.osc.v1.stack:CheckStack
stack_create = heatclient.osc.v1.stack:CreateStack stack_create = heatclient.osc.v1.stack:CreateStack
stack_delete = heatclient.osc.v1.stack:DeleteStack
stack_list = heatclient.osc.v1.stack:ListStack stack_list = heatclient.osc.v1.stack:ListStack
stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack
stack_output_show = heatclient.osc.v1.stack:OutputShowStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack