Skip to content

Commit 5dfedd6

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Enhance exception handling for "network delete" command"
2 parents cb068d8 + 56f9227 commit 5dfedd6

6 files changed

Lines changed: 245 additions & 28 deletions

File tree

doc/source/command-errors.rst

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ to be handled by having the proper options in a set command available to allow
9999
recovery in the case where the primary resource has been created but the
100100
subsequent calls did not complete.
101101

102-
Example
103-
~~~~~~~
102+
Example 1
103+
~~~~~~~~~
104104

105105
This example is taken from the ``volume snapshot set`` command where ``--property``
106106
arguments are set using the volume manager's ``set_metadata()`` method,
@@ -161,3 +161,41 @@ remaining arguments are set using the ``update()`` method.
161161
# without aborting prematurely
162162
if result > 0:
163163
raise SomeNonFatalException
164+
165+
Example 2
166+
~~~~~~~~~
167+
168+
This example is taken from the ``network delete`` command which takes multiple
169+
networks to delete. All networks will be delete in a loop, which makes multiple
170+
``delete_network()`` calls.
171+
172+
.. code-block:: python
173+
174+
class DeleteNetwork(common.NetworkAndComputeCommand):
175+
"""Delete network(s)"""
176+
177+
def update_parser_common(self, parser):
178+
parser.add_argument(
179+
'network',
180+
metavar="<network>",
181+
nargs="+",
182+
help=("Network(s) to delete (name or ID)")
183+
)
184+
return parser
185+
186+
def take_action(self, client, parsed_args):
187+
ret = 0
188+
189+
for network in parsed_args.network:
190+
try:
191+
obj = client.find_network(network, ignore_missing=False)
192+
client.delete_network(obj)
193+
except Exception:
194+
self.app.log.error("Failed to delete network with name "
195+
"or ID %s." % network)
196+
ret += 1
197+
198+
if ret > 0:
199+
total = len(parsed_args.network)
200+
msg = "Failed to delete %s of %s networks." % (ret, total)
201+
raise exceptions.CommandError(msg)

openstackclient/network/common.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import six
1616

1717
from openstackclient.common import command
18+
from openstackclient.common import exceptions
1819

1920

2021
@six.add_metaclass(abc.ABCMeta)
@@ -68,6 +69,42 @@ def take_action_compute(self, client, parsed_args):
6869
pass
6970

7071

