Skip to content

Commit 4d57e9f

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Add --wait to server delete"
2 parents 40634c3 + 224d375 commit 4d57e9f

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)