diff --git a/doc/source/command-objects/consistency-group.rst b/doc/source/command-objects/consistency-group.rst index 658d942502..e54ac65fd3 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_add_volume: +.. describe:: + + Consistency group to contain (name or ID) + +.. describe:: + + Volume(s) to add to (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_remove_volume: +.. describe:: + + Consistency group containing (name or ID) + +.. describe:: + + Volume(s) to remove from (name or ID) + (repeat option to remove multiple volumes) + consistency group set --------------------- Set consistency group properties. .. program:: consistency group set - .. code:: bash +.. code:: bash openstack consistency group set [--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="", + help=_('Consistency group to contain (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='', + nargs='+', + help=_('Volume(s) to add to (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="", + help=_('Consistency group containing (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='', + nargs='+', + help=_('Volume(s) to remove from (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 `_] + diff --git a/setup.cfg b/setup.cfg index c88a4cb57b..bbc5e051ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -522,9 +522,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