72+
@six.add_metaclass(abc.ABCMeta)
73+
class NetworkAndComputeDelete(NetworkAndComputeCommand):
74+
"""Network and Compute Delete
75+
76+
Delete class for commands that support implementation via
77+
the network or compute endpoint. Such commands have different
78+
implementations for take_action() and may even have different
79+
arguments. This class supports bulk deletion, and error handling
80+
following the rules in doc/source/command-errors.rst.
81+
"""
82+
83+
def take_action(self, parsed_args):
84+
ret = 0
85+
resources = getattr(parsed_args, self.resource, [])
86+
87+
for r in resources:
88+
self.r = r
89+
try:
90+
if self.app.client_manager.is_network_endpoint_enabled():
91+
self.take_action_network(self.app.client_manager.network,
92+
parsed_args)
93+
else:
94+
self.take_action_compute(self.app.client_manager.compute,
95+
parsed_args)
96+
except Exception as e:
97+
self.app.log.error("Failed to delete %s with name or ID "
98+
"'%s': %s" % (self.resource, r, e))
99+
ret += 1
100+
101+
if ret:
102+
total = len(resources)
103+
msg = "%s of %s %ss failed to delete." % (ret, total,
104+
self.resource)
105+
raise exceptions.CommandError(msg)
106+
107+
71108
@six.add_metaclass(abc.ABCMeta)
72109
class NetworkAndComputeLister(command.Lister):
73110
"""Network and Compute Lister

openstackclient/network/v2/network.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,30 +248,30 @@ def take_action_compute(self, client, parsed_args):
248248
return (columns, data)
249249

250250

251-
class DeleteNetwork(common.NetworkAndComputeCommand):
251+
class DeleteNetwork(common.NetworkAndComputeDelete):
252252
"""Delete network(s)"""
253253

254+
# Used by base class to find resources in parsed_args.
255+
resource = 'network'
256+
r = None
257+
254258
def update_parser_common(self, parser):
255259
parser.add_argument(
256260
'network',
257261
metavar="<network>",
258262
nargs="+",
259263
help=_("Network(s) to delete (name or ID)")
260264
)
265+
261266
return parser
262267

263268
def take_action_network(self, client, parsed_args):
264-
for network in parsed_args.network:
265-
obj = client.find_network(network)
266-
client.delete_network(obj)
269+
obj = client.find_network(self.r, ignore_missing=False)
270+
client.delete_network(obj)
267271

268272
def take_action_compute(self, client, parsed_args):
269-
for network in parsed_args.network:
270-
network = utils.find_resource(
271-
client.networks,
272-
network,
273-
)
274-
client.networks.delete(network.id)
273+
network = utils.find_resource(client.networks, self.r)
274+
client.networks.delete(network.id)
275275

276276

277277
class ListNetwork(common.NetworkAndComputeLister):

openstackclient/tests/compute/v2/fakes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,25 @@ def create_networks(attrs=None, count=2):
859859

860860
return networks
861861

862+
@staticmethod
863+
def get_networks(networks=None, count=2):
864+
"""Get an iterable MagicMock object with a list of faked networks.
865+
866+
If networks list is provided, then initialize the Mock object with the
867+
list. Otherwise create one.
868+
869+
:param List networks:
870+
A list of FakeResource objects faking networks
871+
:param int count:
872+
The number of networks to fake
873+
:return:
874+
An iterable Mock object with side_effect set to a list of faked
875+
networks
876+
"""
877+
if networks is None:
878+
networks = FakeNetwork.create_networks(count=count)
879+
return mock.Mock(side_effect=networks)
880+
862881

863882
class FakeHost(object):
864883
"""Fake one host."""

openstackclient/tests/network/v2/test_network.py

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import copy
1515
import mock
1616

17+
from mock import call
1718
from openstackclient.common import exceptions
1819
from openstackclient.common import utils
1920
from openstackclient.network.v2 import network
@@ -323,33 +324,88 @@ def test_create_with_domain_identityv2(self):
323324

324325
class TestDeleteNetwork(TestNetwork):
325326

326-
# The network to delete.
327-
_network = network_fakes.FakeNetwork.create_one_network()
328-
329327
def setUp(self):
330328
super(TestDeleteNetwork, self).setUp()
331329

330+
# The networks to delete
331+
self._networks = network_fakes.FakeNetwork.create_networks(count=3)
332+
332333
self.network.delete_network = mock.Mock(return_value=None)
333334

334-
self.network.find_network = mock.Mock(return_value=self._network)
335+
self.network.find_network = network_fakes.FakeNetwork.get_networks(
336+
networks=self._networks)
335337

336338
# Get the command object to test
337339
self.cmd = network.DeleteNetwork(self.app, self.namespace)
338340

339-
def test_delete(self):
341+
def test_delete_one_network(self):
340342
arglist = [
341-
self._network.name,
343+
self._networks[0].name,
342344
]
343345
verifylist = [
344-
('network', [self._network.name]),
346+
('network', [self._networks[0].name]),
345347
]
348+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
349+
350+
result = self.cmd.take_action(parsed_args)
346351

352+
self.network.delete_network.assert_called_once_with(self._networks[0])
353+
self.assertIsNone(result)
354+
355+
def test_delete_multiple_networks(self):
356+
arglist = []
357+
for n in self._networks:
358+
arglist.append(n.id)
359+
verifylist = [
360+
('network', arglist),
361+
]
347362
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
363+
348364
result = self.cmd.take_action(parsed_args)
349365

350-
self.network.delete_network.assert_called_once_with(self._network)
366+
calls = []
367+
for n in self._networks:
368+
calls.append(call(n))
369+
self.network.delete_network.assert_has_calls(calls)
351370
self.assertIsNone(result)
352371

372+
def test_delete_multiple_networks_exception(self):
373+
arglist = [
374+
self._networks[0].id,
375+
'xxxx-yyyy-zzzz',
376+
self._networks[1].id,
377+
]
378+
verifylist = [
379+
('network', arglist),
380+
]
381+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
382+
383+
# Fake exception in find_network()
384+
ret_find = [
385+
self._networks[0],
386+
exceptions.NotFound('404'),
387+
self._networks[1],
388+
]
389+
self.network.find_network = mock.Mock(side_effect=ret_find)
390+
391+
# Fake exception in delete_network()
392+
ret_delete = [
393+
None,
394+
exceptions.NotFound('404'),
395+
]
396+
self.network.delete_network = mock.Mock(side_effect=ret_delete)
397+
398+
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
399+
parsed_args)
400+
401+
# The second call of find_network() should fail. So delete_network()
402+
# was only called twice.
403+
calls = [
404+
call(self._networks[0]),
405+
call(self._networks[1]),
406+
]
407+
self.network.delete_network.assert_has_calls(calls)
408+
353409

354410
class TestListNetwork(TestNetwork):
355411

@@ -752,36 +808,97 @@ def test_create_default_options(self):
752808

753809
class TestDeleteNetworkCompute(TestNetworkCompute):
754810

755-
# The network to delete.
756-
_network = compute_fakes.FakeNetwork.create_one_network()
757-
758811
def setUp(self):
759812
super(TestDeleteNetworkCompute, self).setUp()
760813

761814
self.app.client_manager.network_endpoint_enabled = False
762815

816+
# The networks to delete
817+
self._networks = compute_fakes.FakeNetwork.create_networks(count=3)
818+
763819
self.compute.networks.delete.return_value = None
764820

765821
# Return value of utils.find_resource()
766-
self.compute.networks.get.return_value = self._network
822+
self.compute.networks.get = \
823+
compute_fakes.FakeNetwork.get_networks(networks=self._networks)
767824

768825
# Get the command object to test
769826
self.cmd = network.DeleteNetwork(self.app, None)
770827

771-
def test_network_delete(self):
828+
def test_delete_one_network(self):
772829
arglist = [
773-
self._network.label,
830+
self._networks[0].label,
774831
]
775832
verifylist = [
776-
('network', [self._network.label]),
833+
('network', [self._networks[0].label]),
777834
]
835+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
836+
837+
result = self.cmd.take_action(parsed_args)
778838

839+
self.compute.networks.delete.assert_called_once_with(
840+
self._networks[0].id)
841+
self.assertIsNone(result)
842+
843+
def test_delete_multiple_networks(self):
844+
arglist = []
845+
for n in self._networks:
846+
arglist.append(n.label)
847+
verifylist = [
848+
('network', arglist),
849+
]
779850
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
851+
780852
result = self.cmd.take_action(parsed_args)
781853

782-
self.compute.networks.delete.assert_called_once_with(self._network.id)
854+
calls = []
855+
for n in self._networks:
856+
calls.append(call(n.id))
857+
self.compute.networks.delete.assert_has_calls(calls)
783858
self.assertIsNone(result)
784859

860+
def test_delete_multiple_networks_exception(self):
861+
arglist = [
862+
self._networks[0].id,
863+
'xxxx-yyyy-zzzz',
864+
self._networks[1].id,
865+
]
866+
verifylist = [
867+
('network', arglist),
868+
]
869+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
870+
871+
# Fake exception in utils.find_resource()
872+
# In compute v2, we use utils.find_resource() to find a network.
873+
# It calls get() several times, but find() only one time. So we
874+
# choose to fake get() always raise exception, then pass through.
875+
# And fake find() to find the real network or not.
876+
self.compute.networks.get.side_effect = Exception()
877+
ret_find = [
878+
self._networks[0],
879+
Exception(),
880+
self._networks[1],
881+
]
882+
self.compute.networks.find.side_effect = ret_find
883+
884+
# Fake exception in delete()
885+
ret_delete = [
886+
None,
887+
Exception(),
888+
]
889+
self.compute.networks.delete = mock.Mock(side_effect=ret_delete)
890+
891+
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
892+
parsed_args)
893+
894+
# The second call of utils.find_resource() should fail. So delete()
895+
# was only called twice.
896+
calls = [
897+
call(self._networks[0].id),
898+
call(self._networks[1].id),
899+
]
900+
self.compute.networks.delete.assert_has_calls(calls)
901+
785902

786903
class TestListNetworkCompute(TestNetworkCompute):
787904

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- Command ``network delete`` will delete as many networks as possible, log
4+
and report failures in the end.
5+
[Bug `1556719 <https://bugs.launchpad.net/python-openstackclient/+bug/1556719>`_]
6+
[Bug `1537856 <https://bugs.launchpad.net/python-openstackclient/+bug/1537856>`_]

0 commit comments

Comments
 (0)