From 2f2603d90896d0765e1bb2bb1cfb223fdba75835 Mon Sep 17 00:00:00 2001
From: "zhiyong.dai" <zhiyong.dai@easystack.cn>
Date: Tue, 6 Dec 2016 19:08:49 +0800
Subject: [PATCH] Add two consistency group commands

Add commands:
    consistency group add volume
    consistency group remove volume
in volume v2 (v2 only)

Change-Id: I70ff287d3b5df78196b8f4b9e50402c471aef284
Implements: bp cinder-command-support
Closes-Bug: #1613964
---
 .../command-objects/consistency-group.rst     |  46 +++-
 .../unit/volume/v2/test_consistency_group.py  | 206 ++++++++++++++++++
 .../volume/v2/consistency_group.py            |  92 ++++++++
 .../notes/bug-1613964-b3e8d9d828a3822c.yaml   |   6 +
 setup.cfg                                     |   2 +
 5 files changed, 351 insertions(+), 1 deletion(-)
 create mode 100644 releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml

diff --git a/doc/source/command-objects/consistency-group.rst b/doc/source/command-objects/consistency-group.rst
index 910fbba9a1..b3af60b420 100644
--- a/doc/source/command-objects/consistency-group.rst
+++ b/doc/source/command-objects/consistency-group.rst
@@ -4,6 +4,28 @@ consistency group
 
 Block Storage v2
 
+consistency group add volume
+----------------------------
+
+Add volume(s) to consistency group.
+
+.. program:: consistency group add volume
+.. code:: bash
+
+    os consistency group add volume
+        <consistency-group>
+        <volume> [<volume> ...]
+
+.. _consistency_group_add_volume:
+.. describe:: <consistency-group>
+
+    Consistency group to contain <volume> (name or ID)
+
+.. describe:: <volume>
+
+    Volume(s) to add to <consistency-group> (name or ID)
+    (repeat option to add multiple volumes)
+
 consistency group create
 ------------------------
 
@@ -86,13 +108,35 @@ List consistency groups.
 
     List additional fields in output
 
+consistency group remove volume
+-------------------------------
+
+Remove volume(s) from consistency group.
+
+.. program:: consistency group remove volume
+.. code:: bash
+
+    os consistency group remove volume
+        <consistency-group>
+        <volume> [<volume> ...]
+
+.. _consistency_group_remove_volume:
+.. describe:: <consistency-group>
+
+    Consistency group containing <volume> (name or ID)
+
+.. describe:: <volume>
+
+    Volume(s) to remove from <consistency-group> (name or ID)
+    (repeat option to remove multiple volumes)
+
 consistency group set
 ---------------------
 
 Set consistency group properties.
 
 .. program:: consistency group set
-   .. code:: bash
+.. code:: bash
 
     os consistency group set
         [--name <name>]
diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py
index bc99ca8dce..6eeeae393e 100644
--- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py
+++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py
@@ -36,10 +36,115 @@ class TestConsistencyGroup(volume_fakes.TestVolume):
             self.app.client_manager.volume.cgsnapshots)
         self.cgsnapshots_mock.reset_mock()
 
+        self.volumes_mock = (
+            self.app.client_manager.volume.volumes)
+        self.volumes_mock.reset_mock()
+
         self.types_mock = self.app.client_manager.volume.volume_types
         self.types_mock.reset_mock()
 
 
+class TestConsistencyGroupAddVolume(TestConsistencyGroup):
+
+    _consistency_group = (
+        volume_fakes.FakeConsistencyGroup.create_one_consistency_group())
+
+    def setUp(self):
+        super(TestConsistencyGroupAddVolume, self).setUp()
+
+        self.consistencygroups_mock.get.return_value = (
+            self._consistency_group)
+        # Get the command object to test
+        self.cmd = \
+            consistency_group.AddVolumeToConsistencyGroup(self.app, None)
+
+    def test_add_one_volume_to_consistency_group(self):
+        volume = volume_fakes.FakeVolume.create_one_volume()
+        self.volumes_mock.get.return_value = volume
+        arglist = [
+            self._consistency_group.id,
+            volume.id,
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volume.id]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        # Set expected values
+        kwargs = {
+            'add_volumes': volume.id,
+        }
+        self.consistencygroups_mock.update.assert_called_once_with(
+            self._consistency_group.id,
+            **kwargs
+        )
+        self.assertIsNone(result)
+
+    def test_add_multiple_volumes_to_consistency_group(self):
+        volumes = volume_fakes.FakeVolume.create_volumes(count=2)
+        self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes)
+        arglist = [
+            self._consistency_group.id,
+            volumes[0].id,
+            volumes[1].id,
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volumes[0].id, volumes[1].id]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        # Set expected values
+        kwargs = {
+            'add_volumes': volumes[0].id + ',' + volumes[1].id,
+        }
+        self.consistencygroups_mock.update.assert_called_once_with(
+            self._consistency_group.id,
+            **kwargs
+        )
+        self.assertIsNone(result)
+
+    @mock.patch.object(consistency_group.LOG, 'error')
+    def test_add_multiple_volumes_to_consistency_group_with_exception(
+            self, mock_error):
+        volume = volume_fakes.FakeVolume.create_one_volume()
+        arglist = [
+            self._consistency_group.id,
+            volume.id,
+            'unexist_volume',
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volume.id, 'unexist_volume']),
+        ]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        find_mock_result = [volume,
+                            exceptions.CommandError,
+                            self._consistency_group]
+        with mock.patch.object(utils, 'find_resource',
+                               side_effect=find_mock_result) as find_mock:
+            result = self.cmd.take_action(parsed_args)
+            mock_error.assert_called_with("1 of 2 volumes failed to add.")
+            self.assertIsNone(result)
+            find_mock.assert_any_call(self.consistencygroups_mock,
+                                      self._consistency_group.id)
+            find_mock.assert_any_call(self.volumes_mock,
+                                      volume.id)
+            find_mock.assert_any_call(self.volumes_mock,
+                                      'unexist_volume')
+            self.assertEqual(3, find_mock.call_count)
+            self.consistencygroups_mock.update.assert_called_once_with(
+                self._consistency_group.id, add_volumes=volume.id
+            )
+
+
 class TestConsistencyGroupCreate(TestConsistencyGroup):
 
     volume_type = volume_fakes.FakeType.create_one_type()
