From 20e89b9691a5416e585245dd9c36702930a9e14b Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Sun, 21 Aug 2016 15:23:35 -0400 Subject: [PATCH] NetApp cDOT: Add Intra-Vserver migration support Add migration support in the cDOT driver. Change-Id: I72e045b1c978b752f38cc3018cb2a7084e3f3e36 Implements: blueprint netapp-cdot-driver-optimized-migration --- manila/opts.py | 2 +- .../drivers/netapp/dataontap/client/api.py | 2 + .../netapp/dataontap/client/client_cmode.py | 120 +++++ .../dataontap/cluster_mode/data_motion.py | 7 +- .../dataontap/cluster_mode/drv_multi_svm.py | 50 ++ .../dataontap/cluster_mode/drv_single_svm.py | 49 ++ .../netapp/dataontap/cluster_mode/lib_base.py | 282 ++++++++++- .../netapp/dataontap/protocols/cifs_cmode.py | 6 +- .../netapp/dataontap/protocols/nfs_cmode.py | 6 +- manila/share/drivers/netapp/options.py | 12 +- .../drivers/netapp/dataontap/client/fakes.py | 21 + .../dataontap/client/test_client_cmode.py | 121 +++++ .../cluster_mode/test_data_motion.py | 4 +- .../dataontap/cluster_mode/test_lib_base.py | 463 +++++++++++++++++- manila/tests/utils.py | 51 ++ ...-within-share-server-92cfa1bcf0c317fc.yaml | 5 + 16 files changed, 1184 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/netapp-cdot-optimized-migration-within-share-server-92cfa1bcf0c317fc.yaml diff --git a/manila/opts.py b/manila/opts.py index 4d45c7baa3..02f707b4ab 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -141,7 +141,7 @@ _global_opt_lists = [ manila.share.drivers.netapp.options.netapp_transport_opts, manila.share.drivers.netapp.options.netapp_basicauth_opts, manila.share.drivers.netapp.options.netapp_provisioning_opts, - manila.share.drivers.netapp.options.netapp_replication_opts, + manila.share.drivers.netapp.options.netapp_data_motion_opts, manila.share.drivers.nexenta.options.nexenta_connection_opts, manila.share.drivers.nexenta.options.nexenta_dataset_opts, manila.share.drivers.nexenta.options.nexenta_nfs_opts, diff --git a/manila/share/drivers/netapp/dataontap/client/api.py b/manila/share/drivers/netapp/dataontap/client/api.py index d2dd6526c7..10e0a67928 100644 --- a/manila/share/drivers/netapp/dataontap/client/api.py +++ b/manila/share/drivers/netapp/dataontap/client/api.py @@ -38,6 +38,8 @@ EVOLUMEOFFLINE = '13042' EINTERNALERROR = '13114' EDUPLICATEENTRY = '13130' EVOLNOTCLONE = '13170' +EVOLMOVE_CANNOT_MOVE_TO_CFO = '13633' +EAGGRDOESNOTEXIST = '14420' EVOL_NOT_MOUNTED = '14716' ESIS_CLONE_NOT_LICENSED = '14956' EOBJECTNOTFOUND = '15661' diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 33aac303ab..c936d87eda 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -36,6 +36,12 @@ LOG = log.getLogger(__name__) DELETED_PREFIX = 'deleted_manila_' DEFAULT_IPSPACE = 'Default' DEFAULT_MAX_PAGE_LENGTH = 50 +CUTOVER_ACTION_MAP = { + 'defer': 'defer_on_failure', + 'abort': 'abort_on_failure', + 'force': 'force', + 'wait': 'wait', +} class NetAppCmodeClient(client_base.NetAppBaseClient): @@ -3257,3 +3263,117 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): has_snapmirrors = False return has_snapmirrors + + @na_utils.trace + def start_volume_move(self, volume_name, vserver, destination_aggregate, + cutover_action='wait'): + """Moves a FlexVol across Vserver aggregates. + + Requires cluster-scoped credentials. + """ + self._send_volume_move_request( + volume_name, vserver, destination_aggregate, + cutover_action=cutover_action) + + @na_utils.trace + def check_volume_move(self, volume_name, vserver, destination_aggregate): + """Moves a FlexVol across Vserver aggregates. + + Requires cluster-scoped credentials. + """ + self._send_volume_move_request( + volume_name, vserver, destination_aggregate, validation_only=True) + + @na_utils.trace + def _send_volume_move_request(self, volume_name, vserver, + destination_aggregate, + cutover_action='wait', + validation_only=False): + """Send request to check if vol move is possible, or start it. + + :param volume_name: Name of the FlexVol to be moved. + :param destination_aggregate: Name of the destination aggregate + :param cutover_action: can have one of ['force', 'defer', 'abort', + 'wait']. 'force' will force a cutover despite errors (causing + possible client disruptions), 'wait' will wait for cutover to be + triggered manually. 'abort' will rollback move on errors on + cutover, 'defer' will attempt a cutover, but wait for manual + intervention in case of errors. + :param validation_only: If set to True, only validates if the volume + move is possible, does not trigger data copy. + """ + api_args = { + 'source-volume': volume_name, + 'vserver': vserver, + 'dest-aggr': destination_aggregate, + 'cutover-action': CUTOVER_ACTION_MAP[cutover_action], + } + if validation_only: + api_args['perform-validation-only'] = 'true' + self.send_request('volume-move-start', api_args) + + @na_utils.trace + def abort_volume_move(self, volume_name, vserver): + """Aborts an existing volume move operation.""" + api_args = { + 'source-volume': volume_name, + 'vserver': vserver, + } + self.send_request('volume-move-trigger-abort', api_args) + + @na_utils.trace + def trigger_volume_move_cutover(self, volume_name, vserver, force=True): + """Triggers the cut-over for a volume in data motion.""" + api_args = { + 'source-volume': volume_name, + 'vserver': vserver, + 'force': 'true' if force else 'false', + } + self.send_request('volume-move-trigger-cutover', api_args) + + @na_utils.trace + def get_volume_move_status(self, volume_name, vserver): + """Gets the current state of a volume move operation.""" + api_args = { + 'query': { + 'volume-move-info': { + 'volume': volume_name, + 'vserver': vserver, + }, + }, + 'desired-attributes': { + 'volume-move-info': { + 'percent-complete': None, + 'estimated-completion-time': None, + 'state': None, + 'details': None, + 'cutover-action': None, + 'phase': None, + }, + }, + } + result = self.send_iter_request('volume-move-get-iter', api_args) + + if not self._has_records(result): + msg = _("Volume %(vol)s in Vserver %(server)s is not part of any " + "data motion operations.") + msg_args = {'vol': volume_name, 'server': vserver} + raise exception.NetAppException(msg % msg_args) + + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + volume_move_info = attributes_list.get_child_by_name( + 'volume-move-info') or netapp_api.NaElement('none') + + status_info = { + 'percent-complete': volume_move_info.get_child_content( + 'percent-complete'), + 'estimated-completion-time': volume_move_info.get_child_content( + 'estimated-completion-time'), + 'state': volume_move_info.get_child_content('state'), + 'details': volume_move_info.get_child_content('details'), + 'cutover-action': volume_move_info.get_child_content( + 'cutover-action'), + 'phase': volume_move_info.get_child_content('phase'), + } + return status_info diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py b/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py index 12991245a5..3cbda9dd88 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py @@ -44,8 +44,9 @@ def get_backend_configuration(backend_name): config_stanzas = CONF.list_all_sections() if backend_name not in config_stanzas: msg = _("Could not find backend stanza %(backend_name)s in " - "configuration which is required for replication with " - "the backend. Available stanzas are %(stanzas)s") + "configuration which is required for replication or migration " + "workflows with the source backend. Available stanzas are " + "%(stanzas)s") params = { "stanzas": config_stanzas, "backend_name": backend_name, @@ -60,7 +61,7 @@ def get_backend_configuration(backend_name): config.append_config_values(na_opts.netapp_transport_opts) config.append_config_values(na_opts.netapp_support_opts) config.append_config_values(na_opts.netapp_provisioning_opts) - config.append_config_values(na_opts.netapp_replication_opts) + config.append_config_values(na_opts.netapp_data_motion_opts) return config 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 eac385f52f..30d173ac00 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 @@ -164,3 +164,53 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): share_replica, replica_snapshots, replica_snapshot, share_server=None): raise NotImplementedError() + + def migration_check_compatibility(self, context, source_share, + destination_share, share_server=None, + destination_share_server=None): + return self.library.migration_check_compatibility( + context, source_share, destination_share, + share_server=share_server, + destination_share_server=destination_share_server) + + def migration_start(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_start( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=share_server, + destination_share_server=destination_share_server) + + def migration_continue(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_continue( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_get_progress(self, context, source_share, + destination_share, source_snapshots, + snapshot_mappings, share_server=None, + destination_share_server=None): + return self.library.migration_get_progress( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_cancel(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_cancel( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_complete(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_complete( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) 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 dc19742ed0..814654896f 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 @@ -179,3 +179,52 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver): return self.library.update_replicated_snapshot( replica_list, share_replica, replica_snapshots, replica_snapshot, share_server=share_server) + + def migration_check_compatibility(self, context, source_share, + destination_share, share_server=None, + destination_share_server=None): + return self.library.migration_check_compatibility( + context, source_share, destination_share, + share_server=share_server, + destination_share_server=destination_share_server) + + def migration_start(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_start( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_continue(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_continue( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_get_progress(self, context, source_share, + destination_share, source_snapshots, + snapshot_mappings, share_server=None, + destination_share_server=None): + return self.library.migration_get_progress( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_cancel(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_cancel( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) + + def migration_complete(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + return self.library.migration_complete( + context, source_share, destination_share, + source_snapshots, snapshot_mappings, share_server=share_server, + destination_share_server=destination_share_server) 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 85fbb48f80..144d6b4c58 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -44,6 +44,7 @@ from manila.share.drivers.netapp import options as na_opts from manila.share.drivers.netapp import utils as na_utils from manila.share import share_types from manila.share import utils as share_utils +from manila import utils as manila_utils LOG = log.getLogger(__name__) CONF = cfg.CONF @@ -96,7 +97,7 @@ class NetAppCmodeFileStorageLibrary(object): self.configuration.append_config_values( na_opts.netapp_provisioning_opts) self.configuration.append_config_values( - na_opts.netapp_replication_opts) + na_opts.netapp_data_motion_opts) self._licenses = [] self._client = None @@ -675,7 +676,8 @@ class NetAppCmodeFileStorageLibrary(object): vserver_client.delete_volume(share_name) @na_utils.trace - def _create_export(self, share, share_server, vserver, vserver_client): + def _create_export(self, share, share_server, vserver, vserver_client, + clear_current_export_policy=True): """Creates NAS storage.""" helper = self._get_helper(share) helper.set_client(vserver_client) @@ -695,7 +697,9 @@ class NetAppCmodeFileStorageLibrary(object): share, share_server, interfaces) # Create the share and get a callback for generating export locations - callback = helper.create_share(share, share_name) + callback = helper.create_share( + share, share_name, + clear_current_export_policy=clear_current_export_policy) # Generate export locations using addresses, metadata and callback export_locations = [ @@ -1618,3 +1622,275 @@ class NetAppCmodeFileStorageLibrary(object): except netapp_api.NaApiError as e: if e.code != netapp_api.EOBJECTNOTFOUND: raise + + def _check_destination_vserver_for_vol_move(self, source_share, + source_vserver, + dest_share_server): + try: + destination_vserver, __ = self._get_vserver( + share_server=dest_share_server) + except exception.InvalidParameterValue: + destination_vserver = None + + if source_vserver != destination_vserver: + msg = _("Cannot migrate %(shr)s efficiently from source " + "VServer %(src)s to destination VServer %(dest)s.") + msg_args = { + 'shr': source_share['id'], + 'src': source_vserver, + 'dest': destination_vserver, + } + raise exception.NetAppException(msg % msg_args) + + def migration_check_compatibility(self, context, source_share, + destination_share, share_server=None, + destination_share_server=None): + """Checks compatibility between self.host and destination host.""" + # We need cluster creds to perform an intra-cluster data motion + compatible = False + destination_host = destination_share['host'] + + if self._have_cluster_creds: + try: + backend = share_utils.extract_host( + destination_host, level='backend_name') + data_motion.get_backend_configuration(backend) + + source_vserver, __ = self._get_vserver( + share_server=share_server) + share_volume = self._get_backend_share_name( + source_share['id']) + destination_aggregate = share_utils.extract_host( + destination_host, level='pool') + + self._check_destination_vserver_for_vol_move( + source_share, source_vserver, destination_share_server) + + self._client.check_volume_move( + share_volume, source_vserver, destination_aggregate) + + except Exception: + msg = _LE("Cannot migrate share %(shr)s efficiently between " + "%(src)s and %(dest)s.") + msg_args = { + 'shr': source_share['id'], + 'src': source_share['host'], + 'dest': destination_host, + } + LOG.exception(msg, msg_args) + else: + compatible = True + else: + msg = _LW("Cluster credentials have not been configured " + "with this share driver. Cannot perform volume move " + "operations.") + LOG.warning(msg) + + compatibility = { + 'compatible': compatible, + 'writable': compatible, + 'nondisruptive': compatible, + 'preserve_metadata': compatible, + 'preserve_snapshots': compatible, + } + + return compatibility + + def migration_start(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + """Begins data motion from source_share to destination_share.""" + # Intra-cluster migration + vserver, vserver_client = self._get_vserver(share_server=share_server) + share_volume = self._get_backend_share_name(source_share['id']) + destination_aggregate = share_utils.extract_host( + destination_share['host'], level='pool') + + self._client.start_volume_move( + share_volume, vserver, destination_aggregate) + + msg = _LI("Began volume move operation of share %(shr)s from %(src)s " + "to %(dest)s.") + msg_args = { + 'shr': source_share['id'], + 'src': source_share['host'], + 'dest': destination_share['host'], + } + LOG.info(msg, msg_args) + + def _get_volume_move_status(self, source_share, share_server): + vserver, vserver_client = self._get_vserver(share_server=share_server) + share_volume = self._get_backend_share_name(source_share['id']) + status = self._client.get_volume_move_status(share_volume, vserver) + return status + + def migration_continue(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + """Check progress of migration, try to repair data motion errors.""" + status = self._get_volume_move_status(source_share, share_server) + completed_phases = ( + 'cutover_hard_deferred', 'cutover_soft_deferred', 'completed') + + move_phase = status['phase'].lower() + if move_phase == 'failed': + msg_args = { + 'shr': source_share['id'], + 'reason': status['details'], + } + msg = _("Volume move operation for share %(shr)s failed. Reason: " + "%(reason)s") % msg_args + LOG.exception(msg) + raise exception.NetAppException(msg) + elif move_phase in completed_phases: + return True + + return False + + def migration_get_progress(self, context, source_share, + destination_share, source_snapshots, + snapshot_mappings, share_server=None, + destination_share_server=None): + """Return detailed progress of the migration in progress.""" + status = self._get_volume_move_status(source_share, share_server) + + # NOTE (gouthamr): If the volume move is waiting for a manual + # intervention to cut-over, the copy is done with respect to the + # user. Volume move copies the rest of the data before cut-over anyway. + if status['phase'] in ('cutover_hard_deferred', + 'cutover_soft_deferred'): + status['percent-complete'] = 100 + + msg = _LI("Volume move status for share %(share)s: (State) %(state)s. " + "(Phase) %(phase)s. Details: %(details)s") + msg_args = { + 'state': status['state'], + 'details': status['details'], + 'share': source_share['id'], + 'phase': status['phase'], + } + LOG.info(msg, msg_args) + + return { + 'total_progress': status['percent-complete'] or 0, + 'state': status['state'], + 'estimated_completion_time': status['estimated-completion-time'], + 'phase': status['phase'], + 'details': status['details'], + } + + def migration_cancel(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + """Abort an ongoing migration.""" + vserver, vserver_client = self._get_vserver(share_server=share_server) + share_volume = self._get_backend_share_name(source_share['id']) + + try: + self._get_volume_move_status(source_share, share_server) + except exception.NetAppException: + LOG.exception(_LE("Could not get volume move status.")) + return + + self._client.abort_volume_move(share_volume, vserver) + + msg = _LI("Share volume move operation for share %(shr)s from host " + "%(src)s to %(dest)s was successfully aborted.") + msg_args = { + 'shr': source_share['id'], + 'src': source_share['host'], + 'dest': destination_share['host'], + } + LOG.info(msg, msg_args) + + def migration_complete(self, context, source_share, destination_share, + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None): + """Initiate the cutover to destination share after move is complete.""" + vserver, vserver_client = self._get_vserver(share_server=share_server) + share_volume = self._get_backend_share_name(source_share['id']) + + status = self._get_volume_move_status(source_share, share_server) + + move_phase = status['phase'].lower() + if move_phase == 'completed': + LOG.debug("Volume move operation was already successfully " + "completed for share %(shr)s.", + {'shr': source_share['id']}) + elif move_phase in ('cutover_hard_deferred', 'cutover_soft_deferred'): + self._client.trigger_volume_move_cutover(share_volume, vserver) + self._wait_for_cutover_completion( + source_share, share_server) + else: + msg_args = { + 'shr': source_share['id'], + 'status': status['state'], + 'phase': status['phase'], + 'details': status['details'], + } + msg = _("Cannot complete volume move operation for share %(shr)s. " + "Current volume move status: %(status)s, phase: " + "%(phase)s. Details: %(details)s") % msg_args + LOG.exception(msg) + raise exception.NetAppException(msg) + + new_share_volume_name = self._get_backend_share_name( + destination_share['id']) + vserver_client.set_volume_name(share_volume, new_share_volume_name) + + msg = _LI("Volume move operation for share %(shr)s has completed " + "successfully. Share has been moved from %(src)s to " + "%(dest)s.") + msg_args = { + 'shr': source_share['id'], + 'src': source_share['host'], + 'dest': destination_share['host'], + } + LOG.info(msg, msg_args) + + # NOTE(gouthamr): For nondisruptive migration, current export + # policy will not be cleared, the export policy will be renamed to + # match the name of the share. + export_locations = self._create_export( + destination_share, share_server, vserver, vserver_client, + clear_current_export_policy=False) + src_snaps_dict = {s['id']: s for s in source_snapshots} + snapshot_updates = {} + + for source_snap_id, destination_snap in snapshot_mappings.items(): + p_location = src_snaps_dict[source_snap_id]['provider_location'] + + snapshot_updates.update( + {destination_snap['id']: {'provider_location': p_location}}) + + return { + 'export_locations': export_locations, + 'snapshot_updates': snapshot_updates, + } + + def _wait_for_cutover_completion(self, source_share, share_server): + + retries = (self.configuration.netapp_volume_move_cutover_timeout / 5 + or 1) + + @manila_utils.retry(exception.ShareBusyException, interval=5, + retries=retries, backoff_rate=1) + def check_move_completion(): + status = self._get_volume_move_status(source_share, share_server) + if status['phase'].lower() != 'completed': + msg_args = { + 'shr': source_share['id'], + 'phs': status['phase'], + } + msg = _('Volume move operation for share %(shr)s is not ' + 'complete. Current Phase: %(phs)s. ' + 'Retrying.') % msg_args + LOG.warning(msg) + raise exception.ShareBusyException(reason=msg) + + try: + check_move_completion() + except exception.ShareBusyException: + msg = _("Volume move operation did not complete after cut-over " + "was triggered. Retries exhausted. Not retrying.") + raise exception.NetAppException(message=msg) diff --git a/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py index 7ce58aeecf..244ba3b86b 100644 --- a/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py +++ b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py @@ -28,10 +28,12 @@ class NetAppCmodeCIFSHelper(base.NetAppBaseHelper): """NetApp cDOT CIFS protocol helper class.""" @na_utils.trace - def create_share(self, share, share_name): + def create_share(self, share, share_name, + clear_current_export_policy=True): """Creates CIFS share on Data ONTAP Vserver.""" self._client.create_cifs_share(share_name) - self._client.remove_cifs_share_access(share_name, 'Everyone') + if clear_current_export_policy: + self._client.remove_cifs_share_access(share_name, 'Everyone') # Return a callback that may be used for generating export paths # for this share. diff --git a/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py b/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py index b8785d7753..a93b053c66 100644 --- a/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py +++ b/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py @@ -35,9 +35,11 @@ class NetAppCmodeNFSHelper(base.NetAppBaseHelper): """NetApp cDOT NFS protocol helper class.""" @na_utils.trace - def create_share(self, share, share_name): + def create_share(self, share, share_name, + clear_current_export_policy=True): """Creates NFS share.""" - self._client.clear_nfs_export_policy_for_volume(share_name) + if clear_current_export_policy: + self._client.clear_nfs_export_policy_for_volume(share_name) self._ensure_export_policy(share, share_name) export_path = self._client.get_volume_junction_path(share_name) diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py index 22d7d6b02e..c1ad21644e 100644 --- a/manila/share/drivers/netapp/options.py +++ b/manila/share/drivers/netapp/options.py @@ -113,13 +113,19 @@ netapp_support_opts = [ 'trace info is written to the debug logs. Values ' 'include method and api.')), ] -netapp_replication_opts = [ +netapp_data_motion_opts = [ cfg.IntOpt('netapp_snapmirror_quiesce_timeout', min=0, default=3600, # One Hour help='The maximum time in seconds to wait for existing ' 'snapmirror transfers to complete before aborting when ' - 'promoting a replica.'), ] + 'promoting a replica.'), + cfg.IntOpt('netapp_volume_move_cutover_timeout', + min=0, + default=3600, # One Hour, + help='The maximum time in seconds to wait for the completion ' + 'of a volume move operation after the cutover ' + 'was triggered.'), ] CONF = cfg.CONF CONF.register_opts(netapp_proxy_opts) @@ -128,4 +134,4 @@ CONF.register_opts(netapp_transport_opts) CONF.register_opts(netapp_basicauth_opts) CONF.register_opts(netapp_provisioning_opts) CONF.register_opts(netapp_support_opts) -CONF.register_opts(netapp_replication_opts) +CONF.register_opts(netapp_data_motion_opts) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index 1dad0eb539..1f0d49d11a 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -2115,6 +2115,27 @@ SNAPMIRROR_INITIALIZE_RESULT = etree.XML(""" """) +VOLUME_MOVE_GET_ITER_RESULT = etree.XML(""" + + + + retry_on_failure +
Cutover Completed::Volume move job finishing move
+ 1481919246 + 82 + finishing + healthy + %(volume)s + %(vserver)s +
+
+ 1 +
+""" % { + 'volume': SHARE_NAME, + 'vserver': VSERVER_NAME, +}) + PERF_OBJECT_COUNTER_TOTAL_CP_MSECS_LABELS = [ 'SETUP', 'PRE_P0', 'P0_SNAP_DEL', 'P1_CLEAN', 'P1_QUOTA', 'IPU_DISK_ADD', 'P2V_INOFILE', 'P2V_INO_PUB', 'P2V_INO_PRI', 'P2V_FSINFO', 'P2V_DLOG1', 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 1b07afb444..861ca658d4 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 @@ -5538,3 +5538,124 @@ class NetAppClientCmodeTestCase(test.TestCase): mock_get_snapmirrors_call.assert_has_calls( expected_get_snapmirrors_calls) self.assertTrue(mock_exc_log.called) + + @ddt.data('start_volume_move', 'check_volume_move') + def test_volume_move_method(self, method_name): + + method = getattr(self.client, method_name) + self.mock_object(self.client, 'send_request') + + retval = method(fake.SHARE_NAME, fake.VSERVER_NAME, + fake.SHARE_AGGREGATE_NAME) + + expected_api_args = { + 'source-volume': fake.SHARE_NAME, + 'vserver': fake.VSERVER_NAME, + 'dest-aggr': fake.SHARE_AGGREGATE_NAME, + 'cutover-action': 'wait', + } + if method_name.startswith('check'): + expected_api_args['perform-validation-only'] = 'true' + + self.assertIsNone(retval) + self.client.send_request.assert_called_once_with( + 'volume-move-start', expected_api_args) + + def test_abort_volume_move(self): + self.mock_object(self.client, 'send_request') + + retval = self.client.abort_volume_move( + fake.SHARE_NAME, fake.VSERVER_NAME) + + expected_api_args = { + 'source-volume': fake.SHARE_NAME, + 'vserver': fake.VSERVER_NAME, + } + self.assertIsNone(retval) + self.client.send_request.assert_called_once_with( + 'volume-move-trigger-abort', expected_api_args) + + @ddt.data(True, False) + def test_trigger_volume_move_cutover_force(self, forced): + self.mock_object(self.client, 'send_request') + + retval = self.client.trigger_volume_move_cutover( + fake.SHARE_NAME, fake.VSERVER_NAME, force=forced) + + expected_api_args = { + 'source-volume': fake.SHARE_NAME, + 'vserver': fake.VSERVER_NAME, + 'force': 'true' if forced else 'false', + } + self.assertIsNone(retval) + self.client.send_request.assert_called_once_with( + 'volume-move-trigger-cutover', expected_api_args) + + def test_get_volume_move_status_no_records(self): + self.mock_object(self.client, 'send_iter_request') + self.mock_object(self.client, '_has_records', + mock.Mock(return_value=False)) + + self.assertRaises(exception.NetAppException, + self.client.get_volume_move_status, + fake.SHARE_NAME, fake.VSERVER_NAME) + + expected_api_args = { + 'query': { + 'volume-move-info': { + 'volume': fake.SHARE_NAME, + 'vserver': fake.VSERVER_NAME, + }, + }, + 'desired-attributes': { + 'volume-move-info': { + 'percent-complete': None, + 'estimated-completion-time': None, + 'state': None, + 'details': None, + 'cutover-action': None, + 'phase': None, + }, + }, + } + self.client.send_iter_request.assert_called_once_with( + 'volume-move-get-iter', expected_api_args) + + def test_get_volume_move_status(self): + move_status = netapp_api.NaElement(fake.VOLUME_MOVE_GET_ITER_RESULT) + self.mock_object(self.client, 'send_iter_request', + mock.Mock(return_value=move_status)) + + actual_status_info = self.client.get_volume_move_status( + fake.SHARE_NAME, fake.VSERVER_NAME) + + expected_api_args = { + 'query': { + 'volume-move-info': { + 'volume': fake.SHARE_NAME, + 'vserver': fake.VSERVER_NAME, + }, + }, + 'desired-attributes': { + 'volume-move-info': { + 'percent-complete': None, + 'estimated-completion-time': None, + 'state': None, + 'details': None, + 'cutover-action': None, + 'phase': None, + }, + }, + } + expected_status_info = { + 'percent-complete': '82', + 'estimated-completion-time': '1481919246', + 'state': 'healthy', + 'details': 'Cutover Completed::Volume move job finishing move', + 'cutover-action': 'retry_on_failure', + 'phase': 'finishing', + } + + self.assertDictMatch(expected_status_info, actual_status_info) + self.client.send_iter_request.assert_called_once_with( + 'volume-move-get-iter', expected_api_args) diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_data_motion.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_data_motion.py index 5f679cbe6a..fee6619850 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_data_motion.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_data_motion.py @@ -51,7 +51,7 @@ class NetAppCDOTDataMotionTestCase(test.TestCase): self.config.append_config_values(na_opts.netapp_transport_opts) self.config.append_config_values(na_opts.netapp_support_opts) self.config.append_config_values(na_opts.netapp_provisioning_opts) - self.config.append_config_values(na_opts.netapp_replication_opts) + self.config.append_config_values(na_opts.netapp_data_motion_opts) CONF.set_override("share_backend_name", self.backend, group=self.backend, enforce_type=True) CONF.set_override("netapp_transport_type", "https", @@ -138,7 +138,7 @@ class NetAppCDOTDataMotionSessionTestCase(test.TestCase): config.append_config_values(na_opts.netapp_transport_opts) config.append_config_values(na_opts.netapp_support_opts) config.append_config_values(na_opts.netapp_provisioning_opts) - config.append_config_values(na_opts.netapp_replication_opts) + config.append_config_values(na_opts.netapp_data_motion_opts) self.mock_object(data_motion, "get_backend_configuration", mock.Mock(return_value=config)) 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 fa49cd5030..9c96472188 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 @@ -45,6 +45,7 @@ from manila.share import utils as share_utils from manila import test from manila.tests import fake_share from manila.tests.share.drivers.netapp.dataontap import fakes as fake +from manila.tests import utils def fake_replica(**kwargs): @@ -1075,7 +1076,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_get_export_addresses_with_metadata.assert_called_once_with( fake.SHARE, fake.SHARE_SERVER, fake.LIFS) protocol_helper.create_share.assert_called_once_with( - fake.SHARE, fake.SHARE_NAME) + fake.SHARE, fake.SHARE_NAME, clear_current_export_policy=True) def test_create_export_lifs_not_found(self): @@ -3619,3 +3620,463 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): replica_list, self.fake_replica, [fake_snapshot], fake_snapshot) self.assertIsNone(model_update) + + def test_migration_check_compatibility_no_cluster_credentials(self): + self.library._have_cluster_creds = False + self.mock_object(data_motion, 'get_backend_configuration') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=None, + destination_share_server=fake.SHARE_SERVER) + + expected_compatibility = { + 'compatible': False, + 'writable': False, + 'nondisruptive': False, + 'preserve_metadata': False, + 'preserve_snapshots': False, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + mock_warning_log.assert_called_once() + self.assertFalse(data_motion.get_backend_configuration.called) + + def test_migration_check_compatibility_destination_not_configured(self): + self.library._have_cluster_creds = True + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object( + data_motion, 'get_backend_configuration', + mock.Mock(side_effect=exception.BadConfigurationException)) + self.mock_object(self.library, '_get_vserver') + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + self.mock_object(share_utils, 'extract_host', mock.Mock( + return_value='destination_backend')) + mock_vserver_compatibility_check = self.mock_object( + self.library, '_check_destination_vserver_for_vol_move') + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server=None) + + expected_compatibility = { + 'compatible': False, + 'writable': False, + 'nondisruptive': False, + 'preserve_metadata': False, + 'preserve_snapshots': False, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + mock_exception_log.assert_called_once() + data_motion.get_backend_configuration.assert_called_once_with( + 'destination_backend') + self.assertFalse(mock_vserver_compatibility_check.called) + self.assertFalse(self.library._get_vserver.called) + + @ddt.data( + utils.annotated( + 'dest_share_server_not_expected', + (('src_vserver', None), exception.InvalidParameterValue)), + utils.annotated( + 'src_share_server_not_expected', + (exception.InvalidParameterValue, ('dest_vserver', None)))) + def test_migration_check_compatibility_errors(self, side_effects): + self.library._have_cluster_creds = True + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(data_motion, 'get_backend_configuration') + self.mock_object(self.library, '_get_vserver', + mock.Mock(side_effect=side_effects)) + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + self.mock_object(share_utils, 'extract_host', mock.Mock( + return_value='destination_backend')) + mock_compatibility_check = self.mock_object( + self.client, 'check_volume_move') + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server=None) + + expected_compatibility = { + 'compatible': False, + 'writable': False, + 'nondisruptive': False, + 'preserve_metadata': False, + 'preserve_snapshots': False, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + mock_exception_log.assert_called_once() + data_motion.get_backend_configuration.assert_called_once_with( + 'destination_backend') + self.assertFalse(mock_compatibility_check.called) + + def test_migration_check_compatibility_incompatible_vservers(self): + self.library._have_cluster_creds = True + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(data_motion, 'get_backend_configuration') + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + get_vserver_returns = [ + (fake.VSERVER1, mock.Mock()), + (fake.VSERVER2, mock.Mock()), + ] + self.mock_object(self.library, '_get_vserver', + mock.Mock(side_effect=get_vserver_returns)) + self.mock_object(share_utils, 'extract_host', mock.Mock( + side_effect=['destination_backend', 'destination_pool'])) + mock_move_check = self.mock_object(self.client, 'check_volume_move') + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + expected_compatibility = { + 'compatible': False, + 'writable': False, + 'nondisruptive': False, + 'preserve_metadata': False, + 'preserve_snapshots': False, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + mock_exception_log.assert_called_once() + data_motion.get_backend_configuration.assert_called_once_with( + 'destination_backend') + self.assertFalse(mock_move_check.called) + self.library._get_vserver.assert_has_calls( + [mock.call(share_server=fake.SHARE_SERVER), + mock.call(share_server='dst_srv')]) + + def test_migration_check_compatibility_client_error(self): + self.library._have_cluster_creds = True + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + self.mock_object(data_motion, 'get_backend_configuration') + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(share_utils, 'extract_host', mock.Mock( + side_effect=['destination_backend', 'destination_pool'])) + mock_move_check = self.mock_object( + self.client, 'check_volume_move', + mock.Mock(side_effect=netapp_api.NaApiError)) + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + expected_compatibility = { + 'compatible': False, + 'writable': False, + 'nondisruptive': False, + 'preserve_metadata': False, + 'preserve_snapshots': False, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + mock_exception_log.assert_called_once() + data_motion.get_backend_configuration.assert_called_once_with( + 'destination_backend') + mock_move_check.assert_called_once_with( + fake.SHARE_NAME, fake.VSERVER1, 'destination_pool') + self.library._get_vserver.assert_has_calls( + [mock.call(share_server=fake.SHARE_SERVER), + mock.call(share_server='dst_srv')]) + + def test_migration_check_compatibility(self): + self.library._have_cluster_creds = True + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(data_motion, 'get_backend_configuration') + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(share_utils, 'extract_host', mock.Mock( + side_effect=['destination_backend', 'destination_pool'])) + mock_move_check = self.mock_object(self.client, 'check_volume_move') + + migration_compatibility = self.library.migration_check_compatibility( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + expected_compatibility = { + 'compatible': True, + 'writable': True, + 'nondisruptive': True, + 'preserve_metadata': True, + 'preserve_snapshots': True, + } + self.assertDictMatch(expected_compatibility, migration_compatibility) + data_motion.get_backend_configuration.assert_called_once_with( + 'destination_backend') + mock_move_check.assert_called_once_with( + fake.SHARE_NAME, fake.VSERVER1, 'destination_pool') + self.library._get_vserver.assert_has_calls( + [mock.call(share_server=fake.SHARE_SERVER), + mock.call(share_server='dst_srv')]) + + def test_migration_start(self): + mock_info_log = self.mock_object(lib_base.LOG, 'info') + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(share_utils, 'extract_host', + mock.Mock(return_value='destination_pool')) + mock_move = self.mock_object(self.client, 'start_volume_move') + + retval = self.library.migration_start( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), + source_snapshots, snapshot_mappings, + share_server=fake.SHARE_SERVER, destination_share_server='dst_srv') + + self.assertIsNone(retval) + self.assertTrue(mock_info_log.called) + mock_move.assert_called_once_with( + fake.SHARE_NAME, fake.VSERVER1, 'destination_pool') + + def test_migration_continue_volume_move_failed(self): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_status_check = self.mock_object( + self.client, 'get_volume_move_status', + mock.Mock(return_value={'phase': 'failed', 'details': 'unknown'})) + + self.assertRaises(exception.NetAppException, + self.library.migration_continue, + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), + source_snapshots, snapshot_mappings, + share_server=None, destination_share_server=None) + + mock_status_check.assert_called_once_with( + fake.SHARE_NAME, fake.VSERVER1) + mock_exception_log.assert_called_once() + + @ddt.data({'phase': 'Queued', 'completed': False}, + {'phase': 'Finishing', 'completed': False}, + {'phase': 'cutover_hard_deferred', 'completed': True}, + {'phase': 'cutover_soft_deferred', 'completed': True}, + {'phase': 'completed', 'completed': True}) + @ddt.unpack + def test_migration_continue(self, phase, completed): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(self.client, 'get_volume_move_status', + mock.Mock(return_value={'phase': phase})) + + migration_completed = self.library.migration_continue( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), source_snapshots, + snapshot_mappings, share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + self.assertEqual(completed, migration_completed) + + @ddt.data('cutover_hard_deferred', 'cutover_soft_deferred', + 'Queued', 'Replicating') + def test_migration_get_progress_at_phase(self, phase): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + mock_info_log = self.mock_object(lib_base.LOG, 'info') + status = { + 'state': 'healthy', + 'details': '%s:: Volume move job in progress' % phase, + 'phase': phase, + 'estimated-completion-time': '1481919246', + 'percent-complete': 80, + } + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(self.client, 'get_volume_move_status', + mock.Mock(return_value=status)) + + migration_progress = self.library.migration_get_progress( + self.context, fake_share.fake_share_instance(), + source_snapshots, snapshot_mappings, + fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + expected_progress = { + 'total_progress': 100 if phase.startswith('cutover') else 80, + 'state': 'healthy', + 'estimated_completion_time': '1481919246', + 'details': '%s:: Volume move job in progress' % phase, + 'phase': phase, + } + self.assertDictMatch(expected_progress, migration_progress) + mock_info_log.assert_called_once() + + @ddt.data(utils.annotated('already_canceled', (True, )), + utils.annotated('not_canceled_yet', (False, ))) + def test_migration_cancel(self, already_canceled): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + already_canceled = already_canceled[0] + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + mock_info_log = self.mock_object(lib_base.LOG, 'info') + vol_move_side_effect = (exception.NetAppException + if already_canceled else None) + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + self.mock_object(self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(self.client, 'abort_volume_move') + self.mock_object(self.client, 'get_volume_move_status', + mock.Mock(side_effect=vol_move_side_effect)) + + retval = self.library.migration_cancel( + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance(), source_snapshots, + snapshot_mappings, share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + self.assertIsNone(retval) + if already_canceled: + mock_exception_log.assert_called_once() + else: + mock_info_log.assert_called_once() + self.assertEqual(not already_canceled, + self.client.abort_volume_move.called) + + def test_migration_complete_invalid_phase(self): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + status = { + 'state': 'healthy', + 'phase': 'Replicating', + 'details': 'Replicating:: Volume move operation is in progress.', + } + mock_exception_log = self.mock_object(lib_base.LOG, 'exception') + vserver_client = mock.Mock() + self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(side_effect=[fake.SHARE_NAME, 'new_share_name'])) + self.mock_object(self.library, '_get_volume_move_status', + mock.Mock(return_value=status)) + self.mock_object(self.library, '_create_export') + + self.assertRaises( + exception.NetAppException, self.library.migration_complete, + self.context, fake_share.fake_share_instance(), + fake_share.fake_share_instance, source_snapshots, + snapshot_mappings, share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + self.assertFalse(vserver_client.set_volume_name.called) + self.assertFalse(self.library._create_export.called) + mock_exception_log.assert_called_once() + + def test_migration_complete_timeout(self): + source_snapshots = mock.Mock() + snapshot_mappings = mock.Mock() + self.library.configuration.netapp_volume_move_cutover_timeout = 15 + vol_move_side_effects = [ + {'phase': 'cutover_hard_deferred'}, + {'phase': 'Cutover'}, + {'phase': 'Finishing'}, + {'phase': 'Finishing'}, + ] + self.mock_object(time, 'sleep') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + vserver_client = mock.Mock() + self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(side_effect=[fake.SHARE_NAME, 'new_share_name'])) + self.mock_object(self.library, '_get_volume_move_status', mock.Mock( + side_effect=vol_move_side_effects)) + self.mock_object(self.library, '_create_export') + src_share = fake_share.fake_share_instance(id='source-share-instance') + dest_share = fake_share.fake_share_instance(id='dest-share-instance') + + self.assertRaises( + exception.NetAppException, self.library.migration_complete, + self.context, src_share, dest_share, source_snapshots, + snapshot_mappings, share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + self.assertFalse(vserver_client.set_volume_name.called) + self.assertFalse(self.library._create_export.called) + self.assertEqual(3, mock_warning_log.call_count) + + @ddt.data('cutover_hard_deferred', 'cutover_soft_deferred', 'completed') + def test_migration_complete(self, phase): + snap = fake_share.fake_snapshot_instance( + id='src-snapshot', provider_location='test-src-provider-location') + dest_snap = fake_share.fake_snapshot_instance(id='dest-snapshot', + as_primitive=True) + source_snapshots = [snap] + snapshot_mappings = {snap['id']: dest_snap} + self.library.configuration.netapp_volume_move_cutover_timeout = 15 + vol_move_side_effects = [ + {'phase': phase}, + {'phase': 'Cutover'}, + {'phase': 'Finishing'}, + {'phase': 'completed'}, + ] + self.mock_object(time, 'sleep') + mock_debug_log = self.mock_object(lib_base.LOG, 'debug') + mock_info_log = self.mock_object(lib_base.LOG, 'info') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + vserver_client = mock.Mock() + self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, vserver_client))) + self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(side_effect=[fake.SHARE_NAME, 'new_share_name'])) + self.mock_object(self.library, '_create_export', mock.Mock( + return_value=fake.NFS_EXPORTS)) + mock_move_status_check = self.mock_object( + self.library, '_get_volume_move_status', + mock.Mock(side_effect=vol_move_side_effects)) + src_share = fake_share.fake_share_instance(id='source-share-instance') + dest_share = fake_share.fake_share_instance(id='dest-share-instance') + + data_updates = self.library.migration_complete( + self.context, src_share, dest_share, source_snapshots, + snapshot_mappings, share_server=fake.SHARE_SERVER, + destination_share_server='dst_srv') + + self.assertEqual(fake.NFS_EXPORTS, data_updates['export_locations']) + expected_dest_snap_updates = { + 'provider_location': snap['provider_location'], + } + self.assertIn(dest_snap['id'], data_updates['snapshot_updates']) + self.assertEqual(expected_dest_snap_updates, + data_updates['snapshot_updates'][dest_snap['id']]) + vserver_client.set_volume_name.assert_called_once_with( + fake.SHARE_NAME, 'new_share_name') + self.library._create_export.assert_called_once_with( + dest_share, fake.SHARE_SERVER, fake.VSERVER1, vserver_client, + clear_current_export_policy=False) + mock_info_log.assert_called_once() + if phase != 'completed': + self.assertEqual(2, mock_warning_log.call_count) + self.assertFalse(mock_debug_log.called) + self.assertEqual(4, mock_move_status_check.call_count) + else: + self.assertFalse(mock_warning_log.called) + mock_debug_log.assert_called_once() + mock_move_status_check.assert_called_once() diff --git a/manila/tests/utils.py b/manila/tests/utils.py index 83e0ca9507..905059895f 100644 --- a/manila/tests/utils.py +++ b/manila/tests/utils.py @@ -26,6 +26,57 @@ from manila import utils CONF = cfg.CONF +class NamedBinaryStr(six.binary_type): + + """Wrapper for six.binary_type to facilitate overriding __name__.""" + + +class NamedUnicodeStr(six.text_type): + + """Unicode string look-alike to facilitate overriding __name__.""" + + def __init__(self, value): + self._value = value + + def __str__(self): + return self._value + + def encode(self, enc): + return self._value.encode(enc) + + def __format__(self, formatstr): + """Workaround for ddt bug. + + DDT will always call __format__ even when __name__ exists, + which blows up for Unicode strings under Py2. + """ + return '' + + +class NamedDict(dict): + + """Wrapper for dict to facilitate overriding __name__.""" + + +class NamedTuple(tuple): + + """Wrapper for dict to facilitate overriding __name__.""" + + +def annotated(test_name, test_input): + if isinstance(test_input, dict): + annotated_input = NamedDict(test_input) + elif isinstance(test_input, six.text_type): + annotated_input = NamedUnicodeStr(test_input) + elif isinstance(test_input, tuple): + annotated_input = NamedTuple(test_input) + else: + annotated_input = NamedBinaryStr(test_input) + + setattr(annotated_input, '__name__', test_name) + return annotated_input + + def get_test_admin_context(): return context.get_admin_context() diff --git a/releasenotes/notes/netapp-cdot-optimized-migration-within-share-server-92cfa1bcf0c317fc.yaml b/releasenotes/notes/netapp-cdot-optimized-migration-within-share-server-92cfa1bcf0c317fc.yaml new file mode 100644 index 0000000000..0a7f05fc54 --- /dev/null +++ b/releasenotes/notes/netapp-cdot-optimized-migration-within-share-server-92cfa1bcf0c317fc.yaml @@ -0,0 +1,5 @@ +--- +features: + - Driver assisted migration support has been added to the NetApp cDOT + driver to efficiently and nondisruptively migrate shares within Vservers + by ensuring data, snapshots and metadata.