From 63867a3ba9de3f5411420d47c7bf474419b3fcca Mon Sep 17 00:00:00 2001 From: Douglas Viroel Date: Mon, 10 Feb 2020 13:49:32 +0000 Subject: [PATCH] [NetApp] Improve create share from snapshot functionality This patch improves the operation of creating share from snapshot to accept new destinations that can be different pools or back ends. Change-Id: Id3b3d5860d6325f368cbebfe7f97c98d64554d72 --- .../netapp/dataontap/client/client_cmode.py | 63 ++ .../dataontap/cluster_mode/drv_multi_svm.py | 3 + .../dataontap/cluster_mode/drv_single_svm.py | 3 + .../netapp/dataontap/cluster_mode/lib_base.py | 446 ++++++++++-- .../dataontap/cluster_mode/lib_multi_svm.py | 50 ++ manila/share/drivers/netapp/options.py | 8 +- .../drivers/netapp/dataontap/client/fakes.py | 40 + .../dataontap/client/test_client_cmode.py | 69 ++ .../dataontap/cluster_mode/test_lib_base.py | 681 +++++++++++++++++- .../cluster_mode/test_lib_multi_svm.py | 105 +++ .../share/drivers/netapp/dataontap/fakes.py | 19 +- ...napshot-another-pool-330639b57aa5f04d.yaml | 7 + 12 files changed, 1433 insertions(+), 61 deletions(-) create mode 100644 releasenotes/notes/netapp-create-share-from-snapshot-another-pool-330639b57aa5f04d.yaml diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 51d65025da..a1144865d0 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -2244,6 +2244,53 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): return raise + @na_utils.trace + def check_volume_clone_split_completed(self, volume_name): + """Check if volume clone split operation already finished""" + return self.get_volume_clone_parent_snaphot(volume_name) is None + + @na_utils.trace + def get_volume_clone_parent_snaphot(self, volume_name): + """Gets volume's clone parent. + + Return the snapshot name of a volume's clone parent, or None if it + doesn't exist. + """ + api_args = { + 'query': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': volume_name + } + } + }, + 'desired-attributes': { + 'volume-attributes': { + 'volume-clone-attributes': { + 'volume-clone-parent-attributes': { + 'snapshot-name': '' + } + } + } + } + } + result = self.send_iter_request('volume-get-iter', api_args) + if not self._has_records(result): + return None + + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + volume_attributes = attributes_list.get_child_by_name( + 'volume-attributes') or netapp_api.NaElement('none') + vol_clone_attrs = volume_attributes.get_child_by_name( + 'volume-clone-attributes') or netapp_api.NaElement('none') + vol_clone_parent_atts = vol_clone_attrs.get_child_by_name( + 'volume-clone-parent-attributes') or netapp_api.NaElement( + 'none') + snapshot_name = vol_clone_parent_atts.get_child_content( + 'snapshot-name') + return snapshot_name + @na_utils.trace def get_clone_children_for_snapshot(self, volume_name, snapshot_name): """Returns volumes that are keeping a snapshot locked.""" @@ -3964,3 +4011,19 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): return { 'ipv6-enabled': ipv6_enabled, } + + @na_utils.trace + def rehost_volume(self, volume_name, vserver, destination_vserver): + """Rehosts a volume from one Vserver into another Vserver. + + :param volume_name: Name of the FlexVol to be rehosted. + :param vserver: Source Vserver name to which target volume belongs. + :param destination_vserver: Destination Vserver name where target + volume must reside after successful volume rehost operation. + """ + api_args = { + 'volume': volume_name, + 'vserver': vserver, + 'destination-vserver': destination_vserver, + } + self.send_request('volume-rehost', api_args) 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 f24c3fd955..3a16a6a86c 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 @@ -283,3 +283,6 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): def unmanage_server(self, server_details, security_services=None): return self.library.unmanage_server(server_details, security_services) + + def get_share_status(self, share_instance, share_server=None): + return self.library.get_share_status(share_instance, 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 bd5f04bf02..9622e3c526 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 @@ -280,3 +280,6 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver): def unmanage_server(self, server_details, security_services=None): raise NotImplementedError + + def get_share_status(self, share_instance, share_server=None): + return self.library.get_share_status(share_instance, 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 7a6448fec6..19f879003b 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -62,6 +62,11 @@ class NetAppCmodeFileStorageLibrary(object): DEFAULT_FILTER_FUNCTION = 'capabilities.utilization < 70' DEFAULT_GOODNESS_FUNCTION = '100 - capabilities.utilization' + # Internal states when dealing with data motion + STATE_SPLITTING_VOLUME_CLONE = 'splitting_volume_clone' + STATE_MOVING_VOLUME = 'moving_volume' + STATE_SNAPMIRROR_DATA_COPYING = 'snapmirror_data_copying' + # Maps NetApp qualified extra specs keys to corresponding backend API # client library argument keywords. When we expose more backend # capabilities here, we will add them to this map. @@ -487,11 +492,278 @@ class NetAppCmodeFileStorageLibrary(object): def create_share_from_snapshot(self, context, share, snapshot, share_server=None, parent_share=None): """Creates new share from snapshot.""" - vserver, vserver_client = self._get_vserver(share_server=share_server) - self._allocate_container_from_snapshot( - share, snapshot, vserver, vserver_client) - return self._create_export(share, share_server, vserver, - vserver_client) + # TODO(dviroel) return progress info in asynchronous answers + if parent_share['host'] == share['host']: + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server) + # Creating a new share from snapshot in the source share's pool + self._allocate_container_from_snapshot( + share, snapshot, src_vserver, src_vserver_client) + return self._create_export(share, share_server, src_vserver, + src_vserver_client) + parent_share_server = {} + if parent_share['share_server'] is not None: + # Get only the information needed by Data Motion + ss_keys = ['id', 'identifier', 'backend_details', 'host'] + for key in ss_keys: + parent_share_server[key] = ( + parent_share['share_server'].get(key)) + + # Information to be saved in the private_storage that will need to be + # retrieved later, in order to continue with the share creation flow + src_share_instance = { + 'id': share['id'], + 'host': parent_share.get('host'), + 'share_server': parent_share_server or None + } + # NOTE(dviroel): Data Motion functions access share's 'share_server' + # attribute to get vserser information. + dest_share = copy.deepcopy(share.to_dict()) + dest_share['share_server'] = (share_server.to_dict() + if share_server else None) + + dm_session = data_motion.DataMotionSession() + # Source host info + __, src_vserver, src_backend = ( + dm_session.get_backend_info_for_share(parent_share)) + src_vserver_client = data_motion.get_client_for_backend( + src_backend, vserver_name=src_vserver) + src_cluster_name = src_vserver_client.get_cluster_name() + + # Destination host info + dest_vserver, dest_vserver_client = self._get_vserver(share_server) + dest_cluster_name = dest_vserver_client.get_cluster_name() + + try: + if (src_cluster_name != dest_cluster_name or + not self._have_cluster_creds): + # 1. Create a clone on source. We don't need to split from + # clone in order to replicate data + self._allocate_container_from_snapshot( + dest_share, snapshot, src_vserver, src_vserver_client, + split=False) + # 2. Create a replica in destination host + self._allocate_container( + dest_share, dest_vserver, dest_vserver_client, + replica=True) + # 3. Initialize snapmirror relationship with cloned share. + src_share_instance['replica_state'] = ( + constants.REPLICA_STATE_ACTIVE) + dm_session.create_snapmirror(src_share_instance, dest_share) + # The snapmirror data copy can take some time to be concluded, + # we'll answer this call asynchronously + state = self.STATE_SNAPMIRROR_DATA_COPYING + else: + # NOTE(dviroel): there's a need to split the cloned share from + # its parent in order to move it to a different aggregate or + # vserver + self._allocate_container_from_snapshot( + dest_share, snapshot, src_vserver, + src_vserver_client, split=True) + # The split volume clone operation can take some time to be + # concluded and we'll answer the call asynchronously + state = self.STATE_SPLITTING_VOLUME_CLONE + except Exception: + # If the share exists on the source vserser, we need to + # delete it since it's a temporary share, not managed by the system + dm_session.delete_snapmirror(src_share_instance, dest_share) + self._delete_share(src_share_instance, src_vserver_client, + remove_export=False) + msg = _('Could not create share %(share_id)s from snapshot ' + '%(snapshot_id)s in the destination host %(dest_host)s.') + msg_args = {'share_id': dest_share['id'], + 'snapshot_id': snapshot['id'], + 'dest_host': dest_share['host']} + raise exception.NetAppException(msg % msg_args) + + # Store source share info on private storage using destination share id + src_share_instance['internal_state'] = state + src_share_instance['status'] = constants.STATUS_ACTIVE + self.private_storage.update(dest_share['id'], { + 'source_share': json.dumps(src_share_instance) + }) + return { + 'status': constants.STATUS_CREATING_FROM_SNAPSHOT, + } + + def _update_create_from_snapshot_status(self, share, share_server=None): + # TODO(dviroel) return progress info in asynchronous answers + # If the share is creating from snapshot and copying data in background + # we'd verify if the operation has finished and trigger new operations + # if necessary. + source_share_str = self.private_storage.get(share['id'], + 'source_share') + if source_share_str is None: + msg = _('Could not update share %(share_id)s status due to invalid' + ' internal state. Aborting share creation.') + msg_args = {'share_id': share['id']} + LOG.error(msg, msg_args) + return {'status': constants.STATUS_ERROR} + try: + # Check if current operation had finished and continue to move the + # source share towards its destination + return self._create_from_snapshot_continue(share, share_server) + except Exception: + # Delete everything associated to the temporary clone created on + # the source host. + source_share = json.loads(source_share_str) + dm_session = data_motion.DataMotionSession() + + dm_session.delete_snapmirror(source_share, share) + __, src_vserver, src_backend = ( + dm_session.get_backend_info_for_share(source_share)) + src_vserver_client = data_motion.get_client_for_backend( + src_backend, vserver_name=src_vserver) + + self._delete_share(source_share, src_vserver_client, + remove_export=False) + # Delete private storage info + self.private_storage.delete(share['id']) + msg = _('Could not complete share %(share_id)s creation due to an ' + 'internal error.') + msg_args = {'share_id': share['id']} + LOG.error(msg, msg_args) + return {'status': constants.STATUS_ERROR} + + def _create_from_snapshot_continue(self, share, share_server=None): + return_values = { + 'status': constants.STATUS_CREATING_FROM_SNAPSHOT + } + apply_qos_on_dest = False + # Data motion session used to extract host info and manage snapmirrors + dm_session = data_motion.DataMotionSession() + # Get info from private storage + src_share_str = self.private_storage.get(share['id'], 'source_share') + src_share = json.loads(src_share_str) + current_state = src_share['internal_state'] + share['share_server'] = share_server + + # Source host info + __, src_vserver, src_backend = ( + dm_session.get_backend_info_for_share(src_share)) + src_aggr = share_utils.extract_host(src_share['host'], level='pool') + src_vserver_client = data_motion.get_client_for_backend( + src_backend, vserver_name=src_vserver) + # Destination host info + dest_vserver, dest_vserver_client = self._get_vserver(share_server) + dest_aggr = share_utils.extract_host(share['host'], level='pool') + + if current_state == self.STATE_SPLITTING_VOLUME_CLONE: + if self._check_volume_clone_split_completed( + src_share, src_vserver_client): + # Rehost volume if source and destination are hosted in + # different vservers + if src_vserver != dest_vserver: + # NOTE(dviroel): some volume policies, policy rules and + # configurations are lost from the source volume after + # rehost operation. + qos_policy_for_share = ( + self._get_backend_qos_policy_group_name(share['id'])) + src_vserver_client.mark_qos_policy_group_for_deletion( + qos_policy_for_share) + # Apply QoS on destination share + apply_qos_on_dest = True + + self._rehost_and_mount_volume( + share, src_vserver, src_vserver_client, + dest_vserver, dest_vserver_client) + # Move the share to the expected aggregate + if src_aggr != dest_aggr: + # Move volume and 'defer' the cutover. If it fails, the + # share will be deleted afterwards + self._move_volume_after_splitting( + src_share, share, share_server, cutover_action='defer') + # Move a volume can take longer, we'll answer + # asynchronously + current_state = self.STATE_MOVING_VOLUME + else: + return_values['status'] = constants.STATUS_AVAILABLE + + elif current_state == self.STATE_MOVING_VOLUME: + if self._check_volume_move_completed(share, share_server): + if src_vserver != dest_vserver: + # NOTE(dviroel): at this point we already rehosted the + # share, but we missed applying the qos since it was moving + # the share between aggregates + apply_qos_on_dest = True + return_values['status'] = constants.STATUS_AVAILABLE + + elif current_state == self.STATE_SNAPMIRROR_DATA_COPYING: + replica_state = self.update_replica_state( + None, # no context is needed + [src_share], + share, + [], # access_rules + [], # snapshot list + share_server) + if replica_state in [None, constants.STATUS_ERROR]: + msg = _("Destination share has failed on replicating data " + "from source share.") + LOG.exception(msg) + raise exception.NetAppException(msg) + elif replica_state == constants.REPLICA_STATE_IN_SYNC: + try: + # 1. Start an update to try to get a last minute + # transfer before we quiesce and break + dm_session.update_snapmirror(src_share, share) + except exception.StorageCommunicationException: + # Ignore any errors since the current source replica + # may be unreachable + pass + # 2. Break SnapMirror + # NOTE(dviroel): if it fails on break/delete a snapmirror + # relationship, we won't be able to delete the share. + dm_session.break_snapmirror(src_share, share) + dm_session.delete_snapmirror(src_share, share) + # 3. Delete the source volume + self._delete_share(src_share, src_vserver_client, + remove_export=False) + share_name = self._get_backend_share_name(src_share['id']) + # 4. Set File system size fixed to false + dest_vserver_client.set_volume_filesys_size_fixed( + share_name, filesys_size_fixed=False) + apply_qos_on_dest = True + return_values['status'] = constants.STATUS_AVAILABLE + else: + # Delete this share from private storage since we'll abort this + # operation. + self.private_storage.delete(share['id']) + msg_args = { + 'state': current_state, + 'id': share['id'], + } + msg = _("Caught an unexpected internal state '%(state)s' for " + "share %(id)s. Aborting operation.") % msg_args + LOG.exception(msg) + raise exception.NetAppException(msg) + + if return_values['status'] == constants.STATUS_AVAILABLE: + if apply_qos_on_dest: + extra_specs = share_types.get_extra_specs_from_share(share) + provisioning_options = self._get_provisioning_options( + extra_specs) + qos_policy_group_name = ( + self._modify_or_create_qos_for_existing_share( + share, extra_specs, dest_vserver, dest_vserver_client)) + if qos_policy_group_name: + provisioning_options['qos_policy_group'] = ( + qos_policy_group_name) + share_name = self._get_backend_share_name(share['id']) + # Modify volume to match extra specs + dest_vserver_client.modify_volume( + dest_aggr, share_name, **provisioning_options) + + self.private_storage.delete(share['id']) + return_values['export_locations'] = self._create_export( + share, share_server, dest_vserver, dest_vserver_client, + clear_current_export_policy=False) + else: + new_src_share = copy.deepcopy(src_share) + new_src_share['internal_state'] = current_state + self.private_storage.update(share['id'], { + 'source_share': json.dumps(new_src_share) + }) + return return_values @na_utils.trace def _allocate_container(self, share, vserver, vserver_client, @@ -506,7 +778,7 @@ class NetAppCmodeFileStorageLibrary(object): raise exception.InvalidHost(reason=msg) provisioning_options = self._get_provisioning_options_for_share( - share, vserver, replica=replica) + share, vserver, vserver_client=vserver_client, replica=replica) if replica: # If this volume is intended to be a replication destination, @@ -694,17 +966,19 @@ class NetAppCmodeFileStorageLibrary(object): int(qos_specs['maxbpspergib']) * int(share_size)) @na_utils.trace - def _create_qos_policy_group(self, share, vserver, qos_specs): + def _create_qos_policy_group(self, share, vserver, qos_specs, + vserver_client=None): max_throughput = self._get_max_throughput(share['size'], qos_specs) qos_policy_group_name = self._get_backend_qos_policy_group_name( share['id']) - self._client.qos_policy_group_create(qos_policy_group_name, vserver, - max_throughput=max_throughput) + client = vserver_client or self._client + client.qos_policy_group_create(qos_policy_group_name, vserver, + max_throughput=max_throughput) return qos_policy_group_name @na_utils.trace - def _get_provisioning_options_for_share(self, share, vserver, - replica=False): + def _get_provisioning_options_for_share( + self, share, vserver, vserver_client=None, replica=False): """Return provisioning options from a share. Starting with a share, this method gets the extra specs, rationalizes @@ -719,7 +993,7 @@ class NetAppCmodeFileStorageLibrary(object): qos_specs = self._get_normalized_qos_specs(extra_specs) if qos_specs and not replica: qos_policy_group = self._create_qos_policy_group( - share, vserver, qos_specs) + share, vserver, qos_specs, vserver_client) provisioning_options['qos_policy_group'] = qos_policy_group return provisioning_options @@ -766,7 +1040,7 @@ class NetAppCmodeFileStorageLibrary(object): @na_utils.trace def _allocate_container_from_snapshot( self, share, snapshot, vserver, vserver_client, - snapshot_name_func=_get_backend_snapshot_name): + snapshot_name_func=_get_backend_snapshot_name, split=None): """Clones existing share.""" share_name = self._get_backend_share_name(share['id']) parent_share_name = self._get_backend_share_name(snapshot['share_id']) @@ -776,14 +1050,17 @@ class NetAppCmodeFileStorageLibrary(object): parent_snapshot_name = snapshot['provider_location'] provisioning_options = self._get_provisioning_options_for_share( - share, vserver) + share, vserver, vserver_client=vserver_client) hide_snapdir = provisioning_options.pop('hide_snapdir') + if split is not None: + provisioning_options['split'] = split LOG.debug('Creating share from snapshot %s', snapshot['id']) - vserver_client.create_volume_clone(share_name, parent_share_name, - parent_snapshot_name, - **provisioning_options) + vserver_client.create_volume_clone( + share_name, parent_share_name, parent_snapshot_name, + **provisioning_options) + if share['size'] > snapshot['size']: vserver_client.set_volume_size(share_name, share['size']) @@ -795,6 +1072,20 @@ class NetAppCmodeFileStorageLibrary(object): def _share_exists(self, share_name, vserver_client): return vserver_client.volume_exists(share_name) + @na_utils.trace + def _delete_share(self, share, vserver_client, remove_export=True): + share_name = self._get_backend_share_name(share['id']) + if self._share_exists(share_name, vserver_client): + if remove_export: + self._remove_export(share, vserver_client) + self._deallocate_container(share_name, vserver_client) + qos_policy_for_share = self._get_backend_qos_policy_group_name( + share['id']) + vserver_client.mark_qos_policy_group_for_deletion( + qos_policy_for_share) + else: + LOG.info("Share %s does not exist.", share['id']) + @na_utils.trace def delete_share(self, context, share, share_server=None): """Deletes share.""" @@ -809,17 +1100,7 @@ class NetAppCmodeFileStorageLibrary(object): "will proceed anyway. Error: %(error)s", {'share': share['id'], 'error': error}) return - - share_name = self._get_backend_share_name(share['id']) - if self._share_exists(share_name, vserver_client): - self._remove_export(share, vserver_client) - self._deallocate_container(share_name, vserver_client) - qos_policy_for_share = self._get_backend_qos_policy_group_name( - share['id']) - self._client.mark_qos_policy_group_for_deletion( - qos_policy_for_share) - else: - LOG.info("Share %s does not exist.", share['id']) + self._delete_share(share, vserver_client) @na_utils.trace def _deallocate_container(self, share_name, vserver_client): @@ -2061,10 +2342,42 @@ class NetAppCmodeFileStorageLibrary(object): 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.""" + def _move_volume_after_splitting(self, source_share, destination_share, + share_server=None, cutover_action='wait'): + retries = (self.configuration.netapp_start_volume_move_timeout / 5 + or 1) + + @manila_utils.retry(exception.ShareBusyException, interval=5, + retries=retries, backoff_rate=1) + def try_move_volume(): + try: + self._move_volume(source_share, destination_share, + share_server, cutover_action) + except netapp_api.NaApiError as e: + undergoing_split = 'undergoing a clone split' + msg_args = {'id': source_share['id']} + if (e.code == netapp_api.EAPIERROR and + undergoing_split in e.message): + msg = _('The volume %(id)s is undergoing a clone split ' + 'operation. Will retry the operation.') % msg_args + LOG.warning(msg) + raise exception.ShareBusyException(reason=msg) + else: + msg = _("Unable to perform move operation for the volume " + "%(id)s. Caught an unexpected error. Not " + "retrying.") % msg_args + raise exception.NetAppException(message=msg) + try: + try_move_volume() + except exception.ShareBusyException: + msg_args = {'id': source_share['id']} + msg = _("Unable to perform move operation for the volume %(id)s " + "because a clone split operation is still in progress. " + "Retries exhausted. Not retrying.") % msg_args + raise exception.NetAppException(message=msg) + + def _move_volume(self, source_share, destination_share, share_server=None, + cutover_action='wait'): # Intra-cluster migration vserver, vserver_client = self._get_vserver(share_server=share_server) share_volume = self._get_backend_share_name(source_share['id']) @@ -2082,6 +2395,7 @@ class NetAppCmodeFileStorageLibrary(object): share_volume, vserver, destination_aggregate, + cutover_action=cutover_action, encrypt_destination=encrypt_dest) msg = ("Began volume move operation of share %(shr)s from %(src)s " @@ -2093,12 +2407,22 @@ class NetAppCmodeFileStorageLibrary(object): } LOG.info(msg, msg_args) + 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.""" + self._move_volume(source_share, destination_share, share_server) + 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 _check_volume_clone_split_completed(self, share, vserver_client): + share_volume = self._get_backend_share_name(share['id']) + return vserver_client.check_volume_clone_split_completed(share_volume) + def _get_dest_flexvol_encryption_value(self, destination_share): dest_share_type_encrypted_val = share_types.get_share_type_extra_specs( destination_share['share_type_id'], @@ -2108,10 +2432,8 @@ class NetAppCmodeFileStorageLibrary(object): return encrypt_destination - 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.""" + def _check_volume_move_completed(self, source_share, share_server): + """Check progress of volume move operation.""" status = self._get_volume_move_status(source_share, share_server) completed_phases = ( 'cutover_hard_deferred', 'cutover_soft_deferred', 'completed') @@ -2131,11 +2453,13 @@ class NetAppCmodeFileStorageLibrary(object): 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.""" + 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.""" + return self._check_volume_move_completed(source_share, share_server) + + def _get_volume_move_progress(self, source_share, share_server): status = self._get_volume_move_status(source_share, share_server) # NOTE (gouthamr): If the volume move is waiting for a manual @@ -2163,6 +2487,13 @@ class NetAppCmodeFileStorageLibrary(object): 'details': status['details'], } + 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.""" + return self._get_volume_move_progress(source_share, share_server) + def migration_cancel(self, context, source_share, destination_share, source_snapshots, snapshot_mappings, share_server=None, destination_share_server=None): @@ -2342,7 +2673,8 @@ class NetAppCmodeFileStorageLibrary(object): LOG.debug("No existing QoS policy group found for " "volume. Creating a new one with name %s.", qos_policy_group_name) - self._create_qos_policy_group(share_obj, vserver, qos_specs) + self._create_qos_policy_group(share_obj, vserver, qos_specs, + vserver_client=vserver_client) return qos_policy_group_name def _wait_for_cutover_completion(self, source_share, share_server): @@ -2389,3 +2721,33 @@ class NetAppCmodeFileStorageLibrary(object): share_name = self._get_backend_share_name(share['id']) self._apply_snapdir_visibility( hide_snapdir, share_name, vserver_client) + + def get_share_status(self, share, share_server=None): + if share['status'] == constants.STATUS_CREATING_FROM_SNAPSHOT: + return self._update_create_from_snapshot_status(share, + share_server) + else: + LOG.warning("Caught an unexpected share status '%s' during share " + "status update routine. Skipping.", share['status']) + + def volume_rehost(self, share, src_vserver, dest_vserver): + volume_name = self._get_backend_share_name(share['id']) + msg = ("Rehosting volume of share %(shr)s from vserver %(src)s " + "to vserver %(dest)s.") + msg_args = { + 'shr': share['id'], + 'src': src_vserver, + 'dest': dest_vserver, + } + LOG.info(msg, msg_args) + self._client.rehost_volume(volume_name, src_vserver, dest_vserver) + + def _rehost_and_mount_volume(self, share, src_vserver, src_vserver_client, + dest_vserver, dest_vserver_client): + volume_name = self._get_backend_share_name(share['id']) + # Unmount volume in the source vserver: + src_vserver_client.unmount_volume(volume_name) + # Rehost the volume + self.volume_rehost(share, src_vserver, dest_vserver) + # Mount the volume on the destination vserver + dest_vserver_client.mount_volume(volume_name) diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py index 6d08a1d3c1..f1f93ab66c 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py @@ -20,6 +20,7 @@ variant creates Data ONTAP storage virtual machines (i.e. 'vservers') as needed to provision shares. """ +import copy import re from oslo_log import log @@ -553,3 +554,52 @@ class NetAppCmodeMultiSVMFileStorageLibrary( def _delete_vserver_peer(self, vserver, peer_vserver): self._client.delete_vserver_peer(vserver, peer_vserver) + + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None, parent_share=None): + # NOTE(dviroel): If both parent and child shares are in the same host, + # they belong to the same cluster, and we can skip all the processing + # below. + if parent_share['host'] != share['host']: + # 1. Retrieve source and destination vservers from source and + # destination shares + new_share = copy.deepcopy(share.to_dict()) + new_share['share_server'] = share_server.to_dict() + + dm_session = data_motion.DataMotionSession() + src_vserver = dm_session.get_vserver_from_share(parent_share) + dest_vserver = dm_session.get_vserver_from_share(new_share) + + # 2. Retrieve the source share host's client and cluster name + src_share_host = share_utils.extract_host( + parent_share['host'], level='backend_name') + src_share_client = data_motion.get_client_for_backend( + src_share_host, vserver_name=src_vserver) + # Cluster name is needed for setting up the vserver peering + src_share_cluster_name = src_share_client.get_cluster_name() + + # 3. Retrieve new share host's client + dest_share_host = share_utils.extract_host( + new_share['host'], level='backend_name') + dest_share_client = data_motion.get_client_for_backend( + dest_share_host, vserver_name=dest_vserver) + dest_share_cluster_name = dest_share_client.get_cluster_name() + # If source and destination shares are placed in a different + # clusters, we'll need the both vserver peered. + if src_share_cluster_name != dest_share_cluster_name: + if not self._get_vserver_peers(dest_vserver, src_vserver): + # 3.1. Request vserver peer creation from new_replica's + # host to active replica's host + dest_share_client.create_vserver_peer( + dest_vserver, src_vserver, + peer_cluster_name=src_share_cluster_name) + + # 3.2. Accepts the vserver peering using active replica + # host's client + src_share_client.accept_vserver_peer(src_vserver, + dest_vserver) + + return (super(NetAppCmodeMultiSVMFileStorageLibrary, self) + .create_share_from_snapshot( + context, share, snapshot, share_server=share_server, + parent_share=parent_share)) diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py index 534b32b26a..c5af307c41 100644 --- a/manila/share/drivers/netapp/options.py +++ b/manila/share/drivers/netapp/options.py @@ -150,7 +150,13 @@ netapp_data_motion_opts = [ 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.'), ] + 'was triggered.'), + cfg.IntOpt('netapp_start_volume_move_timeout', + min=0, + default=3600, # One Hour, + help='The maximum time in seconds to wait for the completion ' + 'of a volume clone split operation in order to start a ' + 'volume move.'), ] CONF = cfg.CONF CONF.register_opts(netapp_proxy_opts) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index 1c2bc973ea..9cc5e1da8c 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -2195,6 +2195,46 @@ VOLUME_GET_ITER_CLONE_CHILDREN_RESPONSE = etree.XML(""" 'clone2': CLONE_CHILD_2, }) +VOLUME_GET_ITER_PARENT_SNAP_EMPTY_RESPONSE = etree.XML(""" + + + + + %(name)s + %(vserver)s + + + + 1 + +""" % { + 'vserver': VSERVER_NAME, + 'name': SHARE_NAME, +}) + +VOLUME_GET_ITER_PARENT_SNAP_RESPONSE = etree.XML(""" + + + + + + %(snapshot_name)s + + + + %(name)s + %(vserver)s + + + + 1 + +""" % { + 'snapshot_name': SNAPSHOT_NAME, + 'vserver': VSERVER_NAME, + 'name': SHARE_NAME, +}) + SIS_GET_ITER_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 09e45e692f..1e5f40e960 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 @@ -6698,3 +6698,72 @@ class NetAppClientCmodeTestCase(test.TestCase): self.assertEqual(fake.CLUSTER_NAME, result) self.client.send_request.assert_called_once_with( 'cluster-identity-get', api_args, enable_tunneling=False) + + @ddt.data('fake_snapshot_name', None) + def test_check_volume_clone_split_completed(self, get_clone_parent): + volume_name = fake.SHARE_NAME + mock_get_vol_clone_parent = self.mock_object( + self.client, 'get_volume_clone_parent_snaphot', + mock.Mock(return_value=get_clone_parent)) + + result = self.client.check_volume_clone_split_completed(volume_name) + + mock_get_vol_clone_parent.assert_called_once_with(volume_name) + expected_result = get_clone_parent is None + self.assertEqual(expected_result, result) + + def test_rehost_volume(self): + volume_name = fake.SHARE_NAME + vserver = fake.VSERVER_NAME + dest_vserver = fake.VSERVER_NAME_2 + api_args = { + 'volume': volume_name, + 'vserver': vserver, + 'destination-vserver': dest_vserver, + } + self.mock_object(self.client, 'send_request') + + self.client.rehost_volume(volume_name, vserver, dest_vserver) + + self.client.send_request.assert_called_once_with('volume-rehost', + api_args) + + @ddt.data( + {'fake_api_response': fake.VOLUME_GET_ITER_PARENT_SNAP_EMPTY_RESPONSE, + 'expected_snapshot_name': None}, + {'fake_api_response': fake.VOLUME_GET_ITER_PARENT_SNAP_RESPONSE, + 'expected_snapshot_name': fake.SNAPSHOT_NAME}, + {'fake_api_response': fake.NO_RECORDS_RESPONSE, + 'expected_snapshot_name': None}) + @ddt.unpack + def test_get_volume_clone_parent_snaphot(self, fake_api_response, + expected_snapshot_name): + + api_response = netapp_api.NaElement(fake_api_response) + self.mock_object(self.client, + 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_volume_clone_parent_snaphot(fake.SHARE_NAME) + + expected_api_args = { + 'query': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': fake.SHARE_NAME + } + } + }, + 'desired-attributes': { + 'volume-attributes': { + 'volume-clone-attributes': { + 'volume-clone-parent-attributes': { + 'snapshot-name': '' + } + } + } + } + } + self.client.send_iter_request.assert_called_once_with( + 'volume-get-iter', expected_api_args) + self.assertEqual(expected_snapshot_name, result) 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 2156e45d01..442a666a92 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 @@ -677,7 +677,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.context, fake.SHARE, fake.SNAPSHOT, - share_server=fake.SHARE_SERVER) + share_server=fake.SHARE_SERVER, + parent_share=fake.SHARE) mock_allocate_container_from_snapshot.assert_called_once_with( fake.SHARE, @@ -690,6 +691,516 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client) self.assertEqual('fake_export_location', result) + def _setup_mocks_for_create_share_from_snapshot( + self, allocate_attr=None, dest_cluster=fake.CLUSTER_NAME): + class FakeDBObj(dict): + def to_dict(self): + return self + + if allocate_attr is None: + allocate_attr = mock.Mock() + + self.src_vserver_client = mock.Mock() + self.mock_dm_session = mock.Mock() + self.fake_share = FakeDBObj(fake.SHARE) + self.fake_share_server = FakeDBObj(fake.SHARE_SERVER) + + self.mock_dm_constr = self.mock_object( + data_motion, "DataMotionSession", + mock.Mock(return_value=self.mock_dm_session)) + self.mock_dm_backend = self.mock_object( + self.mock_dm_session, 'get_backend_info_for_share', + mock.Mock(return_value=(None, + fake.VSERVER1, fake.BACKEND_NAME))) + self.mock_dm_get_src_client = self.mock_object( + data_motion, 'get_client_for_backend', + mock.Mock(return_value=self.src_vserver_client)) + self.mock_get_src_cluster = self.mock_object( + self.src_vserver_client, 'get_cluster_name', + mock.Mock(return_value=fake.CLUSTER_NAME)) + self.dest_vserver_client = mock.Mock() + self.mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER2, self.dest_vserver_client))) + self.mock_get_dest_cluster = self.mock_object( + self.dest_vserver_client, 'get_cluster_name', + mock.Mock(return_value=dest_cluster)) + self.mock_allocate_container_from_snapshot = self.mock_object( + self.library, '_allocate_container_from_snapshot', allocate_attr) + self.mock_allocate_container = self.mock_object( + self.library, '_allocate_container') + self.mock_dm_create_snapmirror = self.mock_object( + self.mock_dm_session, 'create_snapmirror') + self.mock_storage_update = self.mock_object( + self.library.private_storage, 'update') + self.mock_object(self.library, '_have_cluster_creds', + mock.Mock(return_value=True)) + + # Parent share on MANILA_HOST_2 + self.parent_share = copy.copy(fake.SHARE) + self.parent_share['share_server'] = fake.SHARE_SERVER_2 + self.parent_share['host'] = fake.MANILA_HOST_NAME_2 + self.parent_share_server = {} + ss_keys = ['id', 'identifier', 'backend_details', 'host'] + for key in ss_keys: + self.parent_share_server[key] = ( + self.parent_share['share_server'].get(key, None)) + self.temp_src_share = { + 'id': self.fake_share['id'], + 'host': self.parent_share['host'], + 'share_server': self.parent_share_server or None + } + + @ddt.data(fake.CLUSTER_NAME, fake.CLUSTER_NAME_2) + def test_create_share_from_snapshot_another_host(self, dest_cluster): + self._setup_mocks_for_create_share_from_snapshot( + dest_cluster=dest_cluster) + result = self.library.create_share_from_snapshot( + self.context, + self.fake_share, + fake.SNAPSHOT, + share_server=self.fake_share_server, + parent_share=self.parent_share) + + self.fake_share['share_server'] = self.fake_share_server + + self.mock_dm_constr.assert_called_once() + self.mock_dm_backend.assert_called_once_with(self.parent_share) + self.mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + self.mock_get_src_cluster.assert_called_once() + self.mock_get_vserver.assert_called_once_with(self.fake_share_server) + self.mock_get_dest_cluster.assert_called_once() + + if dest_cluster != fake.CLUSTER_NAME: + self.mock_allocate_container_from_snapshot.assert_called_once_with( + self.fake_share, fake.SNAPSHOT, fake.VSERVER1, + self.src_vserver_client, split=False) + self.mock_allocate_container.assert_called_once_with( + self.fake_share, fake.VSERVER2, + self.dest_vserver_client, replica=True) + self.mock_dm_create_snapmirror.asser_called_once() + self.temp_src_share['replica_state'] = ( + constants.REPLICA_STATE_ACTIVE) + state = self.library.STATE_SNAPMIRROR_DATA_COPYING + else: + self.mock_allocate_container_from_snapshot.assert_called_once_with( + self.fake_share, fake.SNAPSHOT, fake.VSERVER1, + self.src_vserver_client, split=True) + state = self.library.STATE_SPLITTING_VOLUME_CLONE + + self.temp_src_share['internal_state'] = state + self.temp_src_share['status'] = constants.STATUS_ACTIVE + str_temp_src_share = json.dumps(self.temp_src_share) + self.mock_storage_update.assert_called_once_with( + self.fake_share['id'], { + 'source_share': str_temp_src_share + }) + expected_return = {'status': constants.STATUS_CREATING_FROM_SNAPSHOT} + self.assertEqual(expected_return, result) + + def test_create_share_from_snapshot_another_host_driver_error(self): + self._setup_mocks_for_create_share_from_snapshot( + allocate_attr=mock.Mock(side_effect=exception.NetAppException)) + mock_delete_snapmirror = self.mock_object( + self.mock_dm_session, 'delete_snapmirror') + mock_get_backend_shr_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_share_exits = self.mock_object( + self.library, '_share_exists', + mock.Mock(return_value=True)) + mock_deallocate_container = self.mock_object( + self.library, '_deallocate_container') + + self.assertRaises(exception.NetAppException, + self.library.create_share_from_snapshot, + self.context, + self.fake_share, + fake.SNAPSHOT, + share_server=self.fake_share_server, + parent_share=self.parent_share) + + self.fake_share['share_server'] = self.fake_share_server + + self.mock_dm_constr.assert_called_once() + self.mock_dm_backend.assert_called_once_with(self.parent_share) + self.mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + self.mock_get_src_cluster.assert_called_once() + self.mock_get_vserver.assert_called_once_with(self.fake_share_server) + self.mock_get_dest_cluster.assert_called_once() + self.mock_allocate_container_from_snapshot.assert_called_once_with( + self.fake_share, fake.SNAPSHOT, fake.VSERVER1, + self.src_vserver_client, split=True) + mock_delete_snapmirror.assert_called_once_with(self.temp_src_share, + self.fake_share) + mock_get_backend_shr_name.assert_called_once_with( + self.fake_share['id']) + mock_share_exits.assert_called_once_with(fake.SHARE_NAME, + self.src_vserver_client) + mock_deallocate_container.assert_called_once_with( + fake.SHARE_NAME, self.src_vserver_client) + + def test__update_create_from_snapshot_status(self): + fake_result = mock.Mock() + mock_pvt_storage_get = self.mock_object( + self.library.private_storage, 'get', + mock.Mock(return_value=fake.SHARE)) + mock__create_continue = self.mock_object( + self.library, '_create_from_snapshot_continue', + mock.Mock(return_value=fake_result)) + + result = self.library._update_create_from_snapshot_status( + fake.SHARE, fake.SHARE_SERVER) + + mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + mock__create_continue.assert_called_once_with(fake.SHARE, + fake.SHARE_SERVER) + self.assertEqual(fake_result, result) + + def test__update_create_from_snapshot_status_missing_source_share(self): + mock_pvt_storage_get = self.mock_object( + self.library.private_storage, 'get', + mock.Mock(return_value=None)) + expected_result = {'status': constants.STATUS_ERROR} + result = self.library._update_create_from_snapshot_status( + fake.SHARE, fake.SHARE_SERVER) + mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + self.assertEqual(expected_result, result) + + def test__update_create_from_snapshot_status_driver_error(self): + fake_src_share = { + 'id': fake.SHARE['id'], + 'host': fake.SHARE['host'], + 'internal_state': 'fake_internal_state', + } + copy_fake_src_share = copy.deepcopy(fake_src_share) + src_vserver_client = mock.Mock() + mock_dm_session = mock.Mock() + mock_pvt_storage_get = self.mock_object( + self.library.private_storage, 'get', + mock.Mock(return_value=json.dumps(copy_fake_src_share))) + mock__create_continue = self.mock_object( + self.library, '_create_from_snapshot_continue', + mock.Mock(side_effect=exception.NetAppException)) + mock_dm_constr = self.mock_object( + data_motion, "DataMotionSession", + mock.Mock(return_value=mock_dm_session)) + mock_delete_snapmirror = self.mock_object( + mock_dm_session, 'delete_snapmirror') + mock_dm_backend = self.mock_object( + mock_dm_session, 'get_backend_info_for_share', + mock.Mock(return_value=(None, + fake.VSERVER1, fake.BACKEND_NAME))) + mock_dm_get_src_client = self.mock_object( + data_motion, 'get_client_for_backend', + mock.Mock(return_value=src_vserver_client)) + mock_get_backend_shr_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_share_exits = self.mock_object( + self.library, '_share_exists', + mock.Mock(return_value=True)) + mock_deallocate_container = self.mock_object( + self.library, '_deallocate_container') + mock_pvt_storage_delete = self.mock_object( + self.library.private_storage, 'delete') + + result = self.library._update_create_from_snapshot_status( + fake.SHARE, fake.SHARE_SERVER) + expected_result = {'status': constants.STATUS_ERROR} + + mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + mock__create_continue.assert_called_once_with(fake.SHARE, + fake.SHARE_SERVER) + mock_dm_constr.assert_called_once() + mock_delete_snapmirror.assert_called_once_with(fake_src_share, + fake.SHARE) + mock_dm_backend.assert_called_once_with(fake_src_share) + mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + mock_get_backend_shr_name.assert_called_once_with(fake_src_share['id']) + mock_share_exits.assert_called_once_with(fake.SHARE_NAME, + src_vserver_client) + mock_deallocate_container.assert_called_once_with(fake.SHARE_NAME, + src_vserver_client) + mock_pvt_storage_delete.assert_called_once_with(fake.SHARE['id']) + self.assertEqual(expected_result, result) + + def _setup_mocks_for_create_from_snapshot_continue( + self, src_host=fake.MANILA_HOST_NAME, + dest_host=fake.MANILA_HOST_NAME, split_completed_result=True, + move_completed_result=True, share_internal_state='fake_state', + replica_state='in_sync'): + self.fake_export_location = 'fake_export_location' + self.fake_src_share = { + 'id': fake.SHARE['id'], + 'host': src_host, + 'internal_state': share_internal_state, + } + self.copy_fake_src_share = copy.deepcopy(self.fake_src_share) + src_pool = src_host.split('#')[1] + dest_pool = dest_host.split('#')[1] + self.src_vserver_client = mock.Mock() + self.dest_vserver_client = mock.Mock() + self.mock_dm_session = mock.Mock() + + self.mock_dm_constr = self.mock_object( + data_motion, "DataMotionSession", + mock.Mock(return_value=self.mock_dm_session)) + self.mock_pvt_storage_get = self.mock_object( + self.library.private_storage, 'get', + mock.Mock(return_value=json.dumps(self.copy_fake_src_share))) + self.mock_dm_backend = self.mock_object( + self.mock_dm_session, 'get_backend_info_for_share', + mock.Mock(return_value=(None, + fake.VSERVER1, fake.BACKEND_NAME))) + self.mock_extract_host = self.mock_object( + share_utils, 'extract_host', + mock.Mock(side_effect=[src_pool, dest_pool])) + self.mock_dm_get_src_client = self.mock_object( + data_motion, 'get_client_for_backend', + mock.Mock(return_value=self.src_vserver_client)) + self.mock_get_vserver = self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER2, self.dest_vserver_client))) + self.mock_split_completed = self.mock_object( + self.library, '_check_volume_clone_split_completed', + mock.Mock(return_value=split_completed_result)) + self.mock_rehost_vol = self.mock_object( + self.library, '_rehost_and_mount_volume') + self.mock_move_vol = self.mock_object(self.library, + '_move_volume_after_splitting') + self.mock_move_completed = self.mock_object( + self.library, '_check_volume_move_completed', + mock.Mock(return_value=move_completed_result)) + self.mock_update_rep_state = self.mock_object( + self.library, 'update_replica_state', + mock.Mock(return_value=replica_state) + ) + self.mock_update_snapmirror = self.mock_object( + self.mock_dm_session, 'update_snapmirror') + self.mock_break_snapmirror = self.mock_object( + self.mock_dm_session, 'break_snapmirror') + self.mock_delete_snapmirror = self.mock_object( + self.mock_dm_session, 'delete_snapmirror') + self.mock_get_backend_shr_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock__delete_share = self.mock_object(self.library, + '_delete_share') + self.mock_set_vol_size_fixes = self.mock_object( + self.dest_vserver_client, 'set_volume_filesys_size_fixed') + self.mock_create_export = self.mock_object( + self.library, '_create_export', + mock.Mock(return_value=self.fake_export_location)) + self.mock_pvt_storage_update = self.mock_object( + self.library.private_storage, 'update') + self.mock_pvt_storage_delete = self.mock_object( + self.library.private_storage, 'delete') + self.mock_get_extra_specs_qos = self.mock_object( + share_types, 'get_extra_specs_from_share', + mock.Mock(return_value=fake.EXTRA_SPEC_WITH_QOS)) + self.mock__get_provisioning_opts = self.mock_object( + self.library, '_get_provisioning_options', + mock.Mock(return_value=copy.deepcopy(fake.PROVISIONING_OPTIONS)) + ) + self.mock_modify_create_qos = self.mock_object( + self.library, '_modify_or_create_qos_for_existing_share', + mock.Mock(return_value=fake.QOS_POLICY_GROUP_NAME)) + self.mock_modify_vol = self.mock_object(self.dest_vserver_client, + 'modify_volume') + self.mock_get_backend_qos_name = self.mock_object( + self.library, '_get_backend_qos_policy_group_name', + mock.Mock(return_value=fake.QOS_POLICY_GROUP_NAME)) + self.mock_mark_qos_deletion = self.mock_object( + self.src_vserver_client, 'mark_qos_policy_group_for_deletion') + + @ddt.data(fake.MANILA_HOST_NAME, fake.MANILA_HOST_NAME_2) + def test__create_from_snapshot_continue_state_splitting(self, src_host): + self._setup_mocks_for_create_from_snapshot_continue( + src_host=src_host, + share_internal_state=self.library.STATE_SPLITTING_VOLUME_CLONE) + + result = self.library._create_from_snapshot_continue(fake.SHARE, + fake.SHARE_SERVER) + fake.SHARE['share_server'] = fake.SHARE_SERVER + self.mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + self.mock_dm_backend.assert_called_once_with(self.fake_src_share) + self.mock_extract_host.assert_has_calls([ + mock.call(self.fake_src_share['host'], level='pool'), + mock.call(fake.SHARE['host'], level='pool'), + ]) + self.mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + self.mock_get_vserver.assert_called_once_with(fake.SHARE_SERVER) + self.mock_split_completed.assert_called_once_with( + self.fake_src_share, self.src_vserver_client) + self.mock_get_backend_qos_name.assert_called_once_with(fake.SHARE_ID) + self.mock_mark_qos_deletion.assert_called_once_with( + fake.QOS_POLICY_GROUP_NAME) + self.mock_rehost_vol.assert_called_once_with( + fake.SHARE, fake.VSERVER1, self.src_vserver_client, + fake.VSERVER2, self.dest_vserver_client) + if src_host != fake.MANILA_HOST_NAME: + expected_result = { + 'status': constants.STATUS_CREATING_FROM_SNAPSHOT + } + self.mock_move_vol.assert_called_once_with( + self.fake_src_share, fake.SHARE, fake.SHARE_SERVER, + cutover_action='defer') + self.fake_src_share['internal_state'] = ( + self.library.STATE_MOVING_VOLUME) + self.mock_pvt_storage_update.asser_called_once_with( + fake.SHARE['id'], + {'source_share': json.dumps(self.fake_src_share)} + ) + self.assertEqual(expected_result, result) + else: + self.mock_get_extra_specs_qos.assert_called_once_with(fake.SHARE) + self.mock__get_provisioning_opts.assert_called_once_with( + fake.EXTRA_SPEC_WITH_QOS) + self.mock_modify_create_qos.assert_called_once_with( + fake.SHARE, fake.EXTRA_SPEC_WITH_QOS, fake.VSERVER2, + self.dest_vserver_client) + self.mock_get_backend_shr_name.assert_called_once_with( + fake.SHARE_ID) + self.mock_modify_vol.assert_called_once_with( + fake.POOL_NAME, fake.SHARE_NAME, + **fake.PROVISIONING_OPTIONS_WITH_QOS) + self.mock_pvt_storage_delete.assert_called_once_with( + fake.SHARE['id']) + self.mock_create_export.assert_called_once_with( + fake.SHARE, fake.SHARE_SERVER, fake.VSERVER2, + self.dest_vserver_client, clear_current_export_policy=False) + expected_result = { + 'status': constants.STATUS_AVAILABLE, + 'export_locations': self.fake_export_location, + } + self.assertEqual(expected_result, result) + + @ddt.data(True, False) + def test__create_from_snapshot_continue_state_moving(self, move_completed): + self._setup_mocks_for_create_from_snapshot_continue( + share_internal_state=self.library.STATE_MOVING_VOLUME, + move_completed_result=move_completed) + + result = self.library._create_from_snapshot_continue(fake.SHARE, + fake.SHARE_SERVER) + expect_result = { + 'status': constants.STATUS_CREATING_FROM_SNAPSHOT + } + fake.SHARE['share_server'] = fake.SHARE_SERVER + self.mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + self.mock_dm_backend.assert_called_once_with(self.fake_src_share) + self.mock_extract_host.assert_has_calls([ + mock.call(self.fake_src_share['host'], level='pool'), + mock.call(fake.SHARE['host'], level='pool'), + ]) + self.mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + self.mock_get_vserver.assert_called_once_with(fake.SHARE_SERVER) + + self.mock_move_completed.assert_called_once_with( + fake.SHARE, fake.SHARE_SERVER) + if move_completed: + expect_result['status'] = constants.STATUS_AVAILABLE + self.mock_pvt_storage_delete.assert_called_once_with( + fake.SHARE['id']) + self.mock_create_export.assert_called_once_with( + fake.SHARE, fake.SHARE_SERVER, fake.VSERVER2, + self.dest_vserver_client, clear_current_export_policy=False) + expect_result['export_locations'] = self.fake_export_location + self.assertEqual(expect_result, result) + else: + self.mock_pvt_storage_update.asser_called_once_with( + fake.SHARE['id'], + {'source_share': json.dumps(self.fake_src_share)} + ) + self.assertEqual(expect_result, result) + + @ddt.data('in_sync', 'out_of_sync') + def test__create_from_snapshot_continue_state_snapmirror(self, + replica_state): + self._setup_mocks_for_create_from_snapshot_continue( + share_internal_state=self.library.STATE_SNAPMIRROR_DATA_COPYING, + replica_state=replica_state) + + result = self.library._create_from_snapshot_continue(fake.SHARE, + fake.SHARE_SERVER) + expect_result = { + 'status': constants.STATUS_CREATING_FROM_SNAPSHOT + } + fake.SHARE['share_server'] = fake.SHARE_SERVER + self.mock_pvt_storage_get.assert_called_once_with(fake.SHARE['id'], + 'source_share') + self.mock_dm_backend.assert_called_once_with(self.fake_src_share) + self.mock_extract_host.assert_has_calls([ + mock.call(self.fake_src_share['host'], level='pool'), + mock.call(fake.SHARE['host'], level='pool'), + ]) + self.mock_dm_get_src_client.assert_called_once_with( + fake.BACKEND_NAME, vserver_name=fake.VSERVER1) + self.mock_get_vserver.assert_called_once_with(fake.SHARE_SERVER) + + self.mock_update_rep_state.assert_called_once_with( + None, [self.fake_src_share], fake.SHARE, [], [], fake.SHARE_SERVER) + if replica_state == constants.REPLICA_STATE_IN_SYNC: + self.mock_update_snapmirror.assert_called_once_with( + self.fake_src_share, fake.SHARE) + self.mock_break_snapmirror.assert_called_once_with( + self.fake_src_share, fake.SHARE) + self.mock_delete_snapmirror.assert_called_once_with( + self.fake_src_share, fake.SHARE) + self.mock_get_backend_shr_name.assert_has_calls( + [mock.call(self.fake_src_share['id']), + mock.call(fake.SHARE_ID)]) + self.mock__delete_share.assert_called_once_with( + self.fake_src_share, self.src_vserver_client, + remove_export=False) + self.mock_set_vol_size_fixes.assert_called_once_with( + fake.SHARE_NAME, filesys_size_fixed=False) + self.mock_get_extra_specs_qos.assert_called_once_with(fake.SHARE) + self.mock__get_provisioning_opts.assert_called_once_with( + fake.EXTRA_SPEC_WITH_QOS) + self.mock_modify_create_qos.assert_called_once_with( + fake.SHARE, fake.EXTRA_SPEC_WITH_QOS, fake.VSERVER2, + self.dest_vserver_client) + self.mock_modify_vol.assert_called_once_with( + fake.POOL_NAME, fake.SHARE_NAME, + **fake.PROVISIONING_OPTIONS_WITH_QOS) + expect_result['status'] = constants.STATUS_AVAILABLE + self.mock_pvt_storage_delete.assert_called_once_with( + fake.SHARE['id']) + self.mock_create_export.assert_called_once_with( + fake.SHARE, fake.SHARE_SERVER, fake.VSERVER2, + self.dest_vserver_client, clear_current_export_policy=False) + expect_result['export_locations'] = self.fake_export_location + self.assertEqual(expect_result, result) + elif replica_state not in [constants.STATUS_ERROR, None]: + self.mock_pvt_storage_update.asser_called_once_with( + fake.SHARE['id'], + {'source_share': json.dumps(self.fake_src_share)} + ) + self.assertEqual(expect_result, result) + + def test__create_from_snapshot_continue_state_unknown(self): + self._setup_mocks_for_create_from_snapshot_continue( + share_internal_state='unknown_state') + + self.assertRaises(exception.NetAppException, + self.library._create_from_snapshot_continue, + fake.SHARE, + fake.SHARE_SERVER) + + self.mock_pvt_storage_delete.assert_called_once_with(fake.SHARE_ID) + @ddt.data(False, True) def test_allocate_container(self, hide_snapdir): @@ -709,7 +1220,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client) mock_get_provisioning_opts.assert_called_once_with( - fake.SHARE_INSTANCE, fake.VSERVER1, replica=False) + fake.SHARE_INSTANCE, fake.VSERVER1, vserver_client=vserver_client, + replica=False) vserver_client.create_volume.assert_called_once_with( fake.POOL_NAME, fake.SHARE_NAME, fake.SHARE['size'], @@ -745,7 +1257,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client, replica=True) mock_get_provisioning_opts.assert_called_once_with( - fake.SHARE_INSTANCE, fake.VSERVER1, replica=True) + fake.SHARE_INSTANCE, fake.VSERVER1, vserver_client=vserver_client, + replica=True) vserver_client.create_volume.assert_called_once_with( fake.POOL_NAME, fake.SHARE_NAME, fake.SHARE['size'], @@ -842,6 +1355,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): def test_get_provisioning_options_for_share(self, extra_specs, is_replica): qos = True if fake.QOS_EXTRA_SPEC in extra_specs else False + vserver_client = mock.Mock() mock_get_extra_specs_from_share = self.mock_object( share_types, 'get_extra_specs_from_share', mock.Mock(return_value=extra_specs)) @@ -861,7 +1375,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): return_value=fake.QOS_POLICY_GROUP_NAME)) result = self.library._get_provisioning_options_for_share( - fake.SHARE_INSTANCE, fake.VSERVER1, replica=is_replica) + fake.SHARE_INSTANCE, fake.VSERVER1, vserver_client=vserver_client, + replica=is_replica) if qos and is_replica: expected_provisioning_opts = fake.PROVISIONING_OPTIONS @@ -870,7 +1385,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): expected_provisioning_opts = fake.PROVISIONING_OPTIONS_WITH_QOS mock_create_qos_policy_group.assert_called_once_with( fake.SHARE_INSTANCE, fake.VSERVER1, - {fake.QOS_NORMALIZED_SPEC: 3000}) + {fake.QOS_NORMALIZED_SPEC: 3000}, vserver_client) self.assertEqual(expected_provisioning_opts, result) mock_get_extra_specs_from_share.assert_called_once_with( @@ -1053,14 +1568,15 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.AGGREGATES[1], fake.EXTRA_SPEC) - @ddt.data({'provider_location': None, 'size': 50, 'hide_snapdir': True}, + @ddt.data({'provider_location': None, 'size': 50, 'hide_snapdir': True, + 'split': None}, {'provider_location': 'fake_location', 'size': 30, - 'hide_snapdir': False}, + 'hide_snapdir': False, 'split': True}, {'provider_location': 'fake_location', 'size': 20, - 'hide_snapdir': True}) + 'hide_snapdir': True, 'split': False}) @ddt.unpack def test_allocate_container_from_snapshot( - self, provider_location, size, hide_snapdir): + self, provider_location, size, hide_snapdir, split): provisioning_options = copy.deepcopy(fake.PROVISIONING_OPTIONS) provisioning_options['hide_snapdir'] = hide_snapdir @@ -1070,6 +1586,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver = fake.VSERVER1 vserver_client = mock.Mock() original_snapshot_size = 20 + expected_split_op = split or fake.PROVISIONING_OPTIONS['split'] fake_share_inst = copy.deepcopy(fake.SHARE_INSTANCE) fake_share_inst['size'] = size @@ -1089,12 +1606,12 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): parent_snapshot_name = self.library._get_backend_snapshot_name( fake_snapshot['id']) if not provider_location else 'fake_location' mock_get_provisioning_opts.assert_called_once_with( - fake_share_inst, fake.VSERVER1) + fake_share_inst, fake.VSERVER1, vserver_client=vserver_client) vserver_client.create_volume_clone.assert_called_once_with( share_name, parent_share_name, parent_snapshot_name, thin_provisioned=True, snapshot_policy='default', - language='en-US', dedup_enabled=True, split=True, encrypt=False, - compression_enabled=False, max_files=5000) + language='en-US', dedup_enabled=True, split=expected_split_op, + encrypt=False, compression_enabled=False, max_files=5000) if size > original_snapshot_size: vserver_client.set_volume_size.assert_called_once_with( share_name, size) @@ -1150,7 +1667,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_remove_export.assert_called_once_with(fake.SHARE, vserver_client) mock_deallocate_container.assert_called_once_with(share_name, vserver_client) - (self.library._client.mark_qos_policy_group_for_deletion + (vserver_client.mark_qos_policy_group_for_deletion .assert_called_once_with(qos_policy_name)) self.assertEqual(0, lib_base.LOG.info.call_count) @@ -4555,7 +5072,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertTrue(mock_info_log.called) mock_move.assert_called_once_with( fake.SHARE_NAME, fake.VSERVER1, 'destination_pool', - encrypt_destination=False) + cutover_action='wait', encrypt_destination=False) def test_migration_start_encrypted_destination(self): mock_info_log = self.mock_object(lib_base.LOG, 'info') @@ -4581,7 +5098,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertTrue(mock_info_log.called) mock_move.assert_called_once_with( fake.SHARE_NAME, fake.VSERVER1, 'destination_pool', - encrypt_destination=True) + cutover_action='wait', encrypt_destination=True) def test_migration_continue_volume_move_failed(self): source_snapshots = mock.Mock() @@ -4881,7 +5398,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertEqual(qos_policy_name, retval) self.library._client.qos_policy_group_modify.assert_not_called() self.library._create_qos_policy_group.assert_called_once_with( - share_obj, fake.VSERVER1, {'maxiops': '3000'}) + share_obj, fake.VSERVER1, {'maxiops': '3000'}, + vserver_client=vserver_client) @ddt.data(utils.annotated('volume_has_shared_qos_policy', (2, )), utils.annotated('volume_has_nonshared_qos_policy', (1, ))) @@ -4920,7 +5438,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'id': fake.SHARE['id'], } mock_create_qos_policy.assert_called_once_with( - share_obj, fake.VSERVER1, {'maxiops': '3000'}) + share_obj, fake.VSERVER1, {'maxiops': '3000'}, + vserver_client=vserver_client) self.library._client.qos_policy_group_modify.assert_not_called() self.library._client.qos_policy_group_rename.assert_not_called() @@ -5072,3 +5591,131 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.call('share_s_2', True), mock.call('share_s_3', True), ]) + + def test__check_volume_clone_split_completed(self): + vserver_client = mock.Mock() + mock_share_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + vserver_client.check_volume_clone_split_completed.return_value = ( + fake.CDOT_SNAPSHOT_BUSY_SNAPMIRROR) + + self.library._check_volume_clone_split_completed(fake.SHARE, + vserver_client) + + mock_share_name.assert_called_once_with(fake.SHARE_ID) + check_call = vserver_client.check_volume_clone_split_completed + check_call.assert_called_once_with(fake.SHARE_NAME) + + @ddt.data(constants.STATUS_ACTIVE, constants.STATUS_CREATING_FROM_SNAPSHOT) + def test_get_share_status(self, status): + mock_update_from_snap = self.mock_object( + self.library, '_update_create_from_snapshot_status') + fake.SHARE['status'] = status + + self.library.get_share_status(fake.SHARE, fake.SHARE_SERVER) + + if status == constants.STATUS_CREATING_FROM_SNAPSHOT: + mock_update_from_snap.assert_called_once_with(fake.SHARE, + fake.SHARE_SERVER) + else: + mock_update_from_snap.assert_not_called() + + def test_volume_rehost(self): + mock_share_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_rehost = self.mock_object(self.client, 'rehost_volume') + + self.library.volume_rehost(fake.SHARE, fake.VSERVER1, fake.VSERVER2) + + mock_share_name.assert_called_once_with(fake.SHARE_ID) + mock_rehost.assert_called_once_with(fake.SHARE_NAME, fake.VSERVER1, + fake.VSERVER2) + + def test__rehost_and_mount_volume(self): + mock_share_name = self.mock_object( + self.library, '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + mock_rehost = self.mock_object(self.library, 'volume_rehost', + mock.Mock()) + src_vserver_client = mock.Mock() + mock_unmount = self.mock_object(src_vserver_client, 'unmount_volume') + dst_vserver_client = mock.Mock() + mock_mount = self.mock_object(dst_vserver_client, 'mount_volume') + + self.library._rehost_and_mount_volume( + fake.SHARE, fake.VSERVER1, src_vserver_client, fake.VSERVER2, + dst_vserver_client) + + mock_share_name.assert_called_once_with(fake.SHARE_ID) + mock_unmount.assert_called_once_with(fake.SHARE_NAME) + mock_rehost.assert_called_once_with(fake.SHARE, fake.VSERVER1, + fake.VSERVER2) + mock_mount.assert_called_once_with(fake.SHARE_NAME) + + def test__move_volume_after_splitting(self): + src_share = fake_share.fake_share_instance(id='source-share-instance') + dest_share = fake_share.fake_share_instance(id='dest-share-instance') + cutover_action = 'defer' + self.library.configuration.netapp_start_volume_move_timeout = 15 + + self.mock_object(time, 'sleep') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + mock_vol_move = self.mock_object(self.library, '_move_volume') + + self.library._move_volume_after_splitting( + src_share, dest_share, share_server=fake.SHARE_SERVER, + cutover_action=cutover_action) + + mock_vol_move.assert_called_once_with(src_share, dest_share, + fake.SHARE_SERVER, + cutover_action) + self.assertEqual(0, mock_warning_log.call_count) + + def test__move_volume_after_splitting_timeout(self): + src_share = fake_share.fake_share_instance(id='source-share-instance') + dest_share = fake_share.fake_share_instance(id='dest-share-instance') + self.library.configuration.netapp_start_volume_move_timeout = 15 + cutover_action = 'defer' + + self.mock_object(time, 'sleep') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + undergoing_split_op_msg = ( + 'The volume is undergoing a clone split operation.') + na_api_error = netapp_api.NaApiError(code=netapp_api.EAPIERROR, + message=undergoing_split_op_msg) + mock_move_vol = self.mock_object( + self.library, '_move_volume', mock.Mock(side_effect=na_api_error)) + + self.assertRaises(exception.NetAppException, + self.library._move_volume_after_splitting, + src_share, dest_share, + share_server=fake.SHARE_SERVER, + cutover_action=cutover_action) + + self.assertEqual(3, mock_move_vol.call_count) + self.assertEqual(3, mock_warning_log.call_count) + + def test__move_volume_after_splitting_api_not_found(self): + src_share = fake_share.fake_share_instance(id='source-share-instance') + dest_share = fake_share.fake_share_instance(id='dest-share-instance') + self.library.configuration.netapp_start_volume_move_timeout = 15 + cutover_action = 'defer' + + self.mock_object(time, 'sleep') + mock_warning_log = self.mock_object(lib_base.LOG, 'warning') + na_api_error = netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND) + mock_move_vol = self.mock_object( + self.library, '_move_volume', mock.Mock(side_effect=na_api_error)) + + self.assertRaises(exception.NetAppException, + self.library._move_volume_after_splitting, + src_share, dest_share, + share_server=fake.SHARE_SERVER, + cutover_action=cutover_action) + + mock_move_vol.assert_called_once_with(src_share, dest_share, + fake.SHARE_SERVER, + cutover_action) + mock_warning_log.assert_not_called() diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py index 209b6e774a..bbb474adb5 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py @@ -1108,3 +1108,108 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library._client.delete_vserver_peer.assert_called_once_with( self.fake_vserver, self.fake_new_vserver_name ) + + def test_create_share_from_snaphot(self): + fake_parent_share = copy.deepcopy(fake.SHARE) + fake_parent_share['id'] = fake.SHARE_ID2 + mock_create_from_snap = self.mock_object( + lib_base.NetAppCmodeFileStorageLibrary, + 'create_share_from_snapshot') + + self.library.create_share_from_snapshot( + None, fake.SHARE, fake.SNAPSHOT, share_server=fake.SHARE_SERVER, + parent_share=fake_parent_share) + + mock_create_from_snap.assert_called_once_with( + None, fake.SHARE, fake.SNAPSHOT, share_server=fake.SHARE_SERVER, + parent_share=fake_parent_share + ) + + @ddt.data( + {'src_cluster_name': fake.CLUSTER_NAME, + 'dest_cluster_name': fake.CLUSTER_NAME, 'has_vserver_peers': None}, + {'src_cluster_name': fake.CLUSTER_NAME, + 'dest_cluster_name': fake.CLUSTER_NAME_2, 'has_vserver_peers': False}, + {'src_cluster_name': fake.CLUSTER_NAME, + 'dest_cluster_name': fake.CLUSTER_NAME_2, 'has_vserver_peers': True} + ) + @ddt.unpack + def test_create_share_from_snaphot_different_hosts(self, src_cluster_name, + dest_cluster_name, + has_vserver_peers): + class FakeDBObj(dict): + def to_dict(self): + return self + fake_parent_share = copy.deepcopy(fake.SHARE) + fake_parent_share['id'] = fake.SHARE_ID2 + fake_parent_share['host'] = fake.MANILA_HOST_NAME_2 + fake_share = FakeDBObj(fake.SHARE) + fake_share_server = FakeDBObj(fake.SHARE_SERVER) + src_vserver = fake.VSERVER2 + dest_vserver = fake.VSERVER1 + src_backend = fake.BACKEND_NAME + dest_backend = fake.BACKEND_NAME_2 + mock_dm_session = mock.Mock() + + mock_dm_constr = self.mock_object( + data_motion, "DataMotionSession", + mock.Mock(return_value=mock_dm_session)) + mock_get_vserver = self.mock_object( + mock_dm_session, 'get_vserver_from_share', + mock.Mock(side_effect=[src_vserver, dest_vserver])) + src_vserver_client = mock.Mock() + dest_vserver_client = mock.Mock() + mock_extract_host = self.mock_object( + share_utils, 'extract_host', + mock.Mock(side_effect=[src_backend, dest_backend])) + mock_dm_get_client = self.mock_object( + data_motion, 'get_client_for_backend', + mock.Mock(side_effect=[src_vserver_client, dest_vserver_client])) + mock_get_src_cluster_name = self.mock_object( + src_vserver_client, 'get_cluster_name', + mock.Mock(return_value=src_cluster_name)) + mock_get_dest_cluster_name = self.mock_object( + dest_vserver_client, 'get_cluster_name', + mock.Mock(return_value=dest_cluster_name)) + mock_get_vserver_peers = self.mock_object( + self.library, '_get_vserver_peers', + mock.Mock(return_value=has_vserver_peers)) + mock_create_vserver_peer = self.mock_object(dest_vserver_client, + 'create_vserver_peer') + mock_accept_peer = self.mock_object(src_vserver_client, + 'accept_vserver_peer') + mock_create_from_snap = self.mock_object( + lib_base.NetAppCmodeFileStorageLibrary, + 'create_share_from_snapshot') + + self.library.create_share_from_snapshot( + None, fake_share, fake.SNAPSHOT, share_server=fake_share_server, + parent_share=fake_parent_share) + + internal_share = copy.deepcopy(fake.SHARE) + internal_share['share_server'] = copy.deepcopy(fake.SHARE_SERVER) + + mock_dm_constr.assert_called_once() + mock_get_vserver.assert_has_calls([mock.call(fake_parent_share), + mock.call(internal_share)]) + mock_extract_host.assert_has_calls([ + mock.call(fake_parent_share['host'], level='backend_name'), + mock.call(internal_share['host'], level='backend_name')]) + mock_dm_get_client.assert_has_calls([ + mock.call(src_backend, vserver_name=src_vserver), + mock.call(dest_backend, vserver_name=dest_vserver) + ]) + mock_get_src_cluster_name.assert_called_once() + mock_get_dest_cluster_name.assert_called_once() + if src_cluster_name != dest_cluster_name: + mock_get_vserver_peers.assert_called_once_with(dest_vserver, + src_vserver) + if not has_vserver_peers: + mock_create_vserver_peer.assert_called_once_with( + dest_vserver, src_vserver, + peer_cluster_name=src_cluster_name) + mock_accept_peer.assert_called_once_with(src_vserver, + dest_vserver) + mock_create_from_snap.assert_called_once_with( + None, fake.SHARE, fake.SNAPSHOT, share_server=fake.SHARE_SERVER, + parent_share=fake_parent_share) diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index d2de19d000..a5bf13ce38 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -18,12 +18,15 @@ import copy from manila.common import constants import manila.tests.share.drivers.netapp.fakes as na_fakes - +CLUSTER_NAME = 'fake_cluster' +CLUSTER_NAME_2 = 'fake_cluster_2' BACKEND_NAME = 'fake_backend_name' +BACKEND_NAME_2 = 'fake_backend_name_2' DRIVER_NAME = 'fake_driver_name' APP_VERSION = 'fake_app_vsersion' HOST_NAME = 'fake_host' POOL_NAME = 'fake_pool' +POOL_NAME_2 = 'fake_pool_2' VSERVER1 = 'fake_vserver_1' VSERVER2 = 'fake_vserver_2' LICENSES = ('base', 'cifs', 'fcp', 'flexclone', 'iscsi', 'nfs', 'snapmirror', @@ -73,6 +76,10 @@ MTU = 1234 DEFAULT_MTU = 1500 MANILA_HOST_NAME = '%(host)s@%(backend)s#%(pool)s' % { 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME} +MANILA_HOST_NAME_2 = '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME, 'pool': POOL_NAME_2} +MANILA_HOST_NAME_3 = '%(host)s@%(backend)s#%(pool)s' % { + 'host': HOST_NAME, 'backend': BACKEND_NAME_2, 'pool': POOL_NAME_2} QOS_EXTRA_SPEC = 'netapp:maxiops' QOS_SIZE_DEPENDENT_EXTRA_SPEC = 'netapp:maxbpspergib' QOS_NORMALIZED_SPEC = 'maxiops' @@ -365,6 +372,16 @@ SHARE_SERVER = { ADMIN_NETWORK_ALLOCATIONS), } +SHARE_SERVER_2 = { + 'id': 'fake_id_2', + 'share_network_id': 'c5b3a865-56d0-4d88-abe5-879965e099c9', + 'backend_details': { + 'vserver_name': VSERVER2 + }, + 'network_allocations': (USER_NETWORK_ALLOCATIONS + + ADMIN_NETWORK_ALLOCATIONS), +} + VSERVER_PEER = [{ 'vserver': VSERVER1, 'peer-vserver': VSERVER2, diff --git a/releasenotes/notes/netapp-create-share-from-snapshot-another-pool-330639b57aa5f04d.yaml b/releasenotes/notes/netapp-create-share-from-snapshot-another-pool-330639b57aa5f04d.yaml new file mode 100644 index 0000000000..e843bc3bc3 --- /dev/null +++ b/releasenotes/notes/netapp-create-share-from-snapshot-another-pool-330639b57aa5f04d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The NetApp driver now supports efficiently creating new shares from + snapshots in pools or back ends different than that of the source share. In + order to have this functionality working across different back ends, + replication must be enabled and configured accordingly.