diff --git a/manila/opts.py b/manila/opts.py index e836605126..ec0d81970a 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -175,6 +175,7 @@ _global_opt_lists = [ manila.share.drivers.netapp.options.netapp_basicauth_opts, manila.share.drivers.netapp.options.netapp_provisioning_opts, manila.share.drivers.netapp.options.netapp_data_motion_opts, + manila.share.drivers.netapp.options.netapp_backup_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/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 03b6edb88f..7f04476c33 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -2899,6 +2899,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): }, 'volume-space-attributes': { 'size': None, + 'size-used': None, }, }, }, @@ -2946,6 +2947,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): 'type': volume_id_attributes.get_child_content('type'), 'style': volume_id_attributes.get_child_content('style'), 'size': volume_space_attributes.get_child_content('size'), + 'size-used': volume_space_attributes.get_child_content( + 'size-used'), 'qos-policy-group-name': volume_qos_attributes.get_child_content( 'policy-group-name'), 'style-extended': volume_id_attributes.get_child_content( @@ -3333,9 +3336,12 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): self.send_request('volume-destroy', {'name': volume_name}) @na_utils.trace - def create_snapshot(self, volume_name, snapshot_name): + def create_snapshot(self, volume_name, snapshot_name, + snapmirror_label=None): """Creates a volume snapshot.""" api_args = {'volume': volume_name, 'snapshot': snapshot_name} + if snapmirror_label is not None: + api_args['snapmirror-label'] = snapmirror_label self.send_request('snapshot-create', api_args) @na_utils.trace @@ -3345,7 +3351,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): 'volume %(volume)s', {'snapshot': snapshot_name, 'volume': volume_name}) - """Gets a single snapshot.""" + # Gets a single snapshot. api_args = { 'query': { 'snapshot-info': { @@ -5016,15 +5022,20 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): for snapshot_info in attributes_list.get_children()] @na_utils.trace - def create_snapmirror_policy(self, policy_name, type='async_mirror', + def create_snapmirror_policy(self, policy_name, + policy_type='async_mirror', discard_network_info=True, - preserve_snapshots=True): + preserve_snapshots=True, + snapmirror_label='all_source_snapshots', + keep=1 + ): """Creates a SnapMirror policy for a vServer.""" + self._ensure_snapmirror_v2() api_args = { 'policy-name': policy_name, - 'type': type, + 'type': policy_type, } if discard_network_info: @@ -5037,8 +5048,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): if preserve_snapshots: api_args = { 'policy-name': policy_name, - 'snapmirror-label': 'all_source_snapshots', - 'keep': '1', + 'snapmirror-label': snapmirror_label, + 'keep': keep, 'preserve': 'false' } @@ -6239,3 +6250,41 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): # Convert Bytes to GBs. return (total_volumes_size / 1024**3) + + @na_utils.trace + def snapmirror_restore_vol(self, source_path=None, dest_path=None, + source_vserver=None, dest_vserver=None, + source_volume=None, dest_volume=None, + source_snapshot=None): + """Restore snapshot copy from destination volume to source volume""" + self._ensure_snapmirror_v2() + + api_args = self._build_snapmirror_request( + source_path, dest_path, source_vserver, + dest_vserver, source_volume, dest_volume) + if source_snapshot: + api_args["source-snapshot"] = source_snapshot + self.send_request('snapmirror-restore', api_args) + + @na_utils.trace + def list_volume_snapshots(self, volume_name, snapmirror_label=None, + newer_than=None): + """Gets SnapMirror snapshots on a volume.""" + api_args = { + 'query': { + 'snapshot-info': { + 'volume': volume_name, + }, + }, + } + if newer_than: + api_args['query']['snapshot-info'][ + 'access-time'] = '>' + newer_than + if snapmirror_label: + api_args['query']['snapshot-info'][ + 'snapmirror-label'] = snapmirror_label + result = self.send_iter_request('snapshot-get-iter', api_args) + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + return [snapshot_info.get_child_content('name') + for snapshot_info in attributes_list.get_children()] diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py b/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py index 3873ef957b..5729108aab 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -857,7 +857,7 @@ class NetAppRestClient(object): query = { 'name': volume_name, 'fields': 'aggregates.name,nas.path,name,svm.name,type,style,' - 'qos.policy.name,space.size' + 'qos.policy.name,space.size,space.used' } result = self.send_request('/storage/volumes', 'get', query=query) @@ -888,6 +888,7 @@ class NetAppRestClient(object): 'type': volume_infos.get('type'), 'style': volume_infos.get('style'), 'size': volume_infos.get('space', {}).get('size'), + 'size-used': volume_infos.get('space', {}).get('used'), 'qos-policy-group-name': ( volume_infos.get('qos', {}).get('policy', {}).get('name')), 'style-extended': volume_infos.get('style') @@ -1833,7 +1834,8 @@ class NetAppRestClient(object): 'patch', body=body) @na_utils.trace - def create_snapshot(self, volume_name, snapshot_name): + def create_snapshot(self, volume_name, snapshot_name, + snapmirror_label=None): """Creates a volume snapshot.""" volume = self._get_volume_by_args(vol_name=volume_name) @@ -1841,6 +1843,8 @@ class NetAppRestClient(object): body = { 'name': snapshot_name, } + if snapmirror_label is not None: + body['snapmirror_label'] = snapmirror_label self.send_request(f'/storage/volumes/{uuid}/snapshots', 'post', body=body) @@ -2323,7 +2327,10 @@ class NetAppRestClient(object): else record.get('state')), 'transferring-state': record.get('transfer', {}).get('state'), 'mirror-state': record.get('state'), - 'schedule': record['transfer_schedule']['name'], + 'schedule': ( + record['transfer_schedule']['name'] + if record.get('transfer_schedule') + else None), 'source-vserver': record['source']['svm']['name'], 'source-volume': (record['source']['path'].split(':')[1] if record.get('source') else None), @@ -4930,6 +4937,31 @@ class NetAppRestClient(object): policy_name.append(record.get('name')) return policy_name + @na_utils.trace + def create_snapmirror_policy(self, policy_name, + policy_type='async', + discard_network_info=True, + preserve_snapshots=True, + snapmirror_label='all_source_snapshots', + keep=1): + """Create SnapMirror Policy""" + + if policy_type == "vault": + body = {"name": policy_name, "type": "async", + "create_snapshot_on_source": False} + else: + body = {"name": policy_name, "type": policy_type} + if discard_network_info: + body["exclude_network_config"] = {'svmdr-config-obj': 'network'} + if preserve_snapshots: + body["retention"] = [{"label": snapmirror_label, "count": keep}] + try: + self.send_request('/snapmirror/policies/', 'post', body=body) + except netapp_api.api.NaApiError as e: + LOG.debug('Failed to create SnapMirror policy. ' + 'Error: %s. Code: %s', e.message, e.code) + raise + @na_utils.trace def delete_snapmirror_policy(self, policy_name): """Deletes a SnapMirror policy.""" @@ -5362,3 +5394,56 @@ class NetAppRestClient(object): # Convert Bytes to GBs. return (total_volumes_size / 1024**3) + + def snapmirror_restore_vol(self, source_path=None, dest_path=None, + source_vserver=None, dest_vserver=None, + source_volume=None, dest_volume=None, + source_snapshot=None): + """Restore snapshot copy from destination volume to source volume""" + snapmirror_info = self.get_snapmirror_destinations(dest_path, + source_path, + dest_vserver, + source_vserver, + dest_volume, + source_volume, + ) + if not snapmirror_info: + msg = _("There is no relationship between source " + "'%(source_path)s' and destination cluster" + " '%(des_path)s'") + msg_args = {'source_path': source_path, + 'des_path': dest_path, + } + raise exception.NetAppException(msg % msg_args) + uuid = snapmirror_info[0].get('uuid') + body = {"destination": {"path": dest_path}, + "source_snapshot": source_snapshot} + try: + self.send_request(f"/snapmirror/relationships/{uuid}/restore", + 'post', body=body) + except netapp_api.api.NaApiError as e: + LOG.debug('Snapmirror restore has failed. Error: %s. Code: %s', + e.message, e.code) + raise + + @na_utils.trace + def list_volume_snapshots(self, volume_name, snapmirror_label=None, + newer_than=None): + """Gets list of snapshots of volume.""" + volume = self._get_volume_by_args(vol_name=volume_name) + uuid = volume['uuid'] + query = {} + if snapmirror_label: + query = { + 'snapmirror_label': snapmirror_label, + } + + if newer_than: + query['create_time'] = '>' + newer_than + + response = self.send_request( + f'/storage/volumes/{uuid}/snapshots/', + 'get', query=query) + + return [snapshot_info['name'] + for snapshot_info in response['records']] 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 5814cf2dc6..058da55a47 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py @@ -67,10 +67,30 @@ def get_backend_configuration(backend_name): 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_data_motion_opts) + config.append_config_values(na_opts.netapp_proxy_opts) + config.append_config_values(na_opts.netapp_backup_opts) return config +def get_backup_configuration(backup_name): + config_stanzas = CONF.list_all_sections() + if backup_name not in config_stanzas: + msg = _("Could not find backend stanza %(backup_name)s in " + "configuration which is required for backup workflows " + "with the source share. Available stanzas are " + "%(stanzas)s") + params = { + "stanzas": config_stanzas, + "backend_name": backup_name, + } + raise exception.BadConfigurationException(reason=msg % params) + config = configuration.Configuration(driver.share_opts, + config_group=backup_name) + config.append_config_values(na_opts.netapp_backup_opts) + return config + + def get_client_for_backend(backend_name, vserver_name=None): config = get_backend_configuration(backend_name) if config.netapp_use_legacy_client: @@ -913,3 +933,58 @@ class DataMotionSession(object): LOG.exception( 'Error releasing snapmirror destination %s for ' 'replica %s.', destination['id'], replica['id']) + + def get_most_available_aggr_of_vserver(self, vserver_client): + """Get most available aggregate""" + aggrs_space_attr = vserver_client.get_vserver_aggregate_capacities() + if not aggrs_space_attr: + return None + aggr_list = list(aggrs_space_attr.keys()) + most_available_aggr = aggr_list[0] + for aggr in aggr_list: + if (aggrs_space_attr.get(aggr).get('available') + > aggrs_space_attr.get( + most_available_aggr).get('available')): + most_available_aggr = aggr + return most_available_aggr + + def initialize_and_wait_snapmirror_vol(self, vserver_client, + source_vserver, source_volume, + dest_vserver, dest_volume, + source_snapshot=None, + transfer_priority=None, + timeout=300): + """Initialize and wait for SnapMirror relationship""" + interval = 10 + retries = (timeout / interval or 1) + vserver_client.initialize_snapmirror_vol( + source_vserver, + source_volume, + dest_vserver, + dest_volume, + source_snapshot=source_snapshot, + transfer_priority=transfer_priority, + ) + + @utils.retry(exception.NetAppException, interval=interval, + retries=retries, backoff_rate=1) + def wait_for_initialization(): + source_path = f"{source_vserver}:{source_volume}" + des_path = f"{dest_vserver}:{dest_volume}" + snapmirror_info = vserver_client.get_snapmirrors( + source_path=source_path, dest_path=des_path) + relationship_status = snapmirror_info[0].get("relationship-status") + if relationship_status == "idle": + return + else: + msg = (_('Snapmirror relationship status is: %s. Waiting ' + 'until it has been initialized.') % + relationship_status) + raise exception.NetAppException(message=msg) + + try: + wait_for_initialization() + except exception.NetAppException: + msg = _("Timed out while wait for SnapMirror relationship to " + "be initialized") + raise exception.NetAppException(message=msg) 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 1d3c11961e..47b4aa6f93 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 @@ -380,3 +380,21 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): return self.library.update_share_server_network_allocations( context, share_server, current_network_allocations, new_network_allocations, security_services, shares, snapshots) + + def create_backup(self, context, share, backup, **kwargs): + return self.library.create_backup(context, share, backup, **kwargs) + + def create_backup_continue(self, context, share, backup, **kwargs): + return self.library.create_backup_continue(context, share, backup, + **kwargs) + + def restore_backup(self, context, backup, share, **kwargs): + return self.library.restore_backup(context, backup, share, + **kwargs) + + def restore_backup_continue(self, context, backup, share, **kwargs): + return self.library.restore_backup_continue(context, backup, share, + **kwargs) + + def delete_backup(self, context, backup, share, **kwargs): + return self.library.delete_backup(context, backup, share, **kwargs) 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 671300c8f9..c4d5602a26 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 @@ -345,3 +345,20 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver): self, context, share_server, current_network_allocations, new_network_allocations, security_services, shares, snapshots): raise NotImplementedError + + def create_backup(self, context, share, backup, **kwargs): + return self.library.create_backup(context, share, backup, **kwargs) + + def create_backup_continue(self, context, share, backup, **kwargs): + return self.library.create_backup_continue(context, share, backup, + **kwargs) + + def restore_backup(self, context, backup, share, **kwargs): + return self.library.restore_backup(context, backup, share, **kwargs) + + def restore_backup_continue(self, context, backup, share, **kwargs): + return self.library.restore_backup_continue(context, backup, share, + **kwargs) + + def delete_backup(self, context, backup, share, **kwargs): + return self.library.delete_backup(context, backup, share, **kwargs) 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 d7dd9267c1..89120a24fb 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -21,11 +21,13 @@ single-SVM or multi-SVM functionality needed by the cDOT Manila drivers. import copy import datetime +from enum import Enum import json import math import re import socket +from manila.exception import SnapshotResourceNotFound from oslo_config import cfg from oslo_log import log from oslo_service import loopingcall @@ -57,8 +59,22 @@ LOG = log.getLogger(__name__) CONF = cfg.CONF -class NetAppCmodeFileStorageLibrary(object): +class Backup(Enum): + """Enum for share backup""" + BACKUP_TYPE = "backup_type" + BACKEND_NAME = "netapp_backup_backend_section_name" + DES_VSERVER = "netapp_backup_vserver" + DES_VOLUME = "netapp_backup_share" + SM_LABEL = "backup" + DES_VSERVER_PREFIX = "backup_vserver" + DES_VOLUME_PREFIX = "backup_volume" + VOLUME_TYPE = "dp" + SM_POLICY = "os_backup_policy" + TOTAL_PROGRESS_HUNDRED = "100" + TOTAL_PROGRESS_ZERO = "0" + +class NetAppCmodeFileStorageLibrary(object): AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly SSC_UPDATE_INTERVAL_SECONDS = 3600 # hourly HOUSEKEEPING_INTERVAL_SECONDS = 600 # ten minutes @@ -157,6 +173,7 @@ class NetAppCmodeFileStorageLibrary(object): self._licenses = [] self._client = None self._clients = {} + self._backend_clients = {} self._ssc_stats = {} self._have_cluster_creds = None self._revert_to_snapshot_support = False @@ -177,6 +194,7 @@ class NetAppCmodeFileStorageLibrary(object): self._snapmirror_schedule = self._convert_schedule_to_seconds( schedule=self.configuration.netapp_snapmirror_schedule) self._cluster_name = self.configuration.netapp_cluster_name + self.is_volume_backup_before = False @na_utils.trace def do_setup(self, context): @@ -218,39 +236,51 @@ class NetAppCmodeFileStorageLibrary(object): def _get_vserver(self, share_server=None): raise NotImplementedError() + def _get_client(self, config, vserver=None): + if config.netapp_use_legacy_client: + client = client_cmode.NetAppCmodeClient( + transport_type=config.netapp_transport_type, + ssl_cert_path=config.netapp_ssl_cert_path, + username=config.netapp_login, + password=config.netapp_password, + hostname=config.netapp_server_hostname, + port=config.netapp_server_port, + vserver=vserver, + trace=na_utils.TRACE_API, + api_trace_pattern=na_utils.API_TRACE_PATTERN) + else: + client = client_cmode_rest.NetAppRestClient( + transport_type=config.netapp_transport_type, + ssl_cert_path=config.netapp_ssl_cert_path, + username=config.netapp_login, + password=config.netapp_password, + hostname=config.netapp_server_hostname, + port=config.netapp_server_port, + vserver=vserver, + trace=na_utils.TRACE_API, + async_rest_timeout=( + config.netapp_rest_operation_timeout), + api_trace_pattern=na_utils.API_TRACE_PATTERN) + return client + @na_utils.trace def _get_api_client(self, vserver=None): # Use cached value to prevent redo calls during client initialization. client = self._clients.get(vserver) - if not client: - if self.configuration.netapp_use_legacy_client: - client = client_cmode.NetAppCmodeClient( - transport_type=self.configuration.netapp_transport_type, - ssl_cert_path=self.configuration.netapp_ssl_cert_path, - username=self.configuration.netapp_login, - password=self.configuration.netapp_password, - hostname=self.configuration.netapp_server_hostname, - port=self.configuration.netapp_server_port, - vserver=vserver, - trace=na_utils.TRACE_API, - api_trace_pattern=na_utils.API_TRACE_PATTERN) - else: - client = client_cmode_rest.NetAppRestClient( - transport_type=self.configuration.netapp_transport_type, - ssl_cert_path=self.configuration.netapp_ssl_cert_path, - username=self.configuration.netapp_login, - password=self.configuration.netapp_password, - hostname=self.configuration.netapp_server_hostname, - port=self.configuration.netapp_server_port, - vserver=vserver, - trace=na_utils.TRACE_API, - async_rest_timeout=( - self.configuration.netapp_rest_operation_timeout), - api_trace_pattern=na_utils.API_TRACE_PATTERN) + client = self._get_client(self.configuration, vserver=vserver) self._clients[vserver] = client + return client + @na_utils.trace + def _get_api_client_for_backend(self, backend_name, vserver=None): + key = f"{backend_name}-{vserver}" + client = self._backend_clients.get(key) + if not client: + config = data_motion.get_backend_configuration(backend_name) + client = self._get_client(config, vserver=vserver) + self._backend_clients[key] = client return client @na_utils.trace @@ -648,6 +678,14 @@ class NetAppCmodeFileStorageLibrary(object): """Find all aggregates match pattern.""" raise NotImplementedError() + def _get_backup_vserver(self, backup, share_server=None): + """Get/Create the vserver for backup """ + raise NotImplementedError() + + def _delete_backup_vserver(self, backup, des_vserver): + """Delete the vserver for backup """ + raise NotImplementedError() + @na_utils.trace def _get_flexgroup_aggr_set(self): aggr = set() @@ -4281,3 +4319,639 @@ class NetAppCmodeFileStorageLibrary(object): pool_name = share_utils.extract_host(host, level='pool') return pool_name in pools + + @na_utils.trace + def create_backup(self, context, share_instance, backup, + share_server=None): + """Create backup for NetApp share""" + + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server) + src_cluster = src_vserver_client.get_cluster_name() + src_vol = self._get_backend_share_name(share_instance['id']) + backup_options = backup.get('backup_options', {}) + backup_type = backup_options.get(Backup.BACKUP_TYPE.value) + + # Check if valid backup type is provided + if not backup_type: + raise exception.BackupException("Driver needs a valid backup type" + " from command line or API.") + + # check the backend is related to NetApp + backup_config = data_motion.get_backup_configuration(backup_type) + backend_name = backup_config.safe_get(Backup.BACKEND_NAME.value) + backend_config = data_motion.get_backend_configuration( + backend_name) + if (backend_config.safe_get("netapp_storage_family") + != 'ontap_cluster'): + err_msg = _("Wrong vendor backend %s is provided, provide" + " only NetApp backend.") % backend_name + raise exception.BackupException(err_msg) + + # Check backend has compatible backup type + if (backend_config.safe_get("netapp_enabled_backup_types") is None or + backup_type not in backend_config.safe_get( + "netapp_enabled_backup_types")): + err_msg = _("Backup type '%(backup_type)s' is not compatible with" + " backend '%(backend_name)s'.") + msg_args = { + 'backup_type': backup_type, + 'backend_name': backend_name, + } + raise exception.BackupException(err_msg % msg_args) + + # Verify that both source and destination cluster are peered + des_cluster_api_client = self._get_api_client_for_backend( + backend_name) + des_cluster = des_cluster_api_client.get_cluster_name() + if src_cluster != des_cluster: + cluster_peer_info = self._client.get_cluster_peers( + remote_cluster_name=des_cluster) + if not cluster_peer_info: + err_msg = _("Source cluster '%(src_cluster)s' and destination" + " cluster '%(des_cluster)s' are not peered" + " backend %(backend_name)s.") + msg_args = { + 'src_cluster': src_cluster, + 'des_cluster': des_cluster, + 'backend_name': backend_name + } + raise exception.NetAppException(err_msg % msg_args) + + # Get the destination vserver and volume for relationship + source_path = f"{src_vserver}:{src_vol}" + snapmirror_info = src_vserver_client.get_snapmirror_destinations( + source_path=source_path) + if len(snapmirror_info) > 1: + err_msg = _("Source path %(path)s has more than one relationships." + " To create the share backup, delete the all source" + " volume's SnapMirror relationships using 'snapmirror'" + " ONTAP CLI or System Manger.") + msg_args = { + 'path': source_path + } + raise exception.NetAppException(err_msg % msg_args) + elif len(snapmirror_info) == 1: + des_vserver, des_volume = self._get_destination_vserver_and_vol( + src_vserver_client, source_path, False) + des_vserver_client = self._get_api_client_for_backend( + backend_name, vserver=des_vserver) + else: + if (backup_config.safe_get(Backup.DES_VOLUME.value) and + not backup_config.safe_get(Backup.DES_VSERVER.value)): + msg = _("Could not find vserver name under stanza" + " '%(backup_type)s' in configuration while volume" + " name is provided.") + params = {"backup_type": backup_type} + raise exception.BadConfigurationException(reason=msg % params) + + des_vserver = self._get_vserver_for_backup( + backup, share_server=share_server) + des_vserver_client = self._get_api_client_for_backend( + backend_name, vserver=des_vserver) + try: + des_volume = self._get_volume_for_backup(backup, + share_instance, + src_vserver_client, + des_vserver_client) + except (netapp_api.NaApiError, exception.NetAppException): + # Delete the vserver + if share_server: + self._delete_backup_vserver(backup, des_vserver) + + msg = _("Failed to create a volume in vserver %(des_vserver)s") + msg_args = {'des_vserver': des_vserver} + raise exception.NetAppException(msg % msg_args) + + if (src_vserver != des_vserver and + len(src_vserver_client.get_vserver_peers( + src_vserver, des_vserver)) == 0): + src_vserver_client.create_vserver_peer( + src_vserver, des_vserver, + peer_cluster_name=des_cluster) + if des_cluster is not None and src_cluster != des_cluster: + des_vserver_client.accept_vserver_peer(des_vserver, + src_vserver) + des_snapshot_list = (des_vserver_client. + list_volume_snapshots(des_volume)) + snap_list_with_backup = [ + snap for snap in des_snapshot_list if snap.startswith( + Backup.SM_LABEL.value) + ] + if len(snap_list_with_backup) == 1: + self.is_volume_backup_before = True + + policy_name = f"{Backup.SM_POLICY.value}_{share_instance['id']}" + try: + des_vserver_client.create_snapmirror_policy( + policy_name, + policy_type="vault", + discard_network_info=False, + snapmirror_label=Backup.SM_LABEL.value, + keep=250) + except netapp_api.NaApiError as e: + with excutils.save_and_reraise_exception() as exc_context: + if 'policy with this name already exists' in e.message: + exc_context.reraise = False + try: + des_vserver_client.create_snapmirror_vol( + src_vserver, + src_vol, + des_vserver, + des_volume, + "extended_data_protection", + policy=policy_name, + ) + db_session = data_motion.DataMotionSession() + db_session.initialize_and_wait_snapmirror_vol( + des_vserver_client, + src_vserver, + src_vol, + des_vserver, + des_volume, + timeout=backup_config.netapp_snapmirror_job_timeout + ) + except netapp_api.NaApiError: + self._resource_cleanup_for_backup(backup, + share_instance, + des_vserver, + des_volume, + share_server=share_server) + msg = _("SnapVault relationship creation or initialization" + " failed between source %(source_vserver)s:" + "%(source_volume)s and destination %(des_vserver)s:" + "%(des_volume)s for share id %(share_id)s.") + + msg_args = { + 'source_vserver': src_vserver, + 'source_volume': src_vol, + 'des_vserver': des_vserver, + 'des_volume': des_volume, + 'share_id': share_instance['share_id'] + } + raise exception.NetAppException(msg % msg_args) + + snapshot_name = self._get_backup_snapshot_name(backup, + share_instance['id']) + src_vserver_client.create_snapshot( + src_vol, snapshot_name, + snapmirror_label=Backup.SM_LABEL.value) + + # Update the SnapMirror relationship + des_vserver_client.update_snapmirror_vol(src_vserver, + src_vol, + des_vserver, + des_volume) + LOG.debug("SnapMirror relationship updated successfully.") + + @na_utils.trace + def create_backup_continue(self, context, share_instance, backup, + share_server=None): + """Keep tracking the status of share backup""" + + progress_status = {'total_progress': Backup.TOTAL_PROGRESS_ZERO.value} + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server) + src_vol_name = self._get_backend_share_name(share_instance['id']) + backend_name = self._get_backend(backup) + source_path = f"{src_vserver}:{src_vol_name}" + LOG.debug("SnapMirror source path: %s", source_path) + backup_type = backup.get(Backup.BACKUP_TYPE.value) + backup_config = data_motion.get_backup_configuration(backup_type) + + # Make sure SnapMirror relationship is created + snapmirror_info = src_vserver_client.get_snapmirror_destinations( + source_path=source_path, + ) + if not snapmirror_info: + LOG.warning("There is no SnapMirror relationship available for" + " source path yet %s.", source_path) + return progress_status + + des_vserver, des_vol = self._get_destination_vserver_and_vol( + src_vserver_client, + source_path, + ) + if not des_vserver or not des_vol: + raise exception.NetAppException("Not able to find vserver " + " and volume from SnpMirror" + " relationship.") + des_path = f"{des_vserver}:{des_vol}" + LOG.debug("SnapMirror destination path: %s", des_path) + + des_vserver_client = self._get_api_client_for_backend( + backend_name, + vserver=des_vserver, + ) + snapmirror_info = des_vserver_client.get_snapmirrors( + source_path=source_path, dest_path=des_path) + if not snapmirror_info: + msg_args = { + 'source_path': source_path, + 'des_path': des_path, + } + msg = _("There is no SnapMirror relationship available for" + " source path '%(source_path)s' and destination path" + " '%(des_path)s' yet.") % msg_args + LOG.warning(msg, msg_args) + return progress_status + LOG.debug("SnapMirror details %s:", snapmirror_info) + progress_status["total_progress"] = (Backup. + TOTAL_PROGRESS_HUNDRED.value) + if snapmirror_info[0].get("last-transfer-type") != "update": + progress_status["total_progress"] = (Backup. + TOTAL_PROGRESS_ZERO.value) + return progress_status + + if snapmirror_info[0].get("relationship-status") != "idle": + progress_status = self._get_backup_progress_status( + des_vserver_client, snapmirror_info) + LOG.debug("Progress status: %(progress_status)s", + {'progress_status': progress_status}) + return progress_status + + # Verify that snapshot is transferred to destination volume + snap_name = self._get_backup_snapshot_name(backup, + share_instance['id']) + self._verify_and_wait_for_snapshot_to_transfer(des_vserver_client, + des_vol, + snap_name) + LOG.debug("Snapshot '%(snap_name)s' transferred successfully to" + " destination", {'snap_name': snap_name}) + # previously if volume was part of some relationship and if we delete + # all the backup of share then last snapshot will be left on + # destination volume, and we can't delete that snapshot due to ONTAP + # restriction. Next time if user create the first backup then we + # update the destination volume with latest backup and delete the last + # leftover snapshot + is_backup_completed = (progress_status["total_progress"] + == Backup.TOTAL_PROGRESS_HUNDRED.value) + if backup_config.get(Backup.DES_VOLUME.value) and is_backup_completed: + snap_list_with_backup = self._get_des_volume_backup_snapshots( + des_vserver_client, + des_vol, share_instance['id'] + ) + LOG.debug("Snapshot list for backup %(snap_list)s.", + {'snap_list': snap_list_with_backup}) + if (self.is_volume_backup_before and + len(snap_list_with_backup) == 2): + if snap_name == snap_list_with_backup[0]: + snap_to_delete = snap_list_with_backup[1] + else: + snap_to_delete = snap_list_with_backup[0] + self.is_volume_backup_before = False + des_vserver_client.delete_snapshot(des_vol, snap_to_delete, + True) + LOG.debug("Previous snapshot %{snap_name}s deleted" + " successfully. ", {'snap_name': snap_to_delete}) + return progress_status + + @na_utils.trace + def restore_backup(self, context, backup, share_instance, + share_server=None): + """Restore the share backup""" + + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server, + ) + src_vol_name = self._get_backend_share_name(share_instance['id']) + + source_path = f"{src_vserver}:{src_vol_name}" + des_vserver, des_vol = self._get_destination_vserver_and_vol( + src_vserver_client, + source_path, + ) + if not des_vserver or not des_vol: + raise exception.NetAppException("Not able to find vserver " + " and volume from SnpMirror" + " relationship.") + snap_name = self._get_backup_snapshot_name(backup, + share_instance['id']) + source_path = src_vserver + ":" + src_vol_name + des_path = des_vserver + ":" + des_vol + src_vserver_client.snapmirror_restore_vol(source_path=des_path, + dest_path=source_path, + source_snapshot=snap_name) + + @na_utils.trace + def restore_backup_continue(self, context, backup, + share_instance, share_server=None): + """Keep checking the restore operation status""" + + progress_status = {} + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server) + src_vol_name = self._get_backend_share_name(share_instance['id']) + + source_path = f"{src_vserver}:{src_vol_name}" + snapmirror_info = src_vserver_client.get_snapmirrors( + dest_path=source_path, + ) + if snapmirror_info: + progress_status = { + "total_progress": Backup.TOTAL_PROGRESS_ZERO.value + } + return progress_status + LOG.debug("SnapMirror relationship of type RST is deleted") + snap_name = self._get_backup_snapshot_name(backup, + share_instance['id']) + snapshot_list = src_vserver_client.list_volume_snapshots(src_vol_name) + for snapshot in snapshot_list: + if snap_name in snapshot: + progress_status["total_progress"] = ( + Backup.TOTAL_PROGRESS_HUNDRED.value) + return progress_status + if not progress_status: + err_msg = _("Failed to restore the snapshot %s.") % snap_name + raise exception.NetAppException(err_msg) + + @na_utils.trace + def delete_backup(self, context, backup, share_instance, + share_server=None): + """Delete the share backup for netapp share""" + + try: + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server, + ) + except exception.VserverNotFound: + LOG.warning("Vserver associated with share %s was not found.", + share_instance['id']) + return + src_vol_name = self._get_backend_share_name(share_instance['id']) + backend_name = self._get_backend(backup) + if backend_name is None: + return + + source_path = f"{src_vserver}:{src_vol_name}" + des_vserver, des_vol = self._get_destination_vserver_and_vol( + src_vserver_client, + source_path, + False, + ) + + if not des_vserver or not des_vol: + LOG.debug("Not able to find vserver and volume from SnpMirror" + " relationship.") + return + des_path = f"{des_vserver}:{des_vol}" + + # Delete the snapshot from destination volume + snap_name = self._get_backup_snapshot_name(backup, + share_instance['id']) + des_vserver_client = self._get_api_client_for_backend( + backend_name, + vserver=des_vserver, + ) + try: + list_snapshots = self._get_des_volume_backup_snapshots( + des_vserver_client, + des_vol, + share_instance['id'], + ) + except netapp_api.NaApiError: + LOG.exception("Failed to get the snapshots from cluster," + " provide the right backup type or check the" + " backend details are properly configured in" + " manila.conf file.") + return + + snapmirror_info = des_vserver_client.get_snapmirrors( + source_path=source_path, + dest_path=des_path, + ) + is_snapshot_deleted = self._is_snapshot_deleted(True) + if snapmirror_info and len(list_snapshots) == 1: + self._resource_cleanup_for_backup(backup, + share_instance, + des_vserver, + des_vol, + share_server=share_server) + elif len(list_snapshots) > 1: + try: + des_vserver_client.delete_snapshot(des_vol, snap_name, True) + except netapp_api.NaApiError as e: + with excutils.save_and_reraise_exception() as exc_context: + if "entry doesn't exist" in e.message: + exc_context.reraise = False + try: + des_vserver_client.get_snapshot(des_vol, snap_name) + is_snapshot_deleted = self._is_snapshot_deleted(False) + except (SnapshotResourceNotFound, netapp_api.NaApiError): + LOG.debug("Snapshot %s deleted successfully.", snap_name) + if not is_snapshot_deleted: + err_msg = _("Snapshot '%(snapshot_name)s' is not deleted" + " successfully on ONTAP." + % {"snapshot_name": snap_name}) + LOG.exception(err_msg) + raise exception.NetAppException(err_msg) + + @na_utils.trace + def _is_snapshot_deleted(self, is_deleted): + return is_deleted + + @na_utils.trace + def _get_backup_snapshot_name(self, backup, share_id): + backup_id = backup.get('id', "") + return f"{Backup.SM_LABEL.value}_{share_id}_{backup_id}" + + @na_utils.trace + def _get_backend(self, backup): + backup_type = backup.get(Backup.BACKUP_TYPE.value) + try: + backup_config = data_motion.get_backup_configuration(backup_type) + except Exception: + LOG.exception("There is some issue while getting the" + " backup configuration. Make sure correct" + " backup type is provided while creating the" + " backup.") + return None + return backup_config.safe_get(Backup.BACKEND_NAME.value) + + @na_utils.trace + def _get_des_volume_backup_snapshots(self, des_vserver_client, + des_vol, share_id): + """Get the list of snapshot from destination volume""" + + des_snapshot_list = (des_vserver_client. + list_volume_snapshots(des_vol, + Backup.SM_LABEL.value)) + backup_filter = f"{Backup.SM_LABEL.value}_{share_id}" + snap_list_with_backup = [snap for snap in des_snapshot_list + if snap.startswith(backup_filter)] + return snap_list_with_backup + + @na_utils.trace + def _get_vserver_for_backup(self, backup, share_server=None): + """Get the destination vserver + + if vserver not provided we are creating the new one + in case of dhss_true + """ + backup_type_config = data_motion.get_backup_configuration( + backup.get(Backup.BACKUP_TYPE.value)) + if backup_type_config.get(Backup.DES_VSERVER.value): + return backup_type_config.get(Backup.DES_VSERVER.value) + else: + return self._get_backup_vserver(backup, share_server=share_server) + + @na_utils.trace + def _get_volume_for_backup(self, backup, share_instance, + src_vserver_client, des_vserver_client): + """Get the destination volume + + if volume is not provided in config file under backup_type stanza + then create the new one + """ + + dm_session = data_motion.DataMotionSession() + backup_type = backup.get(Backup.BACKUP_TYPE.value) + backup_type_config = data_motion.get_backup_configuration(backup_type) + if (backup_type_config.get(Backup.DES_VSERVER.value) and + backup_type_config.get(Backup.DES_VOLUME.value)): + return backup_type_config.get(Backup.DES_VOLUME.value) + else: + des_aggr = dm_session.get_most_available_aggr_of_vserver( + des_vserver_client) + if not des_aggr: + msg = _("Not able to find any aggregate from ONTAP" + " to create the volume") + raise exception.NetAppException(msg) + src_vol = self._get_backend_share_name(share_instance['id']) + vol_attr = src_vserver_client.get_volume(src_vol) + source_vol_size = vol_attr.get('size') + vol_size_in_gb = int(source_vol_size) / units.Gi + share_id = share_instance['id'].replace('-', '_') + des_volume = f"backup_volume_{share_id}" + des_vserver_client.create_volume(des_aggr, des_volume, + vol_size_in_gb, volume_type='dp') + return des_volume + + @na_utils.trace + def _get_destination_vserver_and_vol(self, src_vserver_client, + source_path, validate_relation=True): + """Get Destination vserver and volume from SM relationship""" + + des_vserver, des_vol = None, None + snapmirror_info = src_vserver_client.get_snapmirror_destinations( + source_path=source_path) + if validate_relation and len(snapmirror_info) != 1: + msg = _("There are more then one relationship with the source." + " '%(source_path)s'." % {'source_path': source_path}) + raise exception.NetAppException(msg) + if len(snapmirror_info) == 1: + des_vserver = snapmirror_info[0].get("destination-vserver") + des_vol = snapmirror_info[0].get("destination-volume") + return des_vserver, des_vol + + @na_utils.trace + def _verify_and_wait_for_snapshot_to_transfer(self, + des_vserver_client, + des_vol, + snap_name, + timeout=300, + ): + """Wait and verify that snapshot is moved to destination""" + + interval = 5 + retries = (timeout / interval or 1) + + @manila_utils.retry(retry_param=(netapp_api.NaApiError, + SnapshotResourceNotFound), + interval=interval, + retries=retries, backoff_rate=1) + def _wait_for_snapshot_to_transfer(): + des_vserver_client.get_snapshot(des_vol, snap_name) + try: + _wait_for_snapshot_to_transfer() + except (netapp_api.NaApiError, SnapshotResourceNotFound): + msg = _("Timed out while wait for snapshot to transfer") + raise exception.NetAppException(message=msg) + + @na_utils.trace + def _get_backup_progress_status(self, des_vserver_client, + snapmirror_details): + """Calculate percentage of SnapMirror data transferred""" + + des_vol = snapmirror_details[0].get("destination-volume") + vol_attr = des_vserver_client.get_volume(des_vol) + size_used = vol_attr.get('size-used') + sm_data_transferred = snapmirror_details[0].get( + "last-transfer-size") + if size_used and sm_data_transferred: + progress_status_percent = (int(sm_data_transferred) / int( + size_used)) * 100 + return str(round(progress_status_percent, 2)) + else: + return Backup.TOTAL_PROGRESS_ZERO.value + + @na_utils.trace + def _resource_cleanup_for_backup(self, backup, share_instance, + des_vserver, des_vol, + share_server=None): + """Cleanup the created resources + + cleanup all created ONTAP resources when delete the last backup + or in case of exception throw while creating the backup. + """ + src_vserver, src_vserver_client = self._get_vserver( + share_server=share_server) + dm_session = data_motion.DataMotionSession() + backup_type_config = data_motion.get_backup_configuration( + backup.get(Backup.BACKUP_TYPE.value)) + backend_name = backup_type_config.safe_get(Backup.BACKEND_NAME.value) + des_vserver_client = self._get_api_client_for_backend( + backend_name, + vserver=des_vserver, + ) + src_vol_name = self._get_backend_share_name(share_instance['id']) + + # Abort relationship + try: + des_vserver_client.abort_snapmirror_vol(src_vserver, + src_vol_name, + des_vserver, + des_vol, + clear_checkpoint=False) + except netapp_api.NaApiError: + pass + try: + des_vserver_client.delete_snapmirror_vol(src_vserver, + src_vol_name, + des_vserver, + des_vol) + except netapp_api.NaApiError as e: + with excutils.save_and_reraise_exception() as exc_context: + if (e.code == netapp_api.EOBJECTNOTFOUND or + e.code == netapp_api.ESOURCE_IS_DIFFERENT or + "(entry doesn't exist)" in e.message): + exc_context.reraise = False + + dm_session.wait_for_snapmirror_release_vol( + src_vserver, des_vserver, src_vol_name, + des_vol, False, src_vserver_client, + timeout=backup_type_config.netapp_snapmirror_job_timeout) + + try: + policy_name = f"{Backup.SM_POLICY.value}_{share_instance['id']}" + des_vserver_client.delete_snapmirror_policy(policy_name) + except netapp_api.NaApiError: + pass + + # Delete the vserver peering + try: + src_vserver_client.delete_vserver_peer(src_vserver, des_vserver) + except netapp_api.NaApiError: + pass + + # Delete volume + if not backup_type_config.safe_get(Backup.DES_VOLUME.value): + try: + des_vserver_client.offline_volume(des_vol) + des_vserver_client.delete_volume(des_vol) + except netapp_api.NaApiError: + pass + + # Delete Vserver + if share_server is not None: + self._delete_backup_vserver(backup, des_vserver) 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 fb8d1ca7ae..e4ddeee4e4 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 @@ -32,6 +32,7 @@ from manila.i18n import _ from manila.message import message_field from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.client import client_cmode +from manila.share.drivers.netapp.dataontap.client import client_cmode_rest from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base from manila.share.drivers.netapp import utils as na_utils @@ -39,6 +40,7 @@ from manila.share import share_types from manila.share import utils as share_utils from manila import utils + LOG = log.getLogger(__name__) SUPPORTED_NETWORK_TYPES = (None, 'flat', 'vlan') SEGMENTED_NETWORK_TYPES = ('vlan',) @@ -2374,3 +2376,52 @@ class NetAppCmodeMultiSVMFileStorageLibrary( current_network_allocations, new_network_allocations, updated_export_locations) return updates + + def _get_backup_vserver(self, backup, share_server=None): + backend_name = self._get_backend(backup) + backend_config = data_motion.get_backend_configuration(backend_name) + des_cluster_api_client = self._get_api_client_for_backend( + backend_name) + + aggr_list = des_cluster_api_client.list_non_root_aggregates() + aggr_pattern = (backend_config. + netapp_aggregate_name_search_pattern) + if aggr_pattern: + aggr_matching_list = [ + element for element in aggr_list if re.search(aggr_pattern, + element) + ] + aggr_list = aggr_matching_list + share_server_id = share_server['id'] + des_vserver = f"backup_{share_server_id}" + LOG.debug("Creating vserver %s:", des_vserver) + try: + des_cluster_api_client.create_vserver( + des_vserver, + None, + None, + aggr_list, + 'Default', + client_cmode_rest.DEFAULT_SECURITY_CERT_EXPIRE_DAYS, + ) + except netapp_api.NaApiError as e: + with excutils.save_and_reraise_exception() as exc_context: + if 'already used' in e.message: + exc_context.reraise = False + return des_vserver + + def _delete_backup_vserver(self, backup, des_vserver): + """Delete the vserver """ + + backend_name = self._get_backend(backup) + des_vserver_client = self._get_api_client_for_backend( + backend_name, vserver=des_vserver) + try: + des_cluster_api_client = self._get_api_client_for_backend( + backend_name) + des_cluster_api_client.delete_vserver(des_vserver, + des_vserver_client) + except exception.NetAppException as e: + with excutils.save_and_reraise_exception() as exc_context: + if 'has shares' in e.msg: + exc_context.reraise = False diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_single_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_single_svm.py index 97644aeba2..6eb2607f1e 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_single_svm.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_single_svm.py @@ -26,6 +26,7 @@ from oslo_log import log from manila import exception from manila.i18n import _ +from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base from manila.share.drivers.netapp import utils as na_utils @@ -177,3 +178,13 @@ class NetAppCmodeSingleSVMFileStorageLibrary( if ipv6: versions.append(6) return versions + + def _get_backup_vserver(self, backup, share_server=None): + + backend_name = self._get_backend(backup) + backend_config = data_motion.get_backend_configuration(backend_name) + if share_server is not None: + msg = _('Share server must not be passed to the driver ' + 'when the driver is not managing share servers.') + raise exception.InvalidParameterValue(err=msg) + return backend_config.netapp_vserver diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py index b6e8788bab..18710e9557 100644 --- a/manila/share/drivers/netapp/options.py +++ b/manila/share/drivers/netapp/options.py @@ -300,6 +300,42 @@ netapp_data_motion_opts = [ 'a replica.'), ] +netapp_backup_opts = [ + cfg.ListOpt('netapp_enabled_backup_types', + default=[], + help='Specify compatible backup_types for backend to provision' + ' backup share for SnapVault relationship. Multiple ' + 'backup_types can be provided. If multiple backup types ' + 'are enabled, create separate config sections for each ' + 'backup type specifying the "netapp_backup_vserver", ' + '"netapp_backup_backend_section_name", ' + '"netapp_backup_share", and ' + '"netapp_snapmirror_job_timeout" as appropriate.' + ' Example- netapp_enabled_backup_types = eng_backup,' + ' finance_backup'), + cfg.StrOpt('netapp_backup_backend_section_name', + help='Backend (ONTAP cluster) name where backup volume will be ' + 'provisioned. This is one of the backend which is enabled ' + 'in manila.conf file.'), + cfg.StrOpt('netapp_backup_vserver', + default='', + help='vserver name of backend that is use for backup the share.' + ' When user provide vserver value then backup volume will ' + ' be created under this vserver '), + cfg.StrOpt('netapp_backup_share', + default='', + help='Specify backup share name in case user wanted to backup ' + 'the share. Some case user has dedicated volume for backup' + ' in this case use can provide dedicated volume. ' + 'backup_share_server must be specified if backup_share is' + ' provided'), + cfg.IntOpt('netapp_snapmirror_job_timeout', + min=0, + default=1800, # 30 minutes + help='The maximum time in seconds to wait for a snapmirror ' + 'related operation to backup to complete.'), +] + CONF = cfg.CONF CONF.register_opts(netapp_proxy_opts) CONF.register_opts(netapp_connection_opts) @@ -308,3 +344,4 @@ CONF.register_opts(netapp_basicauth_opts) CONF.register_opts(netapp_provisioning_opts) CONF.register_opts(netapp_support_opts) CONF.register_opts(netapp_data_motion_opts) +CONF.register_opts(netapp_backup_opts) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index 19d3ea0ee0..e85b22d11f 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -65,6 +65,7 @@ SHARE_AGGREGATE_DISK_TYPES = ['SATA', 'SSD'] EFFECTIVE_TYPE = 'fake_effective_type1' SHARE_NAME = 'fake_share' SHARE_SIZE = '1000000000' +SHARE_USED_SIZE = '3456796' SHARE_NAME_2 = 'fake_share_2' FLEXGROUP_STYLE_EXTENDED = 'flexgroup' FLEXVOL_STYLE_EXTENDED = 'flexvol' @@ -2351,6 +2352,7 @@ VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE = etree.XML(""" %(size)s + %(size-used)s %(qos-policy-group-name)s @@ -2364,6 +2366,7 @@ VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE = etree.XML(""" 'vserver': VSERVER_NAME, 'volume': SHARE_NAME, 'size': SHARE_SIZE, + 'size-used': SHARE_USED_SIZE, 'qos-policy-group-name': QOS_POLICY_GROUP_NAME, 'style-extended': FLEXVOL_STYLE_EXTENDED, }) @@ -2385,6 +2388,7 @@ VOLUME_GET_ITER_FLEXGROUP_VOLUME_TO_MANAGE_RESPONSE = etree.XML(""" %(size)s + %(size-used)s %(qos-policy-group-name)s @@ -2398,6 +2402,7 @@ VOLUME_GET_ITER_FLEXGROUP_VOLUME_TO_MANAGE_RESPONSE = etree.XML(""" 'vserver': VSERVER_NAME, 'volume': SHARE_NAME, 'size': SHARE_SIZE, + 'size-used': SHARE_USED_SIZE, 'qos-policy-group-name': QOS_POLICY_GROUP_NAME, 'style-extended': FLEXGROUP_STYLE_EXTENDED, }) @@ -2417,6 +2422,7 @@ VOLUME_GET_ITER_NO_QOS_RESPONSE = etree.XML(""" %(size)s + %(size-used)s @@ -2427,6 +2433,7 @@ VOLUME_GET_ITER_NO_QOS_RESPONSE = etree.XML(""" 'vserver': VSERVER_NAME, 'volume': SHARE_NAME, 'size': SHARE_SIZE, + 'size-used': SHARE_USED_SIZE, 'style-extended': FLEXVOL_STYLE_EXTENDED, }) @@ -3754,7 +3761,8 @@ GENERIC_EXPORT_POLICY_RESPONSE_AND_VOLUMES = { "path": VOLUME_JUNCTION_PATH }, "space": { - "size": 21474836480 + "size": 21474836480, + 'used': SHARE_USED_SIZE, }, } ], @@ -4796,7 +4804,8 @@ FAKE_VOLUME_MANAGE = { } }, 'space': { - 'size': SHARE_SIZE + 'size': SHARE_SIZE, + 'used': SHARE_USED_SIZE, } } ], 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 512a434da1..a3160db722 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 @@ -4299,6 +4299,7 @@ class NetAppClientCmodeTestCase(test.TestCase): }, 'volume-space-attributes': { 'size': None, + 'size-used': None, }, 'volume-qos-attributes': { 'policy-group-name': None, @@ -4315,6 +4316,7 @@ class NetAppClientCmodeTestCase(test.TestCase): 'type': 'rw', 'style': 'flex', 'size': fake.SHARE_SIZE, + 'size-used': fake.SHARE_USED_SIZE, 'owning-vserver-name': fake.VSERVER_NAME, 'qos-policy-group-name': fake.QOS_POLICY_GROUP_NAME, 'style-extended': (fake.FLEXGROUP_STYLE_EXTENDED @@ -4358,6 +4360,7 @@ class NetAppClientCmodeTestCase(test.TestCase): }, 'volume-space-attributes': { 'size': None, + 'size-used': None, }, 'volume-qos-attributes': { 'policy-group-name': None, @@ -4374,6 +4377,7 @@ class NetAppClientCmodeTestCase(test.TestCase): 'type': 'rw', 'style': 'flex', 'size': fake.SHARE_SIZE, + 'size-used': fake.SHARE_USED_SIZE, 'owning-vserver-name': fake.VSERVER_NAME, 'qos-policy-group-name': None, 'style-extended': fake.FLEXVOL_STYLE_EXTENDED, @@ -7927,8 +7931,7 @@ class NetAppClientCmodeTestCase(test.TestCase): self.client.create_snapmirror_policy( fake.SNAPMIRROR_POLICY_NAME, discard_network_info=discard_network, - preserve_snapshots=preserve_snapshots) - + snapmirror_label="backup", preserve_snapshots=preserve_snapshots) expected_create_api_args = { 'policy-name': fake.SNAPMIRROR_POLICY_NAME, 'type': 'async_mirror', @@ -7944,8 +7947,8 @@ class NetAppClientCmodeTestCase(test.TestCase): if preserve_snapshots: expected_add_rules = { 'policy-name': fake.SNAPMIRROR_POLICY_NAME, - 'snapmirror-label': 'all_source_snapshots', - 'keep': '1', + 'snapmirror-label': 'backup', + 'keep': 1, 'preserve': 'false' } expected_calls.append(mock.call('snapmirror-policy-add-rule', @@ -9160,3 +9163,52 @@ class NetAppClientCmodeTestCase(test.TestCase): self.client.configure_active_directory, fake.CIFS_SECURITY_SERVICE, fake.VSERVER_NAME) + + def test_snapmirror_restore_vol(self): + self.mock_object(self.client, 'send_request') + self.client.snapmirror_restore_vol(source_path=fake.SM_SOURCE_PATH, + dest_path=fake.SM_DEST_PATH, + source_snapshot=fake.SNAPSHOT_NAME, + ) + snapmirror_restore_args = { + 'source-location': fake.SM_SOURCE_PATH, + 'destination-location': fake.SM_DEST_PATH, + 'source-snapshot': fake.SNAPSHOT_NAME, + + } + self.client.send_request.assert_has_calls([ + mock.call('snapmirror-restore', snapmirror_restore_args)]) + + @ddt.data({'snapmirror_label': None, 'newer_than': '2345'}, + {'snapmirror_label': "fake_backup", 'newer_than': None}) + @ddt.unpack + def test_list_volume_snapshots(self, snapmirror_label, newer_than): + print(f"snapmirror_label: {snapmirror_label}") + api_response = netapp_api.NaElement( + fake.SNAPSHOT_GET_ITER_SNAPMIRROR_RESPONSE) + self.mock_object(self.client, + 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_volume_snapshots( + fake.SHARE_NAME, + snapmirror_label=snapmirror_label, + newer_than=newer_than) + snapshot_get_iter_args = { + 'query': { + 'snapshot-info': { + 'volume': fake.SHARE_NAME, + }, + }, + } + if newer_than: + snapshot_get_iter_args['query']['snapshot-info'][ + 'access-time'] = '>' + newer_than + if snapmirror_label: + snapshot_get_iter_args['query']['snapshot-info'][ + 'snapmirror-label'] = snapmirror_label + self.client.send_iter_request.assert_has_calls([ + mock.call('snapshot-get-iter', snapshot_get_iter_args)]) + + expected = [fake.SNAPSHOT_NAME] + self.assertEqual(expected, result) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py index 8b2bb1a22a..6da8a51c00 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py @@ -650,7 +650,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): self.assertEqual(expected, result) @ddt.data({'types': {'FCAL'}, 'expected': ['FCAL']}, - {'types': {'SATA', 'SSD'}, 'expected': ['SATA', 'SSD']},) + {'types': {'SATA', 'SSD'}, 'expected': ['SATA', 'SSD']}, ) @ddt.unpack def test_get_aggregate_disk_types(self, types, expected): @@ -945,9 +945,10 @@ class NetAppRestCmodeClientTestCase(test.TestCase): 'type': fake_volume.get('type', ''), 'style': fake_volume.get('style', ''), 'size': fake_volume.get('space', {}).get('size', ''), + 'size-used': fake_volume.get('space', {}).get('used', ''), 'qos-policy-group-name': fake_volume.get('qos', {}) - .get('policy', {}) - .get('name'), + .get('policy', {}) + .get('name'), 'style-extended': fake_volume.get('style', '') } @@ -1500,7 +1501,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): api_response = fake.EXPORT_POLICY_REST mock_sr = self.mock_object(self.client, 'send_request', mock.Mock( - return_value=api_response)) + return_value=api_response)) if not api_response.get('records'): return @@ -1577,7 +1578,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): mock_send_request.assert_has_calls([ mock.call('/storage/qos/policies', 'get', query=query), mock.call(f'/storage/qos/policies/{uuid}', 'patch', - body=body), + body=body), ]) def test_qos_policy_group_get(self): @@ -1592,7 +1593,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): 'vserver': qos_policy.get('svm', {}).get('name'), 'max-throughput': max_throughput if max_throughput else None, 'num-workloads': int(qos_policy.get('object_count')), - } + } query = { 'name': qos_policy_group_name, @@ -1967,7 +1968,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): mock_get_unique_volume = self.mock_object( self.client, "_get_volume_by_args", mock.Mock(return_value=fake_resp_vol) - ) + ) mock_send_request = self.mock_object( self.client, 'send_request', mock.Mock(return_value=fake.VOLUME_LIST_SIMPLE_RESPONSE_REST)) @@ -1987,7 +1988,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): mock_get_unique_volume = self.mock_object( self.client, "_get_volume_by_args", mock.Mock(return_value=fake_resp_vol) - ) + ) mock_send_request = self.mock_object( self.client, 'send_request', mock.Mock(return_value=fake.VOLUME_LIST_SIMPLE_RESPONSE_REST)) @@ -2922,9 +2923,9 @@ class NetAppRestCmodeClientTestCase(test.TestCase): fake.SNAPMIRROR_GET_ITER_RESPONSE_REST, { "job": - { - "uuid": fake.FAKE_UUID - }, + { + "uuid": fake.FAKE_UUID + }, "num_records": 1 } ] @@ -3164,7 +3165,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): enable_tunneling=False), mock.call(f'/protocols/fpolicy/{svm_id}/policies' f'/{fake.FPOLICY_POLICY_NAME}', 'patch') - ]) + ]) @ddt.data([fake.NO_RECORDS_RESPONSE_REST, None], [fake.SVMS_LIST_SIMPLE_RESPONSE_REST, @@ -3498,7 +3499,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): "vserver": "fake_svm", "volume": "fake_vol", "destination_vserver": "fake_svm_2" - } + } self.client.send_request.assert_called_once_with( "/private/cli/volume/rehost", 'post', body=body) @@ -4310,7 +4311,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): fake_response = copy.deepcopy(fake.PREFERRED_DC_REST) fake_ss = copy.deepcopy(fake.LDAP_AD_SECURITY_SERVICE) self.mock_object(self.client, 'send_request', - mock.Mock(return_value=fake_response)) + mock.Mock(return_value=fake_response)) self.client.remove_preferred_dcs(fake_ss, svm_uuid) query = { 'fqdn': fake.LDAP_AD_SECURITY_SERVICE.get('domain'), @@ -4327,7 +4328,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): fake_response = copy.deepcopy(fake.PREFERRED_DC_REST) fake_ss = copy.deepcopy(fake.LDAP_AD_SECURITY_SERVICE) self.mock_object(self.client, 'send_request', - mock.Mock(return_value=fake_response)) + mock.Mock(return_value=fake_response)) self.mock_object(self.client, 'send_request', mock.Mock(side_effect=netapp_api.api.NaApiError)) self.assertRaises(netapp_api.api.NaApiError, @@ -4386,7 +4387,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): "peer": { "svm": { "name": fake.VSERVER_PEER_NAME, - } + } } }], } @@ -4631,7 +4632,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): mock_ports = ( self.mock_object(self.client, 'get_node_data_ports', mock.Mock( - return_value=fake.REST_SPEED_SORTED_PORTS))) + return_value=fake.REST_SPEED_SORTED_PORTS))) test_result = self.client.list_node_data_ports(fake.NODE_NAME) @@ -5052,7 +5053,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): mock.Mock(return_value=api_response)) result = self.client.get_nfs_config(['tcp-max-xfer-size', - 'udp-max-xfer-size'], + 'udp-max-xfer-size'], fake.VSERVER_NAME) expected = { 'tcp-max-xfer-size': '65536', @@ -6475,9 +6476,10 @@ class NetAppRestCmodeClientTestCase(test.TestCase): 'type': fake_volume.get('type', ''), 'style': fake_volume.get('style', ''), 'size': fake_volume.get('space', {}).get('size', ''), + 'size-used': fake_volume.get('space', {}).get('used', ''), 'qos-policy-group-name': fake_volume.get('qos', {}) - .get('policy', {}) - .get('name', ''), + .get('policy', {}) + .get('name', ''), 'style-extended': fake_volume.get('style', '') } result = self.client.get_volume(fake.VOLUME_NAMES[0]) @@ -6633,7 +6635,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): fake_response = [fake.PREFERRED_DC_REST, netapp_api.api.NaApiError] self.mock_object(self.client, 'send_request', - mock.Mock(side_effect=fake_response)) + mock.Mock(side_effect=fake_response)) self.assertRaises(exception.NetAppException, self.client.remove_preferred_dcs, fake.LDAP_AD_SECURITY_SERVICE, @@ -6809,7 +6811,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase): self.mock_object(self.client, 'send_request', mock.Mock(side_effect=self._mock_api_error( - code=return_code))) + code=return_code))) self.client.set_nfs_export_policy_for_volume( fake.VOLUME_NAMES[0], fake.EXPORT_POLICY_NAME) @@ -6877,3 +6879,77 @@ class NetAppRestCmodeClientTestCase(test.TestCase): self.client.configure_active_directory, fake_security, fake.VSERVER_NAME) + + def test_snapmirror_restore_vol(self): + uuid = fake.VOLUME_ITEM_SIMPLE_RESPONSE_REST["uuid"] + body = { + "destination": {"path": fake.SM_DEST_PATH}, + "source_snapshot": fake.SNAPSHOT_NAME + } + snapmirror_info = [{'destination-vserver': "fake_des_vserver", + 'destination-volume': "fake_des_vol", + 'relationship-status': "idle", + 'uuid': uuid}] + + self.mock_object(self.client, 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + self.mock_object(self.client, 'send_request') + self.client.snapmirror_restore_vol(source_path=fake.SM_SOURCE_PATH, + dest_path=fake.SM_DEST_PATH, + source_snapshot=fake.SNAPSHOT_NAME) + self.client.send_request.assert_called_once_with( + f'/snapmirror/relationships/{uuid}/restore', 'post', body=body) + + @ddt.data({'snapmirror_label': None, 'newer_than': '2345'}, + {'snapmirror_label': "fake_backup", 'newer_than': None}) + @ddt.unpack + def test_list_volume_snapshots(self, snapmirror_label, newer_than): + fake_response = fake.SNAPSHOTS_REST_RESPONSE + api_response = fake.VOLUME_ITEM_SIMPLE_RESPONSE_REST + self.mock_object(self.client, + '_get_volume_by_args', + mock.Mock(return_value=api_response)) + mock_request = self.mock_object(self.client, 'send_request', + mock.Mock(return_value=fake_response)) + self.client.list_volume_snapshots(fake.SHARE_NAME, + snapmirror_label=snapmirror_label, + newer_than=newer_than) + uuid = fake.VOLUME_ITEM_SIMPLE_RESPONSE_REST["uuid"] + query = {} + if snapmirror_label: + query = { + 'snapmirror_label': snapmirror_label, + } + if newer_than: + query['create_time'] = '>' + newer_than + + mock_request.assert_called_once_with( + f'/storage/volumes/{uuid}/snapshots/', + 'get', query=query) + + @ddt.data(('vault', False, True), (None, False, False)) + @ddt.unpack + def test_create_snapmirror_policy_rest(self, policy_type, + discard_network_info, + preserve_snapshots): + fake_response = fake.SNAPSHOTS_REST_RESPONSE + self.mock_object(self.client, 'send_request', + mock.Mock(return_value=fake_response)) + policy_name = fake.SNAPMIRROR_POLICY_NAME + self.client.create_snapmirror_policy( + policy_name, policy_type=policy_type, + discard_network_info=discard_network_info, + preserve_snapshots=preserve_snapshots, + snapmirror_label='backup', + keep=30) + if policy_type == "vault": + body = {"name": policy_name, "type": "async", + "create_snapshot_on_source": False} + else: + body = {"name": policy_name, "type": policy_type} + if discard_network_info: + body["exclude_network_config"] = {'svmdr-config-obj': 'network'} + if preserve_snapshots: + body["retention"] = [{"label": 'backup', "count": 30}] + self.client.send_request.assert_called_once_with( + '/snapmirror/policies/', 'post', body=body) 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 1d3a94a84d..d42a31d0b6 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 @@ -1272,3 +1272,43 @@ class NetAppCDOTDataMotionSessionTestCase(test.TestCase): mock_src_client.release_snapmirror_vol.assert_called() self.assertIsNone(result) + + def test_get_most_available_aggr_of_vserver(self): + vserver_client = mock.Mock() + aggr_space_attr = {fake.AGGREGATE: {'available': 5678}, + 'aggr2': {'available': 2024}} + self.mock_object(vserver_client, + 'get_vserver_aggregate_capacities', + mock.Mock(return_value=aggr_space_attr)) + result = self.dm_session.get_most_available_aggr_of_vserver( + vserver_client) + self.assertEqual(result, fake.AGGREGATE) + + def test_initialize_and_wait_snapmirror_vol(self): + vserver_client = mock.Mock() + snapmirror_info = [{'source-vserver': fake.VSERVER1, + 'source-volume': "fake_source_vol", + 'destination-vserver': fake.VSERVER2, + 'destination-volume': "fake_des_vol", + 'relationship-status': "idle"}] + self.mock_object(vserver_client, + 'get_snapmirrors', + mock.Mock(return_value=snapmirror_info)) + + (self.dm_session. + initialize_and_wait_snapmirror_vol(vserver_client, + fake.VSERVER1, + fake.FLEXVOL_NAME, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + source_snapshot=None, + transfer_priority=None, + timeout=300)) + (vserver_client.initialize_snapmirror_vol. + assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + source_snapshot=mock.ANY, + transfer_priority=mock.ANY, + )) 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 acb5831864..e3f606ec07 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 @@ -24,6 +24,7 @@ import time from unittest import mock import ddt +from oslo_config import cfg from oslo_log import log from oslo_service import loopingcall from oslo_utils import timeutils @@ -32,6 +33,8 @@ from oslo_utils import uuidutils from manila.common import constants from manila import exception +from manila.share import configuration +from manila.share import driver from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.client import client_cmode from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion @@ -39,19 +42,51 @@ from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base from manila.share.drivers.netapp.dataontap.cluster_mode import performance from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode +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 test from manila.tests import fake_share from manila.tests.share.drivers.netapp.dataontap import fakes as fake +from manila.tests.share.drivers.netapp import fakes as na_fakes from manila.tests import utils +CONF = cfg.CONF + def fake_replica(**kwargs): return fake_share.fake_replica(for_manager=True, **kwargs) +def _get_config(): + backup_config = 'backup_config' + config = configuration.Configuration(driver.share_opts, + config_group=backup_config) + config.append_config_values(na_opts.netapp_backup_opts) + config.append_config_values(na_opts.netapp_proxy_opts) + config.append_config_values(na_opts.netapp_connection_opts) + config.append_config_values(na_opts.netapp_basicauth_opts) + config.append_config_values(na_opts.netapp_provisioning_opts) + config.append_config_values(na_opts.netapp_support_opts) + config.append_config_values(na_opts.netapp_data_motion_opts) + config.append_config_values(na_opts.netapp_cluster_opts) + + CONF.set_override("netapp_enabled_backup_types", + [fake.BACKUP_TYPE, "backup2"], + group=backup_config) + CONF.set_override("netapp_backup_backend_section_name", + fake.BACKEND_NAME, + group=backup_config) + CONF.set_override("netapp_backup_vserver", + "fake_backup_share", + group=backup_config) + CONF.set_override("netapp_backup_share", + "fake_share_server", + group=backup_config) + return config + + @ddt.ddt class NetAppFileStorageLibraryTestCase(test.TestCase): @@ -7743,3 +7778,1005 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): ]) mock_extract.assert_called_once_with(fake.HOST_NAME, level='pool') mock_parse.assert_called_once_with(flexgroup_pools) + + def test_create_backup_first_backup(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self._backup_mock_common_method(mock_dest_client) + self.mock_object(vserver_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=[])) + + vserver_peer_info = [{'vserver': fake.VSERVER1, + 'peer-vserver': fake.VSERVER2}] + self.mock_object(vserver_client, + 'get_vserver_peers', + mock.Mock(return_value=vserver_peer_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(mock_dest_client, + 'list_volume_snapshots', + mock.Mock(return_value=snap_list)) + + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup(self.context, share_instance, backup) + (mock_dest_client.create_snapmirror_policy. + assert_called_once_with(mock.ANY, + policy_type='vault', + discard_network_info=False, + snapmirror_label=mock.ANY, + keep=mock.ANY)) + + (mock_dest_client.create_snapmirror_vol. + assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + 'extended_data_protection', + policy=mock.ANY + )) + dm_session = data_motion.DataMotionSession() + (dm_session.initialize_and_wait_snapmirror_vol. + assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + timeout=mock.ANY, + )) + (mock_dest_client.update_snapmirror_vol. + assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + )) + + def test_create_backup_second_backup(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [fake.SNAP_MIRROR_INFO] + self.mock_object(vserver_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup(self.context, share_instance, backup) + mock_dest_client.create_snapmirror_policy.assert_not_called() + mock_dest_client.create_snapmirror_vol.assert_not_called() + (data_motion.DataMotionSession(). + initialize_and_wait_snapmirror_vol.assert_not_called()) + (mock_dest_client.update_snapmirror_vol. + assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + )) + + def test_create_backup_continue(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [fake.SNAP_MIRROR_INFO] + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=snapmirror_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup_continue(self.context, share_instance, + backup) + + def test_restore_backup(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.restore_backup(self.context, backup, share_instance) + vserver_client.snapmirror_restore_vol.assert_called_once_with( + source_path=mock.ANY, + dest_path=mock.ANY, + source_snapshot=mock.ANY) + + def test_restore_backup_continue(self): + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(vserver_client, + 'get_snapmirrors', + mock.Mock(return_value=[])) + snap_list = ["restored_snap1", "snap2", "snap3"] + self.mock_object(vserver_client, + 'list_volume_snapshots', + mock.Mock(return_value=snap_list)) + self.mock_object(self.library, + '_get_backup_snapshot_name', + mock.Mock(return_value="restored_snap1")) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.restore_backup_continue(self.context, backup, + share_instance) + + def test_delete_backup(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=[])) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + self.mock_object( + self.library, '_is_snapshot_deleted', + mock.Mock(return_value=True)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.delete_backup(self.context, share_instance, backup) + + def test_delete_backup_with_resource_cleanup(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [fake.SNAP_MIRROR_INFO] + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=snapmirror_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + self.mock_object( + self.library, '_is_snapshot_deleted', + mock.Mock(return_value=True)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.delete_backup(self.context, share_instance, backup) + + def test__get_backup_snapshot_name(self): + backup = fake.SHARE_BACKUP + actual_result = self.library._get_backup_snapshot_name(backup, + fake.SHARE_ID) + backup_id = backup.get('id', "") + expected_result = f"backup_{fake.SHARE_ID}_{backup_id}" + self.assertEqual(actual_result, expected_result) + + def test__get_backend(self): + backup = fake.SHARE_BACKUP + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + + actual_result = self.library._get_backend(backup) + self.assertEqual(actual_result, fake.BACKEND_NAME) + + def test__get_des_volume_backup_snapshots(self): + mock_dest_client = mock.Mock() + share_id = fake.SHARE_ID + snap_list = [f"backup_{share_id}_snap1", + f"backup_{share_id}_snap2", "snap3"] + self.mock_object(mock_dest_client, + 'list_volume_snapshots', + mock.Mock(return_value=snap_list)) + expected_snap_list = [f"backup_{share_id}_snap1", + f"backup_{share_id}_snap2"] + actual_result = self.library._get_des_volume_backup_snapshots( + mock_dest_client, + fake.FLEXVOL_NAME, + share_id) + self.assertEqual(expected_snap_list, actual_result) + + def test__get_volume_for_backup(self): + mock_dest_client = mock.Mock() + mock_src_client = mock.Mock() + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + self.library._get_volume_for_backup(fake.SHARE_BACKUP, + fake.SHARE_INSTANCE, + mock_src_client, mock_dest_client) + + def test__get_volume_for_backup_create_new_vol(self): + mock_dest_client = mock.Mock() + mock_src_client = mock.Mock() + _get_config() + backup_config = 'backup_config' + fake_config = configuration.Configuration(driver.share_opts, + config_group=backup_config) + CONF.set_override("netapp_backup_share", "", + group=backup_config) + CONF.set_override("netapp_backup_vserver", "", + group=backup_config) + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=fake_config)) + vol_attr = {'name': 'fake_vol', 'size': 12345} + self.mock_object(mock_src_client, + 'get_volume', + mock.Mock(return_value=vol_attr)) + self.library._get_volume_for_backup(fake.SHARE_BACKUP, + fake.SHARE_INSTANCE, + mock_src_client, mock_dest_client) + + def test__get_volume_for_backup_aggr_not_found_negative(self): + mock_dest_client = mock.Mock() + mock_src_client = mock.Mock() + _get_config() + backup_config = 'backup_config' + fake_config = configuration.Configuration(driver.share_opts, + config_group=backup_config) + CONF.set_override("netapp_backup_share", "", + group=backup_config) + CONF.set_override("netapp_backup_vserver", "", + group=backup_config) + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=fake_config)) + self.mock_object(self.mock_dm_session, + 'get_most_available_aggr_of_vserver', + mock.Mock(return_value=None)) + self.assertRaises( + exception.NetAppException, + self.library._get_volume_for_backup, + fake.SHARE_BACKUP, + fake.SHARE_INSTANCE, + mock_src_client, + mock_dest_client, + ) + + def test__get_vserver_for_backup(self): + mock_dest_client = mock.Mock() + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + + mock_backend_config = na_fakes.create_configuration() + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=mock_backend_config)) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=mock_dest_client)) + + self.library._get_vserver_for_backup(fake.SHARE_INSTANCE, + fake.SHARE_BACKUP) + + def test__get_destination_vserver_and_vol(self): + mock_dest_client = mock.Mock() + snapmirror_info = [fake.SNAP_MIRROR_INFO] + source_path = f"{fake.VSERVER1}:{fake.FLEXVOL_NAME}" + self.mock_object(mock_dest_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + actual_result = self.library._get_destination_vserver_and_vol( + mock_dest_client, + source_path, validate_relation=True) + expected_result = (fake.VSERVER2, fake.FLEXVOL_NAME_1) + self.assertEqual(actual_result, expected_result) + + def test__get_destination_vserver_and_vol_negative(self): + mock_dest_client = mock.Mock() + snapmirror_info = [{'source-vserver': fake.VSERVER1, + 'source-volume': fake.FLEXVOL_NAME, + 'destination-vserver': fake.VSERVER2, + 'destination-volume': fake.FLEXVOL_NAME_1 + }, + {'source-vserver': 'fake_vs_1', + 'source-volume': 'fake_vol_1', + 'destination-vserver': 'fake_vs_2', + 'destination-volume': 'fake_vol_2' + } + ] + source_path = f"{fake.VSERVER1}:{fake.FLEXVOL_NAME}" + self.mock_object(mock_dest_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + self.assertRaises( + exception.NetAppException, + self.library._get_destination_vserver_and_vol, + mock_dest_client, + source_path, + validate_relation=True + ) + + def test_verify_and_wait_for_snapshot_to_transfer(self): + vserver_client = mock.Mock() + self.mock_object(vserver_client, + 'get_snapshot', + mock.Mock(return_value=fake.SNAPSHOT_NAME)) + result = self.library._verify_and_wait_for_snapshot_to_transfer( + vserver_client, + fake.FLEXVOL_NAME, + fake.SNAPSHOT_NAME, + ) + self.assertIsNone(result) + + def test_verify_and_wait_for_snapshot_to_transfer_negative(self): + vserver_client = mock.Mock() + self.mock_object(vserver_client, + 'get_snapshot', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.assertRaises( + exception.NetAppException, + self.library._verify_and_wait_for_snapshot_to_transfer, + vserver_client, + fake.FLEXVOL_NAME, + fake.SNAPSHOT_NAME, + timeout=10, + ) + + def test__resource_cleanup_for_backup(self): + src_vserver_client = mock.Mock() + des_vserver_client = mock.Mock() + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + mock_backend_config = na_fakes.create_configuration() + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=mock_backend_config)) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + src_vserver_client))) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=des_vserver_client)) + self.mock_object(self.library, + '_get_backend_share_name', + mock.Mock(return_value=fake.FLEXVOL_NAME)) + + self.library._resource_cleanup_for_backup(backup, + share_instance, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + ) + (des_vserver_client.abort_snapmirror_vol. + assert_called_once_with(fake.VSERVER1, + fake.FLEXVOL_NAME, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + clear_checkpoint=False + )) + (des_vserver_client.delete_snapmirror_vol. + assert_called_once_with(fake.VSERVER1, + fake.FLEXVOL_NAME, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + )) + db_session = data_motion.DataMotionSession() + (db_session.wait_for_snapmirror_release_vol. + assert_called_once_with(fake.VSERVER1, + fake.VSERVER2, + fake.FLEXVOL_NAME, + fake.FLEXVOL_NAME_1, + False, src_vserver_client, + timeout=mock.ANY + )) + + def test__resource_cleanup_for_backup_with_exception(self): + mock_src_vserver_client = mock.Mock() + mock_des_vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + mock_src_vserver_client))) + self._backup_mock_common_method(mock_des_vserver_client) + self.mock_object(mock_des_vserver_client, + 'abort_snapmirror_vol', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.mock_object(mock_des_vserver_client, + 'delete_snapmirror_vol', + mock.Mock(side_effect=netapp_api.NaApiError( + code=netapp_api.EOBJECTNOTFOUND))) + self.mock_object(mock_des_vserver_client, + 'delete_snapmirror_policy', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.mock_object(mock_src_vserver_client, + 'delete_vserver_peer', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.library._resource_cleanup_for_backup(fake.SHARE_BACKUP, + fake.SHARE_INSTANCE, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + ) + + def test__resource_cleanup_for_backup_vserver_volume_none(self): + mock_src_vserver_client = mock.Mock() + mock_des_vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + mock_src_vserver_client))) + self.mock_object(self.library, + '_delete_backup_vserver', + mock.Mock(return_value=None)) + + self.mock_object(mock_des_vserver_client, + 'delete_volume', + mock.Mock(side_effect=netapp_api.NaApiError)) + + self._backup_mock_common_method(mock_des_vserver_client) + backup_config = 'backup_config' + CONF.set_override("netapp_backup_share", "", + group=backup_config) + CONF.set_override("netapp_backup_vserver", "", + group=backup_config) + self.library._resource_cleanup_for_backup( + fake.SHARE_BACKUP, + fake.SHARE_INSTANCE, + fake.VSERVER2, + fake.FLEXVOL_NAME_1, + share_server=fake.SHARE_SERVER, + ) + + def test_create_backup_with_backup_type_none_negative(self): + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + backup = {'id': '242ff47e-518d-4b07-b3c3-0a51e6744149', + 'backup_options': {'backend': 'fake_ontap', + 'backup_type': None + }, + } + self.assertRaises( + exception.BackupException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + backup, + ) + + def test_create_backup_with_non_netapp_backend_negative(self): + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + mock.Mock()))) + fake_config = configuration.Configuration( + driver.share_opts, config_group='backup_config') + CONF.set_override("netapp_storage_family", None, + group='backup_config') + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=fake_config)) + self.assertRaises( + exception.BackupException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_when_enabled_backup_types_none_negative(self): + vserver_src_client = mock.Mock() + vserver_dest_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_src_client))) + self._backup_mock_common_method(vserver_dest_client) + fake_config = configuration.Configuration(driver.share_opts, + config_group='backup_config') + CONF.set_override("netapp_enabled_backup_types", None, + group='backup_config') + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=fake_config)) + self.assertRaises( + exception.BackupException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_source_has_2_more_relationships_negative(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [{'source-vserver': fake.VSERVER1, + 'source-volume': fake.FLEXVOL_NAME, + 'destination-vserver': fake.VSERVER2, + 'destination-volume': fake.FLEXVOL_NAME_1 + }, + {'source-vserver': 'fake_vs_1', + 'source-volume': 'fake_vol_1', + 'destination-vserver': 'fake_vs_2', + 'destination-volume': 'fake_vol_2' + } + ] + self.mock_object(vserver_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + self.assertRaises( + exception.NetAppException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_bad_backup_config_negative(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + fake_config = configuration.Configuration( + driver.share_opts, config_group='backup_config') + CONF.set_override("netapp_backup_vserver", None, + group='backup_config') + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=fake_config)) + self.assertRaises( + exception.BadConfigurationException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_when_cluster_are_not_peered_negative(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + self.mock_object(self.client, + 'get_cluster_peers', + mock.Mock(return_value=[])) + self.mock_object(mock_src_client, + 'get_cluster_name', + mock.Mock(return_value='fake_src_cluster')) + + self.assertRaises( + exception.NetAppException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_when_des_vol_creation_fail_negative(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + self.mock_object(self.library, + '_get_volume_for_backup', + mock.Mock(side_effect=exception.NetAppException)) + self.mock_object(self.library, + '_delete_backup_vserver', + mock.Mock(return_value=[])) + self.assertRaises( + exception.NetAppException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_when_vserver_not_peered(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + mock_cluster_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + self.mock_object(mock_cluster_client, + 'get_cluster_name', + mock.Mock(return_value='fake_src_cluster')) + self.mock_object(mock_src_client, + 'get_vserver_peers', + mock.Mock(return_value=[])) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup(self.context, share_instance, backup) + + def test_create_backup_when_policy_creation_failed_negative(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + + self.mock_object(mock_des_client, + 'create_snapmirror_policy', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.assertRaises( + netapp_api.NaApiError, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_when_duplicate_policy_created(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + msg = 'policy with this name already exists' + self.mock_object(mock_des_client, + 'create_snapmirror_policy', + mock.Mock(side_effect=netapp_api.NaApiError( + message=msg))) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup(self.context, share_instance, backup) + + def test_create_backup_when_snapmirror_creation_failed_negative(self): + mock_src_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method_for_negative(mock_src_client, + mock_des_client) + self.mock_object(mock_des_client, + 'create_snapmirror_vol', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.assertRaises( + exception.NetAppException, + self.library.create_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_create_backup_continue_with_status_inprogress(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [{'source-vserver': fake.VSERVER1, + 'source-volume': fake.FLEXVOL_NAME, + 'destination-vserver': fake.VSERVER2, + 'destination-volume': fake.FLEXVOL_NAME_1, + 'relationship-status': "inprogress", + 'last-transfer-type': "update", + }] + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=snapmirror_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup_continue(self.context, share_instance, + backup) + + def test_create_backup_continue_with_state_not_update(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [{'source-vserver': fake.VSERVER1, + 'source-volume': fake.FLEXVOL_NAME, + 'destination-vserver': fake.VSERVER2, + 'destination-volume': fake.FLEXVOL_NAME_1, + 'relationship-status': "idle", + 'last-transfer-type': "initialize", + }] + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=snapmirror_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup_continue(self.context, share_instance, + backup) + + def test_create_backup_continue_snapmirror_none(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(vserver_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=None)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup_continue(self.context, share_instance, + backup) + + def test_create_backup_continue_snapmirror_none_from_destination(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self._backup_mock_common_method(mock_dest_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(mock_dest_client, + 'get_snapmirrors', + mock.Mock(return_value=None)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.create_backup_continue(self.context, share_instance, + backup) + + def test_create_backup_continue_des_vserver_vol_none_negative(self): + vserver_client = mock.Mock() + mock_des_vserver = mock.Mock() + self._backup_mock_common_method(mock_des_vserver) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + snapmirror_info = [fake.SNAP_MIRROR_INFO] + self.mock_object(vserver_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=snapmirror_info)) + self.mock_object(self.library, + '_get_destination_vserver_and_vol', + mock.Mock(return_value=(None, None))) + self.assertRaises( + exception.NetAppException, + self.library.create_backup_continue, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_restore_backup_with_vserver_volume_none(self): + 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_destination_vserver_and_vol', + mock.Mock(return_value=(None, None))) + self.assertRaises( + exception.NetAppException, + self.library.restore_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_restore_backup_continue_with_rst_relationship(self): + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(vserver_client, + 'get_snapmirrors', + mock.Mock(return_value=fake.SNAP_MIRROR_INFO)) + snap_list = ["restored_snap1", "snap2", "snap3"] + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=snap_list)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.restore_backup_continue(self.context, backup, + share_instance) + + def test_restore_backup_continue_restore_failed_negative(self): + vserver_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(vserver_client, + 'get_snapmirrors', + mock.Mock(return_value=[])) + snap_list = ["restored_snap1", "snap2", "snap3"] + self.mock_object(vserver_client, + 'list_volume_snapshots', + mock.Mock(return_value=snap_list)) + self.mock_object(self.library, + '_get_backup_snapshot_name', + mock.Mock(return_value="restored_snap_test1")) + self.assertRaises( + exception.NetAppException, + self.library.restore_backup_continue, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test_delete_backup_vserver_vol_none_negative(self): + 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_destination_vserver_and_vol', + mock.Mock(return_value=(None, None))) + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.delete_backup(self.context, backup, + share_instance) + + def test_delete_backup_snapshot_not_found_negative(self): + vserver_client = mock.Mock() + mock_dest_client = mock.Mock() + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(self.library, + '_get_destination_vserver_and_vol', + mock.Mock(return_value=(fake.VSERVER2, + fake.FLEXVOL_NAME_1))) + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=mock_dest_client)) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.delete_backup(self.context, backup, + share_instance) + + def test_delete_backup_cleanup_resource(self): + vserver_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method(mock_des_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(mock_des_client, + 'get_snapmirrors', + mock.Mock(return_value=fake.SNAP_MIRROR_INFO)) + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=['fake_snapshot'])) + share_instance = fake.SHARE_INSTANCE + backup = fake.SHARE_BACKUP + self.library.delete_backup(self.context, backup, + share_instance) + + def test_delete_backup_snapshot_delete_fail_negative(self): + vserver_client = mock.Mock() + mock_des_client = mock.Mock() + self._backup_mock_common_method(mock_des_client) + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + vserver_client))) + self.mock_object(mock_des_client, + 'get_snapmirrors', + mock.Mock(return_value=fake.SNAP_MIRROR_INFO)) + self.mock_object(self.library, + '_get_des_volume_backup_snapshots', + mock.Mock(return_value=['fake_snapshot1', + 'fake_snapshot2'])) + self.mock_object(mock_des_client, + 'get_snapshot', + mock.Mock(side_effect=netapp_api.NaApiError)) + self.mock_object(self.library, + '_is_snapshot_deleted', + mock.Mock(return_value=False)) + self.assertRaises( + exception.NetAppException, + self.library.delete_backup, + self.context, + fake.SHARE_INSTANCE, + fake.SHARE_BACKUP, + ) + + def test__get_backup_progress_status(self): + mock_dest_client = mock.Mock() + vol_attr = {'name': 'fake_vol', 'size-used': '123454'} + self.mock_object(mock_dest_client, + 'get_volume', + mock.Mock(return_value=vol_attr)) + snapmirror_info = {'source-vserver': fake.VSERVER1, + 'source-volume': fake.FLEXVOL_NAME, + 'destination-vserver': fake.VSERVER2, + 'destination-volume': fake.FLEXVOL_NAME_1, + 'relationship-status': "idle", + 'last-transfer-size': '3456', + } + self.library._get_backup_progress_status(mock_dest_client, + [snapmirror_info]) + + def _backup_mock_common_method(self, mock_dest_client): + self.mock_object(mock_dest_client, + 'get_cluster_name', + mock.Mock(return_value=fake.CLUSTER_NAME)) + self.mock_object(self.library, + '_get_backend_share_name', + mock.Mock(return_value=fake.SHARE_NAME)) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=mock_dest_client)) + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=_get_config())) + self.mock_object(data_motion, + 'get_backup_configuration', + mock.Mock(return_value=_get_config())) + + self.mock_object(self.library, + '_get_destination_vserver_and_vol', + mock.Mock(return_value=(fake.VSERVER2, + fake.FLEXVOL_NAME))) + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + + def _backup_mock_common_method_for_negative(self, + mock_src_client, + mock_des_client): + self.mock_object(self.library, + '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + mock_src_client))) + self._backup_mock_common_method(mock_des_client) + self.mock_object(mock_src_client, + 'get_snapmirror_destinations', + mock.Mock(return_value=[])) + vserver_peer_info = [{'vserver': fake.VSERVER1, + 'peer-vserver': fake.VSERVER2}] + self.mock_object(mock_src_client, + 'get_vserver_peers', + mock.Mock(return_value=vserver_peer_info)) + snap_list = ["snap1", "snap2", "snap3"] + self.mock_object(mock_des_client, + 'list_volume_snapshots', + mock.Mock(return_value=snap_list)) 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 fdfce85252..0d7c46282c 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 @@ -35,6 +35,8 @@ from manila.share import share_types from manila.share import utils as share_utils from manila import test from manila.tests.share.drivers.netapp.dataontap.client import fakes as c_fake +from manila.tests.share.drivers.netapp.dataontap.cluster_mode.test_lib_base\ + import _get_config from manila.tests.share.drivers.netapp.dataontap import fakes as fake @@ -4078,3 +4080,42 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library._build_model_update.assert_called_once_with( fake_current_network_allocations, fake_new_network_allocations, export_locations=None) + + def test__get_backup_vserver(self): + mock_dest_client = mock.Mock() + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=_get_config())) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=mock_dest_client)) + self.mock_object(mock_dest_client, + 'list_non_root_aggregates', + mock.Mock(return_value=['aggr1', 'aggr2'])) + self.mock_object(mock_dest_client, + 'create_vserver', + mock.Mock(side_effect=netapp_api.NaApiError( + message='Vserver name is already used by another' + ' Vserver'))) + self.library._get_backup_vserver(fake.SHARE_BACKUP, fake.SHARE_SERVER) + + def test__delete_backup_vserver(self): + mock_api_client = mock.Mock() + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + self.mock_object(self.library, + '_get_api_client_for_backend', + mock.Mock(return_value=mock_api_client)) + des_vserver = fake.VSERVER2 + msg = (f"Cannot delete Vserver. Vserver {des_vserver} " + f"has shares.") + self.mock_object(mock_api_client, + 'delete_vserver', + mock.Mock( + side_effect=exception.NetAppException( + message=msg))) + self.library._delete_backup_vserver(fake.SHARE_BACKUP, des_vserver) diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_single_svm.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_single_svm.py index 695568c9fd..69f9c6ca75 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_single_svm.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_single_svm.py @@ -21,10 +21,13 @@ import ddt from oslo_log import log from manila import exception +from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base from manila.share.drivers.netapp.dataontap.cluster_mode import lib_single_svm from manila.share.drivers.netapp import utils as na_utils from manila import test +from manila.tests.share.drivers.netapp.dataontap.cluster_mode.test_lib_base\ + import _get_config import manila.tests.share.drivers.netapp.dataontap.fakes as fake @@ -295,3 +298,26 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): result = self.library.get_admin_network_allocations_number() self.assertEqual(0, result) + + def test__get_backup_vserver(self): + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=_get_config())) + self.library._get_backup_vserver(fake.SHARE_BACKUP) + + def test__get_backup_vserver_with_share_server_negative(self): + self.mock_object(self.library, + '_get_backend', + mock.Mock(return_value=fake.BACKEND_NAME)) + self.mock_object(data_motion, + 'get_backend_configuration', + mock.Mock(return_value=_get_config())) + self.assertRaises( + exception.InvalidParameterValue, + self.library._get_backup_vserver, + fake.SHARE_BACKUP, + fake.SHARE_SERVER, + ) diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index e80fe0455f..c23e072221 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -40,6 +40,7 @@ SHARE_NAME = 'share_7cf7c200_d3af_4e05_b87e_9167c95dfcad' SHARE_NAME2 = 'share_d24e7257_124e_4fb6_b05b_d384f660bc85' SHARE_INSTANCE_NAME = 'share_d24e7257_124e_4fb6_b05b_d384f660bc85' FLEXVOL_NAME = 'fake_volume' +FLEXVOL_NAME_1 = 'fake_volume_1' JUNCTION_PATH = '/%s' % FLEXVOL_NAME EXPORT_LOCATION = '%s:%s' % (HOST_NAME, JUNCTION_PATH) SNAPSHOT_NAME = 'fake_snapshot' @@ -112,6 +113,7 @@ FPOLICY_EXT_TO_INCLUDE = 'avi' FPOLICY_EXT_TO_INCLUDE_LIST = ['avi'] FPOLICY_EXT_TO_EXCLUDE = 'jpg,mp3' FPOLICY_EXT_TO_EXCLUDE_LIST = ['jpg', 'mp3'] +BACKUP_TYPE = "fake_backup_type" JOB_ID = '123' JOB_STATE = 'success' @@ -1869,6 +1871,24 @@ NEW_NETWORK_ALLOCATIONS = { 'network_allocations': USER_NETWORK_ALLOCATIONS } +SHARE_BACKUP = { + 'id': '242ff47e-518d-4b07-b3c3-0a51e6744149', + 'share_id': 'd0a424c3-fee9-4781-9d4a-2c48a63386aa', + 'size': SHARE_SIZE, + 'host': MANILA_HOST_NAME, + 'display_name': 'fake_backup', + 'backup_options': {'backend': BACKEND_NAME, 'backup_type': BACKUP_TYPE}, + } + +SNAP_MIRROR_INFO = {'source-vserver': VSERVER1, + 'source-volume': FLEXVOL_NAME, + 'destination-vserver': VSERVER2, + 'destination-volume': FLEXVOL_NAME_1, + 'relationship-status': "idle", + 'last-transfer-type': "update", + } + + SERVER_MODEL_UPDATE = { 'server_details': { 'ports': '{"%s": "%s", "%s": "%s"}' % ( diff --git a/manila/tests/share/drivers/netapp/fakes.py b/manila/tests/share/drivers/netapp/fakes.py index 6ad24e8aab..1402691556 100644 --- a/manila/tests/share/drivers/netapp/fakes.py +++ b/manila/tests/share/drivers/netapp/fakes.py @@ -25,6 +25,7 @@ def create_configuration(): config.append_config_values(na_opts.netapp_transport_opts) config.append_config_values(na_opts.netapp_basicauth_opts) config.append_config_values(na_opts.netapp_provisioning_opts) + config.append_config_values(na_opts.netapp_backup_opts) return config diff --git a/releasenotes/notes/share-backup-netapp-driver-8bbcf3fbc1d20614.yaml b/releasenotes/notes/share-backup-netapp-driver-8bbcf3fbc1d20614.yaml new file mode 100644 index 0000000000..8c802cb307 --- /dev/null +++ b/releasenotes/notes/share-backup-netapp-driver-8bbcf3fbc1d20614.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The NetApp ONTAP driver now supports driver-advantaged share backup. NetApp + SnapVault technology is used to create and restore backups for NetApp ONTAP + shares. Backup delete workflow just deletes the transferred snapshots from + destination backup volume. How to get the config data for backup, refer + https://etherpad.opendev.org/p/manila-share-backup link.