From 041ea4978b94149d5037b5afc7743db939b75331 Mon Sep 17 00:00:00 2001
From: Huanxuan Ao <huanxuan.ao@easystack.cn>
Date: Thu, 16 Jun 2016 13:09:27 +0800
Subject: [PATCH] Support bulk deletion for delete commands in networkv2

This patch support bulk deletion for delete commands below:
1.subnet delete
2.subnet pool delete

Up to now, all the delete commands in networkv2 support bulk deletion.

Change-Id: I63f6d1d02bad1fcc26e72b7028b53958a68ce2dc
Partially-Implements: blueprint multi-argument-network
Partial-Bug: #1592906
---
 doc/source/command-objects/subnet-pool.rst    |  6 +-
 doc/source/command-objects/subnet.rst         |  6 +-
 openstackclient/network/v2/subnet.py          | 28 ++++++--
 openstackclient/network/v2/subnet_pool.py     | 30 +++++++--
 openstackclient/tests/network/v2/fakes.py     | 38 +++++++++++
 .../tests/network/v2/test_subnet.py           | 66 ++++++++++++++++--
 .../tests/network/v2/test_subnet_pool.py      | 67 ++++++++++++++++---
 ...lti-argument-network-e43e192ac95db94d.yaml |  5 +-
 8 files changed, 215 insertions(+), 31 deletions(-)

diff --git a/doc/source/command-objects/subnet-pool.rst b/doc/source/command-objects/subnet-pool.rst
index 8abf25a671..516b9bf4a8 100644
--- a/doc/source/command-objects/subnet-pool.rst
+++ b/doc/source/command-objects/subnet-pool.rst
@@ -81,18 +81,18 @@ Create subnet pool
 subnet pool delete
 ------------------
 
-Delete subnet pool
+Delete subnet pool(s)
 
 .. program:: subnet pool delete
 .. code:: bash
 
     os subnet pool delete
-        <subnet-pool>
+        <subnet-pool> [<subnet-pool> ...]
 
 .. _subnet_pool_delete-subnet-pool:
 .. describe:: <subnet-pool>
 
-    Subnet pool to delete (name or ID)
+    Subnet pool(s) to delete (name or ID)
 
 subnet pool list
 ----------------
diff --git a/doc/source/command-objects/subnet.rst b/doc/source/command-objects/subnet.rst
index ff6354e658..c52d73f932 100644
--- a/doc/source/command-objects/subnet.rst
+++ b/doc/source/command-objects/subnet.rst
@@ -119,18 +119,18 @@ Create new subnet
 subnet delete
 -------------
 
-Delete a subnet
+Delete subnet(s)
 
 .. program:: subnet delete
 .. code:: bash
 
     os subnet delete
-        <subnet>
+        <subnet> [<subnet> ...]
 
 .. _subnet_delete-subnet:
 .. describe:: <subnet>
 
-    Subnet to delete (name or ID)
+    Subnet(s) to delete (name or ID)
 
 subnet list
 -----------
diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py
index ceb1cb1471..a2e3262262 100644
--- a/openstackclient/network/v2/subnet.py
+++ b/openstackclient/network/v2/subnet.py
@@ -14,6 +14,7 @@
 """Subnet action implementations"""
 
 import copy
+import logging
 
 from osc_lib.cli import parseractions
 from osc_lib.command import command
@@ -24,6 +25,9 @@ from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 
 
+LOG = logging.getLogger(__name__)
+
+
 def _format_allocation_pools(data):
     pool_formatted = ['%s-%s' % (pool.get('start', ''), pool.get('end', ''))
                       for pool in data]
@@ -270,21 +274,37 @@ class CreateSubnet(command.ShowOne):
 
 
 class DeleteSubnet(command.Command):
-    """Delete subnet"""
+    """Delete subnet(s)"""
 
     def get_parser(self, prog_name):
         parser = super(DeleteSubnet, self).get_parser(prog_name)
         parser.add_argument(
             'subnet',
             metavar="<subnet>",
-            help=_("Subnet to delete (name or ID)")
+            nargs='+',
+            help=_("Subnet(s) to delete (name or ID)")
         )
         return parser
 
     def take_action(self, parsed_args):
         client = self.app.client_manager.network
