diff --git a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py index bedcadd66d..1885a1f5e0 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py @@ -15,7 +15,6 @@ from unittest import mock from unittest.mock import call -from cinderclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -176,49 +175,6 @@ class TestTransferCreate(TestTransfer): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_transfer_create_with_no_snapshots(self): - self.volume_client.api_version = api_versions.APIVersion('3.55') - - arglist = [ - '--no-snapshots', - self.volume.id, - ] - verifylist = [ - ('name', None), - ('snapshots', False), - ('volume', self.volume.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.transfer_mock.create.assert_called_once_with( - self.volume.id, None, no_snapshots=True - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_transfer_create_pre_v355(self): - self.volume_client.api_version = api_versions.APIVersion('3.54') - - arglist = [ - '--no-snapshots', - self.volume.id, - ] - verifylist = [ - ('name', None), - ('snapshots', False), - ('volume', self.volume.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - exc = self.assertRaises( - exceptions.CommandError, self.cmd.take_action, parsed_args - ) - self.assertIn( - '--os-volume-api-version 3.55 or greater is required', str(exc) - ) - class TestTransferDelete(TestTransfer): volume_transfers = volume_fakes.create_transfers(count=2) diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index b014efc43b..0868de1423 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -62,6 +62,8 @@ class FakeVolumeClient: self.resource_filters.resource_class = fakes.FakeResource(None, {}) self.restores = mock.Mock() self.restores.resource_class = fakes.FakeResource(None, {}) + self.transfers = mock.Mock() + self.transfers.resource_class = fakes.FakeResource(None, {}) self.volume_encryption_types = mock.Mock() self.volume_encryption_types.resource_class = fakes.FakeResource( None, {} @@ -438,6 +440,69 @@ def create_resource_filters(attrs=None, count=2): return resource_filters +def create_one_transfer(attrs=None): + """Create a fake transfer. + + :param dict attrs: + A dictionary with all attributes of Transfer Request + :return: + A FakeResource object with volume_id, name, id. + """ + # Set default attribute + transfer_info = { + 'volume_id': 'volume-id-' + uuid.uuid4().hex, + 'name': 'fake_transfer_name', + 'id': 'id-' + uuid.uuid4().hex, + 'links': 'links-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + attrs = attrs or {} + + transfer_info.update(attrs) + + transfer = fakes.FakeResource(None, transfer_info, loaded=True) + + return transfer + + +def create_transfers(attrs=None, count=2): + """Create multiple fake transfers. + + :param dict attrs: + A dictionary with all attributes of transfer + :param Integer count: + The number of transfers to be faked + :return: + A list of FakeResource objects + """ + transfers = [] + for n in range(0, count): + transfers.append(create_one_transfer(attrs)) + + return transfers + + +def get_transfers(transfers=None, count=2): + """Get an iterable MagicMock object with a list of faked transfers. + + If transfers list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List transfers: + A list of FakeResource objects faking transfers + :param Integer count: + The number of transfers to be faked + :return + An iterable Mock object with side_effect set to a list of faked + transfers + """ + if transfers is None: + transfers = create_transfers(count) + + return mock.Mock(side_effect=transfers) + + def create_one_type_access(attrs=None): """Create a fake volume type access for project. diff --git a/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py b/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py new file mode 100644 index 0000000000..0781247882 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py @@ -0,0 +1,426 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from unittest import mock +from unittest.mock import call + +from cinderclient import api_versions +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.tests.unit import utils as test_utils +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_transfer_request + + +class TestTransfer(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + # Get a shortcut to the TransferManager Mock + self.transfer_mock = self.volume_client.transfers + self.transfer_mock.reset_mock() + + # Get a shortcut to the VolumeManager Mock + self.volumes_mock = self.volume_client.volumes + self.volumes_mock.reset_mock() + + +class TestTransferAccept(TestTransfer): + columns = ( + 'id', + 'name', + 'volume_id', + ) + + def setUp(self): + super().setUp() + + self.volume_transfer = volume_fakes.create_one_transfer() + self.data = ( + self.volume_transfer.id, + self.volume_transfer.name, + self.volume_transfer.volume_id, + ) + + self.transfer_mock.get.return_value = self.volume_transfer + self.transfer_mock.accept.return_value = self.volume_transfer + + # Get the command object to test + self.cmd = volume_transfer_request.AcceptTransferRequest( + self.app, None + ) + + def test_transfer_accept(self): + arglist = [ + '--auth-key', + 'key_value', + self.volume_transfer.id, + ] + verifylist = [ + ('transfer_request', self.volume_transfer.id), + ('auth_key', 'key_value'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.get.assert_called_once_with( + self.volume_transfer.id, + ) + self.transfer_mock.accept.assert_called_once_with( + self.volume_transfer.id, + 'key_value', + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_accept_no_option(self): + arglist = [ + self.volume_transfer.id, + ] + verifylist = [ + ('transfer_request', self.volume_transfer.id), + ] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestTransferCreate(TestTransfer): + volume = volume_fakes.create_one_volume() + + columns = ( + 'auth_key', + 'created_at', + 'id', + 'name', + 'volume_id', + ) + + def setUp(self): + super().setUp() + + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={ + 'volume_id': self.volume.id, + 'auth_key': 'key', + 'created_at': 'time', + }, + ) + self.data = ( + self.volume_transfer.auth_key, + self.volume_transfer.created_at, + self.volume_transfer.id, + self.volume_transfer.name, + self.volume_transfer.volume_id, + ) + + self.transfer_mock.create.return_value = self.volume_transfer + self.volumes_mock.get.return_value = self.volume + + # Get the command object to test + self.cmd = volume_transfer_request.CreateTransferRequest( + self.app, None + ) + + def test_transfer_create_without_name(self): + arglist = [ + self.volume.id, + ] + verifylist = [ + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.create.assert_called_once_with(self.volume.id, None) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_create_with_name(self): + arglist = [ + '--name', + self.volume_transfer.name, + self.volume.id, + ] + verifylist = [ + ('name', self.volume_transfer.name), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.create.assert_called_once_with( + self.volume.id, + self.volume_transfer.name, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_create_with_no_snapshots(self): + self.volume_client.api_version = api_versions.APIVersion('3.55') + + arglist = [ + '--no-snapshots', + self.volume.id, + ] + verifylist = [ + ('name', None), + ('snapshots', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.create.assert_called_once_with( + self.volume.id, None, no_snapshots=True + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_create_pre_v355(self): + self.volume_client.api_version = api_versions.APIVersion('3.54') + + arglist = [ + '--no-snapshots', + self.volume.id, + ] + verifylist = [ + ('name', None), + ('snapshots', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertIn( + '--os-volume-api-version 3.55 or greater is required', str(exc) + ) + + +class TestTransferDelete(TestTransfer): + volume_transfers = volume_fakes.create_transfers(count=2) + + def setUp(self): + super().setUp() + + self.transfer_mock.get = volume_fakes.get_transfers( + self.volume_transfers, + ) + self.transfer_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = volume_transfer_request.DeleteTransferRequest( + self.app, None + ) + + def test_transfer_delete(self): + arglist = [self.volume_transfers[0].id] + verifylist = [("transfer_request", [self.volume_transfers[0].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.transfer_mock.delete.assert_called_with( + self.volume_transfers[0].id + ) + self.assertIsNone(result) + + def test_delete_multiple_transfers(self): + arglist = [] + for v in self.volume_transfers: + arglist.append(v.id) + verifylist = [ + ('transfer_request', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for v in self.volume_transfers: + calls.append(call(v.id)) + self.transfer_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_transfers_with_exception(self): + arglist = [ + self.volume_transfers[0].id, + 'unexist_transfer', + ] + verifylist = [ + ('transfer_request', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.volume_transfers[0], exceptions.CommandError] + with mock.patch.object( + utils, 'find_resource', side_effect=find_mock_result + ) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 volume transfer requests failed ' 'to delete', + str(e), + ) + + find_mock.assert_any_call( + self.transfer_mock, self.volume_transfers[0].id + ) + find_mock.assert_any_call(self.transfer_mock, 'unexist_transfer') + + self.assertEqual(2, find_mock.call_count) + self.transfer_mock.delete.assert_called_once_with( + self.volume_transfers[0].id, + ) + + +class TestTransferList(TestTransfer): + # The Transfers to be listed + volume_transfers = volume_fakes.create_one_transfer() + + def setUp(self): + super().setUp() + + self.transfer_mock.list.return_value = [self.volume_transfers] + + # Get the command object to test + self.cmd = volume_transfer_request.ListTransferRequest(self.app, None) + + def test_transfer_list_without_argument(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'ID', + 'Name', + 'Volume', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = ( + ( + self.volume_transfers.id, + self.volume_transfers.name, + self.volume_transfers.volume_id, + ), + ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + # checking if proper call was made to list volume_transfers + self.transfer_mock.list.assert_called_with( + detailed=True, search_opts={'all_tenants': 0} + ) + + def test_transfer_list_with_argument(self): + arglist = ["--all-projects"] + verifylist = [("all_projects", True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'ID', + 'Name', + 'Volume', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = ( + ( + self.volume_transfers.id, + self.volume_transfers.name, + self.volume_transfers.volume_id, + ), + ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + # checking if proper call was made to list volume_transfers + self.transfer_mock.list.assert_called_with( + detailed=True, search_opts={'all_tenants': 1} + ) + + +class TestTransferShow(TestTransfer): + columns = ( + 'created_at', + 'id', + 'name', + 'volume_id', + ) + + def setUp(self): + super().setUp() + + self.volume_transfer = volume_fakes.create_one_transfer( + attrs={'created_at': 'time'}, + ) + self.data = ( + self.volume_transfer.created_at, + self.volume_transfer.id, + self.volume_transfer.name, + self.volume_transfer.volume_id, + ) + + self.transfer_mock.get.return_value = self.volume_transfer + + # Get the command object to test + self.cmd = volume_transfer_request.ShowTransferRequest(self.app, None) + + def test_transfer_show(self): + arglist = [ + self.volume_transfer.id, + ] + verifylist = [ + ('transfer_request', self.volume_transfer.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.get.assert_called_once_with(self.volume_transfer.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index d531efc6bf..18094b6262 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -16,7 +16,6 @@ import logging -from cinderclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -76,25 +75,6 @@ class CreateTransferRequest(command.ShowOne): metavar="", help=_('New transfer request name (default to None)'), ) - parser.add_argument( - '--snapshots', - action='store_true', - dest='snapshots', - help=_( - 'Allow transfer volumes without snapshots (default) ' - '(supported by --os-volume-api-version 3.55 or later)' - ), - default=None, - ) - parser.add_argument( - '--no-snapshots', - action='store_false', - dest='snapshots', - help=_( - 'Disallow transfer volumes without snapshots ' - '(supported by --os-volume-api-version 3.55 or later)' - ), - ) parser.add_argument( 'volume', metavar="", @@ -107,18 +87,6 @@ class CreateTransferRequest(command.ShowOne): kwargs = {} - if parsed_args.snapshots is not None: - if volume_client.api_version < api_versions.APIVersion('3.55'): - msg = _( - "--os-volume-api-version 3.55 or greater is required to " - "support the '--(no-)snapshots' option" - ) - raise exceptions.CommandError(msg) - - # unfortunately this option is negative so we have to reverse - # things - kwargs['no_snapshots'] = not parsed_args.snapshots - volume_id = utils.find_resource( volume_client.volumes, parsed_args.volume, diff --git a/openstackclient/volume/v3/volume_transfer_request.py b/openstackclient/volume/v3/volume_transfer_request.py new file mode 100644 index 0000000000..b7add12280 --- /dev/null +++ b/openstackclient/volume/v3/volume_transfer_request.py @@ -0,0 +1,233 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Volume v3 transfer action implementations""" + +import logging + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class AcceptTransferRequest(command.ShowOne): + _description = _("Accept volume transfer request.") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'transfer_request', + metavar="", + help=_('Volume transfer request to accept (ID only)'), + ) + parser.add_argument( + '--auth-key', + metavar="", + required=True, + help=_('Volume transfer request authentication key'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + try: + transfer_request_id = utils.find_resource( + volume_client.transfers, parsed_args.transfer_request + ).id + except exceptions.CommandError: + # Non-admin users will fail to lookup name -> ID so we just + # move on and attempt with the user-supplied information + transfer_request_id = parsed_args.transfer_request + + transfer_accept = volume_client.transfers.accept( + transfer_request_id, + parsed_args.auth_key, + ) + transfer_accept._info.pop("links", None) + + return zip(*sorted(transfer_accept._info.items())) + + +class CreateTransferRequest(command.ShowOne): + _description = _("Create volume transfer request.") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--name', + metavar="", + help=_('New transfer request name (default to None)'), + ) + parser.add_argument( + '--snapshots', + action='store_true', + dest='snapshots', + help=_( + 'Allow transfer volumes without snapshots (default) ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + default=None, + ) + parser.add_argument( + '--no-snapshots', + action='store_false', + dest='snapshots', + help=_( + 'Disallow transfer volumes without snapshots ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + ) + parser.add_argument( + 'volume', + metavar="", + help=_('Volume to transfer (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + kwargs = {} + + if parsed_args.snapshots is not None: + if volume_client.api_version < api_versions.APIVersion('3.55'): + msg = _( + "--os-volume-api-version 3.55 or greater is required to " + "support the '--(no-)snapshots' option" + ) + raise exceptions.CommandError(msg) + + # unfortunately this option is negative so we have to reverse + # things + kwargs['no_snapshots'] = not parsed_args.snapshots + + volume_id = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ).id + volume_transfer_request = volume_client.transfers.create( + volume_id, + parsed_args.name, + **kwargs, + ) + volume_transfer_request._info.pop("links", None) + + return zip(*sorted(volume_transfer_request._info.items())) + + +class DeleteTransferRequest(command.Command): + _description = _("Delete volume transfer request(s).") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'transfer_request', + metavar="", + nargs="+", + help=_('Volume transfer request(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for t in parsed_args.transfer_request: + try: + transfer_request_id = utils.find_resource( + volume_client.transfers, + t, + ).id + volume_client.transfers.delete(transfer_request_id) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete volume transfer request " + "with name or ID '%(transfer)s': %(e)s" + ) + % {'transfer': t, 'e': e} + ) + + if result > 0: + total = len(parsed_args.transfer_request) + msg = _( + "%(result)s of %(total)s volume transfer requests failed" + " to delete" + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListTransferRequest(command.Lister): + _description = _("Lists all volume transfer requests.") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + dest='all_projects', + action="store_true", + default=False, + help=_('Include all projects (admin only)'), + ) + return parser + + def take_action(self, parsed_args): + columns = ['ID', 'Name', 'Volume ID'] + column_headers = ['ID', 'Name', 'Volume'] + + volume_client = self.app.client_manager.volume + + volume_transfer_result = volume_client.transfers.list( + detailed=True, + search_opts={'all_tenants': parsed_args.all_projects}, + ) + + return ( + column_headers, + ( + utils.get_item_properties(s, columns) + for s in volume_transfer_result + ), + ) + + +class ShowTransferRequest(command.ShowOne): + _description = _("Show volume transfer request details.") + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'transfer_request', + metavar="", + help=_('Volume transfer request to display (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume_transfer_request = utils.find_resource( + volume_client.transfers, + parsed_args.transfer_request, + ) + volume_transfer_request._info.pop("links", None) + + return zip(*sorted(volume_transfer_request._info.items())) diff --git a/setup.cfg b/setup.cfg index 04e4c9b9e9..d824afb15d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -850,11 +850,11 @@ openstack.volume.v3 = volume_service_list = openstackclient.volume.v3.service:ListService volume_service_set = openstackclient.volume.v2.service:SetService - volume_transfer_request_accept = openstackclient.volume.v2.volume_transfer_request:AcceptTransferRequest - volume_transfer_request_create = openstackclient.volume.v2.volume_transfer_request:CreateTransferRequest - volume_transfer_request_delete = openstackclient.volume.v2.volume_transfer_request:DeleteTransferRequest - volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest - volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest + volume_transfer_request_accept = openstackclient.volume.v3.volume_transfer_request:AcceptTransferRequest + volume_transfer_request_create = openstackclient.volume.v3.volume_transfer_request:CreateTransferRequest + volume_transfer_request_delete = openstackclient.volume.v3.volume_transfer_request:DeleteTransferRequest + volume_transfer_request_list = openstackclient.volume.v3.volume_transfer_request:ListTransferRequest + volume_transfer_request_show = openstackclient.volume.v3.volume_transfer_request:ShowTransferRequest volume_summary = openstackclient.volume.v3.volume:VolumeSummary volume_revert = openstackclient.volume.v3.volume:VolumeRevertToSnapshot