diff --git a/manilaclient/api_versions.py b/manilaclient/api_versions.py index 26d7e9033..6250d6992 100644 --- a/manilaclient/api_versions.py +++ b/manilaclient/api_versions.py @@ -27,7 +27,7 @@ from manilaclient import utils LOG = logging.getLogger(__name__) -MAX_VERSION = '2.26' +MAX_VERSION = '2.27' MIN_VERSION = '2.0' DEPRECATED_VERSION = '1.0' _VERSIONED_METHOD_MAP = {} diff --git a/manilaclient/tests/functional/base.py b/manilaclient/tests/functional/base.py index d1a5bcabc..137421268 100644 --- a/manilaclient/tests/functional/base.py +++ b/manilaclient/tests/functional/base.py @@ -172,7 +172,8 @@ class BaseTestCase(base.ClientTestBase): @classmethod def create_share_type(cls, name=None, driver_handles_share_servers=True, snapshot_support=None, - create_share_from_snapshot=None, is_public=True, + create_share_from_snapshot=None, + revert_to_snapshot=None, is_public=True, client=None, cleanup_in_class=True, microversion=None, extra_specs=None): if client is None: @@ -185,6 +186,7 @@ class BaseTestCase(base.ClientTestBase): microversion=microversion, extra_specs=extra_specs, create_share_from_snapshot=create_share_from_snapshot, + revert_to_snapshot=revert_to_snapshot ) resource = { "type": "share_type", diff --git a/manilaclient/tests/functional/client.py b/manilaclient/tests/functional/client.py index 184e00ccc..bd94c7c97 100644 --- a/manilaclient/tests/functional/client.py +++ b/manilaclient/tests/functional/client.py @@ -164,7 +164,8 @@ class ManilaCLIClient(base.CLIClient): def create_share_type(self, name=None, driver_handles_share_servers=True, snapshot_support=None, - create_share_from_snapshot=None, is_public=True, + create_share_from_snapshot=None, + revert_to_snapshot=None, is_public=True, microversion=None, extra_specs=None): """Creates share type. @@ -179,6 +180,8 @@ class ManilaCLIClient(base.CLIClient): :param extra_specs: -- dictionary of extra specs Default is None. :param create_share_from_snapshot: -- boolean or its string alias. Default is None. + :param revert_to_snapshot: -- boolean or its string alias. Default is + None. """ if name is None: name = data_utils.rand_name('manilaclient_functional_test') @@ -194,14 +197,20 @@ class ManilaCLIClient(base.CLIClient): if snapshot_support is not None: if not isinstance(snapshot_support, six.string_types): snapshot_support = six.text_type(snapshot_support) - cmd += " --snapshot-support " + snapshot_support + cmd += " --snapshot-support " + snapshot_support if create_share_from_snapshot is not None: if not isinstance(create_share_from_snapshot, six.string_types): create_share_from_snapshot = six.text_type( create_share_from_snapshot) - cmd += (" --create-share-from-snapshot-support " + - create_share_from_snapshot) + cmd += (" --create-share-from-snapshot-support " + + create_share_from_snapshot) + + if revert_to_snapshot is not None: + if not isinstance(revert_to_snapshot, six.string_types): + revert_to_snapshot = six.text_type( + revert_to_snapshot) + cmd += (" --revert-to-snapshot-support " + revert_to_snapshot) if extra_specs is not None: extra_spec_str = '' diff --git a/manilaclient/tests/functional/test_share_types.py b/manilaclient/tests/functional/test_share_types.py index e80c163f2..346df81f5 100644 --- a/manilaclient/tests/functional/test_share_types.py +++ b/manilaclient/tests/functional/test_share_types.py @@ -90,7 +90,7 @@ class ShareTypesReadWriteTest(base.BaseTestCase): self.skip_if_microversion_not_supported('2.0') self._test_create_delete_share_type( '2.0', is_public, dhss, spec_snapshot_support, - None, extra_specs) + None, None, extra_specs) @ddt.data(*unit_test_types.get_valid_type_create_data_2_24()) @ddt.unpack @@ -101,11 +101,25 @@ class ShareTypesReadWriteTest(base.BaseTestCase): self.skip_if_microversion_not_supported('2.24') self._test_create_delete_share_type( '2.24', is_public, dhss, spec_snapshot_support, - spec_create_share_from_snapshot, extra_specs) + spec_create_share_from_snapshot, None, extra_specs) + + @ddt.data(*unit_test_types.get_valid_type_create_data_2_27()) + @ddt.unpack + def test_create_delete_share_type_2_27( + self, is_public, dhss, spec_snapshot_support, + spec_create_share_from_snapshot, spec_revert_to_snapshot_support, + extra_specs): + + self.skip_if_microversion_not_supported('2.27') + self._test_create_delete_share_type( + '2.27', is_public, dhss, spec_snapshot_support, + spec_create_share_from_snapshot, spec_revert_to_snapshot_support, + extra_specs) def _test_create_delete_share_type(self, microversion, is_public, dhss, spec_snapshot_support, spec_create_share_from_snapshot, + spec_revert_to_snapshot_support, extra_specs): share_type_name = data_utils.rand_name('manilaclient_functional_test') @@ -119,6 +133,7 @@ class ShareTypesReadWriteTest(base.BaseTestCase): driver_handles_share_servers=dhss, snapshot_support=spec_snapshot_support, create_share_from_snapshot=spec_create_share_from_snapshot, + revert_to_snapshot=spec_revert_to_snapshot_support, is_public=is_public, microversion=microversion, extra_specs=extra_specs) @@ -163,6 +178,11 @@ class ShareTypesReadWriteTest(base.BaseTestCase): ('{} : {}'.format( 'create_share_from_snapshot_support', spec_create_share_from_snapshot)).strip()) + if spec_revert_to_snapshot_support is not None: + expected_extra_specs.append( + ('{} : {}'.format( + 'revert_to_snapshot_support', + spec_revert_to_snapshot_support)).strip()) # Verify optional extra specs optional_extra_specs = share_type['optional_extra_specs'] diff --git a/manilaclient/tests/unit/v2/fakes.py b/manilaclient/tests/unit/v2/fakes.py index 2fe452c59..1d93777d3 100644 --- a/manilaclient/tests/unit/v2/fakes.py +++ b/manilaclient/tests/unit/v2/fakes.py @@ -402,6 +402,9 @@ class FakeHTTPClient(fakes.FakeHTTPClient): assert body[action]['new_size'] is not None elif action in ('unmanage', ): assert body[action] is None + elif action in ('revert', ): + assert body[action] is not None + assert body[action]['snapshot_id'] is not None elif action in ( 'migration_cancel', 'migration_complete', 'migration_get_progress'): diff --git a/manilaclient/tests/unit/v2/test_shares.py b/manilaclient/tests/unit/v2/test_shares.py index e221e05bc..870aaeb52 100644 --- a/manilaclient/tests/unit/v2/test_shares.py +++ b/manilaclient/tests/unit/v2/test_shares.py @@ -19,6 +19,7 @@ import ddt import mock from manilaclient import api_versions +from manilaclient.common.apiclient import exceptions as client_exceptions from manilaclient import exceptions from manilaclient import extension from manilaclient.tests.unit import utils @@ -239,6 +240,35 @@ class SharesTest(utils.TestCase): manager._action.assert_called_once_with("unmanage", share) self.assertEqual("fake", result) + def test_revert_to_snapshot(self): + + share = 'fake_share' + snapshot = 'fake_snapshot' + version = api_versions.APIVersion("2.27") + mock_microversion = mock.Mock(api_version=version) + manager = shares.ShareManager(api=mock_microversion) + mock_action = self.mock_object( + manager, '_action', mock.Mock(return_value='fake')) + + result = manager.revert_to_snapshot(share, snapshot) + + self.assertEqual('fake', result) + mock_action.assert_called_once_with( + 'revert', 'fake_share', info={'snapshot_id': 'fake_snapshot'}) + + def test_revert_to_snapshot_not_supported(self): + + share = 'fake_share' + snapshot = 'fake_snapshot' + version = api_versions.APIVersion("2.26") + mock_microversion = mock.Mock(api_version=version) + manager = shares.ShareManager(api=mock_microversion) + + self.assertRaises(client_exceptions.UnsupportedVersion, + manager.revert_to_snapshot, + share, + snapshot) + @ddt.data( ("2.6", "os-force_delete"), ("2.7", "force_delete"), diff --git a/manilaclient/tests/unit/v2/test_shell.py b/manilaclient/tests/unit/v2/test_shell.py index 982aef845..9f46d0fb4 100644 --- a/manilaclient/tests/unit/v2/test_shell.py +++ b/manilaclient/tests/unit/v2/test_shell.py @@ -455,6 +455,7 @@ class ShellTest(test_utils.TestCase): 'driver_handles_share_servers': False, 'snapshot_support': True, 'create_share_from_snapshot_support': True, + 'revert_to_snapshot_support': False, }, 'share_type_access:is_public': public } @@ -618,6 +619,19 @@ class ShellTest(test_utils.TestCase): self.assert_called('POST', '/snapshots/1234/action', body={'unmanage': None}) + def test_revert_to_snapshot(self): + + fake_share_snapshot = type( + 'FakeShareSnapshot', (object,), {'id': '5678', 'share_id': '1234'}) + self.mock_object( + shell_v2, '_find_share_snapshot', + mock.Mock(return_value=fake_share_snapshot)) + + self.run_command('revert-to-snapshot 5678') + + self.assert_called('POST', '/shares/1234/action', + body={'revert': {'snapshot_id': '5678'}}) + def test_delete(self): self.run_command('delete 1234') self.assert_called('DELETE', '/shares/1234') @@ -823,6 +837,7 @@ class ShellTest(test_utils.TestCase): "driver_handles_share_servers": expected_bool, "snapshot_support": True, "create_share_from_snapshot_support": True, + "revert_to_snapshot_support": False, } } } @@ -870,6 +885,7 @@ class ShellTest(test_utils.TestCase): "driver_handles_share_servers": False, "snapshot_support": expected_bool, "create_share_from_snapshot_support": True, + "revert_to_snapshot_support": False, "replication_type": replication_type, } } @@ -898,6 +914,7 @@ class ShellTest(test_utils.TestCase): "driver_handles_share_servers": False, "snapshot_support": True, "create_share_from_snapshot_support": expected_bool, + "revert_to_snapshot_support": False, } } } @@ -923,6 +940,41 @@ class ShellTest(test_utils.TestCase): 'type-create test false --extra-specs %s=fake' % value, ) + @ddt.unpack + @ddt.data( + *([{'expected_bool': True, 'text': v} + for v in ('true', 'True', '1', 'TRUE', 'tRuE')] + + [{'expected_bool': False, 'text': v} + for v in ('false', 'False', '0', 'FALSE', 'fAlSe')]) + ) + def test_type_create_with_revert_to_snapshot_support( + self, expected_bool, text): + expected = { + "share_type": { + "name": "test", + "share_type_access:is_public": True, + "extra_specs": { + "driver_handles_share_servers": False, + "snapshot_support": True, + "create_share_from_snapshot_support": True, + "revert_to_snapshot_support": expected_bool, + } + } + } + + self.run_command('type-create test false --snapshot-support true ' + '--revert-to-snapshot-support ' + text) + + self.assert_called('POST', '/types', body=expected) + + @ddt.data('fake', 'FFFalse', 'trueee') + def test_type_create_invalid_revert_to_snapshot_support_value(self, value): + self.assertRaises( + exceptions.CommandError, + self.run_command, + 'type-create test false --revert-to-snapshot-support ' + value, + ) + @ddt.data('--is-public', '--is_public') def test_update(self, alias): # basic rename with positional arguments diff --git a/manilaclient/tests/unit/v2/test_types.py b/manilaclient/tests/unit/v2/test_types.py index ec27ab116..4f4601f26 100644 --- a/manilaclient/tests/unit/v2/test_types.py +++ b/manilaclient/tests/unit/v2/test_types.py @@ -72,6 +72,47 @@ def get_valid_type_create_data_2_24(): return snapshot_none_combos + snapshot_true_combos + snapshot_false_combos +def get_valid_type_create_data_2_27(): + + public = [True, False] + dhss = [True, False] + snapshot = [None] + create_from_snapshot = [None] + revert_to_snapshot = [None] + extra_specs = [None, {'replication_type': 'writable', 'foo': 'bar'}] + + snapshot_none_combos = list(itertools.product(public, dhss, snapshot, + create_from_snapshot, + revert_to_snapshot, + extra_specs)) + + public = [True, False] + dhss = [True, False] + snapshot = [True] + create_from_snapshot = [True, False, None] + revert_to_snapshot = [True, False, None] + extra_specs = [None, {'replication_type': 'readable', 'foo': 'bar'}] + + snapshot_true_combos = list(itertools.product(public, dhss, snapshot, + create_from_snapshot, + revert_to_snapshot, + extra_specs)) + + public = [True, False] + dhss = [True, False] + snapshot = [False] + create_from_snapshot = [False, None] + revert_to_snapshot = [False, None] + extra_specs = [None, {'replication_type': 'dr', 'foo': 'bar'}] + + snapshot_false_combos = list(itertools.product(public, dhss, snapshot, + create_from_snapshot, + revert_to_snapshot, + extra_specs)) + + return snapshot_none_combos + snapshot_true_combos + snapshot_false_combos + + @ddt.ddt class TypesTest(utils.TestCase): @@ -146,9 +187,12 @@ class TypesTest(utils.TestCase): self.assertEqual("fake", result) def _add_standard_extra_specs_to_dict(self, extra_specs, - create_from_snapshot=None): + create_from_snapshot=None, + revert_to_snapshot=None): - if all(spec is None for spec in [create_from_snapshot]): + # Short-circuit checks to allow for extra specs to be (and remain) None + if all(spec is None for spec in [ + create_from_snapshot, revert_to_snapshot]): return extra_specs extra_specs = extra_specs or {} @@ -156,6 +200,9 @@ class TypesTest(utils.TestCase): if create_from_snapshot is not None: extra_specs['create_share_from_snapshot_support'] = ( create_from_snapshot) + if revert_to_snapshot is not None: + extra_specs['revert_to_snapshot_support'] = ( + revert_to_snapshot) return extra_specs @@ -203,6 +250,57 @@ class TypesTest(utils.TestCase): "/types", expected_body, "share_type") self.assertEqual("fake", result) + @ddt.data(*get_valid_type_create_data_2_27()) + @ddt.unpack + def test_create_2_27(self, is_public, dhss, snapshot, create_from_snapshot, + revert_to_snapshot, extra_specs): + + extra_specs = copy.copy(extra_specs) + extra_specs = self._add_standard_extra_specs_to_dict( + extra_specs, create_from_snapshot=create_from_snapshot, + revert_to_snapshot=revert_to_snapshot) + + manager = self._get_share_types_manager("2.27") + self.mock_object(manager, '_create', mock.Mock(return_value="fake")) + + result = manager.create( + 'test-type-3', spec_driver_handles_share_servers=dhss, + spec_snapshot_support=snapshot, + extra_specs=extra_specs, is_public=is_public) + + expected_extra_specs = dict(extra_specs or {}) + expected_extra_specs["driver_handles_share_servers"] = dhss + + if snapshot is None: + expected_extra_specs.pop("snapshot_support", None) + else: + expected_extra_specs["snapshot_support"] = snapshot + + if create_from_snapshot is None: + expected_extra_specs.pop("create_share_from_snapshot_support", + None) + else: + expected_extra_specs["create_share_from_snapshot_support"] = ( + create_from_snapshot) + + if revert_to_snapshot is None: + expected_extra_specs.pop("revert_to_snapshot_support", None) + else: + expected_extra_specs["revert_to_snapshot_support"] = ( + revert_to_snapshot) + + expected_body = { + "share_type": { + "name": 'test-type-3', + 'share_type_access:is_public': is_public, + "extra_specs": expected_extra_specs, + } + } + + manager._create.assert_called_once_with( + "/types", expected_body, "share_type") + self.assertEqual("fake", result) + @ddt.data( (False, False, True, {'snapshot_support': True, 'replication_type': 'fake_repl_type'}), diff --git a/manilaclient/v2/shares.py b/manilaclient/v2/shares.py index 1facb64c7..20cf37305 100644 --- a/manilaclient/v2/shares.py +++ b/manilaclient/v2/shares.py @@ -109,6 +109,10 @@ class Share(common_base.Resource): """List instances of the specified share.""" self.manager.list_instances(self) + def revert_to_snapshot(self, snapshot): + """Reverts a share (in place) to a snapshot.""" + self.manager.revert_to_snapshot(self, snapshot) + class ShareManager(base.ManagerWithFind): """Manage :class:`Share` resources.""" @@ -276,6 +280,19 @@ class ShareManager(base.ManagerWithFind): """ return self._action("unmanage", share) + @api_versions.wraps("2.27") + def revert_to_snapshot(self, share, snapshot): + """Reverts a share (in place) to a snapshot. + + The snapshot must be the most recent one known to manila. + :param share: either share object or text with its ID. + :param snapshot: either snapshot object or text with its ID. + """ + + snapshot_id = common_base.getid(snapshot) + info = {'snapshot_id': snapshot_id} + return self._action('revert', share, info=info) + def get(self, share): """Get a share. diff --git a/manilaclient/v2/shell.py b/manilaclient/v2/shell.py index 25499f54a..7429ce29f 100644 --- a/manilaclient/v2/shell.py +++ b/manilaclient/v2/shell.py @@ -1034,6 +1034,19 @@ def do_snapshot_unmanage(cs, args): "specified snapshots.") +@api_versions.wraps("2.27") +@cliutils.arg( + 'snapshot', + metavar='', + help='Name or ID of the snapshot to restore. The snapshot must be the ' + 'most recent one known to manila.') +def do_revert_to_snapshot(cs, args): + """Revert a share to the specified snapshot.""" + snapshot = _find_share_snapshot(cs, args.snapshot) + share = _find_share(cs, snapshot.share_id) + share.revert_to_snapshot(snapshot) + + @cliutils.arg( 'share', metavar='', @@ -3117,6 +3130,13 @@ def do_extra_specs_list(cs, args): action='single_alias', help="Boolean extra spec used for filtering of back ends by their " "capability to create shares from snapshots.") +@cliutils.arg( + '--revert_to_snapshot_support', + '--revert-to-snapshot-support', + metavar='', + action='single_alias', + help="Boolean extra spec used for filtering of back ends by their " + "capability to revert shares to snapshots. (Default is False).") @cliutils.arg( '--extra-specs', '--extra_specs', # alias @@ -3157,7 +3177,11 @@ def do_type_create(cs, args): "set via positional argument.") raise exceptions.CommandError(msg) - boolean_keys = ('snapshot_support', 'create_share_from_snapshot_support') + boolean_keys = ( + 'snapshot_support', + 'create_share_from_snapshot_support', + 'revert_to_snapshot_support', + ) for key in boolean_keys: value = getattr(args, key) diff --git a/releasenotes/notes/share-revert-to-snapshot-e899a4b7e1126749.yaml b/releasenotes/notes/share-revert-to-snapshot-e899a4b7e1126749.yaml new file mode 100644 index 000000000..395fab30f --- /dev/null +++ b/releasenotes/notes/share-revert-to-snapshot-e899a4b7e1126749.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support for the revert-to-snapshot feature.