@@ -394,6 +499,107 @@ class TestConsistencyGroupList(TestConsistencyGroup):
         self.assertEqual(self.data_long, list(data))
 
 
+class TestConsistencyGroupRemoveVolume(TestConsistencyGroup):
+
+    _consistency_group = (
+        volume_fakes.FakeConsistencyGroup.create_one_consistency_group())
+
+    def setUp(self):
+        super(TestConsistencyGroupRemoveVolume, self).setUp()
+
+        self.consistencygroups_mock.get.return_value = (
+            self._consistency_group)
+        # Get the command object to test
+        self.cmd = \
+            consistency_group.RemoveVolumeFromConsistencyGroup(self.app, None)
+
+    def test_remove_one_volume_from_consistency_group(self):
+        volume = volume_fakes.FakeVolume.create_one_volume()
+        self.volumes_mock.get.return_value = volume
+        arglist = [
+            self._consistency_group.id,
+            volume.id,
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volume.id]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        # Set expected values
+        kwargs = {
+            'remove_volumes': volume.id,
+        }
+        self.consistencygroups_mock.update.assert_called_once_with(
+            self._consistency_group.id,
+            **kwargs
+        )
+        self.assertIsNone(result)
+
+    def test_remove_multi_volumes_from_consistency_group(self):
+        volumes = volume_fakes.FakeVolume.create_volumes(count=2)
+        self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes)
+        arglist = [
+            self._consistency_group.id,
+            volumes[0].id,
+            volumes[1].id,
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volumes[0].id, volumes[1].id]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        # Set expected values
+        kwargs = {
+            'remove_volumes': volumes[0].id + ',' + volumes[1].id,
+        }
+        self.consistencygroups_mock.update.assert_called_once_with(
+            self._consistency_group.id,
+            **kwargs
+        )
+        self.assertIsNone(result)
+
+    @mock.patch.object(consistency_group.LOG, 'error')
+    def test_remove_multiple_volumes_from_consistency_group_with_exception(
+            self, mock_error):
+        volume = volume_fakes.FakeVolume.create_one_volume()
+        arglist = [
+            self._consistency_group.id,
+            volume.id,
+            'unexist_volume',
+        ]
+        verifylist = [
+            ('consistency_group', self._consistency_group.id),
+            ('volumes', [volume.id, 'unexist_volume']),
+        ]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        find_mock_result = [volume,
+                            exceptions.CommandError,
+                            self._consistency_group]
+        with mock.patch.object(utils, 'find_resource',
+                               side_effect=find_mock_result) as find_mock:
+            result = self.cmd.take_action(parsed_args)
+            mock_error.assert_called_with("1 of 2 volumes failed to remove.")
+            self.assertIsNone(result)
+            find_mock.assert_any_call(self.consistencygroups_mock,
+                                      self._consistency_group.id)
+            find_mock.assert_any_call(self.volumes_mock,
+                                      volume.id)
+            find_mock.assert_any_call(self.volumes_mock,
+                                      'unexist_volume')
+            self.assertEqual(3, find_mock.call_count)
+            self.consistencygroups_mock.update.assert_called_once_with(
+                self._consistency_group.id, remove_volumes=volume.id
+            )
+
+
 class TestConsistencyGroupSet(TestConsistencyGroup):
 
     consistency_group = (
diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py
index 2f4f3c9582..0a932f8454 100644
--- a/openstackclient/volume/v2/consistency_group.py
+++ b/openstackclient/volume/v2/consistency_group.py
@@ -27,6 +27,60 @@ from openstackclient.i18n import _
 LOG = logging.getLogger(__name__)
 
 
+def _find_volumes(parsed_args_volumes, volume_client):
+    result = 0
+    uuid = ''
+    for volume in parsed_args_volumes:
+        try:
+            volume_id = utils.find_resource(
+                volume_client.volumes, volume).id
+            uuid += volume_id + ','
+        except Exception as e:
+            result += 1
+            LOG.error(_("Failed to find volume with "
+                        "name or ID '%(volume)s':%(e)s")
+                      % {'volume': volume, 'e': e})
+
+    return result, uuid
+
+
+class AddVolumeToConsistencyGroup(command.Command):
+    _description = _("Add volume(s) to consistency group")
+
+    def get_parser(self, prog_name):
+        parser = super(AddVolumeToConsistencyGroup, self).get_parser(prog_name)
+        parser.add_argument(
+            'consistency_group',
+            metavar="<consistency-group>",
+            help=_('Consistency group to contain <volume> (name or ID)'),
+        )
+        parser.add_argument(
+            'volumes',
+            metavar='<volume>',
+            nargs='+',
+            help=_('Volume(s) to add to <consistency-group> (name or ID) '
+                   '(repeat option to add multiple volumes)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        volume_client = self.app.client_manager.volume
+        result, add_uuid = _find_volumes(parsed_args.volumes, volume_client)
+
+        if result > 0:
+            total = len(parsed_args.volumes)
+            LOG.error(_("%(result)s of %(total)s volumes failed "
+                      "to add.") % {'result': result, 'total': total})
+
+        if add_uuid:
+            add_uuid = add_uuid.rstrip(',')
+            consistency_group_id = utils.find_resource(
+                volume_client.consistencygroups,
+                parsed_args.consistency_group).id
+            volume_client.consistencygroups.update(
+                consistency_group_id, add_volumes=add_uuid)
+
+
 class CreateConsistencyGroup(command.ShowOne):
     _description = _("Create new consistency group.")
 
@@ -188,6 +242,44 @@ class ListConsistencyGroup(command.Lister):
             for s in consistency_groups))
 
 
+class RemoveVolumeFromConsistencyGroup(command.Command):
+    _description = _("Remove volume(s) from consistency group")
+
+    def get_parser(self, prog_name):
+        parser = \
+            super(RemoveVolumeFromConsistencyGroup, self).get_parser(prog_name)
+        parser.add_argument(
+            'consistency_group',
+            metavar="<consistency-group>",
+            help=_('Consistency group containing <volume> (name or ID)'),
+        )
+        parser.add_argument(
+            'volumes',
+            metavar='<volume>',
+            nargs='+',
+            help=_('Volume(s) to remove from <consistency-group> (name or ID) '
+                   '(repeat option to remove multiple volumes)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        volume_client = self.app.client_manager.volume
+        result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client)
+
+        if result > 0:
+            total = len(parsed_args.volumes)
+            LOG.error(_("%(result)s of %(total)s volumes failed "
+                      "to remove.") % {'result': result, 'total': total})
+
+        if remove_uuid:
+            remove_uuid = remove_uuid.rstrip(',')
+            consistency_group_id = utils.find_resource(
+                volume_client.consistencygroups,
+                parsed_args.consistency_group).id
+            volume_client.consistencygroups.update(
+                consistency_group_id, remove_volumes=remove_uuid)
+
+
 class SetConsistencyGroup(command.Command):
     _description = _("Set consistency group properties")
 
diff --git a/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml b/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml
new file mode 100644
index 0000000000..b11d0c1f28
--- /dev/null
+++ b/releasenotes/notes/bug-1613964-b3e8d9d828a3822c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - Add ``consistency group add volume`` and ``consistency group remove volume`` commands
+    in volume v2.
+    [Bug `1642238 <https://bugs.launchpad.net/python-openstackclient/+bug/1642238>`_]
+
diff --git a/setup.cfg b/setup.cfg
index 8c5c663055..69ae4cf29a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -515,9 +515,11 @@ openstack.volume.v2 =
     backup_restore = openstackclient.volume.v2.backup:RestoreBackup
     backup_show = openstackclient.volume.v2.backup:ShowBackup
 
+    consistency_group_add_volume = openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup
     consistency_group_create = openstackclient.volume.v2.consistency_group:CreateConsistencyGroup
     consistency_group_delete = openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup
     consistency_group_list = openstackclient.volume.v2.consistency_group:ListConsistencyGroup
+    consistency_group_remove_volume = openstackclient.volume.v2.consistency_group:RemoveVolumeFromConsistencyGroup
     consistency_group_set = openstackclient.volume.v2.consistency_group:SetConsistencyGroup
     consistency_group_show = openstackclient.volume.v2.consistency_group:ShowConsistencyGroup