From b16c9a8d3d70fe2ec69ab44b740011dc2b2bc097 Mon Sep 17 00:00:00 2001 From: reedip Date: Tue, 5 Jan 2016 16:32:36 +0900 Subject: [PATCH] Add support for Bulk Delete in NeutronClient The following patch adds support for BulkDelete in NeutronClient. Currently, the core existing Neutron CLIs are going to support Bulk Deletion in NeutronClient. However, if any extension does not require Bulk Delete, it can be disabled by updating the class attribute 'bulk_delete'. DocImpact Depends-On: Ib23d1e53947b5dffcff8db0dde77cae0a0b31243 Change-Id: I3b8a05698625baad3906784e3ecffb0f0242d660 Closes-Bug: #1495440 --- neutronclient/neutron/v2_0/__init__.py | 60 +++++++++++++++---- neutronclient/tests/unit/test_cli20.py | 25 +++++--- .../tests/unit/test_cli20_network.py | 9 +++ .../bulk-delete-support-94a353db08efec8d.yaml | 9 +++ 4 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml diff --git a/neutronclient/neutron/v2_0/__init__.py b/neutronclient/neutron/v2_0/__init__.py index 51c4b7daf..4468bfb3a 100644 --- a/neutronclient/neutron/v2_0/__init__.py +++ b/neutronclient/neutron/v2_0/__init__.py @@ -478,17 +478,19 @@ class DeleteCommand(NeutronCommand): log = None allow_names = True help_resource = None + bulk_delete = True def get_parser(self, prog_name): parser = super(DeleteCommand, self).get_parser(prog_name) - if self.allow_names: - help_str = _('ID or name of %s to delete.') - else: - help_str = _('ID of %s to delete.') if not self.help_resource: self.help_resource = self.resource + if self.allow_names: + help_str = _('ID(s) or name(s) of %s to delete.') + else: + help_str = _('ID(s) of %s to delete.') parser.add_argument( 'id', metavar=self.resource.upper(), + nargs='+' if self.bulk_delete else 1, help=help_str % self.help_resource) self.add_known_arguments(parser) return parser @@ -498,24 +500,62 @@ class DeleteCommand(NeutronCommand): neutron_client = self.get_client() obj_deleter = getattr(neutron_client, "delete_%s" % self.cmd_resource) + + if self.bulk_delete: + self._bulk_delete(obj_deleter, neutron_client, parsed_args.id) + else: + self.delete_item(obj_deleter, neutron_client, parsed_args.id) + print((_('Deleted %(resource)s: %(id)s') + % {'id': parsed_args.id, + 'resource': self.resource}), + file=self.app.stdout) + return + + def _bulk_delete(self, obj_deleter, neutron_client, parsed_args_ids): + successful_delete = [] + non_existent = [] + multiple_ids = [] + for item_id in parsed_args_ids: + try: + self.delete_item(obj_deleter, neutron_client, item_id) + successful_delete.append(item_id) + except exceptions.NotFound: + non_existent.append(item_id) + except exceptions.NeutronClientNoUniqueMatch: + multiple_ids.append(item_id) + if successful_delete: + print((_('Deleted %(resource)s(s): %(id)s')) + % {'id': ", ".join(successful_delete), + 'resource': self.cmd_resource}, + file=self.app.stdout) + if non_existent: + print((_("Unable to find %(resource)s(s) with id(s) " + "'%(id)s'") % + {'resource': self.cmd_resource, + 'id': ", ".join(non_existent)}), + file=self.app.stdout) + if multiple_ids: + print((_("Multiple %(resource)s(s) matches found for name(s)" + " '%(id)s'. Please use an ID to be more specific.")) % + {'resource': self.cmd_resource, + 'id': ", ".join(multiple_ids)}, + file=self.app.stdout) + + def delete_item(self, obj_deleter, neutron_client, item_id): if self.allow_names: params = {'cmd_resource': self.cmd_resource, 'parent_id': self.parent_id} _id = find_resourceid_by_name_or_id(neutron_client, self.resource, - parsed_args.id, + item_id, **params) else: - _id = parsed_args.id + _id = item_id if self.parent_id: obj_deleter(_id, self.parent_id) else: obj_deleter(_id) - print((_('Deleted %(resource)s: %(id)s') - % {'id': parsed_args.id, - 'resource': self.resource}), - file=self.app.stdout) return diff --git a/neutronclient/tests/unit/test_cli20.py b/neutronclient/tests/unit/test_cli20.py index 324782f0b..11077ad10 100644 --- a/neutronclient/tests/unit/test_cli20.py +++ b/neutronclient/tests/unit/test_cli20.py @@ -504,14 +504,7 @@ class CLITestV20Base(base.BaseTestCase): self.assertIn(myid, _str) self.assertIn('myname', _str) - def _test_delete_resource(self, resource, cmd, myid, args, - cmd_resource=None, parent_id=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - if not cmd_resource: - cmd_resource = resource - path = getattr(self.client, cmd_resource + "_path") + def _test_set_path_and_delete(self, path, parent_id, myid): if parent_id: path = path % (parent_id, myid) else: @@ -521,6 +514,20 @@ class CLITestV20Base(base.BaseTestCase): body=None, headers=mox.ContainsKeyValue( 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) + + def _test_delete_resource(self, resource, cmd, myid, args, + cmd_resource=None, parent_id=None, + extra_ids=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resource: + cmd_resource = resource + path = getattr(self.client, cmd_resource + "_path") + self._test_set_path_and_delete(path, parent_id, myid) + # extra_ids is used to test for bulk_delete + if extra_ids: + self._test_set_path_and_delete(path, parent_id, extra_ids) self.mox.ReplayAll() cmd_parser = cmd.get_parser("delete_" + cmd_resource) shell.run_command(cmd, cmd_parser, args) @@ -528,6 +535,8 @@ class CLITestV20Base(base.BaseTestCase): self.mox.UnsetStubs() _str = self.fake_stdout.make_string() self.assertIn(myid, _str) + if extra_ids: + self.assertIn(extra_ids, _str) def _test_update_resource_action(self, resource, cmd, myid, action, args, body, retval=None, cmd_resource=None): diff --git a/neutronclient/tests/unit/test_cli20_network.py b/neutronclient/tests/unit/test_cli20_network.py index f900d7548..f28b8a448 100644 --- a/neutronclient/tests/unit/test_cli20_network.py +++ b/neutronclient/tests/unit/test_cli20_network.py @@ -588,6 +588,15 @@ class CLITestV20NetworkJSON(test_cli20.CLITestV20Base): args = [myid] self._test_delete_resource(resource, cmd, myid, args) + def test_bulk_delete_network(self): + # Delete net: myid1 myid2. + resource = 'network' + cmd = network.DeleteNetwork(test_cli20.MyApp(sys.stdout), None) + myid1 = 'myid1' + myid2 = 'myid2' + args = [myid1, myid2] + self._test_delete_resource(resource, cmd, myid1, args, extra_ids=myid2) + def _test_extend_list(self, mox_calls): data = [{'id': 'netid%d' % i, 'name': 'net%d' % i, 'subnets': ['mysubid%d' % i]} diff --git a/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml b/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml new file mode 100644 index 000000000..4d2575d9d --- /dev/null +++ b/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + CLI support for bulk delete. + + * By using this feature, multiple resource + can be deleted using a single command. + * Example: ``neutron router-delete router_a router_b`` + deletes both router_a and router_b.