summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/common/utils.py46
-rw-r--r--openstackclient/compute/v2/server.py17
-rw-r--r--openstackclient/tests/common/test_utils.py39
-rw-r--r--openstackclient/tests/compute/v2/test_server.py47
4 files changed, 149 insertions, 0 deletions
diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py
index 4139770c..aad0519c 100644
--- a/openstackclient/common/utils.py
+++ b/openstackclient/common/utils.py
@@ -283,6 +283,52 @@ def wait_for_status(status_f,
return retval
+def wait_for_delete(manager,
+ res_id,
+ status_field='status',
+ sleep_time=5,
+ timeout=300,
+ callback=None):
+ """Wait for resource deletion
+
+ :param res_id: the resource id to watch
+ :param status_field: the status attribute in the returned resource object,
+ this is used to check for error states while the resource is being
+ deleted
+ :param sleep_time: wait this long between checks (seconds)
+ :param timeout: check until this long (seconds)
+ :param callback: called per sleep cycle, useful to display progress; this
+ function is passed a progress value during each iteration of the wait
+ loop
+ :rtype: True on success, False if the resource has gone to error state or
+ the timeout has been reached
+ """
+ total_time = 0
+ while total_time < timeout:
+ try:
+ # might not be a bad idea to re-use find_resource here if it was
+ # a bit more friendly in the exceptions it raised so we could just
+ # handle a NotFound exception here without parsing the message
+ res = manager.get(res_id)
+ except Exception as ex:
+ if type(ex).__name__ == 'NotFound':
+ return True
+ raise
+
+ status = getattr(res, status_field, '').lower()
+ if status == 'error':
+ return False
+
+ if callback:
+ progress = getattr(res, 'progress', None) or 0
+ callback(progress)
+ time.sleep(sleep_time)
+ total_time += sleep_time
+
+ # if we got this far we've timed out
+ return False
+
+
def get_effective_log_level():
"""Returns the lowest logging level considered by logging handlers
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 41c1b904..5007b072 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -572,6 +572,11 @@ class DeleteServer(command.Command):
nargs="+",
help=_('Server(s) to delete (name or ID)'),
)
+ parser.add_argument(
+ '--wait',
+ action='store_true',
+ help=_('Wait for delete to complete'),
+ )
return parser
def take_action(self, parsed_args):
@@ -581,6 +586,18 @@ class DeleteServer(command.Command):
server_obj = utils.find_resource(
compute_client.servers, server)
compute_client.servers.delete(server_obj.id)
+ if parsed_args.wait:
+ if utils.wait_for_delete(
+ compute_client.servers,
+ server_obj.id,
+ callback=_show_progress,
+ ):
+ sys.stdout.write('\n')
+ else:
+ self.log.error(_('Error deleting server: %s'),
+ server_obj.id)
+ sys.stdout.write(_('\nError deleting server'))
+ raise SystemExit
return
diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py
index cda0b135..d9f5b7a5 100644
--- a/openstackclient/tests/common/test_utils.py
+++ b/openstackclient/tests/common/test_utils.py
@@ -13,6 +13,9 @@
# under the License.
#
+import time
+import uuid
+
import mock
from openstackclient.common import exceptions
@@ -120,6 +123,42 @@ class TestUtils(test_utils.TestCase):
utils.sort_items,
items, sort_str)
+ @mock.patch.object(time, 'sleep')
+ def test_wait_for_delete_ok(self, mock_sleep):
+ # Tests the normal flow that the resource is deleted with a 404 coming
+ # back on the 2nd iteration of the wait loop.
+ resource = mock.MagicMock(status='ACTIVE', progress=None)
+ mock_get = mock.Mock(side_effect=[resource,
+ exceptions.NotFound(404)])
+ manager = mock.MagicMock(get=mock_get)
+ res_id = str(uuid.uuid4())
+ callback = mock.Mock()
+ self.assertTrue(utils.wait_for_delete(manager, res_id,
+ callback=callback))
+ mock_sleep.assert_called_once_with(5)
+ callback.assert_called_once_with(0)
+
+ @mock.patch.object(time, 'sleep')
+ def test_wait_for_delete_timeout(self, mock_sleep):
+ # Tests that we fail if the resource is not deleted before the timeout.
+ resource = mock.MagicMock(status='ACTIVE')
+ mock_get = mock.Mock(return_value=resource)
+ manager = mock.MagicMock(get=mock_get)
+ res_id = str(uuid.uuid4())
+ self.assertFalse(utils.wait_for_delete(manager, res_id, sleep_time=1,
+ timeout=1))
+ mock_sleep.assert_called_once_with(1)
+
+ @mock.patch.object(time, 'sleep')
+ def test_wait_for_delete_error(self, mock_sleep):
+ # Tests that we fail if the resource goes to error state while waiting.
+ resource = mock.MagicMock(status='ERROR')
+ mock_get = mock.Mock(return_value=resource)
+ manager = mock.MagicMock(get=mock_get)
+ res_id = str(uuid.uuid4())
+ self.assertFalse(utils.wait_for_delete(manager, res_id))
+ self.assertFalse(mock_sleep.called)
+
class NoUniqueMatch(Exception):
pass
diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py
index baf53742..a8a1936d 100644
--- a/openstackclient/tests/compute/v2/test_server.py
+++ b/openstackclient/tests/compute/v2/test_server.py
@@ -16,6 +16,7 @@
import copy
import mock
+from openstackclient.common import utils as common_utils
from openstackclient.compute.v2 import server
from openstackclient.tests.compute.v2 import fakes as compute_fakes
from openstackclient.tests import fakes
@@ -319,6 +320,52 @@ class TestServerDelete(TestServer):
compute_fakes.server_id,
)
+ @mock.patch.object(common_utils, 'wait_for_delete', return_value=True)
+ def test_server_delete_wait_ok(self, mock_wait_for_delete):
+ arglist = [
+ compute_fakes.server_id, '--wait'
+ ]
+ verifylist = [
+ ('servers', [compute_fakes.server_id]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # DisplayCommandBase.take_action() returns two tuples
+ self.cmd.take_action(parsed_args)
+
+ self.servers_mock.delete.assert_called_with(
+ compute_fakes.server_id,
+ )
+
+ mock_wait_for_delete.assert_called_once_with(
+ self.servers_mock,
+ compute_fakes.server_id,
+ callback=server._show_progress
+ )
+
+ @mock.patch.object(common_utils, 'wait_for_delete', return_value=False)
+ def test_server_delete_wait_fails(self, mock_wait_for_delete):
+ arglist = [
+ compute_fakes.server_id, '--wait'
+ ]
+ verifylist = [
+ ('servers', [compute_fakes.server_id]),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ # DisplayCommandBase.take_action() returns two tuples
+ self.assertRaises(SystemExit, self.cmd.take_action, parsed_args)
+
+ self.servers_mock.delete.assert_called_with(
+ compute_fakes.server_id,
+ )
+
+ mock_wait_for_delete.assert_called_once_with(
+ self.servers_mock,
+ compute_fakes.server_id,
+ callback=server._show_progress
+ )
+
class TestServerImageCreate(TestServer):