-        client.delete_subnet(
-            client.find_subnet(parsed_args.subnet))
+        result = 0
+
+        for subnet in parsed_args.subnet:
+            try:
+                obj = client.find_subnet(subnet, ignore_missing=False)
+                client.delete_subnet(obj)
+            except Exception as e:
+                result += 1
+                LOG.error(_("Failed to delete subnet with "
+                          "name or ID '%(subnet)s': %(e)s")
+                          % {'subnet': subnet, 'e': e})
+
+        if result > 0:
+            total = len(parsed_args.subnet)
+            msg = (_("%(result)s of %(total)s subnets failed "
+                   "to delete.") % {'result': result, 'total': total})
+            raise exceptions.CommandError(msg)
 
 
 class ListSubnet(command.Lister):
diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py
index 79f98fd919..55dfed8392 100644
--- a/openstackclient/network/v2/subnet_pool.py
+++ b/openstackclient/network/v2/subnet_pool.py
@@ -13,14 +13,20 @@
 
 """Subnet pool action implementations"""
 
+import logging
+
 from osc_lib.cli import parseractions
 from osc_lib.command import command
+from osc_lib import exceptions
 from osc_lib import utils
 
 from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 
 
+LOG = logging.getLogger(__name__)
+
+
 def _get_columns(item):
     columns = list(item.keys())
     if 'tenant_id' in columns:
@@ -176,21 +182,37 @@ class CreateSubnetPool(command.ShowOne):
 
 
 class DeleteSubnetPool(command.Command):
-    """Delete subnet pool"""
+    """Delete subnet pool(s)"""
 
     def get_parser(self, prog_name):
         parser = super(DeleteSubnetPool, self).get_parser(prog_name)
         parser.add_argument(
             'subnet_pool',
             metavar='<subnet-pool>',
-            help=_("Subnet pool to delete (name or ID)")
+            nargs='+',
+            help=_("Subnet pool(s) to delete (name or ID)")
         )
         return parser
 
     def take_action(self, parsed_args):
         client = self.app.client_manager.network
-        obj = client.find_subnet_pool(parsed_args.subnet_pool)
-        client.delete_subnet_pool(obj)
+        result = 0
+
+        for pool in parsed_args.subnet_pool:
+            try:
+                obj = client.find_subnet_pool(pool, ignore_missing=False)
+                client.delete_subnet_pool(obj)
+            except Exception as e:
+                result += 1
+                LOG.error(_("Failed to delete subnet pool with "
+                          "name or ID '%(pool)s': %(e)s")
+                          % {'pool': pool, 'e': e})
+
+        if result > 0:
+            total = len(parsed_args.subnet_pool)
+            msg = (_("%(result)s of %(total)s subnet pools failed "
+                   "to delete.") % {'result': result, 'total': total})
+            raise exceptions.CommandError(msg)
 
 
 class ListSubnetPool(command.Lister):
diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py
index 6b09e2978b..a23efc2d1d 100644
--- a/openstackclient/tests/network/v2/fakes.py
+++ b/openstackclient/tests/network/v2/fakes.py
@@ -771,6 +771,25 @@ class FakeSubnet(object):
 
         return subnets
 
+    @staticmethod
+    def get_subnets(subnets=None, count=2):
+        """Get an iterable MagicMock object with a list of faked subnets.
+
+        If subnets list is provided, then initialize the Mock object
+        with the list. Otherwise create one.
+
+        :param List subnets:
+            A list of FakeResource objects faking subnets
+        :param int count:
+            The number of subnets to fake
+        :return:
+            An iterable Mock object with side_effect set to a list of faked
+            subnets
+        """
+        if subnets is None:
+            subnets = FakeSubnet.create_subnets(count)
+        return mock.MagicMock(side_effect=subnets)
+
 
 class FakeFloatingIP(object):
     """Fake one or more floating ip."""
@@ -910,3 +929,22 @@ class FakeSubnetPool(object):
             )
 
         return subnet_pools
