diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 9d1e5c1504..3b55cb56c1 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -1498,6 +1498,32 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): api_args = {'volume': volume_name, 'snapshot': snapshot_name} self.send_request('snapshot-delete', api_args) + @na_utils.trace + def create_cg_snapshot(self, volume_names, snapshot_name): + """Creates a consistency group snapshot of one or more flexvols.""" + cg_id = self._start_cg_snapshot(volume_names, snapshot_name) + if not cg_id: + msg = _('Could not start consistency group snapshot %s.') + raise exception.NetAppException(msg % snapshot_name) + self._commit_cg_snapshot(cg_id) + + @na_utils.trace + def _start_cg_snapshot(self, volume_names, snapshot_name): + api_args = { + 'snapshot': snapshot_name, + 'timeout': 'relaxed', + 'volumes': [ + {'volume-name': volume_name} for volume_name in volume_names + ], + } + result = self.send_request('cg-start', api_args) + return result.get_child_content('cg-id') + + @na_utils.trace + def _commit_cg_snapshot(self, cg_id): + api_args = {'cg-id': cg_id} + self.send_request('cg-commit', api_args) + @na_utils.trace def create_cifs_share(self, share_name): share_path = '/%s' % share_name 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 bce85b2c6e..0ce4dd0fd6 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 @@ -67,6 +67,25 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): def shrink_share(self, share, new_size, **kwargs): self.library.shrink_share(share, new_size, **kwargs) + def create_consistency_group(self, context, cg_dict, **kwargs): + return self.library.create_consistency_group(context, cg_dict, + **kwargs) + + def create_consistency_group_from_cgsnapshot(self, context, cg_dict, + cgsnapshot_dict, **kwargs): + return self.library.create_consistency_group_from_cgsnapshot( + context, cg_dict, cgsnapshot_dict, **kwargs) + + def delete_consistency_group(self, context, cg_dict, **kwargs): + return self.library.delete_consistency_group(context, cg_dict, + **kwargs) + + def create_cgsnapshot(self, context, snap_dict, **kwargs): + return self.library.create_cgsnapshot(context, snap_dict, **kwargs) + + def delete_cgsnapshot(self, context, snap_dict, **kwargs): + return self.library.delete_cgsnapshot(context, snap_dict, **kwargs) + def ensure_share(self, context, share, **kwargs): pass 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 c9531e0c97..adc507a01f 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 @@ -67,6 +67,25 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver): def shrink_share(self, share, new_size, **kwargs): self.library.shrink_share(share, new_size, **kwargs) + def create_consistency_group(self, context, cg_dict, **kwargs): + return self.library.create_consistency_group(context, cg_dict, + **kwargs) + + def create_consistency_group_from_cgsnapshot(self, context, cg_dict, + cgsnapshot_dict, **kwargs): + return self.library.create_consistency_group_from_cgsnapshot( + context, cg_dict, cgsnapshot_dict, **kwargs) + + def delete_consistency_group(self, context, cg_dict, **kwargs): + return self.library.delete_consistency_group(context, cg_dict, + **kwargs) + + def create_cgsnapshot(self, context, snap_dict, **kwargs): + return self.library.create_cgsnapshot(context, snap_dict, **kwargs) + + def delete_cgsnapshot(self, context, snap_dict, **kwargs): + return self.library.delete_cgsnapshot(context, snap_dict, **kwargs) + def ensure_share(self, context, share, **kwargs): pass 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 cdb70f4aeb..3628a64c91 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -171,17 +171,19 @@ class NetAppCmodeFileStorageLibrary(object): housekeeping_periodic_task.start( interval=self.HOUSEKEEPING_INTERVAL_SECONDS, initial_delay=0) - @na_utils.trace def _get_valid_share_name(self, share_id): """Get share name according to share name template.""" return self.configuration.netapp_volume_name_template % { 'share_id': share_id.replace('-', '_')} - @na_utils.trace def _get_valid_snapshot_name(self, snapshot_id): """Get snapshot name according to snapshot name template.""" return 'share_snapshot_' + snapshot_id.replace('-', '_') + def _get_valid_cg_snapshot_name(self, snapshot_id): + """Get snapshot name according to snapshot name template.""" + return 'share_cg_snapshot_' + snapshot_id.replace('-', '_') + @na_utils.trace def _get_aggregate_space(self): aggregates = self._find_matching_aggregates() @@ -203,6 +205,7 @@ class NetAppCmodeFileStorageLibrary(object): 'storage_protocol': 'NFS_CIFS', 'total_capacity_gb': 0.0, 'free_capacity_gb': 0.0, + 'consistency_group_support': 'host', 'pools': self._get_pools(), } return data @@ -503,12 +506,13 @@ class NetAppCmodeFileStorageLibrary(object): raise exception.NetAppException(msg % msg_args) @na_utils.trace - def _allocate_container_from_snapshot(self, share, snapshot, - vserver_client): + def _allocate_container_from_snapshot( + self, share, snapshot, vserver_client, + snapshot_name_func=_get_valid_snapshot_name): """Clones existing share.""" share_name = self._get_valid_share_name(share['id']) parent_share_name = self._get_valid_share_name(snapshot['share_id']) - parent_snapshot_name = self._get_valid_snapshot_name(snapshot['id']) + parent_snapshot_name = snapshot_name_func(self, snapshot['id']) LOG.debug('Creating share from snapshot %s', snapshot['id']) vserver_client.create_volume_clone(share_name, parent_share_name, @@ -747,6 +751,148 @@ class NetAppCmodeFileStorageLibrary(object): msg_args = {'volume': volume['name']} raise exception.ManageInvalidShare(reason=msg % msg_args) + @na_utils.trace + def create_consistency_group(self, context, cg_dict, share_server=None): + """Creates a consistency group. + + cDOT has no persistent CG object, so apart from validating the + share_server info is passed correctly, this method has nothing to do. + """ + vserver, vserver_client = self._get_vserver(share_server=share_server) + + @na_utils.trace + def create_consistency_group_from_cgsnapshot( + self, context, cg_dict, cgsnapshot_dict, share_server=None): + """Creates a consistency group from an existing CG snapshot.""" + vserver, vserver_client = self._get_vserver(share_server=share_server) + + # Ensure there is something to do + if not cgsnapshot_dict['cgsnapshot_members']: + return None, None + + clone_list = self._collate_cg_snapshot_info(cg_dict, cgsnapshot_dict) + share_update_list = [] + + LOG.debug('Creating consistency group from CG snapshot %s.', + cgsnapshot_dict['id']) + + for clone in clone_list: + + self._allocate_container_from_snapshot( + clone['share'], clone['snapshot'], vserver_client, + NetAppCmodeFileStorageLibrary._get_valid_cg_snapshot_name) + + export_locations = self._create_export(clone['share'], + vserver, + vserver_client) + share_update_list.append({ + 'id': clone['share']['id'], + 'export_locations': export_locations, + }) + + return None, share_update_list + + def _collate_cg_snapshot_info(self, cg_dict, cgsnapshot_dict): + """Collate the data for a clone of a CG snapshot. + + Given two data structures, a CG snapshot (cgsnapshot_dict) and a new + CG to be cloned from the snapshot (cg_dict), match up both structures + into a list of dicts (share & snapshot) suitable for use by existing + driver methods that clone individual share snapshots. + """ + + clone_list = list() + + for share in cg_dict['shares']: + + clone_info = {'share': share} + + for cgsnapshot_member in cgsnapshot_dict['cgsnapshot_members']: + if (share['source_cgsnapshot_member_id'] == + cgsnapshot_member['id']): + clone_info['snapshot'] = { + 'share_id': cgsnapshot_member['share_id'], + 'id': cgsnapshot_member['cgsnapshot_id'] + } + break + + else: + msg = _("Invalid data supplied for creating consistency group " + "from CG snapshot %s.") % cgsnapshot_dict['id'] + raise exception.InvalidConsistencyGroup(reason=msg) + + clone_list.append(clone_info) + + return clone_list + + @na_utils.trace + def delete_consistency_group(self, context, cg_dict, share_server=None): + """Deletes a consistency group. + + cDOT has no persistent CG object, so apart from validating the + share_server info is passed correctly, this method has nothing to do. + """ + try: + vserver, vserver_client = self._get_vserver( + share_server=share_server) + except (exception.InvalidInput, + exception.VserverNotSpecified, + exception.VserverNotFound) as error: + LOG.warning(_LW("Could not determine share server for consistency " + "group being deleted: %(cg)s. Deletion of CG " + "record will proceed anyway. Error: %(error)s"), + {'cg': cg_dict['id'], 'error': error}) + + @na_utils.trace + def create_cgsnapshot(self, context, snap_dict, share_server=None): + """Creates a consistency group snapshot.""" + vserver, vserver_client = self._get_vserver(share_server=share_server) + + share_names = [self._get_valid_share_name(member['share_id']) + for member in snap_dict.get('cgsnapshot_members', [])] + snapshot_name = self._get_valid_cg_snapshot_name(snap_dict['id']) + + if share_names: + LOG.debug('Creating CG snapshot %s.', snapshot_name) + vserver_client.create_cg_snapshot(share_names, snapshot_name) + + return None, None + + @na_utils.trace + def delete_cgsnapshot(self, context, snap_dict, share_server=None): + """Deletes a consistency group snapshot.""" + try: + vserver, vserver_client = self._get_vserver( + share_server=share_server) + except (exception.InvalidInput, + exception.VserverNotSpecified, + exception.VserverNotFound) as error: + LOG.warning(_LW("Could not determine share server for CG snapshot " + "being deleted: %(snap)s. Deletion of CG snapshot " + "record will proceed anyway. Error: %(error)s"), + {'snap': snap_dict['id'], 'error': error}) + return None, None + + share_names = [self._get_valid_share_name(member['share_id']) + for member in snap_dict.get('cgsnapshot_members', [])] + snapshot_name = self._get_valid_cg_snapshot_name(snap_dict['id']) + + for share_name in share_names: + try: + self._handle_busy_snapshot(vserver_client, share_name, + snapshot_name) + except exception.SnapshotNotFound: + LOG.info(_LI("Snapshot %(snap)s does not exist for share " + "%(share)s."), + {'snap': snapshot_name, 'share': share_name}) + continue + + LOG.debug("Deleting snapshot %(snap)s for share %(share)s.", + {'snap': snapshot_name, 'share': share_name}) + vserver_client.delete_snapshot(share_name, snapshot_name) + + return None, None + @na_utils.trace def extend_share(self, share, new_size, share_server=None): """Extends size of existing share.""" diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index e336fff269..6c02d98c8d 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -36,7 +36,9 @@ SHARE_AGGREGATE_RAID_TYPES = ('raid4', 'raid_dp') SHARE_AGGREGATE_DISK_TYPE = 'FCAL' SHARE_NAME = 'fake_share' SHARE_SIZE = '1000000000' +SHARE_NAME_2 = 'fake_share_2' SNAPSHOT_NAME = 'fake_snapshot' +CG_SNAPSHOT_ID = 'fake_cg_id' PARENT_SHARE_NAME = 'fake_parent_share' PARENT_SNAPSHOT_NAME = 'fake_parent_snapshot' MAX_FILES = 5000 @@ -84,6 +86,10 @@ NO_RECORDS_RESPONSE = etree.XML(""" """) +PASSED_RESPONSE = etree.XML(""" + +""") + VSERVER_GET_ITER_RESPONSE = etree.XML(""" @@ -1170,6 +1176,18 @@ SNAPSHOT_GET_ITER_OTHER_ERROR_RESPONSE = etree.XML(""" """ % {'volume': SHARE_NAME, 'vserver': VSERVER_NAME}) +SNAPSHOT_MULTIDELETE_ERROR_RESPONSE = etree.XML(""" + + + + 13021 + %(volume)s + No such snapshot. + + + +""" % {'volume': SHARE_NAME}) + NFS_EXPORT_RULES = ('10.10.10.10', '10.10.10.20') NFS_EXPORTFS_LIST_RULES_2_NO_RULES_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 e6f7956d50..baddee80a8 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 @@ -2631,6 +2631,67 @@ class NetAppClientCmodeTestCase(test.TestCase): self.client.send_request.assert_has_calls([ mock.call('snapshot-delete', snapshot_delete_args)]) + def test_create_cg_snapshot(self): + + mock_start_cg_snapshot = self.mock_object( + self.client, '_start_cg_snapshot', + mock.Mock(return_value=fake.CG_SNAPSHOT_ID)) + mock_commit_cg_snapshot = self.mock_object( + self.client, '_commit_cg_snapshot') + + self.client.create_cg_snapshot([fake.SHARE_NAME, fake.SHARE_NAME_2], + fake.SNAPSHOT_NAME) + + mock_start_cg_snapshot.assert_called_once_with( + [fake.SHARE_NAME, fake.SHARE_NAME_2], fake.SNAPSHOT_NAME) + mock_commit_cg_snapshot.assert_called_once_with(fake.CG_SNAPSHOT_ID) + + def test_create_cg_snapshot_no_id(self): + + mock_start_cg_snapshot = self.mock_object( + self.client, '_start_cg_snapshot', mock.Mock(return_value=None)) + mock_commit_cg_snapshot = self.mock_object( + self.client, '_commit_cg_snapshot') + + self.assertRaises(exception.NetAppException, + self.client.create_cg_snapshot, + [fake.SHARE_NAME, fake.SHARE_NAME_2], + fake.SNAPSHOT_NAME) + + mock_start_cg_snapshot.assert_called_once_with( + [fake.SHARE_NAME, fake.SHARE_NAME_2], fake.SNAPSHOT_NAME) + self.assertFalse(mock_commit_cg_snapshot.called) + + def test_start_cg_snapshot(self): + + self.mock_object(self.client, 'send_request') + + self.client._start_cg_snapshot([fake.SHARE_NAME, fake.SHARE_NAME_2], + fake.SNAPSHOT_NAME) + + cg_start_args = { + 'snapshot': fake.SNAPSHOT_NAME, + 'timeout': 'relaxed', + 'volumes': [ + {'volume-name': fake.SHARE_NAME}, + {'volume-name': fake.SHARE_NAME_2}, + ], + } + + self.client.send_request.assert_has_calls([ + mock.call('cg-start', cg_start_args)]) + + def test_commit_cg_snapshot(self): + + self.mock_object(self.client, 'send_request') + + self.client._commit_cg_snapshot(fake.CG_SNAPSHOT_ID) + + cg_commit_args = {'cg-id': fake.CG_SNAPSHOT_ID} + + self.client.send_request.assert_has_calls([ + mock.call('cg-commit', cg_commit_args)]) + def test_create_cifs_share(self): self.mock_object(self.client, 'send_request') 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 7942136d64..8823c848b6 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 @@ -301,6 +301,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'storage_protocol': 'NFS_CIFS', 'total_capacity_gb': 0.0, 'free_capacity_gb': 0.0, + 'consistency_group_support': 'host', 'pools': fake.POOLS, } self.assertDictEqual(expected, result) @@ -1334,6 +1335,324 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.FLEXVOL_TO_MANAGE, vserver_client) + def test_create_consistency_group(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + + result = self.library.create_consistency_group( + self.context, fake.EMPTY_CONSISTENCY_GROUP, + share_server=fake.SHARE_SERVER) + + self.assertIsNone(result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + @ddt.data(exception.InvalidInput(reason='fake_reason'), + exception.VserverNotSpecified(), + exception.VserverNotFound(vserver='fake_vserver')) + def test_create_consistency_group_no_share_server(self, + get_vserver_exception): + + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(side_effect=get_vserver_exception)) + + self.assertRaises(type(get_vserver_exception), + self.library.create_consistency_group, + self.context, + fake.EMPTY_CONSISTENCY_GROUP, + share_server=fake.SHARE_SERVER) + + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_create_consistency_group_from_cgsnapshot(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + mock_allocate_container_from_snapshot = self.mock_object( + self.library, '_allocate_container_from_snapshot') + mock_create_export = self.mock_object( + self.library, '_create_export', + mock.Mock(side_effect=[['loc3'], ['loc4']])) + + result = self.library.create_consistency_group_from_cgsnapshot( + self.context, + fake.CONSISTENCY_GROUP_DEST, + fake.CG_SNAPSHOT, + share_server=fake.SHARE_SERVER) + + share_update_list = [ + {'id': fake.SHARE_ID3, 'export_locations': ['loc3']}, + {'id': fake.SHARE_ID4, 'export_locations': ['loc4']} + ] + expected = (None, share_update_list) + self.assertEqual(expected, result) + + mock_allocate_container_from_snapshot.assert_has_calls([ + mock.call(fake.COLLATED_CGSNAPSHOT_INFO[0]['share'], + fake.COLLATED_CGSNAPSHOT_INFO[0]['snapshot'], + vserver_client, + mock.ANY), + mock.call(fake.COLLATED_CGSNAPSHOT_INFO[1]['share'], + fake.COLLATED_CGSNAPSHOT_INFO[1]['snapshot'], + vserver_client, + mock.ANY), + ]) + mock_create_export.assert_has_calls([ + mock.call(fake.COLLATED_CGSNAPSHOT_INFO[0]['share'], + fake.VSERVER1, + vserver_client), + mock.call(fake.COLLATED_CGSNAPSHOT_INFO[1]['share'], + fake.VSERVER1, + vserver_client), + ]) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_create_consistency_group_from_cgsnapshot_no_members(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + mock_allocate_container_from_snapshot = self.mock_object( + self.library, '_allocate_container_from_snapshot') + mock_create_export = self.mock_object( + self.library, '_create_export', + mock.Mock(side_effect=[['loc3'], ['loc4']])) + + fake_cg_snapshot = copy.deepcopy(fake.CG_SNAPSHOT) + fake_cg_snapshot['cgsnapshot_members'] = [] + + result = self.library.create_consistency_group_from_cgsnapshot( + self.context, + fake.CONSISTENCY_GROUP_DEST, + fake_cg_snapshot, + share_server=fake.SHARE_SERVER) + + self.assertEqual((None, None), result) + + self.assertFalse(mock_allocate_container_from_snapshot.called) + self.assertFalse(mock_create_export.called) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_collate_cg_snapshot_info(self): + + result = self.library._collate_cg_snapshot_info( + fake.CONSISTENCY_GROUP_DEST, fake.CG_SNAPSHOT) + + self.assertEqual(fake.COLLATED_CGSNAPSHOT_INFO, result) + + def test_collate_cg_snapshot_info_invalid(self): + + fake_cg_snapshot = copy.deepcopy(fake.CG_SNAPSHOT) + fake_cg_snapshot['cgsnapshot_members'] = [] + + self.assertRaises(exception.InvalidConsistencyGroup, + self.library._collate_cg_snapshot_info, + fake.CONSISTENCY_GROUP_DEST, fake_cg_snapshot) + + def test_delete_consistency_group(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + + result = self.library.delete_consistency_group( + self.context, + fake.EMPTY_CONSISTENCY_GROUP, + share_server=fake.SHARE_SERVER) + + self.assertIsNone(result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + @ddt.data(exception.InvalidInput(reason='fake_reason'), + exception.VserverNotSpecified(), + exception.VserverNotFound(vserver='fake_vserver')) + def test_delete_consistency_group_no_share_server(self, + get_vserver_exception): + + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(side_effect=get_vserver_exception)) + + result = self.library.delete_consistency_group( + self.context, + fake.EMPTY_CONSISTENCY_GROUP, + share_server=fake.SHARE_SERVER) + + self.assertIsNone(result) + self.assertEqual(1, lib_base.LOG.warning.call_count) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_create_cgsnapshot(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + + result = self.library.create_cgsnapshot( + self.context, + fake.CG_SNAPSHOT, + share_server=fake.SHARE_SERVER) + + share_names = [ + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_1['share_id']), + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_2['share_id']) + ] + snapshot_name = self.library._get_valid_cg_snapshot_name( + fake.CG_SNAPSHOT['id']) + vserver_client.create_cg_snapshot.assert_called_once_with( + share_names, snapshot_name) + self.assertEqual((None, None), result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_create_cgsnapshot_no_members(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + + fake_cg_snapshot = copy.deepcopy(fake.CG_SNAPSHOT) + fake_cg_snapshot['cgsnapshot_members'] = [] + + result = self.library.create_cgsnapshot( + self.context, + fake_cg_snapshot, + share_server=fake.SHARE_SERVER) + + self.assertFalse(vserver_client.create_cg_snapshot.called) + self.assertEqual((None, None), result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_delete_cgsnapshot(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + mock_handle_busy_snapshot = self.mock_object(self.library, + '_handle_busy_snapshot') + + result = self.library.delete_cgsnapshot( + self.context, + fake.CG_SNAPSHOT, + share_server=fake.SHARE_SERVER) + + share_names = [ + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_1['share_id']), + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_2['share_id']) + ] + snapshot_name = self.library._get_valid_cg_snapshot_name( + fake.CG_SNAPSHOT['id']) + + mock_handle_busy_snapshot.assert_has_calls([ + mock.call(vserver_client, share_names[0], snapshot_name), + mock.call(vserver_client, share_names[1], snapshot_name) + ]) + vserver_client.delete_snapshot.assert_has_calls([ + mock.call(share_names[0], snapshot_name), + mock.call(share_names[1], snapshot_name) + ]) + self.assertEqual((None, None), result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_delete_cgsnapshot_no_members(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + mock_handle_busy_snapshot = self.mock_object(self.library, + '_handle_busy_snapshot') + + fake_cg_snapshot = copy.deepcopy(fake.CG_SNAPSHOT) + fake_cg_snapshot['cgsnapshot_members'] = [] + + result = self.library.delete_cgsnapshot( + self.context, + fake_cg_snapshot, + share_server=fake.SHARE_SERVER) + + self.assertFalse(mock_handle_busy_snapshot.called) + self.assertFalse(vserver_client.delete_snapshot.called) + self.assertEqual((None, None), result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + def test_delete_cgsnapshot_snapshots_not_found(self): + + vserver_client = mock.Mock() + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + mock_handle_busy_snapshot = self.mock_object( + self.library, + '_handle_busy_snapshot', + mock.Mock(side_effect=exception.SnapshotNotFound(name='fake'))) + + result = self.library.delete_cgsnapshot( + self.context, + fake.CG_SNAPSHOT, + share_server=fake.SHARE_SERVER) + + share_names = [ + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_1['share_id']), + self.library._get_valid_share_name( + fake.CG_SNAPSHOT_MEMBER_2['share_id']) + ] + snapshot_name = self.library._get_valid_cg_snapshot_name( + fake.CG_SNAPSHOT['id']) + + mock_handle_busy_snapshot.assert_has_calls([ + mock.call(vserver_client, share_names[0], snapshot_name), + mock.call(vserver_client, share_names[1], snapshot_name) + ]) + self.assertFalse(vserver_client.delete_snapshot.called) + self.assertEqual((None, None), result) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + + @ddt.data(exception.InvalidInput(reason='fake_reason'), + exception.VserverNotSpecified(), + exception.VserverNotFound(vserver='fake_vserver')) + def test_delete_cgsnapshot_no_share_server(self, + get_vserver_exception): + + mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(side_effect=get_vserver_exception)) + + result = self.library.delete_cgsnapshot( + self.context, + fake.EMPTY_CONSISTENCY_GROUP, + share_server=fake.SHARE_SERVER) + + self.assertEqual((None, None), result) + self.assertEqual(1, lib_base.LOG.warning.call_count) + mock_get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) + def test_extend_share(self): vserver_client = mock.Mock() diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index 19c4877a36..368c23c2ee 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -35,11 +35,20 @@ FLEXVOL_NAME = 'fake_volume' JUNCTION_PATH = '/%s' % FLEXVOL_NAME EXPORT_LOCATION = '%s:%s' % (HOST_NAME, JUNCTION_PATH) SNAPSHOT_NAME = 'fake_snapshot' +CONSISTENCY_GROUP_NAME = 'fake_consistency_group' SHARE_SIZE = 10 TENANT_ID = '24cb2448-13d8-4f41-afd9-eff5c4fd2a57' SHARE_ID = '7cf7c200-d3af-4e05-b87e-9167c95dfcad' +SHARE_ID2 = 'b51c5a31-aa5b-4254-9ee8-7d39fa4c8c38' +SHARE_ID3 = '1379991d-037b-4897-bf3a-81b4aac72eff' +SHARE_ID4 = '1cb41aad-fd9b-4964-8059-646f69de925e' PARENT_SHARE_ID = '585c3935-2aa9-437c-8bad-5abae1076555' SNAPSHOT_ID = 'de4c9050-e2f9-4ce1-ade4-5ed0c9f26451' +CONSISTENCY_GROUP_ID = '65bfa2c9-dc6c-4513-951a-b8d15b453ad8' +CONSISTENCY_GROUP_ID2 = '35f5c1ea-45fb-40c4-98ae-2a2a17554159' +CG_SNAPSHOT_ID = '6ddd8a6b-5df7-417b-a2ae-3f6e449f4eea' +CG_SNAPSHOT_MEMBER_ID1 = '629f79ef-b27e-4596-9737-30f084e5ba29' +CG_SNAPSHOT_MEMBER_ID2 = 'e876aa9c-a322-4391-bd88-9266178262be' FREE_CAPACITY = 10000000000 TOTAL_CAPACITY = 20000000000 AGGREGATES = ('manila_aggr_1', 'manila_aggr_2') @@ -224,6 +233,113 @@ CDOT_SNAPSHOT_BUSY_VOLUME_CLONE = { 'owners': {'volume clone'}, } +SHARE_FOR_CG1 = { + 'id': SHARE_ID, + 'host': '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME}, + 'name': 'share1', + 'share_proto': 'NFS', + 'source_cgsnapshot_member_id': None, +} + +SHARE_FOR_CG2 = { + 'id': SHARE_ID2, + 'host': '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME}, + 'name': 'share2', + 'share_proto': 'NFS', + 'source_cgsnapshot_member_id': None, +} + +# Clone dest of SHARE_FOR_CG1 +SHARE_FOR_CG3 = { + 'id': SHARE_ID3, + 'host': '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME}, + 'name': 'share3', + 'share_proto': 'NFS', + 'source_cgsnapshot_member_id': CG_SNAPSHOT_MEMBER_ID1, +} + +# Clone dest of SHARE_FOR_CG2 +SHARE_FOR_CG4 = { + 'id': SHARE_ID4, + 'host': '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME}, + 'name': 'share4', + 'share_proto': 'NFS', + 'source_cgsnapshot_member_id': CG_SNAPSHOT_MEMBER_ID2, +} + +EMPTY_CONSISTENCY_GROUP = { + 'cgsnapshots': [], + 'description': 'fake description', + 'host': '%(host)s@%(backend)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME}, + 'id': CONSISTENCY_GROUP_ID, + 'name': CONSISTENCY_GROUP_NAME, + 'shares': [], +} + +CONSISTENCY_GROUP = { + 'cgsnapshots': [], + 'description': 'fake description', + 'host': '%(host)s@%(backend)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME}, + 'id': CONSISTENCY_GROUP_ID, + 'name': CONSISTENCY_GROUP_NAME, + 'shares': [SHARE_FOR_CG1, SHARE_FOR_CG2], +} + +CONSISTENCY_GROUP_DEST = { + 'cgsnapshots': [], + 'description': 'fake description', + 'host': '%(host)s@%(backend)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME}, + 'id': CONSISTENCY_GROUP_ID, + 'name': CONSISTENCY_GROUP_NAME, + 'shares': [SHARE_FOR_CG3, SHARE_FOR_CG4], +} + +CG_SNAPSHOT_MEMBER_1 = { + 'cgsnapshot_id': CG_SNAPSHOT_ID, + 'id': CG_SNAPSHOT_MEMBER_ID1, + 'share_id': SHARE_ID, + 'share_proto': 'NFS', +} + +CG_SNAPSHOT_MEMBER_2 = { + 'cgsnapshot_id': CG_SNAPSHOT_ID, + 'id': CG_SNAPSHOT_MEMBER_ID2, + 'share_id': SHARE_ID2, + 'share_proto': 'NFS', +} + +CG_SNAPSHOT = { + 'cgsnapshot_members': [CG_SNAPSHOT_MEMBER_1, CG_SNAPSHOT_MEMBER_2], + 'consistency_group': CONSISTENCY_GROUP, + 'consistency_group_id': CONSISTENCY_GROUP_ID, + 'id': CG_SNAPSHOT_ID, + 'project_id': TENANT_ID, +} + +COLLATED_CGSNAPSHOT_INFO = [ + { + 'share': SHARE_FOR_CG3, + 'snapshot': { + 'share_id': SHARE_ID, + 'id': CG_SNAPSHOT_ID + } + }, + { + 'share': SHARE_FOR_CG4, + 'snapshot': { + 'share_id': SHARE_ID2, + 'id': CG_SNAPSHOT_ID + } + }, +] + LIF_NAMES = [] LIF_ADDRESSES = ['10.10.10.10', '10.10.10.20'] LIFS = (