diff --git a/manila/share/drivers/netapp/dataontap/client/api.py b/manila/share/drivers/netapp/dataontap/client/api.py index ecc2373dca..856b9e838a 100644 --- a/manila/share/drivers/netapp/dataontap/client/api.py +++ b/manila/share/drivers/netapp/dataontap/client/api.py @@ -23,12 +23,14 @@ import re from lxml import etree from oslo_log import log +from oslo_serialization import jsonutils import requests from requests import auth import six from manila import exception from manila.i18n import _ +from manila.share.drivers.netapp.dataontap.client import rest_endpoints from manila.share.drivers.netapp import utils LOG = log.getLogger(__name__) @@ -62,28 +64,23 @@ EPOLICYNOTFOUND = '18251' EEVENTNOTFOUND = '18253' ESCOPENOTFOUND = '18259' ESVMDR_CANNOT_PERFORM_OP_FOR_STATUS = '18815' +ENFS_V4_0_ENABLED_MIGRATION_FAILURE = '13172940' +EVSERVER_MIGRATION_TO_NON_AFF_CLUSTER = '13172984' + +STYLE_LOGIN_PASSWORD = 'basic_auth' +TRANSPORT_TYPE_HTTP = 'http' +TRANSPORT_TYPE_HTTPS = 'https' +STYLE_CERTIFICATE = 'certificate_auth' -class NaServer(object): +class BaseClient(object): """Encapsulates server connection logic.""" - TRANSPORT_TYPE_HTTP = 'http' - TRANSPORT_TYPE_HTTPS = 'https' - SERVER_TYPE_FILER = 'filer' - SERVER_TYPE_DFM = 'dfm' - URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer' - URL_DFM = 'apis/XMLrequest' - NETAPP_NS = 'http://www.netapp.com/filer/admin' - STYLE_LOGIN_PASSWORD = 'basic_auth' - STYLE_CERTIFICATE = 'certificate_auth' - - def __init__(self, host, server_type=SERVER_TYPE_FILER, - transport_type=TRANSPORT_TYPE_HTTP, - style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, - password=None, port=None, trace=False, - api_trace_pattern=utils.API_TRACE_PATTERN): + def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP, style=None, + ssl_cert_path=None, username=None, password=None, port=None, + trace=False, api_trace_pattern=None): + super(BaseClient, self).__init__() self._host = host - self.set_server_type(server_type) self.set_transport_type(transport_type) self.set_style(style) if port: @@ -99,9 +96,21 @@ class NaServer(object): # Note(felipe_rodrigues): it will verify with the mozila CA roots, # given by certifi package. self._ssl_verify = True - LOG.debug('Using NetApp controller: %s', self._host) + def get_style(self): + """Get the authorization style for communicating with the server.""" + return self._auth_style + + def set_style(self, style): + """Set the authorization style for communicating with the server. + + Supports basic_auth for now. Certificate_auth mode to be done. + """ + if style.lower() not in (STYLE_LOGIN_PASSWORD, STYLE_CERTIFICATE): + raise ValueError('Unsupported authentication style') + self._auth_style = style.lower() + def get_transport_type(self): """Get the transport type protocol.""" return self._protocol @@ -112,38 +121,13 @@ class NaServer(object): Supports http and https transport types. """ if transport_type.lower() not in ( - NaServer.TRANSPORT_TYPE_HTTP, - NaServer.TRANSPORT_TYPE_HTTPS): + TRANSPORT_TYPE_HTTP, TRANSPORT_TYPE_HTTPS): raise ValueError('Unsupported transport type') self._protocol = transport_type.lower() - if self._protocol == NaServer.TRANSPORT_TYPE_HTTP: - if self._server_type == NaServer.SERVER_TYPE_FILER: - self.set_port(80) - else: - self.set_port(8088) - else: - if self._server_type == NaServer.SERVER_TYPE_FILER: - self.set_port(443) - else: - self.set_port(8488) self._refresh_conn = True - def get_style(self): - """Get the authorization style for communicating with the server.""" - return self._auth_style - - def set_style(self, style): - """Set the authorization style for communicating with the server. - - Supports basic_auth for now. Certificate_auth mode to be done. - """ - if style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD, - NaServer.STYLE_CERTIFICATE): - raise ValueError('Unsupported authentication style') - self._auth_style = style.lower() - def get_server_type(self): - """Get the target server type.""" + """Get the server type.""" return self._server_type def set_server_type(self, server_type): @@ -151,16 +135,7 @@ class NaServer(object): Supports filer and dfm server types. """ - if server_type.lower() not in (NaServer.SERVER_TYPE_FILER, - NaServer.SERVER_TYPE_DFM): - raise ValueError('Unsupported server type') - self._server_type = server_type.lower() - if self._server_type == NaServer.SERVER_TYPE_FILER: - self._url = NaServer.URL_FILER - else: - self._url = NaServer.URL_DFM - self._ns = NaServer.NETAPP_NS - self._refresh_conn = True + raise NotImplementedError() def set_api_version(self, major, minor): """Set the API version.""" @@ -216,14 +191,6 @@ class NaServer(object): return self._timeout return None - def get_vfiler(self): - """Get the vfiler to use in tunneling.""" - return self._vfiler - - def set_vfiler(self, vfiler): - """Set the vfiler to use if tunneling gets enabled.""" - self._vfiler = vfiler - def get_vserver(self): """Get the vserver to use in tunneling.""" return self._vserver @@ -242,10 +209,110 @@ class NaServer(object): self._password = password self._refresh_conn = True + def invoke_successfully(self, na_element, api_args=None, + enable_tunneling=False, use_zapi=True): + """Invokes API and checks execution status as success. + + Need to set enable_tunneling to True explicitly to achieve it. + This helps to use same connection instance to enable or disable + tunneling. The vserver or vfiler should be set before this call + otherwise tunneling remains disabled. + """ + pass + + def _build_session(self): + """Builds a session in the client.""" + if self._auth_style == STYLE_LOGIN_PASSWORD: + auth_handler = self._create_basic_auth_handler() + else: + auth_handler = self._create_certificate_auth_handler() + + self._session = requests.Session() + self._session.auth = auth_handler + self._session.verify = self._ssl_verify + headers = self._build_headers() + + self._session.headers = headers + + def _build_headers(self): + """Adds the necessary headers to the session.""" + raise NotImplementedError() + + def _create_basic_auth_handler(self): + """Creates and returns a basic HTTP auth handler.""" + return auth.HTTPBasicAuth(self._username, self._password) + + def _create_certificate_auth_handler(self): + """Creates and returns a certificate auth handler.""" + raise NotImplementedError() + + def __str__(self): + """Gets a representation of the client.""" + return "server: %s" % (self._host) + + +class ZapiClient(BaseClient): + + SERVER_TYPE_FILER = 'filer' + SERVER_TYPE_DFM = 'dfm' + URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer' + URL_DFM = 'apis/XMLrequest' + NETAPP_NS = 'http://www.netapp.com/filer/admin' + + def __init__(self, host, server_type=SERVER_TYPE_FILER, + transport_type=TRANSPORT_TYPE_HTTP, + style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, + password=None, port=None, trace=False, + api_trace_pattern=utils.API_TRACE_PATTERN): + super(ZapiClient, self).__init__( + host, transport_type=transport_type, style=style, + ssl_cert_path=ssl_cert_path, username=username, password=password, + port=port, trace=trace, api_trace_pattern=api_trace_pattern) + self.set_server_type(server_type) + self._set_port() + + def _set_port(self): + """Defines which port will be used to communicate with ONTAP.""" + if self._protocol == TRANSPORT_TYPE_HTTP: + if self._server_type == ZapiClient.SERVER_TYPE_FILER: + self.set_port(80) + else: + self.set_port(8088) + else: + if self._server_type == ZapiClient.SERVER_TYPE_FILER: + self.set_port(443) + else: + self.set_port(8488) + + def set_server_type(self, server_type): + """Set the target server type. + + Supports filer and dfm server types. + """ + if server_type.lower() not in (ZapiClient.SERVER_TYPE_FILER, + ZapiClient.SERVER_TYPE_DFM): + raise ValueError('Unsupported server type') + self._server_type = server_type.lower() + if self._server_type == ZapiClient.SERVER_TYPE_FILER: + self._url = ZapiClient.URL_FILER + else: + self._url = ZapiClient.URL_DFM + self._ns = ZapiClient.NETAPP_NS + self._refresh_conn = True + + def get_vfiler(self): + """Get the vfiler to use in tunneling.""" + return self._vfiler + + def set_vfiler(self, vfiler): + """Set the vfiler to use if tunneling gets enabled.""" + self._vfiler = vfiler + def invoke_elem(self, na_element, enable_tunneling=False): """Invoke the API on the server.""" if na_element and not isinstance(na_element, NaElement): ValueError('NaElement must be supplied to invoke API') + request_element = self._create_request(na_element, enable_tunneling) request_d = request_element.to_string() @@ -282,7 +349,8 @@ class NaServer(object): return response_element - def invoke_successfully(self, na_element, enable_tunneling=False): + def invoke_successfully(self, na_element, api_args=None, + enable_tunneling=False, use_zapi=True): """Invokes API and checks execution status as success. Need to set enable_tunneling to True explicitly to achieve it. @@ -290,7 +358,12 @@ class NaServer(object): tunneling. The vserver or vfiler should be set before this call otherwise tunneling remains disabled. """ - result = self.invoke_elem(na_element, enable_tunneling) + if api_args: + na_element.translate_struct(api_args) + + result = self.invoke_elem( + na_element, enable_tunneling=enable_tunneling) + if result.has_attr('status') and result.get_attr('status') == 'passed': return result code = (result.get_attr('errno') @@ -336,7 +409,8 @@ class NaServer(object): raise ValueError('ontapi version has to be atleast 1.15' ' to send request to vserver') - def _parse_response(self, response): + @staticmethod + def _parse_response(response): """Get the NaElement for the response.""" if not response: raise NaApiError('No response received') @@ -349,28 +423,287 @@ class NaServer(object): return processed_response.get_child_by_name('results') def _get_url(self): + """Get the base url to send the request.""" host = self._host if ':' in host: host = '[%s]' % host return '%s://%s:%s/%s' % (self._protocol, host, self._port, self._url) - def _build_session(self): - if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD: - auth_handler = self._create_basic_auth_handler() + def _build_headers(self): + """Build and return headers.""" + return {'Content-Type': 'text/xml'} + + +class RestClient(BaseClient): + + def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP, + style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, + password=None, port=None, trace=False, + api_trace_pattern=utils.API_TRACE_PATTERN): + super(RestClient, self).__init__( + host, transport_type=transport_type, style=style, + ssl_cert_path=ssl_cert_path, username=username, password=password, + port=port, trace=trace, api_trace_pattern=api_trace_pattern) + self._set_port() + + def _set_port(self): + if self._protocol == TRANSPORT_TYPE_HTTP: + self.set_port(80) else: - auth_handler = self._create_certificate_auth_handler() + self.set_port(443) - self._session = requests.Session() - self._session.auth = auth_handler - self._session.verify = self._ssl_verify - self._session.headers = { - 'Content-Type': 'text/xml', 'charset': 'utf-8'} + def _get_request_info(self, api_name, session): + """Returns the request method and url to be used in the REST call.""" - def _create_basic_auth_handler(self): - return auth.HTTPBasicAuth(self._username, self._password) + request_methods = { + 'post': session.post, + 'get': session.get, + 'put': session.put, + 'delete': session.delete, + 'patch': session.patch, + } + rest_call = rest_endpoints.endpoints.get(api_name) + return request_methods[rest_call['method']], rest_call['url'] - def _create_certificate_auth_handler(self): - raise NotImplementedError() + def _add_query_params_to_url(self, url, query): + """Populates the URL with specified filters.""" + filters = "" + for k, v in query.items(): + filters += "%(key)s=%(value)s&" % {"key": k, "value": v} + url += "?" + filters + return url + + def invoke_elem(self, na_element, api_args=None): + """Invoke the API on the server.""" + if na_element and not isinstance(na_element, NaElement): + raise ValueError('NaElement must be supplied to invoke API') + + api_name = na_element.get_name() + api_name_matches_regex = (re.match(self._api_trace_pattern, api_name) + is not None) + data = api_args.get("body") if api_args else {} + + if (not hasattr(self, '_session') or not self._session + or self._refresh_conn): + self._build_session() + request_method, action_url = self._get_request_info( + api_name, self._session) + + url_params = api_args.get("url_params") if api_args else None + if url_params: + action_url = action_url % url_params + + query = api_args.get("query") if api_args else None + if query: + action_url = self._add_query_params_to_url( + action_url, api_args['query']) + + url = self._get_base_url() + action_url + data = jsonutils.dumps(data) if data else data + + if self._trace and api_name_matches_regex: + message = ("Request: %(method)s %(url)s. Request body " + "%(body)s") % { + "method": request_method, + "url": action_url, + "body": api_args.get("body") if api_args else {} + } + LOG.debug(message) + + try: + if hasattr(self, '_timeout'): + response = request_method( + url, data=data, timeout=self._timeout) + else: + response = request_method(url, data=data) + except requests.HTTPError as e: + raise NaApiError(e.errno, e.strerror) + except requests.URLRequired as e: + raise exception.StorageCommunicationException(six.text_type(e)) + except Exception as e: + raise NaApiError(message=e) + + response = ( + jsonutils.loads(response.content) if response.content else None) + if self._trace and api_name_matches_regex: + LOG.debug("Response: %s", response) + + return response + + def invoke_successfully(self, na_element, api_args=None, + enable_tunneling=False, use_zapi=False): + """Invokes API and checks execution status as success. + + Need to set enable_tunneling to True explicitly to achieve it. + This helps to use same connection instance to enable or disable + tunneling. The vserver or vfiler should be set before this call + otherwise tunneling remains disabled. + """ + result = self.invoke_elem(na_element, api_args=api_args) + if not result.get('error'): + return result + result_error = result.get('error') + code = (result_error.get('code') + or 'ESTATUSFAILED') + if code == ESIS_CLONE_NOT_LICENSED: + msg = 'Clone operation failed: FlexClone not licensed.' + else: + msg = (result_error.get('message') + or 'Execution status is failed due to unknown reason') + raise NaApiError(code, msg) + + def _get_base_url(self): + """Get the base URL for REST requests.""" + host = self._host + if ':' in host: + host = '[%s]' % host + return '%s://%s:%s/api/' % (self._protocol, host, self._port) + + def _build_headers(self): + """Build and return headers for a REST request.""" + headers = { + "Accept": "application/json", + "Content-Type": "application/json" + } + return headers + + +class NaServer(object): + """Encapsulates server connection logic.""" + + def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP, + style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, + password=None, port=None, trace=False, + api_trace_pattern=utils.API_TRACE_PATTERN): + self.zapi_client = ZapiClient( + host, transport_type=transport_type, style=style, + ssl_cert_path=ssl_cert_path, username=username, password=password, + port=port, trace=trace, api_trace_pattern=api_trace_pattern) + self.rest_client = RestClient( + host, transport_type=transport_type, style=style, + ssl_cert_path=ssl_cert_path, username=username, password=password, + port=port, trace=trace, api_trace_pattern=api_trace_pattern + ) + self._host = host + + LOG.debug('Using NetApp controller: %s', self._host) + + def get_transport_type(self, use_zapi_client=True): + """Get the transport type protocol.""" + return self.get_client(use_zapi=use_zapi_client).get_transport_type() + + def set_transport_type(self, transport_type): + """Set the transport type protocol for API. + + Supports http and https transport types. + """ + self.zapi_client.set_transport_type(transport_type) + self.rest_client.set_transport_type(transport_type) + + def get_style(self, use_zapi_client=True): + """Get the authorization style for communicating with the server.""" + return self.get_client(use_zapi=use_zapi_client).get_style() + + def set_style(self, style): + """Set the authorization style for communicating with the server. + + Supports basic_auth for now. Certificate_auth mode to be done. + """ + self.zapi_client.set_style(style) + self.rest_client.set_style(style) + + def get_server_type(self, use_zapi_client=True): + """Get the target server type.""" + return self.get_client(use_zapi=use_zapi_client).get_server_type() + + def set_server_type(self, server_type): + """Set the target server type. + + Supports filer and dfm server types. + """ + self.zapi_client.set_server_type(server_type) + self.rest_client.set_server_type(server_type) + + def set_api_version(self, major, minor): + """Set the API version.""" + self.zapi_client.set_api_version(major, minor) + self.rest_client.set_api_version(1, 0) + + def set_system_version(self, system_version): + """Set the ONTAP system version.""" + self.zapi_client.set_system_version(system_version) + self.rest_client.set_system_version(system_version) + + def get_api_version(self, use_zapi_client=True): + """Gets the API version tuple.""" + return self.get_client(use_zapi=use_zapi_client).get_api_version() + + def get_system_version(self, use_zapi_client=True): + """Gets the ONTAP system version.""" + return self.get_client(use_zapi=use_zapi_client).get_system_version() + + def set_port(self, port): + """Set the server communication port.""" + self.zapi_client.set_port(port) + self.rest_client.set_port(port) + + def get_port(self, use_zapi_client=True): + """Get the server communication port.""" + return self.get_client(use_zapi=use_zapi_client).get_port() + + def set_timeout(self, seconds): + """Sets the timeout in seconds.""" + self.zapi_client.set_timeout(seconds) + self.rest_client.set_timeout(seconds) + + def get_timeout(self, use_zapi_client=True): + """Gets the timeout in seconds if set.""" + return self.get_client(use_zapi=use_zapi_client).get_timeout() + + def get_vfiler(self): + """Get the vfiler to use in tunneling.""" + return self.zapi_client.get_vfiler() + + def set_vfiler(self, vfiler): + """Set the vfiler to use if tunneling gets enabled.""" + self.zapi_client.set_vfiler(vfiler) + + def get_vserver(self, use_zapi_client=True): + """Get the vserver to use in tunneling.""" + return self.get_client(use_zapi=use_zapi_client).get_vserver() + + def set_vserver(self, vserver): + """Set the vserver to use if tunneling gets enabled.""" + self.zapi_client.set_vserver(vserver) + self.rest_client.set_vserver(vserver) + + def set_username(self, username): + """Set the user name for authentication.""" + self.zapi_client.set_username(username) + self.rest_client.set_username(username) + + def set_password(self, password): + """Set the password for authentication.""" + self.zapi_client.set_password(password) + self.rest_client.set_password(password) + + def get_client(self, use_zapi=True): + """Chooses the client to be used in the request.""" + if use_zapi: + return self.zapi_client + return self.rest_client + + def invoke_successfully(self, na_element, api_args=None, + enable_tunneling=False, use_zapi=True): + """Invokes API and checks execution status as success. + + Need to set enable_tunneling to True explicitly to achieve it. + This helps to use same connection instance to enable or disable + tunneling. The vserver or vfiler should be set before this call + otherwise tunneling remains disabled. + """ + return self.get_client(use_zapi=use_zapi).invoke_successfully( + na_element, api_args=api_args, enable_tunneling=enable_tunneling) def __str__(self): return "server: %s" % (self._host) diff --git a/manila/share/drivers/netapp/dataontap/client/client_base.py b/manila/share/drivers/netapp/dataontap/client/client_base.py index 131294e8bb..fcbc6c7c31 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_base.py +++ b/manila/share/drivers/netapp/dataontap/client/client_base.py @@ -81,12 +81,13 @@ class NetAppBaseClient(object): return string.split('}', 1)[1] return string - def send_request(self, api_name, api_args=None, enable_tunneling=True): + def send_request(self, api_name, api_args=None, enable_tunneling=True, + use_zapi=True): """Sends request to Ontapi.""" request = netapp_api.NaElement(api_name) - if api_args: - request.translate_struct(api_args) - return self.connection.invoke_successfully(request, enable_tunneling) + return self.connection.invoke_successfully( + request, api_args=api_args, enable_tunneling=enable_tunneling, + use_zapi=use_zapi) @na_utils.trace def get_licenses(self): diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index 34768ece7e..40fc78da74 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -74,6 +74,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): ontapi_1_120 = ontapi_version >= (1, 120) ontapi_1_140 = ontapi_version >= (1, 140) ontapi_1_150 = ontapi_version >= (1, 150) + ontap_9_10 = self.get_system_version()['version-tuple'] >= (9, 10, 0) self.features.add_feature('SNAPMIRROR_V2', supported=ontapi_1_20) self.features.add_feature('SYSTEM_METRICS', supported=ontapi_1_2x) @@ -95,6 +96,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): supported=ontapi_1_150) self.features.add_feature('LDAP_LDAP_SERVERS', supported=ontapi_1_120) + self.features.add_feature('SVM_MIGRATE', supported=ontap_9_10) def _invoke_vserver_api(self, na_element, vserver): server = copy.copy(self.connection) @@ -1040,6 +1042,24 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): return interfaces + @na_utils.trace + def disable_network_interface(self, vserver_name, interface_name): + api_args = { + 'administrative-status': 'down', + 'interface-name': interface_name, + 'vserver': vserver_name, + } + self.send_request('net-interface-modify', api_args) + + @na_utils.trace + def delete_network_interface(self, vserver_name, interface_name): + self.disable_network_interface(vserver_name, interface_name) + api_args = { + 'interface-name': interface_name, + 'vserver': vserver_name + } + self.send_request('net-interface-delete', api_args) + @na_utils.trace def get_ipspace_name_for_vlan_port(self, vlan_node, vlan_port, vlan_id): """Gets IPSpace name for specified VLAN""" @@ -3605,7 +3625,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): # NOTE(cknight): Cannot use deepcopy on the connection context node_client = copy.copy(self) - node_client.connection = copy.copy(self.connection) + node_client.connection = copy.copy(self.connection.get_client()) node_client.connection.set_timeout(25) try: @@ -5453,3 +5473,173 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): raise exception.NetAppException(msg) return fpolicy_status + + @na_utils.trace + def is_svm_migrate_supported(self): + """Checks if the cluster supports SVM Migrate.""" + return self.features.SVM_MIGRATE + + # ------------------------ REST CALLS ONLY ------------------------ + + @na_utils.trace + def _format_request(self, request_data, headers={}, query={}, + url_params={}): + """Receives the request data and formats it into a request pattern. + + :param request_data: the body to be sent to the request. + :param headers: additional headers to the request. + :param query: filters to the request. + :param url_params: parameters to be added to the request. + """ + request = { + "body": request_data, + "headers": headers, + "query": query, + "url_params": url_params + } + return request + + @na_utils.trace + def svm_migration_start( + self, source_cluster_name, source_share_server_name, + dest_aggregates, dest_ipspace=None, check_only=False): + """Send a request to start the SVM migration in the backend. + + :param source_cluster_name: the name of the source cluster. + :param source_share_server_name: the name of the source server. + :param dest_aggregates: the aggregates where volumes will be placed in + the migration. + :param dest_ipspace: created IPspace for the migration. + :param check_only: If the call will only check the feasibility. + deleted after the cutover or not. + """ + request = { + "auto_cutover": False, + "auto_source_cleanup": True, + "check_only": check_only, + "source": { + "cluster": {"name": source_cluster_name}, + "svm": {"name": source_share_server_name}, + }, + "destination": { + "volume_placement": { + "aggregates": dest_aggregates, + }, + }, + } + + if dest_ipspace: + ipspace_data = { + "ipspace": { + "name": dest_ipspace, + } + } + request["destination"].update(ipspace_data) + + api_args = self._format_request(request) + + return self.send_request( + 'svm-migration-start', api_args=api_args, use_zapi=False) + + @na_utils.trace + def get_migration_check_job_state(self, job_id): + """Get the job state of a share server migration. + + :param job_id: id of the job to be searched. + """ + try: + job = self.get_job(job_id) + return job + except netapp_api.NaApiError as e: + if e.code == netapp_api.ENFS_V4_0_ENABLED_MIGRATION_FAILURE: + msg = _( + 'NFS v4.0 is not supported while migrating vservers.') + LOG.error(msg) + raise exception.NetAppException(message=e.message) + if e.code == netapp_api.EVSERVER_MIGRATION_TO_NON_AFF_CLUSTER: + msg = _('Both source and destination clusters must be AFF ' + 'systems.') + LOG.error(msg) + raise exception.NetAppException(message=e.message) + msg = (_('Failed to check migration support. Reason: ' + '%s' % e.message)) + raise exception.NetAppException(msg) + + @na_utils.trace + def svm_migrate_complete(self, migration_id): + """Send a request to complete the SVM migration. + + :param migration_id: the id of the migration provided by the storage. + """ + request = { + "action": "cutover" + } + url_params = { + "svm_migration_id": migration_id + } + api_args = self._format_request( + request, url_params=url_params) + + return self.send_request( + 'svm-migration-complete', api_args=api_args, use_zapi=False) + + @na_utils.trace + def svm_migrate_cancel(self, migration_id): + """Send a request to cancel the SVM migration. + + :param migration_id: the id of the migration provided by the storage. + """ + request = {} + url_params = { + "svm_migration_id": migration_id + } + api_args = self._format_request(request, url_params=url_params) + return self.send_request( + 'svm-migration-cancel', api_args=api_args, use_zapi=False) + + @na_utils.trace + def svm_migration_get(self, migration_id): + """Send a request to get the progress of the SVM migration. + + :param migration_id: the id of the migration provided by the storage. + """ + request = {} + url_params = { + "svm_migration_id": migration_id + } + api_args = self._format_request(request, url_params=url_params) + return self.send_request( + 'svm-migration-get', api_args=api_args, use_zapi=False) + + @na_utils.trace + def svm_migrate_pause(self, migration_id): + """Send a request to pause a migration. + + :param migration_id: the id of the migration provided by the storage. + """ + request = { + "action": "pause" + } + url_params = { + "svm_migration_id": migration_id + } + api_args = self._format_request( + request, url_params=url_params) + return self.send_request( + 'svm-migration-pause', api_args=api_args, use_zapi=False) + + @na_utils.trace + def get_job(self, job_uuid): + """Get a job in ONTAP. + + :param job_uuid: uuid of the job to be searched. + """ + request = {} + url_params = { + "job_uuid": job_uuid + } + + api_args = self._format_request(request, url_params=url_params) + + return self.send_request( + 'get-job', api_args=api_args, use_zapi=False) diff --git a/manila/share/drivers/netapp/dataontap/client/rest_endpoints.py b/manila/share/drivers/netapp/dataontap/client/rest_endpoints.py new file mode 100644 index 0000000000..14566bc232 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/client/rest_endpoints.py @@ -0,0 +1,49 @@ +# Copyright 2021 NetApp, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +ENDPOINT_MIGRATION_ACTIONS = 'svm/migrations/%(svm_migration_id)s' +ENDPOINT_MIGRATIONS = 'svm/migrations' +ENDPOINT_JOB_ACTIONS = 'cluster/jobs/%(job_uuid)s' + +endpoints = { + 'system-get-version': { + 'method': 'get', + 'url': 'cluster?fields=version', + }, + 'svm-migration-start': { + 'method': 'post', + 'url': ENDPOINT_MIGRATIONS + }, + 'svm-migration-complete': { + 'method': 'patch', + 'url': ENDPOINT_MIGRATION_ACTIONS + }, + 'svm-migration-cancel': { + 'method': 'delete', + 'url': ENDPOINT_MIGRATION_ACTIONS + }, + 'svm-migration-get': { + 'method': 'get', + 'url': ENDPOINT_MIGRATION_ACTIONS + }, + 'get-job': { + 'method': 'get', + 'url': ENDPOINT_JOB_ACTIONS + }, + 'svm-migration-pause': { + 'method': 'patch', + 'url': ENDPOINT_MIGRATION_ACTIONS + }, +} 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 9bc47c2779..333db31132 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/data_motion.py @@ -85,6 +85,13 @@ def get_client_for_backend(backend_name, vserver_name=None): return client +def get_client_for_host(host): + """Returns a cluster client to the desired host.""" + backend_name = share_utils.extract_host(host, level='backend_name') + client = get_client_for_backend(backend_name) + return client + + class DataMotionSession(object): def _get_backend_volume_name(self, config, share_obj): 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 ec85eb3a7e..ef075bfd57 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 @@ -304,7 +304,7 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver): def share_server_migration_start(self, context, src_share_server, dest_share_server, shares, snapshots): - self.library.share_server_migration_start( + return self.library.share_server_migration_start( context, src_share_server, dest_share_server, shares, snapshots) def share_server_migration_continue(self, context, src_share_server, diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py index bf01c1fe9b..9f4a2e1d53 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -1310,7 +1310,8 @@ class NetAppCmodeFileStorageLibrary(object): @na_utils.trace def _create_export(self, share, share_server, vserver, vserver_client, clear_current_export_policy=True, - ensure_share_already_exists=False, replica=False): + ensure_share_already_exists=False, replica=False, + share_host=None): """Creates NAS storage.""" helper = self._get_helper(share) helper.set_client(vserver_client) @@ -1325,9 +1326,11 @@ class NetAppCmodeFileStorageLibrary(object): msg_args = {'vserver': vserver, 'proto': share['share_proto']} raise exception.NetAppException(msg % msg_args) + host = share_host if share_host else share['host'] + # Get LIF addresses with metadata export_addresses = self._get_export_addresses_with_metadata( - share, share_server, interfaces) + share, share_server, interfaces, host) # Create the share and get a callback for generating export locations callback = helper.create_share( @@ -1355,11 +1358,11 @@ class NetAppCmodeFileStorageLibrary(object): @na_utils.trace def _get_export_addresses_with_metadata(self, share, share_server, - interfaces): + interfaces, share_host): """Return interface addresses with locality and other metadata.""" # Get home node so we can identify preferred paths - aggregate_name = share_utils.extract_host(share['host'], level='pool') + aggregate_name = share_utils.extract_host(share_host, level='pool') home_node = self._get_aggregate_node(aggregate_name) # Get admin LIF addresses so we can identify admin export locations 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 610cfc7433..876f4f9fcf 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 @@ -44,6 +44,8 @@ SUPPORTED_NETWORK_TYPES = (None, 'flat', 'vlan') SEGMENTED_NETWORK_TYPES = ('vlan',) DEFAULT_MTU = 1500 CLUSTER_IPSPACES = ('Cluster', 'Default') +SERVER_MIGRATE_SVM_DR = 'svm_dr' +SERVER_MIGRATE_SVM_MIGRATE = 'svm_migrate' class NetAppCmodeMultiSVMFileStorageLibrary( @@ -306,10 +308,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return 'ipspace_' + network_id.replace('-', '_') @na_utils.trace - def _create_ipspace(self, network_info): + def _create_ipspace(self, network_info, client=None): """If supported, create an IPspace for a new Vserver.""" - if not self._client.features.IPSPACES: + desired_client = client if client else self._client + + if not desired_client.features.IPSPACES: return None if (network_info['network_allocations'][0]['network_type'] @@ -324,7 +328,7 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return client_cmode.DEFAULT_IPSPACE ipspace_name = self._get_valid_ipspace_name(ipspace_id) - self._client.create_ipspace(ipspace_name) + desired_client.create_ipspace(ipspace_name) return ipspace_name @@ -903,6 +907,213 @@ class NetAppCmodeMultiSVMFileStorageLibrary( manage_existing(share, driver_options, share_server=share_server)) + @na_utils.trace + def _check_compatibility_using_svm_dr( + self, src_client, dest_client, shares_request_spec, pools): + """Send a request to pause a migration. + + :param src_client: source cluster client. + :param dest_client: destination cluster client. + :param shares_request_spec: shares specifications. + :param pools: pools to be used during the migration. + :returns server migration mechanism name and compatibility result + example: (svm_dr, True). + """ + method = SERVER_MIGRATE_SVM_DR + if (not src_client.is_svm_dr_supported() + or not dest_client.is_svm_dr_supported()): + msg = _("Cannot perform server migration because at least one of " + "the backends doesn't support SVM DR.") + LOG.error(msg) + return method, False + + # Check capacity. + server_total_size = (shares_request_spec.get('shares_size', 0) + + shares_request_spec.get('snapshots_size', 0)) + # NOTE(dviroel): If the backend has a 'max_over_subscription_ratio' + # configured and greater than 1, we'll consider thin provisioning + # enable for all shares. + thin_provisioning = self.configuration.max_over_subscription_ratio > 1 + if self.configuration.netapp_server_migration_check_capacity is True: + if not self._check_capacity_compatibility(pools, thin_provisioning, + server_total_size): + msg = _("Cannot perform server migration because destination " + "host doesn't have enough free space.") + LOG.error(msg) + return method, False + return method, True + + @na_utils.trace + def _get_job_uuid(self, job): + """Get the uuid of a job.""" + job = job.get("job", {}) + return job.get("uuid") + + @na_utils.trace + def _wait_for_operation_status( + self, operation_id, func_get_operation, desired_status='success', + timeout=None): + """Waits until a given operation reachs the desired status. + + :param operation_id: ID of the operation to be searched. + :param func_get_operation: Function to be used to get the operation + details. + :param desired_status: Operation expected status. + :param timeout: How long (in seconds) should the driver wait for the + status to be reached. + + """ + if not timeout: + timeout = ( + self.configuration.netapp_server_migration_state_change_timeout + ) + interval = 10 + retries = int(timeout / interval) or 1 + + @utils.retry(exception.ShareBackendException, interval=interval, + retries=retries, backoff_rate=1) + def wait_for_status(): + # Get the job based on its id. + operation = func_get_operation(operation_id) + status = operation.get("status") or operation.get("state") + + if status != desired_status: + msg = _( + "Operation %(operation_id)s didn't reach status " + "%(desired_status)s. Current status is %(status)s.") % { + 'operation_id': operation_id, + 'desired_status': desired_status, + 'status': status + } + LOG.debug(msg) + + # Failed, no need to retry. + if status == 'error': + msg = _('Operation %(operation_id)s is in error status.' + 'Reason: %(message)s') + raise exception.NetAppException( + msg % {'operation_id': operation_id, + 'message': operation.get('message')}) + + # Didn't fail, so we can retry. + raise exception.ShareBackendException(msg) + + elif status == desired_status: + msg = _( + 'Operation %(operation_id)s reached status %(status)s.') + LOG.debug( + msg, {'operation_id': operation_id, 'status': status}) + return + try: + wait_for_status() + except exception.NetAppException: + raise + except exception.ShareBackendException: + msg_args = {'operation_id': operation_id, 'status': desired_status} + msg = _('Timed out while waiting for operation %(operation_id)s ' + 'to reach status %(status)s') % msg_args + raise exception.NetAppException(msg) + + @na_utils.trace + def _check_compatibility_for_svm_migrate( + self, source_cluster_name, source_share_server_name, + source_share_server, dest_aggregates, dest_client): + """Checks if the migration can be performed using SVM Migrate. + + 1. Send the request to the backed to check if the migration is possible + 2. Wait until the job finishes checking the migration status + """ + + # Reuse network information from the source share server in the SVM + # Migrate if the there was no share network changes. + network_info = { + 'network_allocations': + source_share_server['network_allocations'], + 'neutron_subnet_id': + source_share_server['share_network_subnet'].get( + 'neutron_subnet_id') + } + + # 2. Create new ipspace, port and broadcast domain. + node_name = self._client.list_cluster_nodes()[0] + port = self._get_node_data_port(node_name) + vlan = network_info['network_allocations'][0]['segmentation_id'] + destination_ipspace = self._client.get_ipspace_name_for_vlan_port( + node_name, port, vlan) or self._create_ipspace( + network_info, client=dest_client) + self._create_port_and_broadcast_domain( + destination_ipspace, network_info) + + def _cleanup_ipspace(ipspace): + try: + dest_client.delete_ipspace(ipspace) + except Exception: + LOG.info( + 'Did not delete ipspace used to check the compatibility ' + 'for SVM Migrate. It is possible that it was reused and ' + 'there are other entities consuming it.') + + # 1. Sends the request to the backend. + try: + job = dest_client.svm_migration_start( + source_cluster_name, source_share_server_name, dest_aggregates, + dest_ipspace=destination_ipspace, check_only=True) + except Exception: + LOG.error('Failed to check compatibility for migration.') + _cleanup_ipspace(destination_ipspace) + raise + + job_id = self._get_job_uuid(job) + + try: + # 2. Wait until the job to check the migration status concludes. + self._wait_for_operation_status( + job_id, dest_client.get_migration_check_job_state) + _cleanup_ipspace(destination_ipspace) + return True + except exception.NetAppException: + # Performed the check with the given parameters and the backend + # returned an error, so the migration is not compatible + _cleanup_ipspace(destination_ipspace) + return False + + @na_utils.trace + def _check_for_migration_support( + self, src_client, dest_client, source_share_server, + shares_request_spec, src_cluster_name, pools): + """Checks if the migration is supported and chooses the way to do it + + In terms of performance, SVM Migrate is more adequate and it should + be prioritised over a SVM DR migration. If both source and destination + clusters do not support SVM Migrate, then SVM DR is the option to be + used. + 1. Checks if both source and destination clients support SVM Migrate. + 2. Requests the migration. + """ + + # 1. Checks if both source and destination clients support SVM Migrate. + if (dest_client.is_svm_migrate_supported() + and src_client.is_svm_migrate_supported()): + source_share_server_name = self._get_vserver_name( + source_share_server['id']) + + # Check if the migration is supported. + try: + result = self._check_compatibility_for_svm_migrate( + src_cluster_name, source_share_server_name, + source_share_server, self._find_matching_aggregates(), + dest_client) + return SERVER_MIGRATE_SVM_MIGRATE, result + except Exception: + LOG.error('Failed to check the compatibility for the share ' + 'server migration using SVM Migrate.') + return SERVER_MIGRATE_SVM_MIGRATE, False + + # SVM Migrate is not supported, try to check the compatibility using + # SVM DR. + return self._check_compatibility_using_svm_dr( + src_client, dest_client, shares_request_spec, pools) + @na_utils.trace def share_server_migration_check_compatibility( self, context, source_share_server, dest_host, old_share_network, @@ -958,16 +1169,17 @@ class NetAppCmodeMultiSVMFileStorageLibrary( LOG.error(msg) return not_compatible - # Check for SVM DR support + pools = self._get_pools() + # NOTE(dviroel): These clients can only be used for non-tunneling # requests. dst_client = data_motion.get_client_for_backend(dest_backend_name, vserver_name=None) - if (not src_client.is_svm_dr_supported() - or not dst_client.is_svm_dr_supported()): - msg = _("Cannot perform server migration because at leat one of " - "the backends doesn't support SVM DR.") - LOG.error(msg) + migration_method, compatibility = self._check_for_migration_support( + src_client, dst_client, source_share_server, shares_request_spec, + src_cluster_name, pools) + + if not compatibility: return not_compatible # Blocking different security services for now @@ -985,7 +1197,6 @@ class NetAppCmodeMultiSVMFileStorageLibrary( LOG.error(msg) return not_compatible - pools = self._get_pools() # Check 'netapp_flexvol_encryption' and 'revert_to_snapshot_support' specs_to_validate = ('netapp_flexvol_encryption', 'revert_to_snapshot_support') @@ -1000,25 +1211,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return not_compatible # TODO(dviroel): disk_type extra-spec - # Check capacity - server_total_size = (shares_request_spec.get('shares_size', 0) + - shares_request_spec.get('snapshots_size', 0)) - # NOTE(dviroel): If the backend has a 'max_over_subscription_ratio' - # configured and greater than 1, we'll consider thin provisioning - # enable for all shares. - thin_provisioning = self.configuration.max_over_subscription_ratio > 1 - if self.configuration.netapp_server_migration_check_capacity is True: - if not self._check_capacity_compatibility(pools, thin_provisioning, - server_total_size): - msg = _("Cannot perform server migration because destination " - "host doesn't have enough free space.") - LOG.error(msg) - return not_compatible + nondisruptive = (migration_method == SERVER_MIGRATE_SVM_MIGRATE) compatibility = { 'compatible': True, 'writable': True, - 'nondisruptive': False, + 'nondisruptive': nondisruptive, 'preserve_snapshots': True, 'share_network_id': new_share_network['id'], 'migration_cancel': True, @@ -1027,9 +1225,9 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return compatibility - def share_server_migration_start(self, context, source_share_server, - dest_share_server, share_intances, - snapshot_instances): + @na_utils.trace + def _migration_start_using_svm_dr( + self, source_share_server, dest_share_server): """Start share server migration using SVM DR. 1. Create vserver peering between source and destination @@ -1078,14 +1276,126 @@ class NetAppCmodeMultiSVMFileStorageLibrary( msg = _('Could not initialize SnapMirror between %(src)s and ' '%(dest)s vservers.') % msg_args raise exception.NetAppException(message=msg) + return None + + @na_utils.trace + def _migration_start_using_svm_migrate( + self, context, source_share_server, dest_share_server, src_client, + dest_client): + """Start share server migration using SVM Migrate. + + 1. Check if share network reusage is supported + 2. Create a new ipspace, port and broadcast domain to the dest server + 3. Send the request start the share server migration + 4. Read the job id and get the id of the migration + 5. Set the migration uuid in the backend details + """ + + # 1. Check if share network reusage is supported + # NOTE(carloss): if share network was not changed, SVM migrate can + # reuse the network allocation from the source share server, so as + # Manila haven't made new allocations, we can just get allocation data + # from the source share server. + if not dest_share_server['network_allocations']: + share_server_to_get_network_info = source_share_server + else: + share_server_to_get_network_info = dest_share_server + + # Reuse network information from the source share server in the SVM + # Migrate if the there was no share network changes. + network_info = { + 'network_allocations': + share_server_to_get_network_info['network_allocations'], + 'neutron_subnet_id': + share_server_to_get_network_info['share_network_subnet'].get( + 'neutron_subnet_id') + } + + # 2. Create new ipspace, port and broadcast domain. + node_name = self._client.list_cluster_nodes()[0] + port = self._get_node_data_port(node_name) + vlan = network_info['network_allocations'][0]['segmentation_id'] + destination_ipspace = self._client.get_ipspace_name_for_vlan_port( + node_name, port, vlan) or self._create_ipspace( + network_info, client=dest_client) + self._create_port_and_broadcast_domain( + destination_ipspace, network_info) + + # Prepare the migration request. + src_cluster_name = src_client.get_cluster_name() + source_share_server_name = self._get_vserver_name( + source_share_server['id']) + + # 3. Send the migration request to ONTAP. + try: + result = dest_client.svm_migration_start( + src_cluster_name, source_share_server_name, + self._find_matching_aggregates(), + dest_ipspace=destination_ipspace) + + # 4. Read the job id and get the id of the migration. + result_job = result.get("job", {}) + job_details = dest_client.get_job(result_job.get("uuid")) + job_description = job_details.get('description') + migration_uuid = job_description.split('/')[-1] + except Exception: + # As it failed, we must remove the ipspace, ports and broadcast + # domain. + dest_client.delete_ipspace(destination_ipspace) + + msg = _("Unable to start the migration for share server %s." + % source_share_server['id']) + raise exception.NetAppException(msg) + + # 5. Returns migration data to be saved as backend details. + server_info = { + "backend_details": { + na_utils.MIGRATION_OPERATION_ID_KEY: migration_uuid + } + } + return server_info + + @na_utils.trace + def share_server_migration_start( + self, context, source_share_server, dest_share_server, + share_intances, snapshot_instances): + """Start share server migration. + + This method will choose the best migration strategy to perform the + migration, based on the storage functionalities support. + """ + src_backend_name = share_utils.extract_host( + source_share_server['host'], level='backend_name') + dest_backend_name = share_utils.extract_host( + dest_share_server['host'], level='backend_name') + dest_client = data_motion.get_client_for_backend( + dest_backend_name, vserver_name=None) + __, src_client = self._get_vserver( + share_server=source_share_server, backend_name=src_backend_name) + + use_svm_migrate = ( + src_client.is_svm_migrate_supported() + and dest_client.is_svm_migrate_supported()) + + if use_svm_migrate: + result = self._migration_start_using_svm_migrate( + context, source_share_server, dest_share_server, src_client, + dest_client) + else: + result = self._migration_start_using_svm_dr( + source_share_server, dest_share_server) msg_args = { 'src': source_share_server['id'], 'dest': dest_share_server['id'], + 'migration_method': 'SVM Migrate' if use_svm_migrate else 'SVM DR' } - msg = _('Starting share server migration from %(src)s to %(dest)s.') + msg = _('Starting share server migration from %(src)s to %(dest)s ' + 'using %(migration_method)s as migration method.') LOG.info(msg, msg_args) + return result + def _get_snapmirror_svm(self, source_share_server, dest_share_server): dm_session = data_motion.DataMotionSession() try: @@ -1104,9 +1414,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return snapmirrors @na_utils.trace - def share_server_migration_continue(self, context, source_share_server, - dest_share_server, share_instances, - snapshot_instances): + def _share_server_migration_continue_svm_dr( + self, source_share_server, dest_share_server): """Continues a share server migration using SVM DR.""" snapmirrors = self._get_snapmirror_svm(source_share_server, dest_share_server) @@ -1141,10 +1450,69 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return False @na_utils.trace - def share_server_migration_complete(self, context, source_share_server, + def _share_server_migration_continue_svm_migrate(self, dest_share_server, + migration_id): + """Continues the migration for a share server. + + :param dest_share_server: reference for the destination share server. + :param migration_id: ID of the migration. + """ + dest_client = data_motion.get_client_for_host( + dest_share_server['host']) + try: + result = dest_client.svm_migration_get(migration_id) + except netapp_api.NaApiError as e: + msg = (_('Failed to continue the migration for share server ' + '%(server_id)s. Reason: %(reason)s' + ) % {'server_id': dest_share_server['id'], + 'reason': e.message} + ) + raise exception.NetAppException(message=msg) + return ( + result.get("state") == na_utils.MIGRATION_STATE_READY_FOR_CUTOVER) + + @na_utils.trace + def share_server_migration_continue(self, context, source_share_server, dest_share_server, share_instances, - snapshot_instances, new_network_alloc): - """Completes share server migration using SVM DR. + snapshot_instances): + """Continues the migration of a share server.""" + # If the migration operation was started using SVM migrate, it + # returned a migration ID to get information about the job afterwards. + migration_id = self._get_share_server_migration_id( + dest_share_server) + + # Checks the progress for a SVM migrate migration. + if migration_id: + return self._share_server_migration_continue_svm_migrate( + dest_share_server, migration_id) + + # Checks the progress of a SVM DR Migration. + return self._share_server_migration_continue_svm_dr( + source_share_server, dest_share_server) + + def _setup_networking_for_destination_vserver( + self, vserver_client, vserver_name, new_net_allocations): + ipspace_name = vserver_client.get_vserver_ipspace(vserver_name) + + # NOTE(dviroel): Security service and NFS configuration should be + # handled by SVM DR, so no changes will be made here. + vlan = new_net_allocations['segmentation_id'] + + @utils.synchronized('netapp-VLAN-%s' % vlan, external=True) + def setup_network_for_destination_vserver(): + self._setup_network_for_vserver( + vserver_name, vserver_client, new_net_allocations, + ipspace_name, + enable_nfs=False, + security_services=None) + + setup_network_for_destination_vserver() + + @na_utils.trace + def _share_server_migration_complete_svm_dr( + self, source_share_server, dest_share_server, src_vserver, + src_client, share_instances, new_net_allocations): + """Perform steps to complete the SVM DR migration. 1. Do a last SnapMirror update. 2. Quiesce, abort and then break the relationship. @@ -1152,9 +1520,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary( 4. Configure network interfaces in the destination vserver 5. Start the destinarion vserver 6. Delete and release the snapmirror - 7. Build the list of export_locations for each share - 8. Release all resources from the source share server """ + dest_backend_name = share_utils.extract_host( + dest_share_server['host'], level='backend_name') + dest_vserver, dest_client = self._get_vserver( + share_server=dest_share_server, backend_name=dest_backend_name) + dm_session = data_motion.DataMotionSession() try: # 1. Start an update to try to get a last minute transfer before we @@ -1165,15 +1536,6 @@ class NetAppCmodeMultiSVMFileStorageLibrary( # Ignore any errors since the current source may be unreachable pass - src_backend_name = share_utils.extract_host( - source_share_server['host'], level='backend_name') - src_vserver, src_client = self._get_vserver( - share_server=source_share_server, backend_name=src_backend_name) - - dest_backend_name = share_utils.extract_host( - dest_share_server['host'], level='backend_name') - dest_vserver, dest_client = self._get_vserver( - share_server=dest_share_server, backend_name=dest_backend_name) try: # 2. Attempt to quiesce, abort and then break SnapMirror dm_session.quiesce_and_break_snapmirror_svm(source_share_server, @@ -1191,20 +1553,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( src_client.stop_vserver(src_vserver) # 4. Setup network configuration - ipspace_name = dest_client.get_vserver_ipspace(dest_vserver) - - # NOTE(dviroel): Security service and NFS configuration should be - # handled by SVM DR, so no changes will be made here. - vlan = new_network_alloc['segmentation_id'] - - @utils.synchronized('netapp-VLAN-%s' % vlan, external=True) - def setup_network_for_destination_vserver(): - self._setup_network_for_vserver( - dest_vserver, dest_client, new_network_alloc, ipspace_name, - enable_nfs=False, - security_services=None) - - setup_network_for_destination_vserver() + self._setup_networking_for_destination_vserver( + dest_client, dest_vserver, new_net_allocations) # 5. Start the destination. dest_client.start_vserver(dest_vserver) @@ -1237,7 +1587,100 @@ class NetAppCmodeMultiSVMFileStorageLibrary( dm_session.delete_snapmirror_svm(source_share_server, dest_share_server) - # 7. Build a dict with shares/snapshot location updates + @na_utils.trace + def _share_server_migration_complete_svm_migrate( + self, migration_id, dest_share_server): + """Completes share server migration using SVM Migrate. + + 1. Call functions to conclude the migration for SVM Migrate + 2. Waits until the job gets a success status + 3. Wait until the migration cancellation reach the desired status + """ + dest_client = data_motion.get_client_for_host( + dest_share_server['host']) + + try: + # Triggers the migration completion. + job = dest_client.svm_migrate_complete(migration_id) + job_id = self._get_job_uuid(job) + + # Wait until the job is successful. + self._wait_for_operation_status( + job_id, dest_client.get_job) + + # Wait until the migration is entirely finished. + self._wait_for_operation_status( + migration_id, dest_client.svm_migration_get, + desired_status=na_utils.MIGRATION_STATE_MIGRATE_COMPLETE) + except exception.NetAppException: + msg = _( + "Failed to complete the migration for " + "share server %s.") % dest_share_server['id'] + raise exception.NetAppException(msg) + + @na_utils.trace + def share_server_migration_complete(self, context, source_share_server, + dest_share_server, share_instances, + snapshot_instances, new_network_alloc): + """Completes share server migration. + + 1. Call functions to conclude the migration for SVM DR or SVM Migrate + 2. Build the list of export_locations for each share + 3. Release all resources from the source share server + """ + src_backend_name = share_utils.extract_host( + source_share_server['host'], level='backend_name') + src_vserver, src_client = self._get_vserver( + share_server=source_share_server, backend_name=src_backend_name) + dest_backend_name = share_utils.extract_host( + dest_share_server['host'], level='backend_name') + + migration_id = self._get_share_server_migration_id(dest_share_server) + + share_server_to_get_vserver_name_from = ( + source_share_server if migration_id else dest_share_server) + + dest_vserver, dest_client = self._get_vserver( + share_server=share_server_to_get_vserver_name_from, + backend_name=dest_backend_name) + + server_backend_details = {} + # 1. Call functions to conclude the migration for SVM DR or SVM + # Migrate. + if migration_id: + self._share_server_migration_complete_svm_migrate( + migration_id, dest_share_server) + + server_backend_details = source_share_server['backend_details'] + + # If there are new network allocations to be added, do so, and add + # them to the share server's backend details. + if dest_share_server['network_allocations']: + # Teardown the current network allocations + current_network_interfaces = ( + dest_client.list_network_interfaces()) + + # Need a cluster client to be able to remove the current + # network interfaces + dest_cluster_client = data_motion.get_client_for_host( + dest_share_server['host']) + for interface_name in current_network_interfaces: + dest_cluster_client.delete_network_interface( + src_vserver, interface_name) + self._setup_networking_for_destination_vserver( + dest_client, src_vserver, new_network_alloc) + + server_backend_details.pop('ports') + ports = {} + for allocation in dest_share_server['network_allocations']: + ports[allocation['id']] = allocation['ip_address'] + server_backend_details['ports'] = jsonutils.dumps(ports) + else: + self._share_server_migration_complete_svm_dr( + source_share_server, dest_share_server, src_vserver, + src_client, share_instances, new_network_alloc) + + # 2. Build a dict with shares/snapshot location updates. # NOTE(dviroel): For SVM DR, the share names aren't modified, only the # export_locations are updated due to network changes. share_updates = {} @@ -1248,9 +1691,11 @@ class NetAppCmodeMultiSVMFileStorageLibrary( share_name = self._get_backend_share_name(instance['id']) volume = dest_client.get_volume(share_name) dest_aggregate = volume.get('aggregate') - # Update share attributes according with share extra specs - self._update_share_attributes_after_server_migration( - instance, src_client, dest_aggregate, dest_client) + + if not migration_id: + # Update share attributes according with share extra specs. + self._update_share_attributes_after_server_migration( + instance, src_client, dest_aggregate, dest_client) except Exception: msg_args = { @@ -1262,36 +1707,58 @@ class NetAppCmodeMultiSVMFileStorageLibrary( 'in the destination vserver.') % msg_args raise exception.NetAppException(message=msg) + new_share_data = { + 'pool_name': volume.get('aggregate') + } + + share_host = instance['host'] + + # If using SVM migrate, must already ensure the export policies + # using the new host information. + if migration_id: + old_aggregate = share_host.split('#')[1] + share_host = share_host.replace( + old_aggregate, dest_aggregate) + export_locations = self._create_export( instance, dest_share_server, dest_vserver, dest_client, clear_current_export_policy=False, - ensure_share_already_exists=True) + ensure_share_already_exists=True, + share_host=share_host) + new_share_data.update({'export_locations': export_locations}) - share_updates.update({ - instance['id']: { - 'export_locations': export_locations, - 'pool_name': volume.get('aggregate') - }}) + share_updates.update({instance['id']: new_share_data}) # NOTE(dviroel): Nothing to update in snapshot instances since the # provider location didn't change. - # 8. Release source share resources - for instance in share_instances: - self._delete_share(instance, src_vserver, src_client, - remove_export=True) + # NOTE(carloss): as SVM DR works like a replica, we must delete the + # source shares after the migration. In case of SVM Migrate, the shares + # were moved to the destination, so there's no need to remove them. + # Then, we need to delete the source server + if not migration_id: + # 3. Release source share resources. + for instance in share_instances: + self._delete_share(instance, src_vserver, src_client, + remove_export=True) # NOTE(dviroel): source share server deletion must be triggered by # the manager after finishing the migration LOG.info('Share server migration completed.') return { 'share_updates': share_updates, + 'server_backend_details': server_backend_details } - def share_server_migration_cancel(self, context, source_share_server, - dest_share_server, shares, snapshots): - """Cancel a share server migration that is using SVM DR.""" + @na_utils.trace + def _get_share_server_migration_id(self, dest_share_server): + return dest_share_server['backend_details'].get( + na_utils.MIGRATION_OPERATION_ID_KEY) + @na_utils.trace + def _migration_cancel_using_svm_dr( + self, source_share_server, dest_share_server, shares): + """Cancel a share server migration that is using SVM DR.""" dm_session = data_motion.DataMotionSession() dest_backend_name = share_utils.extract_host(dest_share_server['host'], level='backend_name') @@ -1318,6 +1785,73 @@ class NetAppCmodeMultiSVMFileStorageLibrary( 'and %(dest)s vservers.') % msg_args raise exception.NetAppException(message=msg) + @na_utils.trace + def _migration_cancel_using_svm_migrate(self, migration_id, + dest_share_server): + """Cancel a share server migration that is using SVM migrate. + + 1. Gets information about the migration + 2. Pauses the migration, as it can't be cancelled without pausing + 3. Ask to ONTAP to actually cancel the migration + """ + + # 1. Gets information about the migration. + dest_client = data_motion.get_client_for_host( + dest_share_server['host']) + migration_information = dest_client.svm_migration_get(migration_id) + + # Gets the ipspace that was created so we can delete it if it's not + # being used anymore. + dest_ipspace_name = ( + migration_information["destination"]["ipspace"]["name"]) + + # 2. Pauses the migration. + try: + # Request the migration to be paused and wait until the job is + # successful. + job = dest_client.svm_migrate_pause(migration_id) + job_id = self._get_job_uuid(job) + self._wait_for_operation_status(job_id, dest_client.get_job) + + # Wait until the migration get actually paused. + self._wait_for_operation_status( + migration_id, dest_client.svm_migration_get, + desired_status=na_utils.MIGRATION_STATE_MIGRATE_PAUSED) + except exception.NetAppException: + msg = _("Failed to pause the share server migration.") + raise exception.NetAppException(message=msg) + + try: + # 3. Ask to ONTAP to actually cancel the migration. + job = dest_client.svm_migrate_cancel(migration_id) + job_id = self._get_job_uuid(job) + self._wait_for_operation_status( + job_id, dest_client.get_job) + except exception.NetAppException: + msg = _("Failed to cancel the share server migration.") + raise exception.NetAppException(message=msg) + + # If there is need to, remove the ipspace. + if (dest_ipspace_name and dest_ipspace_name not in CLUSTER_IPSPACES + and not dest_client.ipspace_has_data_vservers( + dest_ipspace_name)): + dest_client.delete_ipspace(dest_ipspace_name) + return + + @na_utils.trace + def share_server_migration_cancel(self, context, source_share_server, + dest_share_server, shares, snapshots): + """Send the request to cancel the SVM migration.""" + + migration_id = self._get_share_server_migration_id(dest_share_server) + + if migration_id: + return self._migration_cancel_using_svm_migrate( + migration_id, dest_share_server) + + self._migration_cancel_using_svm_dr( + source_share_server, dest_share_server, shares) + LOG.info('Share server migration was cancelled.') def share_server_migration_get_progress(self, context, src_share_server, diff --git a/manila/share/drivers/netapp/utils.py b/manila/share/drivers/netapp/utils.py index 1fad8d2e1c..652d59081c 100644 --- a/manila/share/drivers/netapp/utils.py +++ b/manila/share/drivers/netapp/utils.py @@ -36,6 +36,12 @@ VALID_TRACE_FLAGS = ['method', 'api'] TRACE_METHOD = False TRACE_API = False API_TRACE_PATTERN = '(.*)' +SVM_MIGRATE_POLICY_TYPE_NAME = 'migrate' +MIGRATION_OPERATION_ID_KEY = 'migration_operation_id' +MIGRATION_STATE_READY_FOR_CUTOVER = 'ready_for_cutover' +MIGRATION_STATE_READY_FOR_SOURCE_CLEANUP = 'ready_for_source_cleanup' +MIGRATION_STATE_MIGRATE_COMPLETE = 'migrate_complete' +MIGRATION_STATE_MIGRATE_PAUSED = 'migrate_paused' def validate_driver_instantiation(**kwargs): diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index 2bd06e4626..9b1e91438a 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -87,6 +87,7 @@ VSERVER_INFO = { 'state': VSERVER_STATE, } SNAPMIRROR_POLICY_NAME = 'fake_snapmirror_policy' +SNAPMIRROR_POLICY_TYPE = 'async_mirror' USER_NAME = 'fake_user' @@ -2742,12 +2743,14 @@ SNAPMIRROR_POLICY_GET_ITER_RESPONSE = etree.XML(""" %(policy_name)s + %(policy_type)s %(vserver_name)s 1 """ % { 'policy_name': SNAPMIRROR_POLICY_NAME, + 'policy_type': SNAPMIRROR_POLICY_TYPE, 'vserver_name': VSERVER_NAME, }) @@ -2899,6 +2902,7 @@ FAKE_NA_ELEMENT = api.NaElement(etree.XML(FAKE_VOL_XML)) FAKE_INVOKE_DATA = 'somecontent' FAKE_XML_STR = 'abc' +FAKE_REST_CALL_STR = 'def' FAKE_API_NAME = 'volume-get-iter' @@ -2960,3 +2964,136 @@ FAKE_MANAGE_VOLUME = { FAKE_KEY_MANAGER_ERROR = "The onboard key manager is not enabled. To enable \ it, run \"security key-manager setup\"." + +FAKE_ACTION_URL = '/endpoint' +FAKE_BASE_URL = '10.0.0.3/api' +FAKE_HTTP_BODY = {'fake_key': 'fake_value'} +FAKE_HTTP_QUERY = {'type': 'fake_type'} +FAKE_HTTP_HEADER = {"fake_header_key": "fake_header_value"} +FAKE_URL_PARAMS = {"fake_url_key": "fake_url_value_to_be_concatenated"} + +FAKE_MIGRATION_RESPONSE_WITH_JOB = { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "job": { + "start_time": "2021-08-27T19:23:41.691Z", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "description": "Fake Job", + "state": "success", + "message": "Complete: Successful", + "end_time": "2021-08-27T19:23:41.691Z", + "code": "0" + } +} +FAKE_JOB_ID = FAKE_MIGRATION_RESPONSE_WITH_JOB['job']['uuid'] +FAKE_MIGRATION_POST_ID = 'fake_migration_id' +FAKE_JOB_SUCCESS_STATE = { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "start_time": "2021-08-27T19:23:41.691Z", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "description": "POST migrations/%s" % FAKE_MIGRATION_POST_ID, + "state": "success", + "message": "Complete: Successful", + "end_time": "2021-08-27T19:23:41.691Z", + "code": "0" +} + +FAKE_MIGRATION_JOB_SUCCESS = { + "auto_cutover": True, + "auto_source_cleanup": True, + "current_operation": "none", + "cutover_complete_time": "2020-12-02T18:36:19-08:00", + "cutover_start_time": "2020-12-02T18:36:19-08:00", + "cutover_trigger_time": "2020-12-02T18:36:19-08:00", + "destination": { + "ipspace": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "exchange", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "volume_placement": { + "aggregates": [ + { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "aggr1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + ], + "volumes": [ + { + "aggregate": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "aggr1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "volume": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "this_volume", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + } + ] + } + }, + "end_time": "2020-12-02T18:36:19-08:00", + "last_failed_state": "precheck_started", + "last_operation": "none", + "last_pause_time": "2020-12-02T18:36:19-08:00", + "last_resume_time": "2020-12-02T18:36:19-08:00", + "messages": [ + { + "code": 852126, + "message": "SVM migrate cannot start since a volume move is " + "running.""Retry the command once volume move has " + "finished." + } + ], + "point_of_no_return": True, + "restart_count": 0, + "source": { + "cluster": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "cluster1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "svm": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + } + }, + "start_time": "2020-12-02T18:36:19-08:00", + "state": "migrate_complete", + "uuid": "4ea7a442-86d1-11e0-ae1c-123478563412" +} diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_api.py b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py index d92fca4fc9..f2c9c6e911 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_api.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py @@ -19,6 +19,7 @@ Tests for NetApp API layer """ +from oslo_serialization import jsonutils from unittest import mock import ddt @@ -26,6 +27,7 @@ import requests from manila import exception from manila.share.drivers.netapp.dataontap.client import api +from manila.share.drivers.netapp.dataontap.client import rest_endpoints from manila import test from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake @@ -174,11 +176,11 @@ class NetAppApiElementTransTests(test.TestCase): @ddt.ddt -class NetAppApiServerTests(test.TestCase): +class NetAppApiServerZapiClientTests(test.TestCase): """Test case for NetApp API server methods""" def setUp(self): - self.root = api.NaServer('127.0.0.1') - super(NetAppApiServerTests, self).setUp() + self.root = api.NaServer('127.0.0.1').zapi_client + super(NetAppApiServerZapiClientTests, self).setUp() @ddt.data(None, fake.FAKE_XML_STR) def test_invoke_elem_value_error(self, na_element): @@ -262,3 +264,201 @@ class NetAppApiServerTests(test.TestCase): expected_log_count = 2 if log else 0 self.assertEqual(expected_log_count, api.LOG.debug.call_count) + + +@ddt.ddt +class NetAppApiServerRestClientTests(test.TestCase): + """Test case for NetApp API Rest server methods""" + def setUp(self): + self.root = api.NaServer('127.0.0.1').rest_client + super(NetAppApiServerRestClientTests, self).setUp() + + def test_invoke_elem_value_error(self): + """Tests whether invalid NaElement parameter causes error""" + na_element = fake.FAKE_REST_CALL_STR + self.assertRaises(ValueError, self.root.invoke_elem, na_element) + + def _setup_mocks_for_invoke_element(self, mock_post_action): + + self.mock_object(api, 'LOG') + self.root._session = fake.FAKE_HTTP_SESSION + self.root._session.post = mock_post_action + self.mock_object(self.root, '_build_session') + self.mock_object( + self.root, '_get_request_info', mock.Mock( + return_value=(self.root._session.post, fake.FAKE_ACTION_URL))) + self.mock_object( + self.root, '_get_base_url', + mock.Mock(return_value=fake.FAKE_BASE_URL)) + + return fake.FAKE_BASE_URL + + def test_invoke_elem_http_error(self): + """Tests handling of HTTPError""" + na_element = fake.FAKE_NA_ELEMENT + element_name = fake.FAKE_NA_ELEMENT.get_name() + self._setup_mocks_for_invoke_element( + mock_post_action=mock.Mock(side_effect=requests.HTTPError())) + + self.assertRaises(api.NaApiError, self.root.invoke_elem, + na_element) + self.assertTrue(self.root._get_base_url.called) + self.root._get_request_info.assert_called_once_with( + element_name, self.root._session) + + def test_invoke_elem_urlerror(self): + """Tests handling of URLError""" + na_element = fake.FAKE_NA_ELEMENT + element_name = fake.FAKE_NA_ELEMENT.get_name() + self._setup_mocks_for_invoke_element( + mock_post_action=mock.Mock(side_effect=requests.URLRequired())) + + self.assertRaises(exception.StorageCommunicationException, + self.root.invoke_elem, + na_element) + + self.assertTrue(self.root._get_base_url.called) + self.root._get_request_info.assert_called_once_with( + element_name, self.root._session) + + def test_invoke_elem_unknown_exception(self): + """Tests handling of Unknown Exception""" + na_element = fake.FAKE_NA_ELEMENT + element_name = fake.FAKE_NA_ELEMENT.get_name() + self._setup_mocks_for_invoke_element( + mock_post_action=mock.Mock(side_effect=Exception)) + + exception = self.assertRaises(api.NaApiError, self.root.invoke_elem, + na_element) + self.assertEqual('unknown', exception.code) + self.assertTrue(self.root._get_base_url.called) + self.root._get_request_info.assert_called_once_with( + element_name, self.root._session) + + @ddt.data( + {'trace_enabled': False, + 'trace_pattern': '(.*)', + 'log': False, + 'query': None, + 'body': fake.FAKE_HTTP_BODY + }, + {'trace_enabled': True, + 'trace_pattern': '(?!(volume)).*', + 'log': False, + 'query': None, + 'body': fake.FAKE_HTTP_BODY + }, + {'trace_enabled': True, + 'trace_pattern': '(.*)', + 'log': True, + 'query': fake.FAKE_HTTP_QUERY, + 'body': fake.FAKE_HTTP_BODY + }, + {'trace_enabled': True, + 'trace_pattern': '^volume-(info|get-iter)$', + 'log': True, + 'query': fake.FAKE_HTTP_QUERY, + 'body': fake.FAKE_HTTP_BODY + } + ) + @ddt.unpack + def test_invoke_elem_valid(self, trace_enabled, trace_pattern, log, query, + body): + """Tests the method invoke_elem with valid parameters""" + self.root._session = fake.FAKE_HTTP_SESSION + response = mock.Mock() + response.content = 'fake_response' + self.root._session.post = mock.Mock(return_value=response) + na_element = fake.FAKE_NA_ELEMENT + element_name = fake.FAKE_NA_ELEMENT.get_name() + self.root._trace = trace_enabled + self.root._api_trace_pattern = trace_pattern + expected_url = fake.FAKE_BASE_URL + fake.FAKE_ACTION_URL + + api_args = { + "body": body, + "query": query + } + + self.mock_object(api, 'LOG') + mock_build_session = self.mock_object(self.root, '_build_session') + mock_get_req_info = self.mock_object( + self.root, '_get_request_info', mock.Mock( + return_value=(self.root._session.post, fake.FAKE_ACTION_URL))) + mock_add_query_params = self.mock_object( + self.root, '_add_query_params_to_url', mock.Mock( + return_value=fake.FAKE_ACTION_URL)) + mock_get_base_url = self.mock_object( + self.root, '_get_base_url', + mock.Mock(return_value=fake.FAKE_BASE_URL)) + mock_json_loads = self.mock_object( + jsonutils, 'loads', mock.Mock(return_value='fake_response')) + mock_json_dumps = self.mock_object( + jsonutils, 'dumps', mock.Mock(return_value=body)) + + result = self.root.invoke_elem(na_element, api_args=api_args) + + self.assertEqual('fake_response', result) + expected_log_count = 2 if log else 0 + self.assertEqual(expected_log_count, api.LOG.debug.call_count) + self.assertTrue(mock_build_session.called) + mock_get_req_info.assert_called_once_with( + element_name, self.root._session) + if query: + mock_add_query_params.assert_called_once_with( + fake.FAKE_ACTION_URL, query) + self.assertTrue(mock_get_base_url.called) + self.root._session.post.assert_called_once_with( + expected_url, data=body) + mock_json_loads.assert_called_once_with('fake_response') + mock_json_dumps.assert_called_once_with(body) + + @ddt.data( + ('svm-migration-start', rest_endpoints.ENDPOINT_MIGRATIONS, 'post'), + ('svm-migration-complete', rest_endpoints.ENDPOINT_MIGRATION_ACTIONS, + 'patch') + ) + @ddt.unpack + def test__get_request_info(self, api_name, expected_url, expected_method): + self.root._session = fake.FAKE_HTTP_SESSION + for http_method in ['post', 'get', 'put', 'delete', 'patch']: + setattr(self.root._session, http_method, mock.Mock()) + + method, url = self.root._get_request_info(api_name, self.root._session) + + self.assertEqual(method, getattr(self.root._session, expected_method)) + self.assertEqual(expected_url, url) + + @ddt.data( + {'is_ipv6': False, 'protocol': 'http', 'port': '80'}, + {'is_ipv6': False, 'protocol': 'https', 'port': '443'}, + {'is_ipv6': True, 'protocol': 'http', 'port': '80'}, + {'is_ipv6': True, 'protocol': 'https', 'port': '443'}) + @ddt.unpack + def test__get_base_url(self, is_ipv6, protocol, port): + self.root._host = '10.0.0.3' if not is_ipv6 else 'FF01::1' + self.root._protocol = protocol + self.root._port = port + + host_formated_for_url = ( + '[%s]' % self.root._host if is_ipv6 else self.root._host) + + # example of the expected format: http://10.0.0.3:80/api/ + expected_result = ( + protocol + '://' + host_formated_for_url + ':' + port + '/api/') + + base_url = self.root._get_base_url() + + self.assertEqual(expected_result, base_url) + + def test__add_query_params_to_url(self): + url = 'endpoint/to/get/data' + filters = "?" + for k, v in fake.FAKE_HTTP_QUERY.items(): + filters += "%(key)s=%(value)s&" % {"key": k, "value": v} + expected_formated_url = url + filters + + formatted_url = self.root._add_query_params_to_url( + url, fake.FAKE_HTTP_QUERY) + + self.assertEqual(expected_formated_url, formatted_url) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py index 10da9f39c5..076b0bab53 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py @@ -42,6 +42,8 @@ class NetAppBaseClientTestCase(test.TestCase): self.client = client_base.NetAppBaseClient(**fake.CONNECTION_INFO) self.client.connection = mock.MagicMock() self.connection = self.client.connection + self.connection.zapi_client = mock.Mock() + self.connection.rest_client = mock.Mock() def test_get_ontapi_version(self): version_response = netapp_api.NaElement(fake.ONTAPI_VERSION_RESPONSE) @@ -97,16 +99,23 @@ class NetAppBaseClientTestCase(test.TestCase): self.assertEqual('tag_name', result) - def test_send_request(self): + @ddt.data(True, False) + def test_send_request(self, use_zapi): element = netapp_api.NaElement('fake-api') - self.client.send_request('fake-api') + self.client.send_request('fake-api', use_zapi=use_zapi) self.assertEqual( element.to_string(), self.connection.invoke_successfully.call_args[0][0].to_string()) - self.assertTrue(self.connection.invoke_successfully.call_args[0][1]) + self.assertTrue( + self.connection.invoke_successfully.call_args[1][ + 'enable_tunneling']) + self.assertEqual( + use_zapi, + self.connection.invoke_successfully.call_args[1][ + 'use_zapi']) def test_send_request_no_tunneling(self): @@ -117,20 +126,32 @@ class NetAppBaseClientTestCase(test.TestCase): self.assertEqual( element.to_string(), self.connection.invoke_successfully.call_args[0][0].to_string()) - self.assertFalse(self.connection.invoke_successfully.call_args[0][1]) + self.assertFalse( + self.connection.invoke_successfully.call_args[1][ + 'enable_tunneling']) - def test_send_request_with_args(self): + @ddt.data(True, False) + def test_send_request_with_args(self, use_zapi): element = netapp_api.NaElement('fake-api') api_args = {'arg1': 'data1', 'arg2': 'data2'} - element.translate_struct(api_args) - self.client.send_request('fake-api', api_args=api_args) + self.client.send_request('fake-api', api_args=api_args, + use_zapi=use_zapi) self.assertEqual( element.to_string(), self.connection.invoke_successfully.call_args[0][0].to_string()) - self.assertTrue(self.connection.invoke_successfully.call_args[0][1]) + self.assertEqual( + api_args, self.connection.invoke_successfully.call_args[1][ + 'api_args']) + self.assertTrue( + self.connection.invoke_successfully.call_args[1][ + 'enable_tunneling']) + self.assertEqual( + use_zapi, + self.connection.invoke_successfully.call_args[1][ + 'use_zapi']) def test_get_licenses(self): 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 f4ffdebd3f..2237f2214a 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 @@ -1769,6 +1769,40 @@ class NetAppClientCmodeTestCase(test.TestCase): mock.call('net-interface-get-iter', None)]) self.assertListEqual([], result) + def test_disable_network_interface(self): + interface_name = fake.NETWORK_INTERFACES[0]['interface_name'] + vserver_name = fake.VSERVER_NAME + expected_api_args = { + 'administrative-status': 'down', + 'interface-name': interface_name, + 'vserver': vserver_name, + } + + self.mock_object(self.client, 'send_request') + + self.client.disable_network_interface(vserver_name, interface_name) + + self.client.send_request.assert_called_once_with( + 'net-interface-modify', expected_api_args) + + def test_delete_network_interface(self): + interface_name = fake.NETWORK_INTERFACES[0]['interface_name'] + vserver_name = fake.VSERVER_NAME + expected_api_args = { + 'interface-name': interface_name, + 'vserver': vserver_name, + } + + self.mock_object(self.client, 'disable_network_interface') + self.mock_object(self.client, 'send_request') + + self.client.delete_network_interface(vserver_name, interface_name) + + self.client.disable_network_interface.assert_called_once_with( + vserver_name, interface_name) + self.client.send_request.assert_called_once_with( + 'net-interface-delete', expected_api_args) + def test_get_ipspaces(self): self.client.features.add_feature('IPSPACES') @@ -7627,8 +7661,10 @@ class NetAppClientCmodeTestCase(test.TestCase): fake.SNAPMIRROR_POLICY_GET_ITER_RESPONSE) self.mock_object(self.client, 'send_iter_request', mock.Mock(return_value=api_response)) + result_elem = [fake.SNAPMIRROR_POLICY_NAME] - result = self.client.get_snapmirror_policies(fake.VSERVER_NAME) + result = self.client.get_snapmirror_policies( + fake.VSERVER_NAME) expected_api_args = { 'query': { @@ -7645,7 +7681,7 @@ class NetAppClientCmodeTestCase(test.TestCase): self.client.send_iter_request.assert_called_once_with( 'snapmirror-policy-get-iter', expected_api_args) - self.assertEqual([fake.SNAPMIRROR_POLICY_NAME], result) + self.assertEqual(result_elem, result) @ddt.data(True, False, None) def test_start_vserver(self, force): @@ -8254,3 +8290,231 @@ class NetAppClientCmodeTestCase(test.TestCase): self.assertEqual(expected, result) self.client.send_iter_request.assert_called_once_with( 'fpolicy-policy-status-get-iter', expected_args) + + def test_is_svm_migrate_supported(self): + self.client.features.add_feature('SVM_MIGRATE') + + result = self.client.is_svm_migrate_supported() + + self.assertTrue(result) + + @ddt.data( + {"body": fake.FAKE_HTTP_BODY, + "headers": fake.FAKE_HTTP_HEADER, + "query": {}, + "url_params": fake.FAKE_URL_PARAMS + }, + {"body": {}, + "headers": fake.FAKE_HTTP_HEADER, + "query": fake.FAKE_HTTP_QUERY, + "url_params": fake.FAKE_URL_PARAMS + }, + ) + @ddt.unpack + def test__format_request(self, body, headers, query, url_params): + expected_result = { + "body": body, + "headers": headers, + "query": query, + "url_params": url_params + } + + result = self.client._format_request( + body, headers=headers, query=query, url_params=url_params) + + for k, v in expected_result.items(): + self.assertIn(k, result) + self.assertEqual(result.get(k), v) + + @ddt.data( + {"dest_ipspace": None, "check_only": True}, + {"dest_ipspace": "fake_dest_ipspace", "check_only": False}, + ) + @ddt.unpack + def test_svm_migration_start(self, dest_ipspace, check_only): + api_args = { + "auto_cutover": False, + "auto_source_cleanup": True, + "check_only": check_only, + "source": { + "cluster": {"name": fake.CLUSTER_NAME}, + "svm": {"name": fake.VSERVER_NAME}, + }, + "destination": { + "volume_placement": { + "aggregates": [fake.SHARE_AGGREGATE_NAME], + }, + }, + } + if dest_ipspace: + ipspace_data = { + "ipspace": {"name": dest_ipspace} + } + api_args['destination'].update(ipspace_data) + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=api_args)) + self.mock_object( + self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + + result = self.client.svm_migration_start( + fake.CLUSTER_NAME, fake.VSERVER_NAME, [fake.SHARE_AGGREGATE_NAME], + dest_ipspace=dest_ipspace, check_only=check_only) + + self.client._format_request.assert_called_once_with(api_args) + self.client.send_request.assert_called_once_with( + 'svm-migration-start', api_args=api_args, use_zapi=False) + + self.assertEqual(result, fake.FAKE_MIGRATION_RESPONSE_WITH_JOB) + + @ddt.data({"check_only": False}, {"check_only": True}) + def test_share_server_migration_start_failed(self, check_only): + api_args = {} + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=api_args)) + self.mock_object( + self.client, 'send_request', + mock.Mock(side_effect=netapp_api.NaApiError(message='fake'))) + + self.assertRaises( + netapp_api.NaApiError, + self.client.svm_migration_start, + fake.CLUSTER_NAME, fake.VSERVER_NAME, + [fake.SHARE_AGGREGATE_NAME], + check_only=check_only + ) + + def test_svm_migrate_complete(self): + migration_id = 'ongoing_migration_id' + request = { + 'action': 'cutover' + } + expected_url_params = { + 'svm_migration_id': migration_id + } + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=request)) + self.mock_object( + self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + + self.client.svm_migrate_complete(migration_id) + + self.client._format_request.assert_called_once_with( + request, url_params=expected_url_params) + self.client.send_request.assert_called_once_with( + 'svm-migration-complete', api_args=request, use_zapi=False) + + def test_get_job(self): + request = {} + job_uuid = 'fake_job_uuid' + url_params = { + 'job_uuid': job_uuid + } + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=request)) + self.mock_object(self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_JOB_SUCCESS_STATE)) + + result = self.client.get_job(job_uuid) + + self.assertEqual(fake.FAKE_JOB_SUCCESS_STATE, result) + self.client._format_request.assert_called_once_with( + request, url_params=url_params) + self.client.send_request.assert_called_once_with( + 'get-job', api_args=request, use_zapi=False) + + def test_svm_migrate_cancel(self): + request = {} + migration_id = 'fake_migration_uuid' + url_params = { + "svm_migration_id": migration_id + } + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=request)) + self.mock_object( + self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + + result = self.client.svm_migrate_cancel(migration_id) + + self.assertEqual(fake.FAKE_MIGRATION_RESPONSE_WITH_JOB, result) + self.client._format_request.assert_called_once_with( + request, url_params=url_params) + self.client.send_request.assert_called_once_with( + 'svm-migration-cancel', api_args=request, use_zapi=False) + + def test_svm_migration_get(self): + request = {} + migration_id = 'fake_migration_uuid' + url_params = { + "svm_migration_id": migration_id + } + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=request)) + self.mock_object( + self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_MIGRATION_JOB_SUCCESS)) + + result = self.client.svm_migration_get(migration_id) + + self.assertEqual(fake.FAKE_MIGRATION_JOB_SUCCESS, result) + self.client._format_request.assert_called_once_with( + request, url_params=url_params) + self.client.send_request.assert_called_once_with( + 'svm-migration-get', api_args=request, use_zapi=False) + + def test_svm_migrate_pause(self): + request = { + "action": "pause" + } + migration_id = 'fake_migration_uuid' + url_params = { + "svm_migration_id": migration_id + } + + self.mock_object(self.client, '_format_request', + mock.Mock(return_value=request)) + self.mock_object( + self.client, 'send_request', + mock.Mock(return_value=fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + + result = self.client.svm_migrate_pause(migration_id) + + self.assertEqual(fake.FAKE_MIGRATION_RESPONSE_WITH_JOB, result) + self.client._format_request.assert_called_once_with( + request, url_params=url_params) + self.client.send_request.assert_called_once_with( + 'svm-migration-pause', api_args=request, use_zapi=False) + + def test_migration_check_job_state(self): + self.mock_object(self.client, 'get_job', + mock.Mock(return_value=fake.FAKE_JOB_SUCCESS_STATE)) + + result = self.client.get_migration_check_job_state( + fake.FAKE_JOB_ID + ) + + self.assertEqual(result, fake.FAKE_JOB_SUCCESS_STATE) + self.client.get_job.assert_called_once_with(fake.FAKE_JOB_ID) + + @ddt.data(netapp_api.ENFS_V4_0_ENABLED_MIGRATION_FAILURE, + netapp_api.EVSERVER_MIGRATION_TO_NON_AFF_CLUSTER) + def test_migration_check_job_state_failed(self, error_code): + + self.mock_object( + self.client, 'get_job', + mock.Mock(side_effect=netapp_api.NaApiError(code=error_code))) + + self.assertRaises( + exception.NetAppException, + self.client.get_migration_check_job_state, + fake.FAKE_JOB_ID + ) + self.client.get_job.assert_called_once_with(fake.FAKE_JOB_ID) 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 e6c0b17f70..3308cf3456 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 @@ -26,6 +26,7 @@ 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 from manila.share.drivers.netapp import options as na_opts +from manila.share import utils as share_utils from manila import test from manila.tests.share.drivers.netapp.dataontap import fakes as fake from manila.tests.share.drivers.netapp import fakes as na_fakes @@ -93,6 +94,22 @@ class NetAppCDOTDataMotionTestCase(test.TestCase): ssl_cert_path='/etc/ssl/certs', trace=mock.ANY, vserver='fake_vserver') + def test_get_client_for_host(self): + mock_extract_host = self.mock_object( + share_utils, 'extract_host', + mock.Mock(return_value=fake.BACKEND_NAME)) + mock_get_client = self.mock_object( + data_motion, 'get_client_for_backend', + mock.Mock(return_value=self.mock_cmode_client)) + + returned_client = data_motion.get_client_for_host( + fake.HOST_NAME) + + mock_extract_host.assert_called_once_with( + fake.HOST_NAME, level='backend_name') + mock_get_client.assert_called_once_with(fake.BACKEND_NAME) + self.assertEqual(returned_client, self.mock_cmode_client) + def test_get_config_for_backend(self): self.mock_object(data_motion, "CONF") CONF.set_override("netapp_vserver", 'fake_vserver', 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 d5849f3a0d..04b808c477 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 @@ -1917,12 +1917,14 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client.offline_volume.assert_called_with(fake.SHARE_NAME) vserver_client.delete_volume.assert_called_with(fake.SHARE_NAME) - def test_create_export(self): + @ddt.data(None, fake.MANILA_HOST_NAME_2) + def test_create_export(self, share_host): protocol_helper = mock.Mock() callback = (lambda export_address, export_path='fake_export_path': ':'.join([export_address, export_path])) protocol_helper.create_share.return_value = callback + expected_host = share_host if share_host else fake.SHARE['host'] self.mock_object(self.library, '_get_helper', mock.Mock(return_value=protocol_helper)) @@ -1937,11 +1939,12 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): result = self.library._create_export(fake.SHARE, fake.SHARE_SERVER, fake.VSERVER1, - vserver_client) + vserver_client, + share_host=share_host) self.assertEqual(fake.NFS_EXPORTS, result) mock_get_export_addresses_with_metadata.assert_called_once_with( - fake.SHARE, fake.SHARE_SERVER, fake.LIFS) + fake.SHARE, fake.SHARE_SERVER, fake.LIFS, expected_host) protocol_helper.create_share.assert_called_once_with( fake.SHARE, fake.SHARE_NAME, clear_current_export_policy=True, ensure_share_already_exists=False, replica=False) @@ -1969,7 +1972,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=[fake.LIF_ADDRESSES[1]])) result = self.library._get_export_addresses_with_metadata( - fake.SHARE, fake.SHARE_SERVER, fake.LIFS) + fake.SHARE, fake.SHARE_SERVER, fake.LIFS, fake.SHARE['host']) self.assertEqual(fake.INTERFACE_ADDRESSES_WITH_METADATA, result) mock_get_aggregate_node.assert_called_once_with(fake.POOL_NAME) @@ -1986,7 +1989,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=[fake.LIF_ADDRESSES[1]])) result = self.library._get_export_addresses_with_metadata( - fake.SHARE, fake.SHARE_SERVER, fake.LIFS) + fake.SHARE, fake.SHARE_SERVER, fake.LIFS, fake.SHARE['host']) expected = copy.deepcopy(fake.INTERFACE_ADDRESSES_WITH_METADATA) for key, value in expected.items(): 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 4276ae07a5..24d6c2f93b 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 @@ -1774,13 +1774,22 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertEqual(not_compatible, result) + def _init_mocks_for_svm_dr_check_compatibility( + self, src_svm_dr_supported=True, dest_svm_dr_supported=True, + check_capacity_result=True): + self.mock_object(self.mock_src_client, 'is_svm_dr_supported', + mock.Mock(return_value=src_svm_dr_supported)) + self.mock_object(self.mock_dest_client, 'is_svm_dr_supported', + mock.Mock(return_value=dest_svm_dr_supported)) + self.mock_object(self.library, '_check_capacity_compatibility', + mock.Mock(return_value=check_capacity_result)) + def _configure_mocks_share_server_migration_check_compatibility( self, have_cluster_creds=True, src_cluster_name=fake.CLUSTER_NAME, dest_cluster_name=fake.CLUSTER_NAME_2, - src_svm_dr_support=True, dest_svm_dr_support=True, - check_capacity_result=True, - pools=fake.POOLS): + pools=fake.POOLS, is_svm_dr=True, failure_scenario=False): + migration_method = 'svm_dr' if is_svm_dr else 'svm_migrate' self.library._have_cluster_creds = have_cluster_creds self.mock_object(self.library, '_get_vserver', mock.Mock(return_value=(self.fake_src_vserver, @@ -1791,14 +1800,11 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=dest_cluster_name)) self.mock_object(data_motion, 'get_client_for_backend', mock.Mock(return_value=self.mock_dest_client)) - self.mock_object(self.mock_src_client, 'is_svm_dr_supported', - mock.Mock(return_value=src_svm_dr_support)) - self.mock_object(self.mock_dest_client, 'is_svm_dr_supported', - mock.Mock(return_value=dest_svm_dr_support)) + self.mock_object(self.library, '_check_for_migration_support', + mock.Mock(return_value=( + migration_method, not failure_scenario))) self.mock_object(self.library, '_get_pools', mock.Mock(return_value=pools)) - self.mock_object(self.library, '_check_capacity_compatibility', - mock.Mock(return_value=check_capacity_result)) def test_share_server_migration_check_compatibility_dest_with_pool( self): @@ -1832,30 +1838,40 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertTrue(self.mock_src_client.get_cluster_name.called) self.assertTrue(self.client.get_cluster_name.called) - def test_share_server_migration_check_compatibility_svm_dr_not_supported( - self): - not_compatible = fake.SERVER_MIGRATION_CHECK_NOT_COMPATIBLE - self._configure_mocks_share_server_migration_check_compatibility( - dest_svm_dr_support=False, - ) + @ddt.data( + {'src_svm_dr_supported': False, + 'dest_svm_dr_supported': False, + 'check_capacity_result': False + }, + {'src_svm_dr_supported': True, + 'dest_svm_dr_supported': True, + 'check_capacity_result': False + }, + ) + @ddt.unpack + def test__check_compatibility_svm_dr_not_compatible( + self, src_svm_dr_supported, dest_svm_dr_supported, + check_capacity_result): + server_total_size = (fake.SHARE_REQ_SPEC.get('shares_size', 0) + + fake.SHARE_REQ_SPEC.get('snapshots_size', 0)) - result = self.library.share_server_migration_check_compatibility( - None, self.fake_src_share_server, - self.fake_dest_share_server['host'], - None, None, None) + self._init_mocks_for_svm_dr_check_compatibility( + src_svm_dr_supported=src_svm_dr_supported, + dest_svm_dr_supported=dest_svm_dr_supported, + check_capacity_result=check_capacity_result) - self.assertEqual(not_compatible, result) - self.library._get_vserver.assert_called_once_with( - self.fake_src_share_server, - backend_name=self.fake_src_backend_name - ) - self.assertTrue(self.mock_src_client.get_cluster_name.called) - self.assertTrue(self.client.get_cluster_name.called) - data_motion.get_client_for_backend.assert_called_once_with( - self.fake_dest_backend_name, vserver_name=None - ) + method, result = self.library._check_compatibility_using_svm_dr( + self.mock_src_client, self.mock_dest_client, fake.SHARE_REQ_SPEC, + fake.POOLS) + + self.assertEqual(method, 'svm_dr') + self.assertEqual(result, False) self.assertTrue(self.mock_src_client.is_svm_dr_supported.called) - self.assertTrue(self.mock_dest_client.is_svm_dr_supported.called) + + if check_capacity_result and not src_svm_dr_supported: + self.assertFalse(self.mock_dest_client.is_svm_dr_supported.called) + self.library._check_capacity_compatibility.assert_called_once_with( + fake.POOLS, True, server_total_size) def test_share_server_migration_check_compatibility_different_sec_service( self): @@ -1882,8 +1898,6 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): data_motion.get_client_for_backend.assert_called_once_with( self.fake_dest_backend_name, vserver_name=None ) - self.assertTrue(self.mock_src_client.is_svm_dr_supported.called) - self.assertTrue(self.mock_dest_client.is_svm_dr_supported.called) @ddt.data('netapp_flexvol_encryption', 'revert_to_snapshot_support') def test_share_server_migration_check_compatibility_invalid_capabilities( @@ -1912,41 +1926,184 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): data_motion.get_client_for_backend.assert_called_once_with( self.fake_dest_backend_name, vserver_name=None ) - self.assertTrue(self.mock_src_client.is_svm_dr_supported.called) - self.assertTrue(self.mock_dest_client.is_svm_dr_supported.called) - def test_share_server_migration_check_compatibility_capacity_false( - self): - not_compatible = fake.SERVER_MIGRATION_CHECK_NOT_COMPATIBLE - self._configure_mocks_share_server_migration_check_compatibility( - check_capacity_result=False - ) + @ddt.data((True, "svm_migrate"), (False, "svm_dr")) + @ddt.unpack + def test__check_for_migration_support( + self, svm_migrate_supported, expected_migration_method): + mock_dest_is_svm_migrate_supported = self.mock_object( + self.mock_dest_client, 'is_svm_migrate_supported', + mock.Mock(return_value=svm_migrate_supported)) + mock_src_is_svm_migrate_supported = self.mock_object( + self.mock_src_client, 'is_svm_migrate_supported', + mock.Mock(return_value=svm_migrate_supported)) + mock_find_matching_aggregates = self.mock_object( + self.library, '_find_matching_aggregates', + mock.Mock(return_value=fake.AGGREGATES)) + mock_get_vserver_name = self.mock_object( + self.library, '_get_vserver_name', + mock.Mock(return_value=fake.VSERVER1)) + mock_svm_migration_check_svm_mig = self.mock_object( + self.library, '_check_compatibility_for_svm_migrate', + mock.Mock(return_value=True)) + mock_svm_migration_check_svm_dr = self.mock_object( + self.library, '_check_compatibility_using_svm_dr', + mock.Mock(side_effect=[('svm_dr', True)])) - result = self.library.share_server_migration_check_compatibility( - None, self.fake_src_share_server, - self.fake_dest_share_server['host'], - fake.SHARE_NETWORK, fake.SHARE_NETWORK, - fake.SERVER_MIGRATION_REQUEST_SPEC) + migration_method, result = self.library._check_for_migration_support( + self.mock_src_client, self.mock_dest_client, fake.SHARE_SERVER, + fake.SHARE_REQ_SPEC, fake.CLUSTER_NAME, fake.POOLS) - self.assertEqual(not_compatible, result) - self.library._get_vserver.assert_called_once_with( + self.assertIs(True, result) + self.assertEqual(migration_method, expected_migration_method) + + mock_dest_is_svm_migrate_supported.assert_called_once() + if svm_migrate_supported: + mock_src_is_svm_migrate_supported.assert_called_once() + mock_find_matching_aggregates.assert_called_once() + mock_get_vserver_name.assert_called_once_with( + fake.SHARE_SERVER['id']) + mock_svm_migration_check_svm_mig.assert_called_once_with( + fake.CLUSTER_NAME, fake.VSERVER1, fake.SHARE_SERVER, + fake.AGGREGATES, self.mock_dest_client) + else: + mock_svm_migration_check_svm_dr.assert_called_once_with( + self.mock_src_client, self.mock_dest_client, + fake.SHARE_REQ_SPEC, fake.POOLS) + + def test__check_for_migration_support_svm_migrate_exception(self): + svm_migrate_supported = True + expected_migration_method = 'svm_migrate' + mock_dest_is_svm_migrate_supported = self.mock_object( + self.mock_dest_client, 'is_svm_migrate_supported', + mock.Mock(return_value=svm_migrate_supported)) + mock_src_is_svm_migrate_supported = self.mock_object( + self.mock_src_client, 'is_svm_migrate_supported', + mock.Mock(return_value=svm_migrate_supported)) + mock_find_matching_aggregates = self.mock_object( + self.library, '_find_matching_aggregates', + mock.Mock(return_value=fake.AGGREGATES)) + mock_get_vserver_name = self.mock_object( + self.library, '_get_vserver_name', + mock.Mock(return_value=fake.VSERVER1)) + mock_svm_migration_check_svm_mig = self.mock_object( + self.library, '_check_compatibility_for_svm_migrate', + mock.Mock(side_effect=exception.NetAppException())) + + migration_method, result = self.library._check_for_migration_support( + self.mock_src_client, self.mock_dest_client, fake.SHARE_SERVER, + fake.SHARE_REQ_SPEC, fake.CLUSTER_NAME, fake.POOLS) + + self.assertIs(False, result) + self.assertEqual(migration_method, expected_migration_method) + + mock_dest_is_svm_migrate_supported.assert_called_once() + mock_src_is_svm_migrate_supported.assert_called_once() + mock_find_matching_aggregates.assert_called_once() + mock_get_vserver_name.assert_called_once_with( + fake.SHARE_SERVER['id']) + mock_svm_migration_check_svm_mig.assert_called_once_with( + fake.CLUSTER_NAME, fake.VSERVER1, fake.SHARE_SERVER, + fake.AGGREGATES, self.mock_dest_client) + + @ddt.data( + (mock.Mock, True), + (exception.NetAppException, False) + ) + @ddt.unpack + def test__check_compatibility_for_svm_migrate(self, expected_exception, + expected_compatibility): + network_info = { + 'network_allocations': + self.fake_src_share_server['network_allocations'], + 'neutron_subnet_id': + self.fake_src_share_server['share_network_subnet'].get( + 'neutron_subnet_id') + } + self.mock_object(self.library._client, 'list_cluster_nodes', + mock.Mock(return_value=fake.CLUSTER_NODES)) + self.mock_object(self.library, '_get_node_data_port', + mock.Mock(return_value=fake.NODE_DATA_PORT)) + self.mock_object( + self.library._client, 'get_ipspace_name_for_vlan_port', + mock.Mock(return_value=fake.IPSPACE)) + self.mock_object(self.library, '_create_port_and_broadcast_domain') + self.mock_object(self.mock_dest_client, 'get_ipspaces', + mock.Mock(return_value=[{'uuid': fake.IPSPACE_ID}])) + self.mock_object( + self.mock_dest_client, 'svm_migration_start', + mock.Mock(return_value=c_fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + self.mock_object(self.library, '_get_job_uuid', + mock.Mock(return_value=c_fake.FAKE_JOB_ID)) + self.mock_object(self.library, '_wait_for_operation_status', + mock.Mock(side_effect=expected_exception)) + + compatibility = self.library._check_compatibility_for_svm_migrate( + fake.CLUSTER_NAME, fake.VSERVER1, self.fake_src_share_server, + fake.AGGREGATES, self.mock_dest_client) + + self.assertIs(expected_compatibility, compatibility) + self.mock_dest_client.svm_migration_start.assert_called_once_with( + fake.CLUSTER_NAME, fake.VSERVER1, fake.AGGREGATES, check_only=True, + dest_ipspace=fake.IPSPACE) + self.library._get_job_uuid.assert_called_once_with( + c_fake.FAKE_MIGRATION_RESPONSE_WITH_JOB) + self.library._client.list_cluster_nodes.assert_called_once() + self.library._get_node_data_port.assert_called_once_with( + fake.CLUSTER_NODES[0]) + (self.library._client.get_ipspace_name_for_vlan_port + .assert_called_once_with( + fake.CLUSTER_NODES[0], fake.NODE_DATA_PORT, + self.fake_src_share_server['network_allocations'][0][ + 'segmentation_id'])) + self.library._create_port_and_broadcast_domain.assert_called_once_with( + fake.IPSPACE, network_info) + + def test__check_compatibility_for_svm_migrate_check_failure(self): + network_info = { + 'network_allocations': + self.fake_src_share_server['network_allocations'], + 'neutron_subnet_id': + self.fake_src_share_server['share_network_subnet'].get( + 'neutron_subnet_id') + } + + self.mock_object(self.library._client, 'list_cluster_nodes', + mock.Mock(return_value=fake.CLUSTER_NODES)) + self.mock_object(self.library, '_get_node_data_port', + mock.Mock(return_value=fake.NODE_DATA_PORT)) + self.mock_object( + self.library._client, 'get_ipspace_name_for_vlan_port', + mock.Mock(return_value=fake.IPSPACE)) + self.mock_object(self.library, '_create_port_and_broadcast_domain') + self.mock_object(self.mock_dest_client, 'get_ipspaces', + mock.Mock(return_value=[{'uuid': fake.IPSPACE_ID}])) + self.mock_object( + self.mock_dest_client, 'svm_migration_start', + mock.Mock(side_effect=exception.NetAppException())) + self.mock_object(self.mock_dest_client, 'delete_ipspace') + + self.assertRaises( + exception.NetAppException, + self.library._check_compatibility_for_svm_migrate, + fake.CLUSTER_NAME, + fake.VSERVER1, self.fake_src_share_server, - backend_name=self.fake_src_backend_name - ) - self.assertTrue(self.mock_src_client.get_cluster_name.called) - self.assertTrue(self.client.get_cluster_name.called) - data_motion.get_client_for_backend.assert_called_once_with( - self.fake_dest_backend_name, vserver_name=None - ) - self.assertTrue(self.mock_src_client.is_svm_dr_supported.called) - self.assertTrue(self.mock_dest_client.is_svm_dr_supported.called) - total_size = (fake.SERVER_MIGRATION_REQUEST_SPEC['shares_size'] + - fake.SERVER_MIGRATION_REQUEST_SPEC['snapshots_size']) - self.library._check_capacity_compatibility.assert_called_once_with( - fake.POOLS, - self.library.configuration.max_over_subscription_ratio > 1, - total_size - ) + fake.AGGREGATES, + self.mock_dest_client) + + self.library._client.list_cluster_nodes.assert_called_once() + self.library._get_node_data_port.assert_called_once_with( + fake.CLUSTER_NODES[0]) + (self.library._client.get_ipspace_name_for_vlan_port + .assert_called_once_with( + fake.CLUSTER_NODES[0], fake.NODE_DATA_PORT, + self.fake_src_share_server['network_allocations'][0][ + 'segmentation_id'])) + self.library._create_port_and_broadcast_domain.assert_called_once_with( + fake.IPSPACE, network_info) + self.mock_dest_client.delete_ipspace.assert_called_once_with( + fake.IPSPACE) def test_share_server_migration_check_compatibility_compatible(self): compatible = { @@ -1958,7 +2115,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'migration_get_progress': False, 'share_network_id': fake.SHARE_NETWORK['id'], } - self._configure_mocks_share_server_migration_check_compatibility() + self._configure_mocks_share_server_migration_check_compatibility( + is_svm_dr=True) result = self.library.share_server_migration_check_compatibility( None, self.fake_src_share_server, @@ -1976,23 +2134,101 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): data_motion.get_client_for_backend.assert_called_once_with( self.fake_dest_backend_name, vserver_name=None ) - self.assertTrue(self.mock_src_client.is_svm_dr_supported.called) - self.assertTrue(self.mock_dest_client.is_svm_dr_supported.called) - total_size = (fake.SERVER_MIGRATION_REQUEST_SPEC['shares_size'] + - fake.SERVER_MIGRATION_REQUEST_SPEC['snapshots_size']) - self.library._check_capacity_compatibility.assert_called_once_with( - fake.POOLS, - self.library.configuration.max_over_subscription_ratio > 1, - total_size + + def test__get_job_uuid(self): + self.assertEqual( + self.library._get_job_uuid( + c_fake.FAKE_MIGRATION_RESPONSE_WITH_JOB), + c_fake.FAKE_JOB_ID ) + def test__wait_for_operation_status(self): + job_starting_state = copy.copy(c_fake.FAKE_JOB_SUCCESS_STATE) + job_starting_state['state'] = 'starting' + returned_jobs = [ + job_starting_state, + c_fake.FAKE_JOB_SUCCESS_STATE, + ] + + self.mock_object(self.mock_dest_client, 'get_job', + mock.Mock(side_effect=returned_jobs)) + + self.library._wait_for_operation_status( + c_fake.FAKE_JOB_ID, self.mock_dest_client.get_job + ) + + self.assertEqual( + self.mock_dest_client.get_job.call_count, len(returned_jobs)) + + def test__wait_for_operation_status_error(self): + starting_job = copy.copy(c_fake.FAKE_JOB_SUCCESS_STATE) + starting_job['state'] = 'starting' + errored_job = copy.copy(c_fake.FAKE_JOB_SUCCESS_STATE) + errored_job['state'] = constants.STATUS_ERROR + returned_jobs = [starting_job, errored_job] + + self.mock_object(self.mock_dest_client, 'get_job', + mock.Mock(side_effect=returned_jobs)) + + self.assertRaises( + exception.NetAppException, + self.library._wait_for_operation_status, + c_fake.FAKE_JOB_ID, + self.mock_dest_client.get_job + ) + + @ddt.data( + {'src_supports_svm_migrate': True, 'dest_supports_svm_migrate': True}, + {'src_supports_svm_migrate': True, 'dest_supports_svm_migrate': False}, + {'src_supports_svm_migrate': False, 'dest_supports_svm_migrate': True}, + {'src_supports_svm_migrate': False, 'dest_supports_svm_migrate': False} + ) + @ddt.unpack + def test_share_server_migration_start(self, src_supports_svm_migrate, + dest_supports_svm_migrate): + fake_migration_data = {'fake_migration_key': 'fake_migration_value'} + self.mock_object( + self.library, '_get_vserver', + mock.Mock( + side_effect=[(self.fake_src_vserver, self.mock_src_client)])) + self.mock_object(data_motion, 'get_client_for_backend', + mock.Mock(return_value=self.mock_dest_client)) + mock_start_using_svm_migrate = self.mock_object( + self.library, '_migration_start_using_svm_migrate', + mock.Mock(return_value=fake_migration_data)) + mock_start_using_svm_dr = self.mock_object( + self.library, '_migration_start_using_svm_dr', + mock.Mock(return_value=fake_migration_data)) + + self.mock_src_client.is_svm_migrate_supported.return_value = ( + src_supports_svm_migrate) + self.mock_dest_client.is_svm_migrate_supported.return_value = ( + dest_supports_svm_migrate) + src_and_dest_support_svm_migrate = all( + [src_supports_svm_migrate, dest_supports_svm_migrate]) + + result = self.library.share_server_migration_start( + None, self.fake_src_share_server, self.fake_dest_share_server, + [fake.SHARE_INSTANCE], []) + + self.library._get_vserver.assert_called_once_with( + share_server=self.fake_src_share_server, + backend_name=self.fake_src_backend_name) + if src_and_dest_support_svm_migrate: + mock_start_using_svm_migrate.assert_called_once_with( + None, self.fake_src_share_server, self.fake_dest_share_server, + self.mock_src_client, self.mock_dest_client) + else: + mock_start_using_svm_dr.assert_called_once_with( + self.fake_src_share_server, self.fake_dest_share_server + ) + self.assertEqual(result, fake_migration_data) + @ddt.data({'vserver_peered': True, 'src_cluster': fake.CLUSTER_NAME}, {'vserver_peered': False, 'src_cluster': fake.CLUSTER_NAME}, - {'vserver_peered': False, - 'src_cluster': fake.CLUSTER_NAME_2}) + {'vserver_peered': False, 'src_cluster': fake.CLUSTER_NAME_2}) @ddt.unpack - def test_share_server_migration_start(self, vserver_peered, - src_cluster): + def test__migration_start_using_svm_dr(self, vserver_peered, src_cluster): dest_cluster = fake.CLUSTER_NAME dm_session_mock = mock.Mock() self.mock_object(self.library, '_get_vserver', @@ -2009,9 +2245,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_object(data_motion, "DataMotionSession", mock.Mock(return_value=dm_session_mock)) - self.library.share_server_migration_start( - None, self.fake_src_share_server, self.fake_dest_share_server, - [fake.SHARE_INSTANCE], []) + self.library._migration_start_using_svm_dr( + self.fake_src_share_server, self.fake_dest_share_server) self.library._get_vserver.assert_has_calls([ mock.call(share_server=self.fake_src_share_server, @@ -2061,10 +2296,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): side_effect=exception.NetAppException(message='fake'))) self.assertRaises(exception.NetAppException, - self.library.share_server_migration_start, - None, self.fake_src_share_server, - self.fake_dest_share_server, - [fake.SHARE_INSTANCE], []) + self.library._migration_start_using_svm_dr, + self.fake_src_share_server, + self.fake_dest_share_server) self.library._get_vserver.assert_has_calls([ mock.call(share_server=self.fake_src_share_server, @@ -2085,6 +2319,159 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) + @ddt.data( + {'network_change_during_migration': True}, + {'network_change_during_migration': False}) + @ddt.unpack + def test__migration_start_using_svm_migrate( + self, network_change_during_migration): + + self.fake_src_share_server['share_network_subnet_id'] = 'fake_sns_id' + self.fake_dest_share_server['share_network_subnet_id'] = 'fake_sns_id' + node_name = fake.CLUSTER_NODES[0] + expected_server_info = { + 'backend_details': { + 'migration_operation_id': c_fake.FAKE_MIGRATION_POST_ID + } + } + + if not network_change_during_migration: + self.fake_dest_share_server['network_allocations'] = None + server_to_get_network_info = ( + self.fake_dest_share_server + if network_change_during_migration else self.fake_src_share_server) + + if network_change_during_migration: + self.fake_dest_share_server['share_network_subnet_id'] = ( + 'different_sns_id') + + segmentation_id = ( + server_to_get_network_info['network_allocations'][0][ + 'segmentation_id']) + + network_info = { + 'network_allocations': + server_to_get_network_info['network_allocations'], + 'neutron_subnet_id': + server_to_get_network_info['share_network_subnet'][ + 'neutron_subnet_id'] + } + + mock_list_cluster_nodes = self.mock_object( + self.library._client, 'list_cluster_nodes', + mock.Mock(return_value=fake.CLUSTER_NODES)) + mock_get_data_port = self.mock_object( + self.library, '_get_node_data_port', + mock.Mock(return_value=fake.NODE_DATA_PORT)) + mock_get_ipspace = self.mock_object( + self.library._client, 'get_ipspace_name_for_vlan_port', + mock.Mock(return_value=fake.IPSPACE)) + mock_create_port = self.mock_object( + self.library, '_create_port_and_broadcast_domain') + mock_get_vserver_name = self.mock_object( + self.library, '_get_vserver_name', + mock.Mock(return_value=fake.VSERVER1)) + mock_get_cluster_name = self.mock_object( + self.mock_src_client, 'get_cluster_name', + mock.Mock(return_value=fake.CLUSTER_NAME)) + mock_get_aggregates = self.mock_object( + self.library, '_find_matching_aggregates', + mock.Mock(return_value=fake.AGGREGATES)) + mock_svm_migration_start = self.mock_object( + self.mock_dest_client, 'svm_migration_start', + mock.Mock(return_value=c_fake.FAKE_MIGRATION_RESPONSE_WITH_JOB)) + mock_get_job = self.mock_object( + self.mock_dest_client, 'get_job', + mock.Mock(return_value=c_fake.FAKE_JOB_SUCCESS_STATE)) + + server_info = self.library._migration_start_using_svm_migrate( + None, self.fake_src_share_server, self.fake_dest_share_server, + self.mock_src_client, self.mock_dest_client) + + self.assertTrue(mock_list_cluster_nodes.called) + mock_get_data_port.assert_called_once_with(node_name) + mock_get_ipspace.assert_called_once_with( + node_name, fake.NODE_DATA_PORT, segmentation_id) + mock_create_port.assert_called_once_with( + fake.IPSPACE, network_info) + mock_get_vserver_name.assert_called_once_with( + self.fake_src_share_server['id']) + self.assertTrue(mock_get_cluster_name.called) + mock_svm_migration_start.assert_called_once_with( + fake.CLUSTER_NAME, fake.VSERVER1, fake.AGGREGATES, + dest_ipspace=fake.IPSPACE) + self.assertTrue(mock_get_aggregates.called) + self.assertEqual(expected_server_info, server_info) + mock_get_job.assert_called_once_with(c_fake.FAKE_JOB_ID) + + def test__migration_start_using_svm_migrate_exception(self): + + self.fake_src_share_server['share_network_subnet_id'] = 'fake_sns_id' + self.fake_dest_share_server['share_network_subnet_id'] = 'fake_sns_id' + node_name = fake.CLUSTER_NODES[0] + + server_to_get_network_info = self.fake_dest_share_server + + segmentation_id = ( + server_to_get_network_info['network_allocations'][0][ + 'segmentation_id']) + + network_info = { + 'network_allocations': + server_to_get_network_info['network_allocations'], + 'neutron_subnet_id': + server_to_get_network_info['share_network_subnet'][ + 'neutron_subnet_id'] + } + + mock_list_cluster_nodes = self.mock_object( + self.library._client, 'list_cluster_nodes', + mock.Mock(return_value=fake.CLUSTER_NODES)) + mock_get_data_port = self.mock_object( + self.library, '_get_node_data_port', + mock.Mock(return_value=fake.NODE_DATA_PORT)) + mock_get_ipspace = self.mock_object( + self.library._client, 'get_ipspace_name_for_vlan_port', + mock.Mock(return_value=fake.IPSPACE)) + mock_create_port = self.mock_object( + self.library, '_create_port_and_broadcast_domain') + mock_get_vserver_name = self.mock_object( + self.library, '_get_vserver_name', + mock.Mock(return_value=fake.VSERVER1)) + mock_get_cluster_name = self.mock_object( + self.mock_src_client, 'get_cluster_name', + mock.Mock(return_value=fake.CLUSTER_NAME)) + mock_get_aggregates = self.mock_object( + self.library, '_find_matching_aggregates', + mock.Mock(return_value=fake.AGGREGATES)) + mock_svm_migration_start = self.mock_object( + self.mock_dest_client, 'svm_migration_start', + mock.Mock(side_effect=exception.NetAppException())) + mock_delete_ipspace = self.mock_object( + self.mock_dest_client, 'delete_ipspace') + + self.assertRaises( + exception.NetAppException, + self.library._migration_start_using_svm_migrate, + None, + self.fake_src_share_server, self.fake_dest_share_server, + self.mock_src_client, self.mock_dest_client) + + self.assertTrue(mock_list_cluster_nodes.called) + mock_get_data_port.assert_called_once_with(node_name) + mock_get_ipspace.assert_called_once_with( + node_name, fake.NODE_DATA_PORT, segmentation_id) + mock_create_port.assert_called_once_with( + fake.IPSPACE, network_info) + mock_get_vserver_name.assert_called_once_with( + self.fake_src_share_server['id']) + self.assertTrue(mock_get_cluster_name.called) + mock_svm_migration_start.assert_called_once_with( + fake.CLUSTER_NAME, fake.VSERVER1, fake.AGGREGATES, + dest_ipspace=fake.IPSPACE) + self.assertTrue(mock_get_aggregates.called) + mock_delete_ipspace.assert_called_once_with(fake.IPSPACE) + def test__get_snapmirror_svm(self): dm_session_mock = mock.Mock() self.mock_object(data_motion, "DataMotionSession", @@ -2118,16 +2505,14 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) - def test_share_server_migration_continue_no_snapmirror(self): + def test_share_server_migration_continue_svm_dr_no_snapmirror(self): self.mock_object(self.library, '_get_snapmirror_svm', mock.Mock(return_value=[])) self.assertRaises(exception.NetAppException, - self.library.share_server_migration_continue, - None, + self.library._share_server_migration_continue_svm_dr, self.fake_src_share_server, - self.fake_dest_share_server, - [], []) + self.fake_dest_share_server) self.library._get_snapmirror_svm.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server @@ -2137,7 +2522,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): {'mirror_state': 'uninitialized', 'status': 'transferring'}, {'mirror_state': 'snapmirrored', 'status': 'quiescing'},) @ddt.unpack - def test_share_server_migration_continue(self, mirror_state, status): + def test_share_server_migration_continue_svm_dr(self, mirror_state, + status): fake_snapmirror = { 'mirror-state': mirror_state, 'relationship-status': status, @@ -2146,11 +2532,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=[fake_snapmirror])) expected = mirror_state == 'snapmirrored' and status == 'idle' - result = self.library.share_server_migration_continue( - None, + result = self.library._share_server_migration_continue_svm_dr( self.fake_src_share_server, - self.fake_dest_share_server, - [], [] + self.fake_dest_share_server ) self.assertEqual(expected, result) @@ -2158,56 +2542,105 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) - def test_share_server_migration_complete(self): - dm_session_mock = mock.Mock() - self.mock_object(data_motion, "DataMotionSession", - mock.Mock(return_value=dm_session_mock)) - self.mock_object(self.library, '_get_vserver', - mock.Mock(side_effect=[ - (self.fake_src_vserver, self.mock_src_client), - (self.fake_dest_vserver, self.mock_dest_client)])) - fake_ipspace = 'fake_ipspace' - self.mock_object(self.mock_dest_client, 'get_vserver_ipspace', - mock.Mock(return_value=fake_ipspace)) - fake_share_name = self.library._get_backend_share_name( - fake.SHARE_INSTANCE['id']) - self.mock_object(self.library, '_setup_network_for_vserver') - fake_volume = copy.deepcopy(fake.CLIENT_GET_VOLUME_RESPONSE) - self.mock_object(self.mock_dest_client, 'get_volume', - mock.Mock(return_value=fake_volume)) - self.mock_object(self.library, '_create_export', - mock.Mock(return_value=fake.NFS_EXPORTS)) - self.mock_object(self.library, '_delete_share') - mock_update_share_attrs = self.mock_object( - self.library, '_update_share_attributes_after_server_migration') + @ddt.data( + ('ready_for_cutover', True), + ('transferring', False) + ) + @ddt.unpack + def test_share_server_migration_continue_svm_migrate( + self, job_state, first_phase_completed): + c_fake.FAKE_MIGRATION_JOB_SUCCESS.update({"state": job_state}) - result = self.library.share_server_migration_complete( - None, - self.fake_src_share_server, - self.fake_dest_share_server, - [fake.SHARE_INSTANCE], [], - fake.NETWORK_INFO + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object( + self.mock_dest_client, 'svm_migration_get', + mock.Mock(return_value=c_fake.FAKE_MIGRATION_JOB_SUCCESS)) + + result = self.library._share_server_migration_continue_svm_migrate( + self.fake_dest_share_server, c_fake.FAKE_MIGRATION_POST_ID) + + self.assertEqual(first_phase_completed, result) + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.svm_migration_get.assert_called_once_with( + c_fake.FAKE_MIGRATION_POST_ID) + + def test_share_server_migration_continue_svm_migrate_exception(self): + + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'svm_migration_get', + mock.Mock(side_effect=netapp_api.NaApiError())) + + self.assertRaises( + exception.NetAppException, + self.library._share_server_migration_continue_svm_migrate, + self.fake_dest_share_server, c_fake.FAKE_MIGRATION_POST_ID) + + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.svm_migration_get.assert_called_once_with( + c_fake.FAKE_MIGRATION_POST_ID) + + @ddt.data(None, 'fake_migration_id') + def test_share_server_migration_continue(self, migration_id): + expected_result = True + self.mock_object( + self.library, '_get_share_server_migration_id', + mock.Mock(return_value=migration_id)) + self.mock_object( + self.library, '_share_server_migration_continue_svm_migrate', + mock.Mock(return_value=expected_result)) + self.mock_object( + self.library, '_share_server_migration_continue_svm_dr', + mock.Mock(return_value=expected_result)) + + result = self.library.share_server_migration_continue( + None, self.fake_src_share_server, self.fake_dest_share_server, + [], [] ) - expected_share_updates = { - fake.SHARE_INSTANCE['id']: { - 'export_locations': fake.NFS_EXPORTS, - 'pool_name': fake_volume['aggregate'] - } - } - expected_result = { - 'share_updates': expected_share_updates, - } - self.assertEqual(expected_result, result) + + def test__setup_networking_for_destination_vserver(self): + self.mock_object(self.mock_dest_client, 'get_vserver_ipspace', + mock.Mock(return_value=fake.IPSPACE)) + self.mock_object(self.library, '_setup_network_for_vserver') + + self.library._setup_networking_for_destination_vserver( + self.mock_dest_client, self.fake_vserver, + fake.NETWORK_INFO) + + self.mock_dest_client.get_vserver_ipspace.assert_called_once_with( + self.fake_vserver) + self.library._setup_network_for_vserver.assert_called_once_with( + self.fake_vserver, self.mock_dest_client, fake.NETWORK_INFO, + fake.IPSPACE, enable_nfs=False, security_services=None) + + def test__migration_complete_svm_dr(self): + dm_session_mock = mock.Mock() + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(self.fake_dest_vserver, + self.mock_dest_client))) + self.mock_object(data_motion, "DataMotionSession", + mock.Mock(return_value=dm_session_mock)) + self.mock_object( + self.library, '_setup_networking_for_destination_vserver') + + self.library._share_server_migration_complete_svm_dr( + self.fake_src_share_server, self.fake_dest_share_server, + self.fake_src_vserver, self.mock_src_client, + [fake.SHARE_INSTANCE], fake.NETWORK_INFO + ) + + self.library._get_vserver.assert_called_once_with( + share_server=self.fake_dest_share_server, + backend_name=self.fake_dest_backend_name + ) dm_session_mock.update_snapmirror_svm.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server ) - self.library._get_vserver.assert_has_calls([ - mock.call(share_server=self.fake_src_share_server, - backend_name=self.fake_src_backend_name), - mock.call(share_server=self.fake_dest_share_server, - backend_name=self.fake_dest_backend_name)]) quiesce_break_mock = dm_session_mock.quiesce_and_break_snapmirror_svm quiesce_break_mock.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server @@ -2221,56 +2654,165 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_src_client.stop_vserver.assert_called_once_with( self.fake_src_vserver ) - self.mock_dest_client.get_vserver_ipspace.assert_called_once_with( - self.fake_dest_vserver - ) - self.library._setup_network_for_vserver.assert_called_once_with( - self.fake_dest_vserver, self.mock_dest_client, fake.NETWORK_INFO, - fake_ipspace, enable_nfs=False, security_services=None - ) + (self.library._setup_networking_for_destination_vserver + .assert_called_once_with( + self.mock_dest_client, self.fake_dest_vserver, + fake.NETWORK_INFO)) self.mock_dest_client.start_vserver.assert_called_once_with( self.fake_dest_vserver ) dm_session_mock.delete_snapmirror_svm.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server ) + + @ddt.data( + {'is_svm_dr': True, 'network_change': True}, + {'is_svm_dr': False, 'network_change': True}, + {'is_svm_dr': False, 'network_change': False}, + ) + @ddt.unpack + def test_share_server_migration_complete(self, is_svm_dr, network_change): + current_interfaces = ['interface_1', 'interface_2'] + self.mock_object(self.library, '_get_vserver', + mock.Mock(side_effect=[ + (self.fake_src_vserver, self.mock_src_client), + (self.fake_dest_vserver, self.mock_dest_client)])) + mock_complete_svm_migrate = self.mock_object( + self.library, '_share_server_migration_complete_svm_migrate') + mock_complete_svm_dr = self.mock_object( + self.library, '_share_server_migration_complete_svm_dr') + fake_share_name = self.library._get_backend_share_name( + fake.SHARE_INSTANCE['id']) + fake_volume = copy.deepcopy(fake.CLIENT_GET_VOLUME_RESPONSE) + self.mock_object(self.mock_dest_client, 'get_volume', + mock.Mock(return_value=fake_volume)) + self.mock_object(self.library, '_create_export', + mock.Mock(return_value=fake.NFS_EXPORTS)) + self.mock_object(self.library, '_delete_share') + mock_update_share_attrs = self.mock_object( + self.library, '_update_share_attributes_after_server_migration') + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'list_network_interfaces', + mock.Mock(return_value=current_interfaces)) + self.mock_object(self.mock_dest_client, 'delete_network_interface') + self.mock_object(self.library, + '_setup_networking_for_destination_vserver') + + sns_id = 'fake_sns_id' + new_sns_id = 'fake_sns_id_2' + self.fake_src_share_server['share_network_subnet_id'] = sns_id + self.fake_dest_share_server['share_network_subnet_id'] = ( + sns_id if not network_change else new_sns_id) + share_instances = [fake.SHARE_INSTANCE] + migration_id = 'fake_migration_id' + share_host = fake.SHARE_INSTANCE['host'] + self.fake_src_share_server['backend_details']['ports'] = [] + + if not is_svm_dr: + self.fake_dest_share_server['backend_details'][ + 'migration_operation_id'] = ( + migration_id) + share_host = share_host.replace( + share_host.split('#')[1], fake_volume['aggregate']) + should_recreate_export = is_svm_dr or network_change + share_server_to_get_vserver_name = ( + self.fake_dest_share_server + if is_svm_dr else self.fake_src_share_server) + + result = self.library.share_server_migration_complete( + None, + self.fake_src_share_server, + self.fake_dest_share_server, + share_instances, [], + fake.NETWORK_INFO + ) + + expected_share_updates = { + fake.SHARE_INSTANCE['id']: { + 'pool_name': fake_volume['aggregate'] + } + } + expected_share_updates[fake.SHARE_INSTANCE['id']].update( + {'export_locations': fake.NFS_EXPORTS}) + expected_backend_details = ( + {} if is_svm_dr else self.fake_src_share_server['backend_details']) + expected_result = { + 'share_updates': expected_share_updates, + 'server_backend_details': expected_backend_details + } + + self.assertEqual(expected_result, result) + self.library._get_vserver.assert_has_calls([ + mock.call(share_server=self.fake_src_share_server, + backend_name=self.fake_src_backend_name), + mock.call(share_server=share_server_to_get_vserver_name, + backend_name=self.fake_dest_backend_name)]) + if is_svm_dr: + mock_complete_svm_dr.assert_called_once_with( + self.fake_src_share_server, self.fake_dest_share_server, + self.fake_src_vserver, self.mock_src_client, + share_instances, fake.NETWORK_INFO + ) + self.library._delete_share.assert_called_once_with( + fake.SHARE_INSTANCE, self.fake_src_vserver, + self.mock_src_client, remove_export=True) + mock_update_share_attrs.assert_called_once_with( + fake.SHARE_INSTANCE, self.mock_src_client, + fake_volume['aggregate'], self.mock_dest_client) + else: + mock_complete_svm_migrate.assert_called_once_with( + migration_id, self.fake_dest_share_server) + self.mock_dest_client.list_network_interfaces.assert_called_once() + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.delete_network_interface.assert_has_calls( + [mock.call(self.fake_src_vserver, interface_name) + for interface_name in current_interfaces]) + (self.library._setup_networking_for_destination_vserver. + assert_called_once_with(self.mock_dest_client, + self.fake_src_vserver, + fake.NETWORK_INFO)) + if should_recreate_export: + create_export_calls = [ + mock.call( + instance, self.fake_dest_share_server, + self.fake_dest_vserver, self.mock_dest_client, + clear_current_export_policy=False, + ensure_share_already_exists=True, + share_host=share_host) + for instance in share_instances + ] + self.library._create_export.assert_has_calls(create_export_calls) self.mock_dest_client.get_volume.assert_called_once_with( fake_share_name) - mock_update_share_attrs.assert_called_once_with( - fake.SHARE_INSTANCE, self.mock_src_client, - fake_volume['aggregate'], self.mock_dest_client) - self.library._delete_share.assert_called_once_with( - fake.SHARE_INSTANCE, self.fake_src_vserver, - self.mock_src_client, remove_export=True) def test_share_server_migration_complete_failure_breaking(self): dm_session_mock = mock.Mock() self.mock_object(data_motion, "DataMotionSession", mock.Mock(return_value=dm_session_mock)) - self.mock_object(self.library, '_get_vserver', - mock.Mock(side_effect=[ - (self.fake_src_vserver, self.mock_src_client), - (self.fake_dest_vserver, self.mock_dest_client)])) + self.mock_object( + self.library, '_get_vserver', + mock.Mock(return_value=(self.fake_dest_vserver, + self.mock_dest_client))) self.mock_object(dm_session_mock, 'quiesce_and_break_snapmirror_svm', mock.Mock(side_effect=exception.NetAppException)) self.mock_object(self.library, '_delete_share') self.assertRaises(exception.NetAppException, - self.library.share_server_migration_complete, - None, + self.library._share_server_migration_complete_svm_dr, self.fake_src_share_server, self.fake_dest_share_server, - [fake.SHARE_INSTANCE], [], + self.fake_src_vserver, + self.mock_src_client, [fake.SHARE_INSTANCE], fake.NETWORK_INFO) dm_session_mock.update_snapmirror_svm.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server ) - self.library._get_vserver.assert_has_calls([ - mock.call(share_server=self.fake_src_share_server, - backend_name=self.fake_src_backend_name), - mock.call(share_server=self.fake_dest_share_server, - backend_name=self.fake_dest_backend_name)]) + self.library._get_vserver.assert_called_once_with( + share_server=self.fake_dest_share_server, + backend_name=self.fake_dest_backend_name) quiesce_break_mock = dm_session_mock.quiesce_and_break_snapmirror_svm quiesce_break_mock.assert_called_once_with( self.fake_src_share_server, self.fake_dest_share_server @@ -2287,18 +2829,18 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): def test_share_server_migration_complete_failure_get_new_volume(self): dm_session_mock = mock.Mock() + fake_share_name = self.library._get_backend_share_name( + fake.SHARE_INSTANCE['id']) self.mock_object(data_motion, "DataMotionSession", mock.Mock(return_value=dm_session_mock)) self.mock_object(self.library, '_get_vserver', mock.Mock(side_effect=[ (self.fake_src_vserver, self.mock_src_client), (self.fake_dest_vserver, self.mock_dest_client)])) - fake_ipspace = 'fake_ipspace' - self.mock_object(self.mock_dest_client, 'get_vserver_ipspace', - mock.Mock(return_value=fake_ipspace)) - fake_share_name = self.library._get_backend_share_name( - fake.SHARE_INSTANCE['id']) - self.mock_object(self.library, '_setup_network_for_vserver') + self.mock_object(self.library, + '_share_server_migration_complete_svm_dr') + self.mock_object(self.library, '_get_share_server_migration_id', + mock.Mock(return_value=None)) self.mock_object(self.mock_dest_client, 'get_volume', mock.Mock(side_effect=exception.NetAppException)) @@ -2310,45 +2852,68 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): [fake.SHARE_INSTANCE], [], fake.NETWORK_INFO) - dm_session_mock.update_snapmirror_svm.assert_called_once_with( - self.fake_src_share_server, self.fake_dest_share_server - ) self.library._get_vserver.assert_has_calls([ mock.call(share_server=self.fake_src_share_server, backend_name=self.fake_src_backend_name), mock.call(share_server=self.fake_dest_share_server, backend_name=self.fake_dest_backend_name)]) - quiesce_break_mock = dm_session_mock.quiesce_and_break_snapmirror_svm - quiesce_break_mock.assert_called_once_with( - self.fake_src_share_server, self.fake_dest_share_server - ) - dm_session_mock.wait_for_vserver_state.assert_called_once_with( - self.fake_dest_vserver, self.mock_dest_client, subtype='default', - state='running', operational_state='stopped', - timeout=(self.library.configuration. - netapp_server_migration_state_change_timeout) - ) - self.mock_src_client.stop_vserver.assert_called_once_with( - self.fake_src_vserver - ) - self.mock_dest_client.get_vserver_ipspace.assert_called_once_with( - self.fake_dest_vserver - ) - self.library._setup_network_for_vserver.assert_called_once_with( - self.fake_dest_vserver, self.mock_dest_client, fake.NETWORK_INFO, - fake_ipspace, enable_nfs=False, security_services=None - ) - self.mock_dest_client.start_vserver.assert_called_once_with( - self.fake_dest_vserver - ) - dm_session_mock.delete_snapmirror_svm.assert_called_once_with( - self.fake_src_share_server, self.fake_dest_share_server - ) self.mock_dest_client.get_volume.assert_called_once_with( fake_share_name) + def test__share_server_migration_complete_svm_migrate(self): + completion_status = na_utils.MIGRATION_STATE_MIGRATE_COMPLETE + migration_id = 'fake_migration_id' + fake_complete_job_uuid = 'fake_uuid' + fake_complete_job = { + 'job': { + 'state': 'cutover_triggered', + 'uuid': fake_complete_job_uuid + } + } + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'svm_migrate_complete', + mock.Mock(return_value=fake_complete_job)) + self.mock_object(self.library, '_get_job_uuid', + mock.Mock(return_value=fake_complete_job_uuid)) + self.mock_object(self.library, '_wait_for_operation_status') + + self.library._share_server_migration_complete_svm_migrate( + migration_id, self.fake_dest_share_server) + + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.svm_migrate_complete.assert_called_once_with( + migration_id) + self.library._get_job_uuid.assert_called_once_with(fake_complete_job) + self.library._wait_for_operation_status.assert_has_calls( + [mock.call(fake_complete_job_uuid, self.mock_dest_client.get_job), + mock.call(migration_id, self.mock_dest_client.svm_migration_get, + desired_status=completion_status) + ] + ) + + def test__share_server_migration_complete_svm_migrate_failed_to_complete( + self): + migration_id = 'fake_migration_id' + + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'svm_migrate_complete', + mock.Mock(side_effect=exception.NetAppException())) + + self.assertRaises( + exception.NetAppException, + self.library._share_server_migration_complete_svm_migrate, + migration_id, self.fake_dest_share_server) + + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.svm_migrate_complete.assert_called_once_with( + migration_id) + @ddt.data([], ['fake_snapmirror']) - def test_share_server_migration_cancel(self, snapmirrors): + def test_share_server_migration_cancel_svm_dr(self, snapmirrors): dm_session_mock = mock.Mock() self.mock_object(data_motion, "DataMotionSession", mock.Mock(return_value=dm_session_mock)) @@ -2359,11 +2924,10 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=snapmirrors)) self.mock_object(self.library, '_delete_share') - self.library.share_server_migration_cancel( - None, + self.library._migration_cancel_using_svm_dr( self.fake_src_share_server, self.fake_dest_share_server, - [fake.SHARE_INSTANCE], [] + [fake.SHARE_INSTANCE] ) self.library._get_vserver.assert_called_once_with( @@ -2380,7 +2944,110 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.SHARE_INSTANCE, self.fake_dest_vserver, self.mock_dest_client, remove_export=False) - def test_share_server_migration_cancel_snapmirror_failure(self): + @ddt.data(True, False) + def test__migration_cancel_using_svm_migrate(self, has_ipspace): + pause_job_uuid = 'fake_pause_job_id' + cancel_job_uuid = 'fake_cancel_job_id' + ipspace_name = 'fake_ipspace_name' + migration_id = 'fake_migration_id' + pause_job = { + 'uuid': pause_job_uuid + } + cancel_job = { + 'uuid': cancel_job_uuid + } + migration_information = { + "destination": { + "ipspace": { + "name": ipspace_name + } + } + } + + if has_ipspace: + migration_information["destination"]["ipspace"]["name"] = ( + ipspace_name) + + self.mock_object(self.library, '_get_job_uuid', + mock.Mock( + side_effect=[pause_job_uuid, cancel_job_uuid])) + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'svm_migration_get', + mock.Mock(return_value=migration_information)) + self.mock_object(self.mock_dest_client, 'svm_migrate_pause', + mock.Mock(return_value=pause_job)) + self.mock_object(self.library, '_wait_for_operation_status') + self.mock_object(self.mock_dest_client, 'svm_migrate_cancel', + mock.Mock(return_value=cancel_job)) + self.mock_object(self.mock_dest_client, 'ipspace_has_data_vservers', + mock.Mock(return_value=False)) + self.mock_object(self.mock_dest_client, 'delete_ipspace') + + self.library._migration_cancel_using_svm_migrate( + migration_id, self.fake_dest_share_server) + + self.library._get_job_uuid.assert_has_calls( + [mock.call(pause_job), mock.call(cancel_job)] + ) + data_motion.get_client_for_host.assert_called_once_with( + self.fake_dest_share_server['host']) + self.mock_dest_client.svm_migration_get.assert_called_once_with( + migration_id) + self.mock_dest_client.svm_migrate_pause.assert_called_once_with( + migration_id) + self.library._wait_for_operation_status.assert_has_calls( + [mock.call(pause_job_uuid, self.mock_dest_client.get_job), + mock.call(migration_id, self.mock_dest_client.svm_migration_get, + desired_status=na_utils.MIGRATION_STATE_MIGRATE_PAUSED), + mock.call(cancel_job_uuid, self.mock_dest_client.get_job)] + ) + self.mock_dest_client.svm_migrate_cancel.assert_called_once_with( + migration_id) + + if has_ipspace: + self.mock_dest_client.delete_ipspace.assert_called_once_with( + ipspace_name) + + @ddt.data( + (mock.Mock(side_effect=exception.NetAppException()), mock.Mock()), + (mock.Mock(), mock.Mock(side_effect=exception.NetAppException())) + ) + @ddt.unpack + def test__migration_cancel_using_svm_migrate_error( + self, mock_pause, mock_cancel): + pause_job_uuid = 'fake_pause_job_id' + cancel_job_uuid = 'fake_cancel_job_id' + migration_id = 'fake_migration_id' + migration_information = { + "destination": { + "ipspace": { + "name": "ipspace_name" + } + } + } + + self.mock_object(self.library, '_get_job_uuid', + mock.Mock( + side_effect=[pause_job_uuid, cancel_job_uuid])) + self.mock_object(data_motion, 'get_client_for_host', + mock.Mock(return_value=self.mock_dest_client)) + self.mock_object(self.mock_dest_client, 'svm_migration_get', + mock.Mock(return_value=migration_information)) + self.mock_object(self.mock_dest_client, 'svm_migrate_pause', + mock_pause) + self.mock_object(self.library, '_wait_for_operation_status') + self.mock_object(self.mock_dest_client, 'svm_migrate_cancel', + mock_cancel) + + self.assertRaises( + exception.NetAppException, + self.library._migration_cancel_using_svm_migrate, + migration_id, + self.fake_dest_share_server + ) + + def test_share_server_migration_cancel_svm_dr_snapmirror_failure(self): dm_session_mock = mock.Mock() self.mock_object(data_motion, "DataMotionSession", mock.Mock(return_value=dm_session_mock)) @@ -2393,11 +3060,10 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(side_effect=exception.NetAppException)) self.assertRaises(exception.NetAppException, - self.library.share_server_migration_cancel, - None, + self.library._migration_cancel_using_svm_dr, self.fake_src_share_server, self.fake_dest_share_server, - [fake.SHARE_INSTANCE], []) + [fake.SHARE_INSTANCE]) self.library._get_vserver.assert_called_once_with( share_server=self.fake_dest_share_server, @@ -2409,6 +3075,27 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) + @ddt.data(None, 'fake_migration_id') + def test_share_server_migration_cancel(self, migration_id): + self.mock_object(self.library, '_get_share_server_migration_id', + mock.Mock(return_value=migration_id)) + self.mock_object(self.library, '_migration_cancel_using_svm_migrate') + self.mock_object(self.library, '_migration_cancel_using_svm_dr') + + self.library.share_server_migration_cancel( + None, self.fake_src_share_server, self.fake_dest_share_server, + [], []) + + if migration_id: + (self.library._migration_cancel_using_svm_migrate + .assert_called_once_with( + migration_id, self.fake_dest_share_server)) + else: + (self.library._migration_cancel_using_svm_dr + .assert_called_once_with( + self.fake_src_share_server, self.fake_dest_share_server, + [])) + def test_share_server_migration_get_progress(self): expected_result = {'total_progress': 0} diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index 3578c2a756..85c378e22c 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -520,6 +520,10 @@ SHARE_SERVER = { 'network_allocations': (USER_NETWORK_ALLOCATIONS + ADMIN_NETWORK_ALLOCATIONS), 'host': SERVER_HOST, + 'share_network_subnet': { + 'neutron_net_id': 'fake_neutron_net_id', + 'neutron_subnet_id': 'fake_neutron_subnet_id' + } } SHARE_SERVER_2 = { @@ -531,6 +535,10 @@ SHARE_SERVER_2 = { 'network_allocations': (USER_NETWORK_ALLOCATIONS + ADMIN_NETWORK_ALLOCATIONS), 'host': SERVER_HOST_2, + 'share_network_subnet': { + 'neutron_net_id': 'fake_neutron_net_id_2', + 'neutron_subnet_id': 'fake_neutron_subnet_id_2' + } } VSERVER_INFO = { diff --git a/releasenotes/notes/netapp-add-migration-through-svm-migrate-c1e29fce19758324.yaml b/releasenotes/notes/netapp-add-migration-through-svm-migrate-c1e29fce19758324.yaml new file mode 100644 index 0000000000..1a605ff1d1 --- /dev/null +++ b/releasenotes/notes/netapp-add-migration-through-svm-migrate-c1e29fce19758324.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The NetApp ONTAP driver now supports nondisruptive share server migration + for clusters with version >= 9.10.