From bf3e7cb7161dc3f48dbba36e6619f2b924b2795b Mon Sep 17 00:00:00 2001 From: Maari Tamm Date: Sat, 14 Nov 2020 19:06:05 +0000 Subject: [PATCH] [OSC] Implement Share Adopt & Abandon Commands This commit adds 'openstack share adopt' and 'openstack share abandon' commands, that implement the same functionality as 'manila manage' and 'manila unmanage' commands Usage: openstack share adopt openstack share abandon Partially-implements bp openstack-client-support Change-Id: I39919d38854387af21da410849905698ad261e9f --- doc/source/cli/osc/v2/index.rst | 6 + manilaclient/osc/v2/share.py | 184 ++++++++++++++ manilaclient/tests/unit/osc/v2/test_share.py | 248 +++++++++++++++++++ setup.cfg | 2 + 4 files changed, 440 insertions(+) diff --git a/doc/source/cli/osc/v2/index.rst b/doc/source/cli/osc/v2/index.rst index f5ebbcf5b..a964ab737 100644 --- a/doc/source/cli/osc/v2/index.rst +++ b/doc/source/cli/osc/v2/index.rst @@ -29,6 +29,12 @@ shares .. autoprogram-cliff:: openstack.share.v2 :command: share resize +.. autoprogram-cliff:: openstack.share.v2 + :command: share adopt + +.. autoprogram-cliff:: openstack.share.v2 + :command: share abandon + ================== share access rules ================== diff --git a/manilaclient/osc/v2/share.py b/manilaclient/osc/v2/share.py index 2d0037f8d..4a060367b 100644 --- a/manilaclient/osc/v2/share.py +++ b/manilaclient/osc/v2/share.py @@ -21,6 +21,7 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils as oscutils +from manilaclient import api_versions from manilaclient.common._i18n import _ from manilaclient.common.apiclient import utils as apiutils from manilaclient.common import cliutils @@ -777,3 +778,186 @@ class ResizeShare(command.Command): ): raise exceptions.CommandError(_( "Share not available after resize attempt.")) + + +class AdoptShare(command.ShowOne): + """Adopt share not handled by Manila (Admin only).""" + _description = _("Adopt a share") + + def get_parser(self, prog_name): + parser = super(AdoptShare, self).get_parser(prog_name) + parser.add_argument( + 'service_host', + metavar="", + help=_('Service host: some.host@driver#pool.') + ) + parser.add_argument( + 'protocol', + metavar="", + help=_( + 'Protocol of the share to manage, such as NFS or CIFS.') + ) + parser.add_argument( + 'export_path', + metavar="", + help=_('Share export path, NFS share such as: ' + '10.0.0.1:/example_path, CIFS share such as: ' + '\\\\10.0.0.1\\example_cifs_share.') + ) + parser.add_argument( + '--name', + metavar="", + default=None, + help=_('Optional share name. (Default=None)') + ) + parser.add_argument( + '--description', + metavar="", + default=None, + help=_('Optional share description. (Default=None)') + ) + parser.add_argument( + '--share-type', + metavar="", + default=None, + help=_( + 'Optional share type assigned to share. (Default=None)') + ) + parser.add_argument( + '--driver-options', + type=str, + nargs='*', + metavar='', + default=None, + help=_( + 'Optional driver options as key=value pairs (Default=None).') + ) + parser.add_argument( + '--public', + action='store_true', + help=_('Level of visibility for share. Defines whether other ' + 'projects are able to see it or not. Available only for ' + 'microversion >= 2.8. (Default=False)') + ) + parser.add_argument( + '--share-server-id', + metavar="", + help=_('Share server associated with share when using a share ' + 'type with "driver_handles_share_servers" extra_spec ' + 'set to True. Available only for microversion >= 2.49. ' + '(Default=None)') + ) + parser.add_argument( + "--wait", + action='store_true', + help=_("Wait until share is adopted") + ) + return parser + + def take_action(self, parsed_args): + share_client = self.app.client_manager.share + + kwargs = { + 'service_host': parsed_args.service_host, + 'protocol': parsed_args.protocol, + 'export_path': parsed_args.export_path, + 'name': parsed_args.name, + 'description': parsed_args.description + } + + share_type = None + if parsed_args.share_type: + share_type = apiutils.find_resource(share_client.share_types, + parsed_args.share_type).id + kwargs['share_type'] = share_type + + driver_options = None + if parsed_args.driver_options: + driver_options = utils.extract_properties( + parsed_args.driver_options) + kwargs['driver_options'] = driver_options + + if parsed_args.public: + if share_client.api_version >= api_versions.APIVersion("2.8"): + kwargs['public'] = True + else: + raise exceptions.CommandError( + 'Setting share visibility while adopting a share is ' + 'available only for API microversion >= 2.8') + + if parsed_args.share_server_id: + if share_client.api_version >= api_versions.APIVersion("2.49"): + kwargs['share_server_id'] = parsed_args.share_server_id + else: + raise exceptions.CommandError( + 'Selecting a share server ID is available only for ' + 'API microversion >= 2.49') + + share = share_client.shares.manage(**kwargs) + + if parsed_args.wait: + if not oscutils.wait_for_status( + status_f=share_client.shares.get, + res_id=share.id, + success_status=['available'], + error_status=['manage_error', 'error'] + ): + LOG.error(_("ERROR: Share is in error state.")) + + share = apiutils.find_resource(share_client.shares, + share.id) + share._info.pop('links', None) + + return self.dict2columns(share._info) + + +class AbandonShare(command.Command): + """Abandon a share (Admin only).""" + _description = _("Abandon a share") + + def get_parser(self, prog_name): + parser = super(AbandonShare, self).get_parser(prog_name) + parser.add_argument( + 'share', + metavar="", + nargs="+", + help=_('Name or ID of the share(s)') + ) + parser.add_argument( + "--wait", + action='store_true', + help=_("Wait until share is abandoned") + ) + return parser + + def take_action(self, parsed_args): + share_client = self.app.client_manager.share + result = 0 + + for share in parsed_args.share: + try: + share_obj = apiutils.find_resource( + share_client.shares, share + ) + share_client.shares.unmanage(share_obj) + + if parsed_args.wait: + # 'wait_for_delete' checks that the resource is no longer + # retrievable with the given 'res_id' so we can use it + # to check that the share has been abandoned + if not oscutils.wait_for_delete( + manager=share_client.shares, + res_id=share_obj.id): + result += 1 + + except Exception as e: + result += 1 + LOG.error(_("Failed to abandon share with " + "name or ID '%(share)s': %(e)s"), + {'share': share, 'e': e}) + + if result > 0: + total = len(parsed_args.share) + msg = (_("Failed to abandon %(result)s out of %(total)s shares.") + % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) diff --git a/manilaclient/tests/unit/osc/v2/test_share.py b/manilaclient/tests/unit/osc/v2/test_share.py index 662d98786..6dddc9dd8 100644 --- a/manilaclient/tests/unit/osc/v2/test_share.py +++ b/manilaclient/tests/unit/osc/v2/test_share.py @@ -20,6 +20,8 @@ import uuid from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from osc_lib import exceptions as osc_exceptions +from manilaclient import api_versions +from manilaclient.api_versions import MAX_VERSION from manilaclient.common.apiclient import exceptions from manilaclient.common import cliutils from manilaclient.osc.v2 import share as osc_shares @@ -44,6 +46,12 @@ class TestShare(manila_fakes.TestShare): self.snapshots_mock = self.app.client_manager.share.share_snapshots self.snapshots_mock.reset_mock() + self.share_types_mock = self.app.client_manager.share.share_types + self.share_types_mock.reset_mock() + + self.app.client_manager.share.api_version = api_versions.APIVersion( + MAX_VERSION) + def setup_shares_mock(self, count): shares = manila_fakes.FakeShare.create_shares(count=count) @@ -1409,3 +1417,243 @@ class TestResizeShare(TestShare): self.cmd.take_action, parsed_args ) + + +class TestAdoptShare(TestShare): + + def setUp(self): + super(TestAdoptShare, self).setUp() + + self._share_type = manila_fakes.FakeShareType.create_one_sharetype() + self.share_types_mock.get.return_value = self._share_type + + self._share = manila_fakes.FakeShare.create_one_share( + attrs={ + 'status': 'available', + 'share_type': self._share_type.id, + 'share_server_id': 'server-id' + uuid.uuid4().hex}) + self.shares_mock.get.return_value = self._share + + self.shares_mock.manage.return_value = self._share + + # Get the command objects to test + self.cmd = osc_shares.AdoptShare(self.app, None) + + self.datalist = tuple(self._share._info.values()) + self.columns = tuple(self._share._info.keys()) + + def test_share_adopt_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises(osc_utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_share_adopt_required_args(self): + arglist = [ + 'some.host@driver#pool', + 'NFS', + '10.0.0.1:/example_path' + ] + verifylist = [ + ('service_host', 'some.host@driver#pool'), + ('protocol', 'NFS'), + ('export_path', '10.0.0.1:/example_path') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.shares_mock.manage.assert_called_with( + description=None, + export_path='10.0.0.1:/example_path', + name=None, + protocol='NFS', + service_host='some.host@driver#pool' + ) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_share_adopt(self): + arglist = [ + 'some.host@driver#pool', + 'NFS', + '10.0.0.1:/example_path', + '--name', self._share.id, + '--description', self._share.description, + '--share-type', self._share.share_type, + '--driver-options', 'key1=value1', 'key2=value2', + '--wait', + '--public', + '--share-server-id', self._share.share_server_id + + ] + verifylist = [ + ('service_host', 'some.host@driver#pool'), + ('protocol', 'NFS'), + ('export_path', '10.0.0.1:/example_path'), + ('name', self._share.id), + ('description', self._share.description), + ('share_type', self._share_type.id), + ('driver_options', ['key1=value1', 'key2=value2']), + ('wait', True), + ('public', True), + ('share_server_id', self._share.share_server_id) + + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.shares_mock.manage.assert_called_with( + description=self._share.description, + driver_options={'key1': 'value1', 'key2': 'value2'}, + export_path='10.0.0.1:/example_path', + name=self._share.id, + protocol='NFS', + service_host='some.host@driver#pool', + share_server_id=self._share.share_server_id, + share_type=self._share_type.id, + public=True + ) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + @mock.patch('manilaclient.osc.v2.share.LOG') + def test_share_adopt_wait_error(self, mock_logger): + arglist = [ + 'some.host@driver#pool', + 'NFS', + '10.0.0.1:/example_path', + '--wait' + ] + verifylist = [ + ('service_host', 'some.host@driver#pool'), + ('protocol', 'NFS'), + ('export_path', '10.0.0.1:/example_path'), + ('wait', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_status', return_value=False): + columns, data = self.cmd.take_action(parsed_args) + self.shares_mock.manage.assert_called_with( + description=None, + export_path='10.0.0.1:/example_path', + name=None, + protocol='NFS', + service_host='some.host@driver#pool' + ) + + mock_logger.error.assert_called_with( + "ERROR: Share is in error state.") + + self.shares_mock.get.assert_called_with(self._share.id) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) + + def test_share_adopt_visibility_api_version_exception(self): + self.app.client_manager.share.api_version = api_versions.APIVersion( + "2.7") + + arglist = [ + 'some.host@driver#pool', + 'NFS', + '10.0.0.1:/example_path', + '--public' + + ] + verifylist = [ + ('service_host', 'some.host@driver#pool'), + ('protocol', 'NFS'), + ('export_path', '10.0.0.1:/example_path'), + ('public', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + osc_exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_share_adopt_share_server_api_version_exception(self): + self.app.client_manager.share.api_version = api_versions.APIVersion( + "2.48") + + arglist = [ + 'some.host@driver#pool', + 'NFS', + '10.0.0.1:/example_path', + '--share-server-id', self._share.share_server_id + + ] + verifylist = [ + ('service_host', 'some.host@driver#pool'), + ('protocol', 'NFS'), + ('export_path', '10.0.0.1:/example_path'), + ('share_server_id', self._share.share_server_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + osc_exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestAbandonShare(TestShare): + + def setUp(self): + super(TestAbandonShare, self).setUp() + + self._share = manila_fakes.FakeShare.create_one_share( + attrs={'status': 'available'}) + self.shares_mock.get.return_value = self._share + + # Get the command objects to test + self.cmd = osc_shares.AbandonShare(self.app, None) + + def test_share_abandon(self): + arglist = [ + self._share.id, + ] + verifylist = [ + ('share', [self._share.id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.shares_mock.unmanage.assert_called_with(self._share) + self.assertIsNone(result) + + def test_share_abandon_wait(self): + arglist = [ + self._share.id, + '--wait' + ] + verifylist = [ + ('share', [self._share.id]), + ('wait', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=True): + result = self.cmd.take_action(parsed_args) + self.shares_mock.unmanage.assert_called_with(self._share) + self.assertIsNone(result) + + def test_share_abandon_wait_error(self): + arglist = [ + self._share.id, + '--wait' + ] + verifylist = [ + ('share', [self._share.id]), + ('wait', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=False): + self.assertRaises( + osc_exceptions.CommandError, + self.cmd.take_action, + parsed_args + ) diff --git a/setup.cfg b/setup.cfg index 77ea44baf..7fc0fb067 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,8 @@ openstack.share.v2 = share_set = manilaclient.osc.v2.share:SetShare share_unset = manilaclient.osc.v2.share:UnsetShare share_resize = manilaclient.osc.v2.share:ResizeShare + share_adopt = manilaclient.osc.v2.share:AdoptShare + share_abandon = manilaclient.osc.v2.share:AbandonShare share_access_create = manilaclient.osc.v2.share_access_rules:ShareAccessAllow share_access_delete = manilaclient.osc.v2.share_access_rules:ShareAccessDeny share_access_list = manilaclient.osc.v2.share_access_rules:ListShareAccess