From 4bac8b64e0c115786e5cce585e4444c624b44404 Mon Sep 17 00:00:00 2001 From: Clinton Knight Date: Mon, 8 Feb 2016 10:23:32 -0500 Subject: [PATCH] Manage / unmanage snapshot in NetApp cDOT drivers With snapshot manage / unmanage in Manila core, we can support that in the NetApp single-SVM driver. Implements: blueprint netapp-cdot-snapshot-manage-unmanage Change-Id: I7c6c005fb3fd8613da9e9ac04b9dd832781e35ca --- ...hare_back_ends_feature_support_mapping.rst | 2 +- .../netapp/dataontap/client/client_cmode.py | 62 +++++++++++ .../dataontap/cluster_mode/drv_multi_svm.py | 8 +- .../dataontap/cluster_mode/drv_single_svm.py | 6 ++ .../netapp/dataontap/cluster_mode/lib_base.py | 53 +++++++++ .../drivers/netapp/dataontap/client/fakes.py | 21 ++++ .../dataontap/client/test_client_cmode.py | 71 ++++++++++++ .../dataontap/cluster_mode/test_lib_base.py | 102 +++++++++++++++++- .../share/drivers/netapp/dataontap/fakes.py | 8 ++ ...n-netapp-cdot-driver-5cb4b1619c39625a.yaml | 5 + 10 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/manage-unmanage-snapshot-in-netapp-cdot-driver-5cb4b1619c39625a.yaml diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index c2141ebccf..482cf193d8 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -37,7 +37,7 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | Generic (Cinder as back-end) | J | K | L | L | J | J | M | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ -| NetApp Clustered Data ONTAP | J | L | L | L | J | J | \- | +| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | EMC VNX | J | \- | \- | \- | J | J | \- | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 67783a0890..aa1a74febb 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -1498,6 +1498,68 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): result = self.send_iter_request('volume-get-iter', api_args) return self._has_records(result) + @na_utils.trace + def get_volume(self, volume_name): + """Returns the volume with the specified name, if present.""" + + api_args = { + 'query': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': volume_name, + }, + }, + }, + 'desired-attributes': { + 'volume-attributes': { + 'volume-id-attributes': { + 'containing-aggregate-name': None, + 'junction-path': None, + 'name': None, + 'owning-vserver-name': None, + 'type': None, + 'style': None, + }, + 'volume-space-attributes': { + 'size': None, + } + }, + }, + } + result = self.send_request('volume-get-iter', api_args) + + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + volume_attributes_list = attributes_list.get_children() + + if not self._has_records(result): + raise exception.StorageResourceNotFound(name=volume_name) + elif len(volume_attributes_list) > 1: + msg = _('Could not find unique volume %(vol)s.') + msg_args = {'vol': volume_name} + raise exception.NetAppException(msg % msg_args) + + volume_attributes = volume_attributes_list[0] + + volume_id_attributes = volume_attributes.get_child_by_name( + 'volume-id-attributes') or netapp_api.NaElement('none') + volume_space_attributes = volume_attributes.get_child_by_name( + 'volume-space-attributes') or netapp_api.NaElement('none') + + volume = { + 'aggregate': volume_id_attributes.get_child_content( + 'containing-aggregate-name'), + 'junction-path': volume_id_attributes.get_child_content( + 'junction-path'), + 'name': volume_id_attributes.get_child_content('name'), + 'owning-vserver-name': volume_id_attributes.get_child_content( + 'owning-vserver-name'), + 'type': volume_id_attributes.get_child_content('type'), + 'style': volume_id_attributes.get_child_content('style'), + 'size': volume_space_attributes.get_child_content('size'), + } + return volume + @na_utils.trace def get_volume_at_junction_path(self, junction_path): """Returns the volume with the specified junction path, if present.""" diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py index d6fa6e3e8b..460f70d7dd 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py @@ -53,7 +53,7 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): snapshot, **kwargs) def create_snapshot(self, context, snapshot, **kwargs): - self.library.create_snapshot(context, snapshot, **kwargs) + return self.library.create_snapshot(context, snapshot, **kwargs) def delete_share(self, context, share, **kwargs): self.library.delete_share(context, share, **kwargs) @@ -95,6 +95,12 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): def unmanage(self, share): raise NotImplementedError + def manage_existing_snapshot(self, snapshot, driver_options): + raise NotImplementedError + + def unmanage_snapshot(self, snapshot): + raise NotImplementedError + def update_access(self, context, share, access_rules, add_rules, delete_rules, **kwargs): self.library.update_access(context, share, access_rules, add_rules, diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py index a4a6380aaf..a3690fe079 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_single_svm.py @@ -95,6 +95,12 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver): def unmanage(self, share): self.library.unmanage(share) + def manage_existing_snapshot(self, snapshot, driver_options): + return self.library.manage_existing_snapshot(snapshot, driver_options) + + def unmanage_snapshot(self, snapshot): + self.library.unmanage_snapshot(snapshot) + def update_access(self, context, share, access_rules, add_rules, delete_rules, **kwargs): self.library.update_access(context, share, access_rules, add_rules, diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py index 4a5b2169b8..590830f02a 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -873,6 +873,59 @@ class NetAppCmodeFileStorageLibrary(object): msg_args = {'volume': volume['name']} raise exception.ManageInvalidShare(reason=msg % msg_args) + @na_utils.trace + def manage_existing_snapshot(self, snapshot, driver_options): + """Brings an existing snapshot under Manila management.""" + vserver, vserver_client = self._get_vserver(share_server=None) + share_name = self._get_backend_share_name(snapshot['share_id']) + existing_snapshot_name = snapshot.get('provider_location') + new_snapshot_name = self._get_backend_snapshot_name(snapshot['id']) + + if not existing_snapshot_name: + msg = _('provider_location not specified.') + raise exception.ManageInvalidShareSnapshot(reason=msg) + + # Get the volume containing the snapshot so we can report its size + try: + volume = vserver_client.get_volume(share_name) + except (netapp_api.NaApiError, + exception.StorageResourceNotFound, + exception.NetAppException): + msg = _('Could not determine snapshot %(snap)s size from ' + 'volume %(vol)s.') + msg_args = {'snap': existing_snapshot_name, 'vol': share_name} + LOG.exception(msg % msg_args) + raise exception.ShareNotFound(share_id=snapshot['share_id']) + + # Ensure there aren't any mirrors on this volume + if vserver_client.volume_has_snapmirror_relationships(volume): + msg = _('Share %s has SnapMirror relationships.') + msg_args = {'vol': share_name} + raise exception.ManageInvalidShareSnapshot(reason=msg % msg_args) + + # Rename snapshot + try: + vserver_client.rename_snapshot(share_name, + existing_snapshot_name, + new_snapshot_name) + except netapp_api.NaApiError: + msg = _('Could not rename snapshot %(snap)s in share %(vol)s.') + msg_args = {'snap': existing_snapshot_name, 'vol': share_name} + raise exception.ManageInvalidShareSnapshot(reason=msg % msg_args) + + # Save original snapshot info to private storage + original_data = {'original_name': existing_snapshot_name} + self.private_storage.update(snapshot['id'], original_data) + + # When calculating the size, round up to the next GB. + size = int(math.ceil(float(volume['size']) / units.Gi)) + + return {'size': size, 'provider_location': new_snapshot_name} + + @na_utils.trace + def unmanage_snapshot(self, snapshot): + """Removes the specified snapshot from Manila management.""" + @na_utils.trace def create_consistency_group(self, context, cg_dict, share_server=None): """Creates a consistency group. diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index a97846ac89..24b2f3b876 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -1683,6 +1683,27 @@ LUN_GET_ITER_RESPONSE = etree.XML(""" 'volume': SHARE_NAME, }) +VOLUME_GET_ITER_NOT_UNIQUE_RESPONSE = etree.XML(""" + + + + + %(volume1)s + + + + + %(volume2)s + + + + 2 + +""" % { + 'volume1': SHARE_NAME, + 'volume2': SHARE_NAME_2, +}) + VOLUME_GET_ITER_JUNCTIONED_VOLUMES_RESPONSE = etree.XML(""" diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py index fc6c504b91..0769ebb757 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py @@ -2734,6 +2734,77 @@ class NetAppClientCmodeTestCase(test.TestCase): self.assertFalse(result) + def test_get_volume(self): + + api_response = netapp_api.NaElement( + fake.VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_volume(fake.SHARE_NAME) + + volume_get_iter_args = { + 'query': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': fake.SHARE_NAME, + }, + }, + }, + 'desired-attributes': { + 'volume-attributes': { + 'volume-id-attributes': { + 'containing-aggregate-name': None, + 'junction-path': None, + 'name': None, + 'owning-vserver-name': None, + 'type': None, + 'style': None, + }, + 'volume-space-attributes': { + 'size': None, + } + }, + }, + } + + expected = { + 'aggregate': fake.SHARE_AGGREGATE_NAME, + 'junction-path': '/%s' % fake.SHARE_NAME, + 'name': fake.SHARE_NAME, + 'type': 'rw', + 'style': 'flex', + 'size': fake.SHARE_SIZE, + 'owning-vserver-name': fake.VSERVER_NAME, + } + self.client.send_request.assert_has_calls([ + mock.call('volume-get-iter', volume_get_iter_args)]) + self.assertDictEqual(expected, result) + + def test_get_volume_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.StorageResourceNotFound, + self.client.get_volume, + fake.SHARE_NAME) + + def test_get_volume_not_unique(self): + + api_response = netapp_api.NaElement( + fake.VOLUME_GET_ITER_NOT_UNIQUE_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.client.get_volume, + fake.SHARE_NAME) + def test_get_volume_at_junction_path(self): api_response = netapp_api.NaElement( diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py index 7a18724daa..920462e14f 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py @@ -1512,14 +1512,106 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.FLEXVOL_TO_MANAGE, vserver_client) + def test_manage_existing_snapshot(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + vserver_client.get_volume.return_value = fake.FLEXVOL_TO_MANAGE + vserver_client.volume_has_snapmirror_relationships.return_value = False + result = self.library.manage_existing_snapshot( + fake.SNAPSHOT_TO_MANAGE, {}) + + share_name = self.library._get_backend_share_name( + fake.SNAPSHOT['share_id']) + new_snapshot_name = self.library._get_backend_snapshot_name( + fake.SNAPSHOT['id']) + mock_get_vserver.assert_called_once_with(share_server=None) + (vserver_client.volume_has_snapmirror_relationships. + assert_called_once_with(fake.FLEXVOL_TO_MANAGE)) + vserver_client.rename_snapshot.assert_called_once_with( + share_name, fake.SNAPSHOT_NAME, new_snapshot_name) + self.library.private_storage.update.assert_called_once_with( + fake.SNAPSHOT['id'], {'original_name': fake.SNAPSHOT_NAME}) + self.assertEqual({'size': 2, 'provider_location': new_snapshot_name}, + result) + + def test_manage_existing_snapshot_no_snapshot_name(self): + + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + vserver_client.get_volume.return_value = fake.FLEXVOL_TO_MANAGE + vserver_client.volume_has_snapmirror_relationships.return_value = False + fake_snapshot = copy.deepcopy(fake.SNAPSHOT_TO_MANAGE) + fake_snapshot['provider_location'] = '' + + self.assertRaises(exception.ManageInvalidShareSnapshot, + self.library.manage_existing_snapshot, + fake_snapshot, {}) + + @ddt.data(netapp_api.NaApiError, + exception.NetAppException) + def test_manage_existing_snapshot_get_volume_error(self, exception_type): + + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + vserver_client.get_volume.side_effect = exception_type + self.mock_object(self.client, + 'volume_has_snapmirror_relationships', + mock.Mock(return_value=False)) + + self.assertRaises(exception.ShareNotFound, + self.library.manage_existing_snapshot, + fake.SNAPSHOT_TO_MANAGE, {}) + + def test_manage_existing_snapshot_mirrors_present(self): + + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + vserver_client.get_volume.return_value = fake.FLEXVOL_TO_MANAGE + vserver_client.volume_has_snapmirror_relationships.return_value = True + + self.assertRaises(exception.ManageInvalidShareSnapshot, + self.library.manage_existing_snapshot, + fake.SNAPSHOT_TO_MANAGE, {}) + + def test_manage_existing_snapshot_rename_snapshot_error(self): + + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + vserver_client.get_volume.return_value = fake.FLEXVOL_TO_MANAGE + vserver_client.volume_has_snapmirror_relationships.return_value = False + vserver_client.rename_snapshot.side_effect = netapp_api.NaApiError + + self.assertRaises(exception.ManageInvalidShareSnapshot, + self.library.manage_existing_snapshot, + fake.SNAPSHOT_TO_MANAGE, {}) + + def test_unmanage_snapshot(self): + + result = self.library.unmanage_snapshot(fake.SNAPSHOT) + + self.assertIsNone(result) + def test_validate_volume_for_manage_snapmirror_relationships_present(self): vserver_client = mock.Mock() - vserver_client.volume_has_luns = mock.Mock(return_value=False) - vserver_client.volume_has_junctioned_volumes = mock.Mock( - return_value=False) - vserver_client.volume_has_snapmirror_relationships = mock.Mock( - return_value=True) + vserver_client.volume_has_luns.return_value = False + vserver_client.volume_has_junctioned_volumes.return_value = False + vserver_client.volume_has_snapmirror_relationships.return_value = True self.assertRaises(exception.ManageInvalidShare, self.library._validate_volume_for_manage, diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index b12eafbd7d..d44dedea01 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -267,6 +267,14 @@ SNAPSHOT = { 'provider_location': None, } +SNAPSHOT_TO_MANAGE = { + 'id': SNAPSHOT_ID, + 'project_id': TENANT_ID, + 'share_id': PARENT_SHARE_ID, + 'status': constants.STATUS_CREATING, + 'provider_location': SNAPSHOT_NAME, +} + CDOT_SNAPSHOT = { 'name': SNAPSHOT_NAME, 'volume': SHARE_NAME, diff --git a/releasenotes/notes/manage-unmanage-snapshot-in-netapp-cdot-driver-5cb4b1619c39625a.yaml b/releasenotes/notes/manage-unmanage-snapshot-in-netapp-cdot-driver-5cb4b1619c39625a.yaml new file mode 100644 index 0000000000..56caca2871 --- /dev/null +++ b/releasenotes/notes/manage-unmanage-snapshot-in-netapp-cdot-driver-5cb4b1619c39625a.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add support for snapshot manage/unmanage to the + NetApp cDOT driver. +