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.