Merge "[NetApp] Share server migration through SVM migrate"
This commit is contained in:
commit
65f3967efa
|
@ -23,12 +23,14 @@ import re
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
import requests
|
import requests
|
||||||
from requests import auth
|
from requests import auth
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
|
from manila.share.drivers.netapp.dataontap.client import rest_endpoints
|
||||||
from manila.share.drivers.netapp import utils
|
from manila.share.drivers.netapp import utils
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
@ -62,28 +64,23 @@ EPOLICYNOTFOUND = '18251'
|
||||||
EEVENTNOTFOUND = '18253'
|
EEVENTNOTFOUND = '18253'
|
||||||
ESCOPENOTFOUND = '18259'
|
ESCOPENOTFOUND = '18259'
|
||||||
ESVMDR_CANNOT_PERFORM_OP_FOR_STATUS = '18815'
|
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."""
|
"""Encapsulates server connection logic."""
|
||||||
|
|
||||||
TRANSPORT_TYPE_HTTP = 'http'
|
def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP, style=None,
|
||||||
TRANSPORT_TYPE_HTTPS = 'https'
|
ssl_cert_path=None, username=None, password=None, port=None,
|
||||||
SERVER_TYPE_FILER = 'filer'
|
trace=False, api_trace_pattern=None):
|
||||||
SERVER_TYPE_DFM = 'dfm'
|
super(BaseClient, self).__init__()
|
||||||
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):
|
|
||||||
self._host = host
|
self._host = host
|
||||||
self.set_server_type(server_type)
|
|
||||||
self.set_transport_type(transport_type)
|
self.set_transport_type(transport_type)
|
||||||
self.set_style(style)
|
self.set_style(style)
|
||||||
if port:
|
if port:
|
||||||
|
@ -99,9 +96,21 @@ class NaServer(object):
|
||||||
# Note(felipe_rodrigues): it will verify with the mozila CA roots,
|
# Note(felipe_rodrigues): it will verify with the mozila CA roots,
|
||||||
# given by certifi package.
|
# given by certifi package.
|
||||||
self._ssl_verify = True
|
self._ssl_verify = True
|
||||||
|
|
||||||
LOG.debug('Using NetApp controller: %s', self._host)
|
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):
|
def get_transport_type(self):
|
||||||
"""Get the transport type protocol."""
|
"""Get the transport type protocol."""
|
||||||
return self._protocol
|
return self._protocol
|
||||||
|
@ -112,38 +121,13 @@ class NaServer(object):
|
||||||
Supports http and https transport types.
|
Supports http and https transport types.
|
||||||
"""
|
"""
|
||||||
if transport_type.lower() not in (
|
if transport_type.lower() not in (
|
||||||
NaServer.TRANSPORT_TYPE_HTTP,
|
TRANSPORT_TYPE_HTTP, TRANSPORT_TYPE_HTTPS):
|
||||||
NaServer.TRANSPORT_TYPE_HTTPS):
|
|
||||||
raise ValueError('Unsupported transport type')
|
raise ValueError('Unsupported transport type')
|
||||||
self._protocol = transport_type.lower()
|
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
|
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):
|
def get_server_type(self):
|
||||||
"""Get the target server type."""
|
"""Get the server type."""
|
||||||
return self._server_type
|
return self._server_type
|
||||||
|
|
||||||
def set_server_type(self, server_type):
|
def set_server_type(self, server_type):
|
||||||
|
@ -151,16 +135,7 @@ class NaServer(object):
|
||||||
|
|
||||||
Supports filer and dfm server types.
|
Supports filer and dfm server types.
|
||||||
"""
|
"""
|
||||||
if server_type.lower() not in (NaServer.SERVER_TYPE_FILER,
|
raise NotImplementedError()
|
||||||
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
|
|
||||||
|
|
||||||
def set_api_version(self, major, minor):
|
def set_api_version(self, major, minor):
|
||||||
"""Set the API version."""
|
"""Set the API version."""
|
||||||
|
@ -216,14 +191,6 @@ class NaServer(object):
|
||||||
return self._timeout
|
return self._timeout
|
||||||
return None
|
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):
|
def get_vserver(self):
|
||||||
"""Get the vserver to use in tunneling."""
|
"""Get the vserver to use in tunneling."""
|
||||||
return self._vserver
|
return self._vserver
|
||||||
|
@ -242,10 +209,110 @@ class NaServer(object):
|
||||||
self._password = password
|
self._password = password
|
||||||
self._refresh_conn = True
|
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):
|
def invoke_elem(self, na_element, enable_tunneling=False):
|
||||||
"""Invoke the API on the server."""
|
"""Invoke the API on the server."""
|
||||||
if na_element and not isinstance(na_element, NaElement):
|
if na_element and not isinstance(na_element, NaElement):
|
||||||
ValueError('NaElement must be supplied to invoke API')
|
ValueError('NaElement must be supplied to invoke API')
|
||||||
|
|
||||||
request_element = self._create_request(na_element, enable_tunneling)
|
request_element = self._create_request(na_element, enable_tunneling)
|
||||||
request_d = request_element.to_string()
|
request_d = request_element.to_string()
|
||||||
|
|
||||||
|
@ -282,7 +349,8 @@ class NaServer(object):
|
||||||
|
|
||||||
return response_element
|
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.
|
"""Invokes API and checks execution status as success.
|
||||||
|
|
||||||
Need to set enable_tunneling to True explicitly to achieve it.
|
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
|
tunneling. The vserver or vfiler should be set before this call
|
||||||
otherwise tunneling remains disabled.
|
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':
|
if result.has_attr('status') and result.get_attr('status') == 'passed':
|
||||||
return result
|
return result
|
||||||
code = (result.get_attr('errno')
|
code = (result.get_attr('errno')
|
||||||
|
@ -336,7 +409,8 @@ class NaServer(object):
|
||||||
raise ValueError('ontapi version has to be atleast 1.15'
|
raise ValueError('ontapi version has to be atleast 1.15'
|
||||||
' to send request to vserver')
|
' to send request to vserver')
|
||||||
|
|
||||||
def _parse_response(self, response):
|
@staticmethod
|
||||||
|
def _parse_response(response):
|
||||||
"""Get the NaElement for the response."""
|
"""Get the NaElement for the response."""
|
||||||
if not response:
|
if not response:
|
||||||
raise NaApiError('No response received')
|
raise NaApiError('No response received')
|
||||||
|
@ -349,28 +423,287 @@ class NaServer(object):
|
||||||
return processed_response.get_child_by_name('results')
|
return processed_response.get_child_by_name('results')
|
||||||
|
|
||||||
def _get_url(self):
|
def _get_url(self):
|
||||||
|
"""Get the base url to send the request."""
|
||||||
host = self._host
|
host = self._host
|
||||||
if ':' in host:
|
if ':' in host:
|
||||||
host = '[%s]' % host
|
host = '[%s]' % host
|
||||||
return '%s://%s:%s/%s' % (self._protocol, host, self._port, self._url)
|
return '%s://%s:%s/%s' % (self._protocol, host, self._port, self._url)
|
||||||
|
|
||||||
def _build_session(self):
|
def _build_headers(self):
|
||||||
if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD:
|
"""Build and return headers."""
|
||||||
auth_handler = self._create_basic_auth_handler()
|
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:
|
else:
|
||||||
auth_handler = self._create_certificate_auth_handler()
|
self.set_port(443)
|
||||||
|
|
||||||
self._session = requests.Session()
|
def _get_request_info(self, api_name, session):
|
||||||
self._session.auth = auth_handler
|
"""Returns the request method and url to be used in the REST call."""
|
||||||
self._session.verify = self._ssl_verify
|
|
||||||
self._session.headers = {
|
|
||||||
'Content-Type': 'text/xml', 'charset': 'utf-8'}
|
|
||||||
|
|
||||||
def _create_basic_auth_handler(self):
|
request_methods = {
|
||||||
return auth.HTTPBasicAuth(self._username, self._password)
|
'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):
|
def _add_query_params_to_url(self, url, query):
|
||||||
raise NotImplementedError()
|
"""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):
|
def __str__(self):
|
||||||
return "server: %s" % (self._host)
|
return "server: %s" % (self._host)
|
||||||
|
|
|
@ -81,12 +81,13 @@ class NetAppBaseClient(object):
|
||||||
return string.split('}', 1)[1]
|
return string.split('}', 1)[1]
|
||||||
return string
|
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."""
|
"""Sends request to Ontapi."""
|
||||||
request = netapp_api.NaElement(api_name)
|
request = netapp_api.NaElement(api_name)
|
||||||
if api_args:
|
return self.connection.invoke_successfully(
|
||||||
request.translate_struct(api_args)
|
request, api_args=api_args, enable_tunneling=enable_tunneling,
|
||||||
return self.connection.invoke_successfully(request, enable_tunneling)
|
use_zapi=use_zapi)
|
||||||
|
|
||||||
@na_utils.trace
|
@na_utils.trace
|
||||||
def get_licenses(self):
|
def get_licenses(self):
|
||||||
|
|
|
@ -74,6 +74,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||||
ontapi_1_120 = ontapi_version >= (1, 120)
|
ontapi_1_120 = ontapi_version >= (1, 120)
|
||||||
ontapi_1_140 = ontapi_version >= (1, 140)
|
ontapi_1_140 = ontapi_version >= (1, 140)
|
||||||
ontapi_1_150 = ontapi_version >= (1, 150)
|
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('SNAPMIRROR_V2', supported=ontapi_1_20)
|
||||||
self.features.add_feature('SYSTEM_METRICS', supported=ontapi_1_2x)
|
self.features.add_feature('SYSTEM_METRICS', supported=ontapi_1_2x)
|
||||||
|
@ -95,6 +96,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||||
supported=ontapi_1_150)
|
supported=ontapi_1_150)
|
||||||
self.features.add_feature('LDAP_LDAP_SERVERS',
|
self.features.add_feature('LDAP_LDAP_SERVERS',
|
||||||
supported=ontapi_1_120)
|
supported=ontapi_1_120)
|
||||||
|
self.features.add_feature('SVM_MIGRATE', supported=ontap_9_10)
|
||||||
|
|
||||||
def _invoke_vserver_api(self, na_element, vserver):
|
def _invoke_vserver_api(self, na_element, vserver):
|
||||||
server = copy.copy(self.connection)
|
server = copy.copy(self.connection)
|
||||||
|
@ -1040,6 +1042,24 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||||
|
|
||||||
return interfaces
|
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
|
@na_utils.trace
|
||||||
def get_ipspace_name_for_vlan_port(self, vlan_node, vlan_port, vlan_id):
|
def get_ipspace_name_for_vlan_port(self, vlan_node, vlan_port, vlan_id):
|
||||||
"""Gets IPSpace name for specified VLAN"""
|
"""Gets IPSpace name for specified VLAN"""
|
||||||
|
@ -3605,7 +3625,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||||
|
|
||||||
# NOTE(cknight): Cannot use deepcopy on the connection context
|
# NOTE(cknight): Cannot use deepcopy on the connection context
|
||||||
node_client = copy.copy(self)
|
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)
|
node_client.connection.set_timeout(25)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -5453,3 +5473,173 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||||
raise exception.NetAppException(msg)
|
raise exception.NetAppException(msg)
|
||||||
|
|
||||||
return fpolicy_status
|
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)
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -85,6 +85,13 @@ def get_client_for_backend(backend_name, vserver_name=None):
|
||||||
return client
|
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):
|
class DataMotionSession(object):
|
||||||
|
|
||||||
def _get_backend_volume_name(self, config, share_obj):
|
def _get_backend_volume_name(self, config, share_obj):
|
||||||
|
|
|
@ -304,7 +304,7 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver):
|
||||||
|
|
||||||
def share_server_migration_start(self, context, src_share_server,
|
def share_server_migration_start(self, context, src_share_server,
|
||||||
dest_share_server, shares, snapshots):
|
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)
|
context, src_share_server, dest_share_server, shares, snapshots)
|
||||||
|
|
||||||
def share_server_migration_continue(self, context, src_share_server,
|
def share_server_migration_continue(self, context, src_share_server,
|
||||||
|
|
|
@ -1310,7 +1310,8 @@ class NetAppCmodeFileStorageLibrary(object):
|
||||||
@na_utils.trace
|
@na_utils.trace
|
||||||
def _create_export(self, share, share_server, vserver, vserver_client,
|
def _create_export(self, share, share_server, vserver, vserver_client,
|
||||||
clear_current_export_policy=True,
|
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."""
|
"""Creates NAS storage."""
|
||||||
helper = self._get_helper(share)
|
helper = self._get_helper(share)
|
||||||
helper.set_client(vserver_client)
|
helper.set_client(vserver_client)
|
||||||
|
@ -1325,9 +1326,11 @@ class NetAppCmodeFileStorageLibrary(object):
|
||||||
msg_args = {'vserver': vserver, 'proto': share['share_proto']}
|
msg_args = {'vserver': vserver, 'proto': share['share_proto']}
|
||||||
raise exception.NetAppException(msg % msg_args)
|
raise exception.NetAppException(msg % msg_args)
|
||||||
|
|
||||||
|
host = share_host if share_host else share['host']
|
||||||
|
|
||||||
# Get LIF addresses with metadata
|
# Get LIF addresses with metadata
|
||||||
export_addresses = self._get_export_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
|
# Create the share and get a callback for generating export locations
|
||||||
callback = helper.create_share(
|
callback = helper.create_share(
|
||||||
|
@ -1355,11 +1358,11 @@ class NetAppCmodeFileStorageLibrary(object):
|
||||||
|
|
||||||
@na_utils.trace
|
@na_utils.trace
|
||||||
def _get_export_addresses_with_metadata(self, share, share_server,
|
def _get_export_addresses_with_metadata(self, share, share_server,
|
||||||
interfaces):
|
interfaces, share_host):
|
||||||
"""Return interface addresses with locality and other metadata."""
|
"""Return interface addresses with locality and other metadata."""
|
||||||
|
|
||||||
# Get home node so we can identify preferred paths
|
# 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)
|
home_node = self._get_aggregate_node(aggregate_name)
|
||||||
|
|
||||||
# Get admin LIF addresses so we can identify admin export locations
|
# Get admin LIF addresses so we can identify admin export locations
|
||||||
|
|
|
@ -44,6 +44,8 @@ SUPPORTED_NETWORK_TYPES = (None, 'flat', 'vlan')
|
||||||
SEGMENTED_NETWORK_TYPES = ('vlan',)
|
SEGMENTED_NETWORK_TYPES = ('vlan',)
|
||||||
DEFAULT_MTU = 1500
|
DEFAULT_MTU = 1500
|
||||||
CLUSTER_IPSPACES = ('Cluster', 'Default')
|
CLUSTER_IPSPACES = ('Cluster', 'Default')
|
||||||
|
SERVER_MIGRATE_SVM_DR = 'svm_dr'
|
||||||
|
SERVER_MIGRATE_SVM_MIGRATE = 'svm_migrate'
|
||||||
|
|
||||||
|
|
||||||
class NetAppCmodeMultiSVMFileStorageLibrary(
|
class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
|
@ -306,10 +308,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
return 'ipspace_' + network_id.replace('-', '_')
|
return 'ipspace_' + network_id.replace('-', '_')
|
||||||
|
|
||||||
@na_utils.trace
|
@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 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
|
return None
|
||||||
|
|
||||||
if (network_info['network_allocations'][0]['network_type']
|
if (network_info['network_allocations'][0]['network_type']
|
||||||
|
@ -324,7 +328,7 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
return client_cmode.DEFAULT_IPSPACE
|
return client_cmode.DEFAULT_IPSPACE
|
||||||
|
|
||||||
ipspace_name = self._get_valid_ipspace_name(ipspace_id)
|
ipspace_name = self._get_valid_ipspace_name(ipspace_id)
|
||||||
self._client.create_ipspace(ipspace_name)
|
desired_client.create_ipspace(ipspace_name)
|
||||||
|
|
||||||
return ipspace_name
|
return ipspace_name
|
||||||
|
|
||||||
|
@ -903,6 +907,213 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
manage_existing(share, driver_options,
|
manage_existing(share, driver_options,
|
||||||
share_server=share_server))
|
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
|
@na_utils.trace
|
||||||
def share_server_migration_check_compatibility(
|
def share_server_migration_check_compatibility(
|
||||||
self, context, source_share_server, dest_host, old_share_network,
|
self, context, source_share_server, dest_host, old_share_network,
|
||||||
|
@ -958,16 +1169,17 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
return not_compatible
|
return not_compatible
|
||||||
|
|
||||||
# Check for SVM DR support
|
pools = self._get_pools()
|
||||||
|
|
||||||
# NOTE(dviroel): These clients can only be used for non-tunneling
|
# NOTE(dviroel): These clients can only be used for non-tunneling
|
||||||
# requests.
|
# requests.
|
||||||
dst_client = data_motion.get_client_for_backend(dest_backend_name,
|
dst_client = data_motion.get_client_for_backend(dest_backend_name,
|
||||||
vserver_name=None)
|
vserver_name=None)
|
||||||
if (not src_client.is_svm_dr_supported()
|
migration_method, compatibility = self._check_for_migration_support(
|
||||||
or not dst_client.is_svm_dr_supported()):
|
src_client, dst_client, source_share_server, shares_request_spec,
|
||||||
msg = _("Cannot perform server migration because at leat one of "
|
src_cluster_name, pools)
|
||||||
"the backends doesn't support SVM DR.")
|
|
||||||
LOG.error(msg)
|
if not compatibility:
|
||||||
return not_compatible
|
return not_compatible
|
||||||
|
|
||||||
# Blocking different security services for now
|
# Blocking different security services for now
|
||||||
|
@ -985,7 +1197,6 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
return not_compatible
|
return not_compatible
|
||||||
|
|
||||||
pools = self._get_pools()
|
|
||||||
# Check 'netapp_flexvol_encryption' and 'revert_to_snapshot_support'
|
# Check 'netapp_flexvol_encryption' and 'revert_to_snapshot_support'
|
||||||
specs_to_validate = ('netapp_flexvol_encryption',
|
specs_to_validate = ('netapp_flexvol_encryption',
|
||||||
'revert_to_snapshot_support')
|
'revert_to_snapshot_support')
|
||||||
|
@ -1000,25 +1211,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
return not_compatible
|
return not_compatible
|
||||||
# TODO(dviroel): disk_type extra-spec
|
# TODO(dviroel): disk_type extra-spec
|
||||||
|
|
||||||
# Check capacity
|
nondisruptive = (migration_method == SERVER_MIGRATE_SVM_MIGRATE)
|
||||||
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
|
|
||||||
|
|
||||||
compatibility = {
|
compatibility = {
|
||||||
'compatible': True,
|
'compatible': True,
|
||||||
'writable': True,
|
'writable': True,
|
||||||
'nondisruptive': False,
|
'nondisruptive': nondisruptive,
|
||||||
'preserve_snapshots': True,
|
'preserve_snapshots': True,
|
||||||
'share_network_id': new_share_network['id'],
|
'share_network_id': new_share_network['id'],
|
||||||
'migration_cancel': True,
|
'migration_cancel': True,
|
||||||
|
@ -1027,9 +1225,9 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
|
|
||||||
return compatibility
|
return compatibility
|
||||||
|
|
||||||
def share_server_migration_start(self, context, source_share_server,
|
@na_utils.trace
|
||||||
dest_share_server, share_intances,
|
def _migration_start_using_svm_dr(
|
||||||
snapshot_instances):
|
self, source_share_server, dest_share_server):
|
||||||
"""Start share server migration using SVM DR.
|
"""Start share server migration using SVM DR.
|
||||||
|
|
||||||
1. Create vserver peering between source and destination
|
1. Create vserver peering between source and destination
|
||||||
|
@ -1078,14 +1276,126 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
msg = _('Could not initialize SnapMirror between %(src)s and '
|
msg = _('Could not initialize SnapMirror between %(src)s and '
|
||||||
'%(dest)s vservers.') % msg_args
|
'%(dest)s vservers.') % msg_args
|
||||||
raise exception.NetAppException(message=msg)
|
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 = {
|
msg_args = {
|
||||||
'src': source_share_server['id'],
|
'src': source_share_server['id'],
|
||||||
'dest': dest_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)
|
LOG.info(msg, msg_args)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def _get_snapmirror_svm(self, source_share_server, dest_share_server):
|
def _get_snapmirror_svm(self, source_share_server, dest_share_server):
|
||||||
dm_session = data_motion.DataMotionSession()
|
dm_session = data_motion.DataMotionSession()
|
||||||
try:
|
try:
|
||||||
|
@ -1104,9 +1414,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
return snapmirrors
|
return snapmirrors
|
||||||
|
|
||||||
@na_utils.trace
|
@na_utils.trace
|
||||||
def share_server_migration_continue(self, context, source_share_server,
|
def _share_server_migration_continue_svm_dr(
|
||||||
dest_share_server, share_instances,
|
self, source_share_server, dest_share_server):
|
||||||
snapshot_instances):
|
|
||||||
"""Continues a share server migration using SVM DR."""
|
"""Continues a share server migration using SVM DR."""
|
||||||
snapmirrors = self._get_snapmirror_svm(source_share_server,
|
snapmirrors = self._get_snapmirror_svm(source_share_server,
|
||||||
dest_share_server)
|
dest_share_server)
|
||||||
|
@ -1141,10 +1450,69 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@na_utils.trace
|
@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,
|
dest_share_server, share_instances,
|
||||||
snapshot_instances, new_network_alloc):
|
snapshot_instances):
|
||||||
"""Completes share server migration using SVM DR.
|
"""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.
|
1. Do a last SnapMirror update.
|
||||||
2. Quiesce, abort and then break the relationship.
|
2. Quiesce, abort and then break the relationship.
|
||||||
|
@ -1152,9 +1520,12 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
4. Configure network interfaces in the destination vserver
|
4. Configure network interfaces in the destination vserver
|
||||||
5. Start the destinarion vserver
|
5. Start the destinarion vserver
|
||||||
6. Delete and release the snapmirror
|
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()
|
dm_session = data_motion.DataMotionSession()
|
||||||
try:
|
try:
|
||||||
# 1. Start an update to try to get a last minute transfer before we
|
# 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
|
# Ignore any errors since the current source may be unreachable
|
||||||
pass
|
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:
|
try:
|
||||||
# 2. Attempt to quiesce, abort and then break SnapMirror
|
# 2. Attempt to quiesce, abort and then break SnapMirror
|
||||||
dm_session.quiesce_and_break_snapmirror_svm(source_share_server,
|
dm_session.quiesce_and_break_snapmirror_svm(source_share_server,
|
||||||
|
@ -1191,20 +1553,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
src_client.stop_vserver(src_vserver)
|
src_client.stop_vserver(src_vserver)
|
||||||
|
|
||||||
# 4. Setup network configuration
|
# 4. Setup network configuration
|
||||||
ipspace_name = dest_client.get_vserver_ipspace(dest_vserver)
|
self._setup_networking_for_destination_vserver(
|
||||||
|
dest_client, dest_vserver, new_net_allocations)
|
||||||
# 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()
|
|
||||||
|
|
||||||
# 5. Start the destination.
|
# 5. Start the destination.
|
||||||
dest_client.start_vserver(dest_vserver)
|
dest_client.start_vserver(dest_vserver)
|
||||||
|
@ -1237,7 +1587,100 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
dm_session.delete_snapmirror_svm(source_share_server,
|
dm_session.delete_snapmirror_svm(source_share_server,
|
||||||
dest_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
|
# NOTE(dviroel): For SVM DR, the share names aren't modified, only the
|
||||||
# export_locations are updated due to network changes.
|
# export_locations are updated due to network changes.
|
||||||
share_updates = {}
|
share_updates = {}
|
||||||
|
@ -1248,7 +1691,9 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
share_name = self._get_backend_share_name(instance['id'])
|
share_name = self._get_backend_share_name(instance['id'])
|
||||||
volume = dest_client.get_volume(share_name)
|
volume = dest_client.get_volume(share_name)
|
||||||
dest_aggregate = volume.get('aggregate')
|
dest_aggregate = volume.get('aggregate')
|
||||||
# Update share attributes according with share extra specs
|
|
||||||
|
if not migration_id:
|
||||||
|
# Update share attributes according with share extra specs.
|
||||||
self._update_share_attributes_after_server_migration(
|
self._update_share_attributes_after_server_migration(
|
||||||
instance, src_client, dest_aggregate, dest_client)
|
instance, src_client, dest_aggregate, dest_client)
|
||||||
|
|
||||||
|
@ -1262,21 +1707,37 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
'in the destination vserver.') % msg_args
|
'in the destination vserver.') % msg_args
|
||||||
raise exception.NetAppException(message=msg)
|
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(
|
export_locations = self._create_export(
|
||||||
instance, dest_share_server, dest_vserver, dest_client,
|
instance, dest_share_server, dest_vserver, dest_client,
|
||||||
clear_current_export_policy=False,
|
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({
|
share_updates.update({instance['id']: new_share_data})
|
||||||
instance['id']: {
|
|
||||||
'export_locations': export_locations,
|
|
||||||
'pool_name': volume.get('aggregate')
|
|
||||||
}})
|
|
||||||
|
|
||||||
# NOTE(dviroel): Nothing to update in snapshot instances since the
|
# NOTE(dviroel): Nothing to update in snapshot instances since the
|
||||||
# provider location didn't change.
|
# provider location didn't change.
|
||||||
|
|
||||||
# 8. Release source share resources
|
# 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:
|
for instance in share_instances:
|
||||||
self._delete_share(instance, src_vserver, src_client,
|
self._delete_share(instance, src_vserver, src_client,
|
||||||
remove_export=True)
|
remove_export=True)
|
||||||
|
@ -1286,12 +1747,18 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
LOG.info('Share server migration completed.')
|
LOG.info('Share server migration completed.')
|
||||||
return {
|
return {
|
||||||
'share_updates': share_updates,
|
'share_updates': share_updates,
|
||||||
|
'server_backend_details': server_backend_details
|
||||||
}
|
}
|
||||||
|
|
||||||
def share_server_migration_cancel(self, context, source_share_server,
|
@na_utils.trace
|
||||||
dest_share_server, shares, snapshots):
|
def _get_share_server_migration_id(self, dest_share_server):
|
||||||
"""Cancel a share server migration that is using SVM DR."""
|
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()
|
dm_session = data_motion.DataMotionSession()
|
||||||
dest_backend_name = share_utils.extract_host(dest_share_server['host'],
|
dest_backend_name = share_utils.extract_host(dest_share_server['host'],
|
||||||
level='backend_name')
|
level='backend_name')
|
||||||
|
@ -1318,6 +1785,73 @@ class NetAppCmodeMultiSVMFileStorageLibrary(
|
||||||
'and %(dest)s vservers.') % msg_args
|
'and %(dest)s vservers.') % msg_args
|
||||||
raise exception.NetAppException(message=msg)
|
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.')
|
LOG.info('Share server migration was cancelled.')
|
||||||
|
|
||||||
def share_server_migration_get_progress(self, context, src_share_server,
|
def share_server_migration_get_progress(self, context, src_share_server,
|
||||||
|
|
|
@ -36,6 +36,12 @@ VALID_TRACE_FLAGS = ['method', 'api']
|
||||||
TRACE_METHOD = False
|
TRACE_METHOD = False
|
||||||
TRACE_API = False
|
TRACE_API = False
|
||||||
API_TRACE_PATTERN = '(.*)'
|
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):
|
def validate_driver_instantiation(**kwargs):
|
||||||
|
|
|
@ -87,6 +87,7 @@ VSERVER_INFO = {
|
||||||
'state': VSERVER_STATE,
|
'state': VSERVER_STATE,
|
||||||
}
|
}
|
||||||
SNAPMIRROR_POLICY_NAME = 'fake_snapmirror_policy'
|
SNAPMIRROR_POLICY_NAME = 'fake_snapmirror_policy'
|
||||||
|
SNAPMIRROR_POLICY_TYPE = 'async_mirror'
|
||||||
|
|
||||||
USER_NAME = 'fake_user'
|
USER_NAME = 'fake_user'
|
||||||
|
|
||||||
|
@ -2742,12 +2743,14 @@ SNAPMIRROR_POLICY_GET_ITER_RESPONSE = etree.XML("""
|
||||||
<attributes-list>
|
<attributes-list>
|
||||||
<snapmirror-policy-info>
|
<snapmirror-policy-info>
|
||||||
<policy-name>%(policy_name)s</policy-name>
|
<policy-name>%(policy_name)s</policy-name>
|
||||||
|
<type>%(policy_type)s</type>
|
||||||
<vserver-name>%(vserver_name)s</vserver-name>
|
<vserver-name>%(vserver_name)s</vserver-name>
|
||||||
</snapmirror-policy-info>
|
</snapmirror-policy-info>
|
||||||
</attributes-list>
|
</attributes-list>
|
||||||
<num-records>1</num-records>
|
<num-records>1</num-records>
|
||||||
</results>""" % {
|
</results>""" % {
|
||||||
'policy_name': SNAPMIRROR_POLICY_NAME,
|
'policy_name': SNAPMIRROR_POLICY_NAME,
|
||||||
|
'policy_type': SNAPMIRROR_POLICY_TYPE,
|
||||||
'vserver_name': VSERVER_NAME,
|
'vserver_name': VSERVER_NAME,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2899,6 +2902,7 @@ FAKE_NA_ELEMENT = api.NaElement(etree.XML(FAKE_VOL_XML))
|
||||||
FAKE_INVOKE_DATA = 'somecontent'
|
FAKE_INVOKE_DATA = 'somecontent'
|
||||||
|
|
||||||
FAKE_XML_STR = 'abc'
|
FAKE_XML_STR = 'abc'
|
||||||
|
FAKE_REST_CALL_STR = 'def'
|
||||||
|
|
||||||
FAKE_API_NAME = 'volume-get-iter'
|
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 \
|
FAKE_KEY_MANAGER_ERROR = "The onboard key manager is not enabled. To enable \
|
||||||
it, run \"security key-manager setup\"."
|
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"
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
Tests for NetApp API layer
|
Tests for NetApp API layer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
|
@ -26,6 +27,7 @@ import requests
|
||||||
|
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.share.drivers.netapp.dataontap.client import api
|
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 import test
|
||||||
from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake
|
from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake
|
||||||
|
|
||||||
|
@ -174,11 +176,11 @@ class NetAppApiElementTransTests(test.TestCase):
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class NetAppApiServerTests(test.TestCase):
|
class NetAppApiServerZapiClientTests(test.TestCase):
|
||||||
"""Test case for NetApp API server methods"""
|
"""Test case for NetApp API server methods"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.root = api.NaServer('127.0.0.1')
|
self.root = api.NaServer('127.0.0.1').zapi_client
|
||||||
super(NetAppApiServerTests, self).setUp()
|
super(NetAppApiServerZapiClientTests, self).setUp()
|
||||||
|
|
||||||
@ddt.data(None, fake.FAKE_XML_STR)
|
@ddt.data(None, fake.FAKE_XML_STR)
|
||||||
def test_invoke_elem_value_error(self, na_element):
|
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
|
expected_log_count = 2 if log else 0
|
||||||
self.assertEqual(expected_log_count, api.LOG.debug.call_count)
|
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)
|
||||||
|
|
|
@ -42,6 +42,8 @@ class NetAppBaseClientTestCase(test.TestCase):
|
||||||
self.client = client_base.NetAppBaseClient(**fake.CONNECTION_INFO)
|
self.client = client_base.NetAppBaseClient(**fake.CONNECTION_INFO)
|
||||||
self.client.connection = mock.MagicMock()
|
self.client.connection = mock.MagicMock()
|
||||||
self.connection = self.client.connection
|
self.connection = self.client.connection
|
||||||
|
self.connection.zapi_client = mock.Mock()
|
||||||
|
self.connection.rest_client = mock.Mock()
|
||||||
|
|
||||||
def test_get_ontapi_version(self):
|
def test_get_ontapi_version(self):
|
||||||
version_response = netapp_api.NaElement(fake.ONTAPI_VERSION_RESPONSE)
|
version_response = netapp_api.NaElement(fake.ONTAPI_VERSION_RESPONSE)
|
||||||
|
@ -97,16 +99,23 @@ class NetAppBaseClientTestCase(test.TestCase):
|
||||||
|
|
||||||
self.assertEqual('tag_name', result)
|
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')
|
element = netapp_api.NaElement('fake-api')
|
||||||
|
|
||||||
self.client.send_request('fake-api')
|
self.client.send_request('fake-api', use_zapi=use_zapi)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
element.to_string(),
|
element.to_string(),
|
||||||
self.connection.invoke_successfully.call_args[0][0].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):
|
def test_send_request_no_tunneling(self):
|
||||||
|
|
||||||
|
@ -117,20 +126,32 @@ class NetAppBaseClientTestCase(test.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
element.to_string(),
|
element.to_string(),
|
||||||
self.connection.invoke_successfully.call_args[0][0].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')
|
element = netapp_api.NaElement('fake-api')
|
||||||
api_args = {'arg1': 'data1', 'arg2': 'data2'}
|
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(
|
self.assertEqual(
|
||||||
element.to_string(),
|
element.to_string(),
|
||||||
self.connection.invoke_successfully.call_args[0][0].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):
|
def test_get_licenses(self):
|
||||||
|
|
||||||
|
|
|
@ -1769,6 +1769,40 @@ class NetAppClientCmodeTestCase(test.TestCase):
|
||||||
mock.call('net-interface-get-iter', None)])
|
mock.call('net-interface-get-iter', None)])
|
||||||
self.assertListEqual([], result)
|
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):
|
def test_get_ipspaces(self):
|
||||||
|
|
||||||
self.client.features.add_feature('IPSPACES')
|
self.client.features.add_feature('IPSPACES')
|
||||||
|
@ -7627,8 +7661,10 @@ class NetAppClientCmodeTestCase(test.TestCase):
|
||||||
fake.SNAPMIRROR_POLICY_GET_ITER_RESPONSE)
|
fake.SNAPMIRROR_POLICY_GET_ITER_RESPONSE)
|
||||||
self.mock_object(self.client, 'send_iter_request',
|
self.mock_object(self.client, 'send_iter_request',
|
||||||
mock.Mock(return_value=api_response))
|
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 = {
|
expected_api_args = {
|
||||||
'query': {
|
'query': {
|
||||||
|
@ -7645,7 +7681,7 @@ class NetAppClientCmodeTestCase(test.TestCase):
|
||||||
|
|
||||||
self.client.send_iter_request.assert_called_once_with(
|
self.client.send_iter_request.assert_called_once_with(
|
||||||
'snapmirror-policy-get-iter', expected_api_args)
|
'snapmirror-policy-get-iter', expected_api_args)
|
||||||
self.assertEqual([fake.SNAPMIRROR_POLICY_NAME], result)
|
self.assertEqual(result_elem, result)
|
||||||
|
|
||||||
@ddt.data(True, False, None)
|
@ddt.data(True, False, None)
|
||||||
def test_start_vserver(self, force):
|
def test_start_vserver(self, force):
|
||||||
|
@ -8254,3 +8290,231 @@ class NetAppClientCmodeTestCase(test.TestCase):
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
self.client.send_iter_request.assert_called_once_with(
|
self.client.send_iter_request.assert_called_once_with(
|
||||||
'fpolicy-policy-status-get-iter', expected_args)
|
'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)
|
||||||
|
|
|
@ -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.client import client_cmode
|
||||||
from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion
|
from manila.share.drivers.netapp.dataontap.cluster_mode import data_motion
|
||||||
from manila.share.drivers.netapp import options as na_opts
|
from manila.share.drivers.netapp import options as na_opts
|
||||||
|
from manila.share import utils as share_utils
|
||||||
from manila import test
|
from manila import test
|
||||||
from manila.tests.share.drivers.netapp.dataontap import fakes as fake
|
from manila.tests.share.drivers.netapp.dataontap import fakes as fake
|
||||||
from manila.tests.share.drivers.netapp import fakes as na_fakes
|
from manila.tests.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,
|
ssl_cert_path='/etc/ssl/certs', trace=mock.ANY,
|
||||||
vserver='fake_vserver')
|
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):
|
def test_get_config_for_backend(self):
|
||||||
self.mock_object(data_motion, "CONF")
|
self.mock_object(data_motion, "CONF")
|
||||||
CONF.set_override("netapp_vserver", 'fake_vserver',
|
CONF.set_override("netapp_vserver", 'fake_vserver',
|
||||||
|
|
|
@ -1917,12 +1917,14 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
||||||
vserver_client.offline_volume.assert_called_with(fake.SHARE_NAME)
|
vserver_client.offline_volume.assert_called_with(fake.SHARE_NAME)
|
||||||
vserver_client.delete_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()
|
protocol_helper = mock.Mock()
|
||||||
callback = (lambda export_address, export_path='fake_export_path':
|
callback = (lambda export_address, export_path='fake_export_path':
|
||||||
':'.join([export_address, export_path]))
|
':'.join([export_address, export_path]))
|
||||||
protocol_helper.create_share.return_value = callback
|
protocol_helper.create_share.return_value = callback
|
||||||
|
expected_host = share_host if share_host else fake.SHARE['host']
|
||||||
self.mock_object(self.library,
|
self.mock_object(self.library,
|
||||||
'_get_helper',
|
'_get_helper',
|
||||||
mock.Mock(return_value=protocol_helper))
|
mock.Mock(return_value=protocol_helper))
|
||||||
|
@ -1937,11 +1939,12 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
||||||
result = self.library._create_export(fake.SHARE,
|
result = self.library._create_export(fake.SHARE,
|
||||||
fake.SHARE_SERVER,
|
fake.SHARE_SERVER,
|
||||||
fake.VSERVER1,
|
fake.VSERVER1,
|
||||||
vserver_client)
|
vserver_client,
|
||||||
|
share_host=share_host)
|
||||||
|
|
||||||
self.assertEqual(fake.NFS_EXPORTS, result)
|
self.assertEqual(fake.NFS_EXPORTS, result)
|
||||||
mock_get_export_addresses_with_metadata.assert_called_once_with(
|
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(
|
protocol_helper.create_share.assert_called_once_with(
|
||||||
fake.SHARE, fake.SHARE_NAME, clear_current_export_policy=True,
|
fake.SHARE, fake.SHARE_NAME, clear_current_export_policy=True,
|
||||||
ensure_share_already_exists=False, replica=False)
|
ensure_share_already_exists=False, replica=False)
|
||||||
|
@ -1969,7 +1972,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
|
||||||
mock.Mock(return_value=[fake.LIF_ADDRESSES[1]]))
|
mock.Mock(return_value=[fake.LIF_ADDRESSES[1]]))
|
||||||
|
|
||||||
result = self.library._get_export_addresses_with_metadata(
|
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)
|
self.assertEqual(fake.INTERFACE_ADDRESSES_WITH_METADATA, result)
|
||||||
mock_get_aggregate_node.assert_called_once_with(fake.POOL_NAME)
|
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]]))
|
mock.Mock(return_value=[fake.LIF_ADDRESSES[1]]))
|
||||||
|
|
||||||
result = self.library._get_export_addresses_with_metadata(
|
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)
|
expected = copy.deepcopy(fake.INTERFACE_ADDRESSES_WITH_METADATA)
|
||||||
for key, value in expected.items():
|
for key, value in expected.items():
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -520,6 +520,10 @@ SHARE_SERVER = {
|
||||||
'network_allocations': (USER_NETWORK_ALLOCATIONS +
|
'network_allocations': (USER_NETWORK_ALLOCATIONS +
|
||||||
ADMIN_NETWORK_ALLOCATIONS),
|
ADMIN_NETWORK_ALLOCATIONS),
|
||||||
'host': SERVER_HOST,
|
'host': SERVER_HOST,
|
||||||
|
'share_network_subnet': {
|
||||||
|
'neutron_net_id': 'fake_neutron_net_id',
|
||||||
|
'neutron_subnet_id': 'fake_neutron_subnet_id'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SHARE_SERVER_2 = {
|
SHARE_SERVER_2 = {
|
||||||
|
@ -531,6 +535,10 @@ SHARE_SERVER_2 = {
|
||||||
'network_allocations': (USER_NETWORK_ALLOCATIONS +
|
'network_allocations': (USER_NETWORK_ALLOCATIONS +
|
||||||
ADMIN_NETWORK_ALLOCATIONS),
|
ADMIN_NETWORK_ALLOCATIONS),
|
||||||
'host': SERVER_HOST_2,
|
'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 = {
|
VSERVER_INFO = {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
The NetApp ONTAP driver now supports nondisruptive share server migration
|
||||||
|
for clusters with version >= 9.10.
|
Loading…
Reference in New Issue