+
+    @staticmethod
+    def get_subnet_pools(subnet_pools=None, count=2):
+        """Get an iterable MagicMock object with a list of faked subnet pools.
+
+        If subnet_pools list is provided, then initialize the Mock object
+        with the list. Otherwise create one.
+
+        :param List subnet pools:
+            A list of FakeResource objects faking subnet pools
+        :param int count:
+            The number of subnet pools to fake
+        :return:
+            An iterable Mock object with side_effect set to a list of faked
+            subnet pools
+        """
+        if subnet_pools is None:
+            subnet_pools = FakeSubnetPool.create_subnet_pools(count)
+        return mock.MagicMock(side_effect=subnet_pools)
diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py
index de7e182123..a57a030897 100644
--- a/openstackclient/tests/network/v2/test_subnet.py
+++ b/openstackclient/tests/network/v2/test_subnet.py
@@ -13,7 +13,9 @@
 
 import copy
 import mock
+from mock import call
 
+from osc_lib import exceptions
 from osc_lib import utils
 
 from openstackclient.network.v2 import subnet as subnet_v2
@@ -361,32 +363,82 @@ class TestCreateSubnet(TestSubnet):
 
 class TestDeleteSubnet(TestSubnet):
 
-    # The subnet to delete.
-    _subnet = network_fakes.FakeSubnet.create_one_subnet()
+    # The subnets to delete.
+    _subnets = network_fakes.FakeSubnet.create_subnets(count=2)
 
     def setUp(self):
         super(TestDeleteSubnet, self).setUp()
 
         self.network.delete_subnet = mock.Mock(return_value=None)
 
-        self.network.find_subnet = mock.Mock(return_value=self._subnet)
+        self.network.find_subnet = (
+            network_fakes.FakeSubnet.get_subnets(self._subnets))
 
         # Get the command object to test
         self.cmd = subnet_v2.DeleteSubnet(self.app, self.namespace)
 
