Skip to content

Commit 224d375

Browse files
author
Matt Riedemann
committed
Add --wait to server delete
This allows the server delete command to wait for the server to be deleted (obviously). The wait method is the same model that Tempest uses, i.e. wait for a 404 on server GET (successful deletion), fail if the server went to ERROR status, or fail if a timeout is reached. The default timeout of 300 seconds is also what Tempest uses. Closes-Bug: #1460112 Change-Id: I0e66c400903e82832944d1cad61e7eb30177c3e8
1 parent 211c14c commit 224d375

5 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎doc/source/command-objects/server.rst‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ Delete server(s)
157157
.. code:: bash
158158
159159
os server delete
160-
<server> [<server> ...]
160+
<server> [<server> ...] [--wait]
161+
162+
.. option:: --wait
163+
164+
Wait for delete to complete
161165

162166
.. describe:: <server>
163167

‎openstackclient/common/utils.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,52 @@ def wait_for_status(status_f,
283283
return retval
284284

285285

286+
def wait_for_delete(manager,
287+
res_id,
288+
status_field='status',
289+
sleep_time=5,
290+
timeout=300,
291+
callback=None):
292+
"""Wait for resource deletion
293+
294+
:param res_id: the resource id to watch
295+
:param status_field: the status attribute in the returned resource object,
296+
this is used to check for error states while the resource is being
297+
deleted
298+
:param sleep_time: wait this long between checks (seconds)
299+
:param timeout: check until this long (seconds)
300+
:param callback: called per sleep cycle, useful to display progress; this
301+
function is passed a progress value during each iteration of the wait
302+
loop
303+
:rtype: True on success, False if the resource has gone to error state or
304+
the timeout has been reached
305+
"""
306+
total_time = 0
307+
while total_time < timeout:
308+
try:
309+
# might not be a bad idea to re-use find_resource here if it was
310+
# a bit more friendly in the exceptions it raised so we could just
311+
# handle a NotFound exception here without parsing the message
312+
res = manager.get(res_id)
313+
except Exception as ex:
314+
if type(ex).__name__ == 'NotFound':
315+
return True
316+
raise
317+
318+
status = getattr(res, status_field, '').lower()
319+
if status == 'error':
320+
return False
321+
322+
if callback:
323+
progress = getattr(res, 'progress', None) or 0
324+
callback(progress)
325+
time.sleep(sleep_time)
326+
total_time += sleep_time
327+
328+
# if we got this far we've timed out
329+
return False
330+
331+
286332
def get_effective_log_level():
287333
"""Returns the lowest logging level considered by logging handlers
288334

‎openstackclient/compute/v2/server.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@ def get_parser(self, prog_name):
572572
nargs="+",
573573
help=_('Server(s) to delete (name or ID)'),
574574
)
575+
parser.add_argument(
576+
'--wait',
577+
action='store_true',
578+
help=_('Wait for delete to complete'),
579+
)
575580
return parser
576581

577582
def take_action(self, parsed_args):
@@ -581,6 +586,18 @@ def take_action(self, parsed_args):
581586
server_obj = utils.find_resource(
582587
compute_client.servers, server)
583588
compute_client.servers.delete(server_obj.id)
589+
if parsed_args.wait:
590+
if utils.wait_for_delete(
591+
compute_client.servers,
592+
server_obj.id,
593+
callback=_show_progress,
594+
):
595+
sys.stdout.write('\n')
596+
else:
597+
self.log.error(_('Error deleting server: %s'),
598+
server_obj.id)
599+
sys.stdout.write(_('\nError deleting server'))
600+
raise SystemExit
584601
return
585602

586603

‎openstackclient/tests/common/test_utils.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
# under the License.
1414
#
1515

16+
import time
17+
import uuid
18+
1619
import mock
1720

1821
from openstackclient.common import exceptions
@@ -120,6 +123,42 @@ def test_sort_items_with_invalid_direction(self):
120123
utils.sort_items,
121124
items, sort_str)
122125

126+
@mock.patch.object(time, 'sleep')
127+
def test_wait_for_delete_ok(self, mock_sleep):
128+
# Tests the normal flow that the resource is deleted with a 404 coming
129+
# back on the 2nd iteration of the wait loop.
130+
resource = mock.MagicMock(status='ACTIVE', progress=None)
131+
mock_get = mock.Mock(side_effect=[resource,
132+
exceptions.NotFound(404)])
133+
manager = mock.MagicMock(get=mock_get)
134+
res_id = str(uuid.uuid4())
135+
callback = mock.Mock()
136+
self.assertTrue(utils.wait_for_delete(manager, res_id,
137+
callback=callback))
138+
mock_sleep.assert_called_once_with(5)
139+
callback.assert_called_once_with(0)
140+
141+
@mock.patch.object(time, 'sleep')
142+
def test_wait_for_delete_timeout(self, mock_sleep):
143+
# Tests that we fail if the resource is not deleted before the timeout.
144+
resource = mock.MagicMock(status='ACTIVE')
145+
mock_get = mock.Mock(return_value=resource)
146+
manager = mock.MagicMock(get=mock_get)
147+
res_id = str(uuid.uuid4())
148+
self.assertFalse(utils.wait_for_delete(manager, res_id, sleep_time=1,
149+
timeout=1))
150+
mock_sleep.assert_called_once_with(1)
151+
152+
@mock.patch.object(time, 'sleep')
153+
def test_wait_for_delete_error(self, mock_sleep):
154+
# Tests that we fail if the resource goes to error state while waiting.
155+
resource = mock.MagicMock(status='ERROR')
156+
mock_get = mock.Mock(return_value=resource)
157+
manager = mock.MagicMock(get=mock_get)
158+
res_id = str(uuid.uuid4())
159+
self.assertFalse(utils.wait_for_delete(manager, res_id))
160+
self.assertFalse(mock_sleep.called)
161+
123162

124163
class NoUniqueMatch(Exception):
125164
pass

‎openstackclient/tests/compute/v2/test_server.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import copy
1717
import mock
1818

19+
from openstackclient.common import utils as common_utils
1920
from openstackclient.compute.v2 import server
2021
from openstackclient.tests.compute.v2 import fakes as compute_fakes
2122
from openstackclient.tests import fakes
@@ -319,6 +320,52 @@ def test_server_delete_no_options(self):
319320
compute_fakes.server_id,
320321
)
321322

323+
@mock.patch.object(common_utils, 'wait_for_delete', return_value=True)
324+
def test_server_delete_wait_ok(self, mock_wait_for_delete):
325+
arglist = [
326+
compute_fakes.server_id, '--wait'
327+
]
328+
verifylist = [
329+
('servers', [compute_fakes.server_id]),
330+
]
331+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
332+
333+
# DisplayCommandBase.take_action() returns two tuples
334+
self.cmd.take_action(parsed_args)
335+
336+
self.servers_mock.delete.assert_called_with(
337+
compute_fakes.server_id,
338+
)
339+
340+
mock_wait_for_delete.assert_called_once_with(
341+
self.servers_mock,
342+
compute_fakes.server_id,
343+
callback=server._show_progress
344+
)
345+
346+
@mock.patch.object(common_utils, 'wait_for_delete', return_value=False)
347+
def test_server_delete_wait_fails(self, mock_wait_for_delete):
348+
arglist = [
349+
compute_fakes.server_id, '--wait'
350+
]
351+
verifylist = [
352+
('servers', [compute_fakes.server_id]),
353+
]
354+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
355+
356+
# DisplayCommandBase.take_action() returns two tuples
357+
self.assertRaises(SystemExit, self.cmd.take_action, parsed_args)
358+
359+
self.servers_mock.delete.assert_called_with(
360+
compute_fakes.server_id,
361+
)
362+
363+
mock_wait_for_delete.assert_called_once_with(
364+
self.servers_mock,
365+
compute_fakes.server_id,
366+
callback=server._show_progress
367+
)
368+
322369

323370
class TestServerImageCreate(TestServer):
324371

0 commit comments

Comments
 (0)