-    def test_delete(self):
+    def test_subnet_delete(self):
         arglist = [
-            self._subnet.name,
+            self._subnets[0].name,
         ]
         verifylist = [
-            ('subnet', self._subnet.name),
+            ('subnet', [self._subnets[0].name]),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
         result = self.cmd.take_action(parsed_args)
-        self.network.delete_subnet.assert_called_once_with(self._subnet)
+        self.network.delete_subnet.assert_called_once_with(self._subnets[0])
         self.assertIsNone(result)
 
+    def test_multi_subnets_delete(self):
+        arglist = []
+        verifylist = []
+
+        for s in self._subnets:
+            arglist.append(s.name)
+        verifylist = [
+            ('subnet', arglist),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        calls = []
+        for s in self._subnets:
+            calls.append(call(s))
+        self.network.delete_subnet.assert_has_calls(calls)
+        self.assertIsNone(result)
+
+    def test_multi_subnets_delete_with_exception(self):
+        arglist = [
+            self._subnets[0].name,
+            'unexist_subnet',
+        ]
+        verifylist = [
+            ('subnet',
+             [self._subnets[0].name, 'unexist_subnet']),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        find_mock_result = [self._subnets[0], exceptions.CommandError]
+        self.network.find_subnet = (
+            mock.MagicMock(side_effect=find_mock_result)
+        )
+
+        try:
+            self.cmd.take_action(parsed_args)
+            self.fail('CommandError should be raised.')
+        except exceptions.CommandError as e:
+            self.assertEqual('1 of 2 subnets failed to delete.', str(e))
+
+        self.network.find_subnet.assert_any_call(
+            self._subnets[0].name, ignore_missing=False)
+        self.network.find_subnet.assert_any_call(
+            'unexist_subnet', ignore_missing=False)
+        self.network.delete_subnet.assert_called_once_with(
+            self._subnets[0]
+        )
+
 
 class TestListSubnet(TestSubnet):
     # The subnets going to be listed up.
diff --git a/openstackclient/tests/network/v2/test_subnet_pool.py b/openstackclient/tests/network/v2/test_subnet_pool.py
index 10ef76d81a..7a96b30f67 100644
--- a/openstackclient/tests/network/v2/test_subnet_pool.py
+++ b/openstackclient/tests/network/v2/test_subnet_pool.py
@@ -14,7 +14,9 @@
 import argparse
 import copy
 import mock
+from mock import call
 
+from osc_lib import exceptions
 from osc_lib import utils
 
 from openstackclient.network.v2 import subnet_pool
@@ -263,36 +265,85 @@ class TestCreateSubnetPool(TestSubnetPool):
 
 class TestDeleteSubnetPool(TestSubnetPool):
 
-    # The subnet pool to delete.
-    _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool()
+    # The subnet pools to delete.
+    _subnet_pools = network_fakes.FakeSubnetPool.create_subnet_pools(count=2)
 
     def setUp(self):
         super(TestDeleteSubnetPool, self).setUp()
 
         self.network.delete_subnet_pool = mock.Mock(return_value=None)
 
-        self.network.find_subnet_pool = mock.Mock(
-            return_value=self._subnet_pool
+        self.network.find_subnet_pool = (
+            network_fakes.FakeSubnetPool.get_subnet_pools(self._subnet_pools)
         )
 
         # Get the command object to test
         self.cmd = subnet_pool.DeleteSubnetPool(self.app, self.namespace)
 
-    def test_delete(self):
+    def test_subnet_pool_delete(self):
         arglist = [
-            self._subnet_pool.name,
+            self._subnet_pools[0].name,
         ]
         verifylist = [
-            ('subnet_pool', self._subnet_pool.name),
+            ('subnet_pool', [self._subnet_pools[0].name]),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
         result = self.cmd.take_action(parsed_args)
 
         self.network.delete_subnet_pool.assert_called_once_with(
-            self._subnet_pool)
+            self._subnet_pools[0])
         self.assertIsNone(result)
 
+    def test_multi_subnet_pools_delete(self):
+        arglist = []
+        verifylist = []
+
+        for s in self._subnet_pools:
+            arglist.append(s.name)
+        verifylist = [
+            ('subnet_pool', arglist),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        calls = []
+        for s in self._subnet_pools:
+            calls.append(call(s))
+        self.network.delete_subnet_pool.assert_has_calls(calls)
+        self.assertIsNone(result)
+
+    def test_multi_subnet_pools_delete_with_exception(self):
+        arglist = [
+            self._subnet_pools[0].name,
+            'unexist_subnet_pool',
+        ]
+        verifylist = [
+            ('subnet_pool',
+             [self._subnet_pools[0].name, 'unexist_subnet_pool']),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        find_mock_result = [self._subnet_pools[0], exceptions.CommandError]
+        self.network.find_subnet_pool = (
+            mock.MagicMock(side_effect=find_mock_result)
+        )
+
+        try:
+            self.cmd.take_action(parsed_args)
+            self.fail('CommandError should be raised.')
+        except exceptions.CommandError as e:
+            self.assertEqual('1 of 2 subnet pools failed to delete.', str(e))
+
+        self.network.find_subnet_pool.assert_any_call(
+            self._subnet_pools[0].name, ignore_missing=False)
+        self.network.find_subnet_pool.assert_any_call(
+            'unexist_subnet_pool', ignore_missing=False)
+        self.network.delete_subnet_pool.assert_called_once_with(
+            self._subnet_pools[0]
+        )
+
 
 class TestListSubnetPool(TestSubnetPool):
     # The subnet pools going to be listed up.
diff --git a/releasenotes/notes/bp-multi-argument-network-e43e192ac95db94d.yaml b/releasenotes/notes/bp-multi-argument-network-e43e192ac95db94d.yaml
index 0c56e8205e..9bfc86a713 100644
--- a/releasenotes/notes/bp-multi-argument-network-e43e192ac95db94d.yaml
+++ b/releasenotes/notes/bp-multi-argument-network-e43e192ac95db94d.yaml
@@ -1,5 +1,6 @@
 ---
 features:
-  - Support bulk deletion for ``floating ip delete``, ``security group delete``,
-    and ``security group rule delete`` commands in networkv2.
+  - Support bulk deletion for ``subnet pool delete``, ``subnet delete``,
+    ``floating ip delete``, ``security group delete`` and
+    ``security group rule delete``.
     [Blueprint `multi-argument-network <https://blueprints.launchpad.net/python-openstackclient/+spec/multi-argument-network>`_]