Manila NetApp cDOT driver refactoring
The Manila cDOT driver is a single file exceeding 1200 lines. It contains multiple things (driver code, protocol helpers, ZAPI invocation code, options) that should be split apart to allow for easier maintenance and readability and add the potential for code sharing when we reintroduce a 7-mode driver, add a single-SVM cDOT driver, etc. We recently refactored NetApp's DOT Cinder drivers into a 4-layer structure (interface, library, client, API) that separates concerns and achieves the goals set forth above. This commit satisfies a plan to do the same thing in Manila. The implementation steps are: 1. Update directory structure to match that of the Cinder NetApp drivers 2. Create driver interface shim 3. Move driver code to library (with base & C-mode classes, to allow for 7-mode code sharing later) 4. Move protocol helpers to separate files (already organized by base & C-mode classes) 5. Split out ZAPI code to client layer (with base & C-mode classes, to allow for 7-mode code sharing later) 6. Implement NetApp driver factory as in Cinder 7. Implement common NetApp options file as in Cinder 8. Implement cDOT API call optimizations 9. Update all unit tests as needed Note that this patch appears to treble the total number of code lines. This is due to the addition of many more unit tests plus a large amount of fake controller API data to feed the API client tests. Implements: blueprint netapp-manila-cdot-driver-refactoring Closes-Bug: #1410317 Partial-Bug: #1396953 Closes-Bug: #1370965 Closes-Bug: #1418690 Closes-Bug: #1418696 Change-Id: I3fc0d09adf84a3708f110a89a7c8c760f4ce3588
This commit is contained in:
parent
c65fbe533a
commit
12eeedb639
|
@ -81,9 +81,10 @@ The following example shows five configured back ends:
|
|||
path_to_public_key=/home/baruser/.ssh/id_rsa.pub
|
||||
|
||||
[backendNetApp]
|
||||
share_driver=manila.share.drivers.netapp.cluster_mode.NetAppClusteredShareDriver
|
||||
share_driver = manila.share.drivers.netapp.common.NetAppDriver
|
||||
driver_handles_share_servers = True
|
||||
share_backend_name=backendNetApp
|
||||
netapp_nas_login=user
|
||||
netapp_nas_password=password
|
||||
netapp_nas_server_hostname=1.1.1.1
|
||||
netapp_login=user
|
||||
netapp_password=password
|
||||
netapp_server_hostname=1.1.1.1
|
||||
netapp_root_volume_aggregate=aggr01
|
||||
|
|
|
@ -77,7 +77,7 @@ Share backends
|
|||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
cluster_mode_driver
|
||||
netapp_cluster_mode_driver
|
||||
emc_vnx_driver
|
||||
generic_driver
|
||||
huawei_nas_driver
|
||||
|
|
|
@ -55,19 +55,10 @@ Known restrictions
|
|||
external security services and storage should be synchronized. The maximum
|
||||
allowed clock skew is 5 minutes.
|
||||
|
||||
The :mod:`manila.share.drivers.netapp.api` Module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The :mod:`manila.share.drivers.netapp.dataontap.cluster_mode.drv_multi_svm.py` Module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: manila.share.drivers.netapp.api
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
The :mod:`manila.share.drivers.netapp.cluster_mode` Module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: manila.share.drivers.netapp.cluster_mode
|
||||
.. automodule:: manila.share.drivers.netapp.dataontap.cluster_mode.drv_multi_svm
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
|
@ -16,7 +16,6 @@ __all__ = [
|
|||
'list_opts'
|
||||
]
|
||||
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
|
@ -57,7 +56,7 @@ import manila.share.drivers.hds.sop
|
|||
import manila.share.drivers.hp.hp_3par_driver
|
||||
import manila.share.drivers.huawei.huawei_nas
|
||||
import manila.share.drivers.ibm.gpfs
|
||||
import manila.share.drivers.netapp.cluster_mode
|
||||
import manila.share.drivers.netapp.options
|
||||
import manila.share.drivers.service_instance
|
||||
import manila.share.drivers.zfssa.zfssashare
|
||||
import manila.share.manager
|
||||
|
@ -65,6 +64,7 @@ import manila.volume
|
|||
import manila.volume.cinder
|
||||
import manila.wsgi
|
||||
|
||||
|
||||
# List of *all* options in [DEFAULT] namespace of manila.
|
||||
# Any new option list or option needs to be registered here.
|
||||
_global_opt_lists = [
|
||||
|
@ -113,7 +113,11 @@ _global_opt_lists = [
|
|||
manila.share.drivers.hp.hp_3par_driver.HP3PAR_OPTS,
|
||||
manila.share.drivers.huawei.huawei_nas.huawei_opts,
|
||||
manila.share.drivers.ibm.gpfs.gpfs_share_opts,
|
||||
manila.share.drivers.netapp.cluster_mode.NETAPP_NAS_OPTS,
|
||||
manila.share.drivers.netapp.options.netapp_proxy_opts,
|
||||
manila.share.drivers.netapp.options.netapp_connection_opts,
|
||||
manila.share.drivers.netapp.options.netapp_transport_opts,
|
||||
manila.share.drivers.netapp.options.netapp_basicauth_opts,
|
||||
manila.share.drivers.netapp.options.netapp_provisioning_opts,
|
||||
manila.share.drivers.service_instance.common_opts,
|
||||
manila.share.drivers.service_instance.no_share_servers_handling_mode_opts,
|
||||
manila.share.drivers.service_instance.share_servers_handling_mode_opts,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,21 +23,26 @@ from oslo_utils import importutils
|
|||
from manila import exception
|
||||
from manila.i18n import _, _LI
|
||||
from manila.share import driver
|
||||
from manila.share.drivers.netapp import cluster_mode
|
||||
from manila.share.drivers.netapp import options
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
MULTI_SVM = 'multi_svm'
|
||||
SINGLE_SVM = 'single_svm'
|
||||
DATAONTAP_CMODE_PATH = 'manila.share.drivers.netapp.dataontap.cluster_mode'
|
||||
|
||||
# Add new drivers here, no other code changes required.
|
||||
NETAPP_UNIFIED_DRIVER_REGISTRY = {
|
||||
'ontap_cluster':
|
||||
{
|
||||
'multi_svm':
|
||||
'manila.share.drivers.netapp.cluster_mode.NetAppClusteredShareDriver',
|
||||
MULTI_SVM: DATAONTAP_CMODE_PATH +
|
||||
'.drv_multi_svm.NetAppCmodeMultiSvmShareDriver',
|
||||
}
|
||||
}
|
||||
NETAPP_UNIFIED_DRIVER_DEFAULT_MODE = {
|
||||
'ontap_cluster': 'multi_svm',
|
||||
'ontap_cluster': MULTI_SVM,
|
||||
}
|
||||
|
||||
|
||||
|
@ -58,36 +63,47 @@ class NetAppDriver(object):
|
|||
reason=_('Required configuration not found'))
|
||||
|
||||
config.append_config_values(driver.share_opts)
|
||||
config.append_config_values(cluster_mode.NETAPP_NAS_OPTS)
|
||||
config.append_config_values(options.netapp_proxy_opts)
|
||||
na_utils.check_flags(NetAppDriver.REQUIRED_FLAGS, config)
|
||||
|
||||
return NetAppDriver.create_driver(config.netapp_storage_family,
|
||||
config.driver_handles_share_servers,
|
||||
*args, **kwargs)
|
||||
app_version = na_utils.OpenStackInfo().info()
|
||||
LOG.info(_LI('OpenStack OS Version Info: %(info)s') % {
|
||||
'info': app_version})
|
||||
kwargs['app_version'] = app_version
|
||||
|
||||
driver_mode = NetAppDriver._get_driver_mode(
|
||||
config.netapp_storage_family, config.driver_handles_share_servers)
|
||||
|
||||
return NetAppDriver._create_driver(config.netapp_storage_family,
|
||||
driver_mode,
|
||||
*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def create_driver(storage_family, driver_handles_share_servers, *args,
|
||||
**kwargs):
|
||||
""""Creates an appropriate driver based on family and mode."""
|
||||
def _get_driver_mode(storage_family, driver_handles_share_servers):
|
||||
|
||||
storage_family = storage_family.lower()
|
||||
|
||||
# determine driver mode
|
||||
if driver_handles_share_servers is None:
|
||||
driver_mode = NETAPP_UNIFIED_DRIVER_DEFAULT_MODE.get(
|
||||
storage_family)
|
||||
storage_family.lower())
|
||||
|
||||
if driver_mode:
|
||||
LOG.debug('Default driver mode %s selected.' % driver_mode)
|
||||
LOG.debug('Default driver mode %s selected.', driver_mode)
|
||||
else:
|
||||
raise exception.InvalidInput(
|
||||
reason=_('Driver mode was not specified and a default '
|
||||
'value could not be determined from the '
|
||||
'specified storage family'))
|
||||
elif driver_handles_share_servers:
|
||||
driver_mode = 'multi_svm'
|
||||
driver_mode = MULTI_SVM
|
||||
else:
|
||||
driver_mode = 'single_svm'
|
||||
driver_mode = SINGLE_SVM
|
||||
|
||||
return driver_mode
|
||||
|
||||
@staticmethod
|
||||
def _create_driver(storage_family, driver_mode, *args, **kwargs):
|
||||
""""Creates an appropriate driver based on family and mode."""
|
||||
|
||||
storage_family = storage_family.lower()
|
||||
|
||||
fmt = {'storage_family': storage_family,
|
||||
'driver_mode': driver_mode}
|
||||
|
@ -106,7 +122,6 @@ class NetAppDriver(object):
|
|||
reason=_('Driver mode %(driver_mode)s is not supported '
|
||||
'for storage family %(storage_family)s') % fmt)
|
||||
|
||||
kwargs = kwargs or {}
|
||||
kwargs['netapp_mode'] = 'proxy'
|
||||
driver = importutils.import_object(driver_loc, *args, **kwargs)
|
||||
LOG.info(_LI('NetApp driver of family %(storage_family)s and mode '
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2014 NetApp, Inc.
|
||||
# All Rights Reserved.
|
||||
# Copyright (c) 2014 Navneet Singh. All rights reserved.
|
||||
# Copyright (c) 2014 Clinton Knight. 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
|
||||
|
@ -13,23 +13,30 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
NetApp api for ONTAP and OnCommand DFM.
|
||||
NetApp API for Data ONTAP and OnCommand DFM.
|
||||
|
||||
Contains classes required to issue api calls to ONTAP and OnCommand DFM.
|
||||
Contains classes required to issue API calls to Data ONTAP and OnCommand DFM.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import urllib2
|
||||
|
||||
from lxml import etree
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
|
||||
NETAPP_NS = 'http://www.netapp.com/filer/admin'
|
||||
EONTAPI_EINVAL = '22'
|
||||
EVOLUMEOFFLINE = '13042'
|
||||
EINTERNALERROR = '13114'
|
||||
EDUPLICATEENTRY = '13130'
|
||||
ESIS_CLONE_NOT_LICENSED = '14956'
|
||||
EOBJECTNOTFOUND = '15661'
|
||||
|
||||
|
||||
class NaServer(object):
|
||||
|
@ -48,22 +55,27 @@ class NaServer(object):
|
|||
def __init__(self, host, server_type=SERVER_TYPE_FILER,
|
||||
transport_type=TRANSPORT_TYPE_HTTP,
|
||||
style=STYLE_LOGIN_PASSWORD, username=None,
|
||||
password=None, trace=False):
|
||||
password=None, port=None, trace=False):
|
||||
self._host = host
|
||||
self.set_server_type(server_type)
|
||||
self.set_transport_type(transport_type)
|
||||
self.set_style(style)
|
||||
if port:
|
||||
self.set_port(port)
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._trace = trace
|
||||
self._refresh_conn = True
|
||||
self._trace = trace
|
||||
|
||||
LOG.debug('Using NetApp controller: %s', self._host)
|
||||
|
||||
def get_transport_type(self):
|
||||
"""Get the transport type protocol."""
|
||||
return self._protocol
|
||||
|
||||
def set_transport_type(self, transport_type):
|
||||
"""Set the transport type protocol for api.
|
||||
"""Set the transport type protocol for API.
|
||||
|
||||
Supports http and https transport types.
|
||||
"""
|
||||
|
@ -119,17 +131,18 @@ class NaServer(object):
|
|||
self._refresh_conn = True
|
||||
|
||||
def set_api_version(self, major, minor):
|
||||
"""Set the api version."""
|
||||
"""Set the API version."""
|
||||
try:
|
||||
self._api_major_version = int(major)
|
||||
self._api_minor_version = int(minor)
|
||||
self._api_version = str(major) + "." + str(minor)
|
||||
self._api_version = six.text_type(major) + "." + \
|
||||
six.text_type(minor)
|
||||
except ValueError:
|
||||
raise ValueError('Major and minor versions must be integers')
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_api_version(self):
|
||||
"""Gets the api version tuple."""
|
||||
"""Gets the API version tuple."""
|
||||
if hasattr(self, '_api_version'):
|
||||
return (self._api_major_version, self._api_minor_version)
|
||||
return None
|
||||
|
@ -140,7 +153,7 @@ class NaServer(object):
|
|||
int(port)
|
||||
except ValueError:
|
||||
raise ValueError('Port must be integer')
|
||||
self._port = str(port)
|
||||
self._port = six.text_type(port)
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_port(self):
|
||||
|
@ -186,10 +199,14 @@ class NaServer(object):
|
|||
self._password = password
|
||||
self._refresh_conn = True
|
||||
|
||||
def set_trace(self, trace=True):
|
||||
"""Enable or disable the API tracing facility."""
|
||||
self._trace = trace
|
||||
|
||||
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):
|
||||
ValueError('NaElement must be supplied to invoke api')
|
||||
ValueError('NaElement must be supplied to invoke API')
|
||||
|
||||
request, request_element = self._create_request(na_element,
|
||||
enable_tunneling)
|
||||
|
@ -219,7 +236,7 @@ class NaServer(object):
|
|||
return response_element
|
||||
|
||||
def invoke_successfully(self, na_element, enable_tunneling=False):
|
||||
"""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.
|
||||
This helps to use same connection instance to enable or disable
|
||||
|
@ -232,9 +249,12 @@ class NaServer(object):
|
|||
code = result.get_attr('errno')\
|
||||
or result.get_child_content('errorno')\
|
||||
or 'ESTATUSFAILED'
|
||||
msg = result.get_attr('reason')\
|
||||
or result.get_child_content('reason')\
|
||||
or 'Execution status is failed due to unknown reason'
|
||||
if code == ESIS_CLONE_NOT_LICENSED:
|
||||
msg = 'Clone operation failed: FlexClone not licensed.'
|
||||
else:
|
||||
msg = result.get_attr('reason')\
|
||||
or result.get_child_content('reason')\
|
||||
or 'Execution status is failed due to unknown reason'
|
||||
raise NaApiError(code, msg)
|
||||
|
||||
def _create_request(self, na_element, enable_tunneling=False):
|
||||
|
@ -312,7 +332,7 @@ class NaServer(object):
|
|||
|
||||
|
||||
class NaElement(object):
|
||||
"""Class wraps basic building block for NetApp api request."""
|
||||
"""Class wraps basic building block for NetApp API request."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Name of the element or etree.Element."""
|
||||
|
@ -385,7 +405,7 @@ class NaElement(object):
|
|||
def add_new_child(self, name, content, convert=False):
|
||||
"""Add child with tag name and context.
|
||||
|
||||
Convert replaces entity refs to chars.
|
||||
Convert replaces entity refs to chars.
|
||||
"""
|
||||
child = NaElement(name)
|
||||
if convert:
|
||||
|
@ -421,9 +441,9 @@ class NaElement(object):
|
|||
def __getitem__(self, key):
|
||||
"""Dict getter method for NaElement.
|
||||
|
||||
Returns NaElement list if present,
|
||||
text value in case no NaElement node
|
||||
children or attribute value if present.
|
||||
Returns NaElement list if present,
|
||||
text value in case no NaElement node
|
||||
children or attribute value if present.
|
||||
"""
|
||||
|
||||
child = self.get_child_by_name(key)
|
||||
|
@ -448,7 +468,7 @@ class NaElement(object):
|
|||
child.add_child_elem(value)
|
||||
self.add_child_elem(child)
|
||||
elif isinstance(value, (str, int, float, long)):
|
||||
self.add_new_child(key, str(value))
|
||||
self.add_new_child(key, six.text_type(value))
|
||||
elif isinstance(value, (list, tuple, dict)):
|
||||
child = NaElement(key)
|
||||
child.translate_struct(value)
|
||||
|
@ -463,8 +483,6 @@ class NaElement(object):
|
|||
def translate_struct(self, data_struct):
|
||||
"""Convert list, tuple, dict to NaElement and appends.
|
||||
|
||||
::
|
||||
|
||||
Example usage:
|
||||
1.
|
||||
<root>
|
||||
|
@ -500,18 +518,98 @@ class NaElement(object):
|
|||
child.translate_struct(data_struct[k])
|
||||
else:
|
||||
if data_struct[k]:
|
||||
child.set_content(str(data_struct[k]))
|
||||
child.set_content(six.text_type(data_struct[k]))
|
||||
self.add_child_elem(child)
|
||||
else:
|
||||
raise ValueError(_('Type cannot be converted into NaElement.'))
|
||||
|
||||
|
||||
class NaApiError(Exception):
|
||||
"""Base exception class for NetApp api errors."""
|
||||
"""Base exception class for NetApp API errors."""
|
||||
|
||||
def __init__(self, code='unknown', message='unknown'):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def __str__(self, *args, **kwargs):
|
||||
return 'NetApp api failed. Reason - %s:%s' % (self.code, self.message)
|
||||
return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message)
|
||||
|
||||
|
||||
def invoke_api(na_server, api_name, api_family='cm', query=None,
|
||||
des_result=None, additional_elems=None,
|
||||
is_iter=False, records=0, tag=None,
|
||||
timeout=0, tunnel=None):
|
||||
"""Invokes any given API call to a NetApp server.
|
||||
|
||||
:param na_server: na_server instance
|
||||
:param api_name: API name string
|
||||
:param api_family: cm or 7m
|
||||
:param query: API query as dict
|
||||
:param des_result: desired result as dict
|
||||
:param additional_elems: dict other than query and des_result
|
||||
:param is_iter: is iterator API
|
||||
:param records: limit for records, 0 for infinite
|
||||
:param timeout: timeout seconds
|
||||
:param tunnel: tunnel entity, vserver or vfiler name
|
||||
"""
|
||||
record_step = 50
|
||||
if not (na_server or isinstance(na_server, NaServer)):
|
||||
msg = _("Requires an NaServer instance.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
server = copy.copy(na_server)
|
||||
if api_family == 'cm':
|
||||
server.set_vserver(tunnel)
|
||||
else:
|
||||
server.set_vfiler(tunnel)
|
||||
if timeout > 0:
|
||||
server.set_timeout(timeout)
|
||||
iter_records = 0
|
||||
cond = True
|
||||
while cond:
|
||||
na_element = create_api_request(
|
||||
api_name, query, des_result, additional_elems,
|
||||
is_iter, record_step, tag)
|
||||
result = server.invoke_successfully(na_element, True)
|
||||
if is_iter:
|
||||
if records > 0:
|
||||
iter_records = iter_records + record_step
|
||||
if iter_records >= records:
|
||||
cond = False
|
||||
tag_el = result.get_child_by_name('next-tag')
|
||||
tag = tag_el.get_content() if tag_el else None
|
||||
if not tag:
|
||||
cond = False
|
||||
else:
|
||||
cond = False
|
||||
yield result
|
||||
|
||||
|
||||
def create_api_request(api_name, query=None, des_result=None,
|
||||
additional_elems=None, is_iter=False,
|
||||
record_step=50, tag=None):
|
||||
"""Creates a NetApp API request.
|
||||
|
||||
:param api_name: API name string
|
||||
:param query: API query as dict
|
||||
:param des_result: desired result as dict
|
||||
:param additional_elems: dict other than query and des_result
|
||||
:param is_iter: is iterator API
|
||||
:param record_step: records at a time for iter API
|
||||
:param tag: next tag for iter API
|
||||
"""
|
||||
api_el = NaElement(api_name)
|
||||
if query:
|
||||
query_el = NaElement('query')
|
||||
query_el.translate_struct(query)
|
||||
api_el.add_child_elem(query_el)
|
||||
if des_result:
|
||||
res_el = NaElement('desired-attributes')
|
||||
res_el.translate_struct(des_result)
|
||||
api_el.add_child_elem(res_el)
|
||||
if additional_elems:
|
||||
api_el.translate_struct(additional_elems)
|
||||
if is_iter:
|
||||
api_el.add_new_child('max-records', six.text_type(record_step))
|
||||
if tag:
|
||||
api_el.add_new_child('tag', tag, True)
|
||||
return api_el
|
|
@ -0,0 +1,76 @@
|
|||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
|
||||
from manila.i18n import _LE
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NetAppBaseClient(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.connection = netapp_api.NaServer(
|
||||
host=kwargs['hostname'],
|
||||
transport_type=kwargs['transport_type'],
|
||||
port=kwargs['port'],
|
||||
username=kwargs['username'],
|
||||
password=kwargs['password'],
|
||||
trace=kwargs.get('trace', False))
|
||||
|
||||
def get_ontapi_version(self, cached=True):
|
||||
"""Gets the supported ontapi version."""
|
||||
|
||||
if cached:
|
||||
return self.connection.get_api_version()
|
||||
|
||||
ontapi_version = netapp_api.NaElement('system-get-ontapi-version')
|
||||
res = self.connection.invoke_successfully(ontapi_version, False)
|
||||
major = res.get_child_content('major-version')
|
||||
minor = res.get_child_content('minor-version')
|
||||
return major, minor
|
||||
|
||||
def check_is_naelement(self, elem):
|
||||
"""Checks if object is instance of NaElement."""
|
||||
if not isinstance(elem, netapp_api.NaElement):
|
||||
raise ValueError('Expects NaElement')
|
||||
|
||||
def send_request(self, api_name, api_args=None, enable_tunneling=True):
|
||||
"""Sends request to Ontapi."""
|
||||
request = netapp_api.NaElement(api_name)
|
||||
if api_args:
|
||||
request.translate_struct(api_args)
|
||||
return self.connection.invoke_successfully(request, enable_tunneling)
|
||||
|
||||
@na_utils.trace
|
||||
def get_licenses(self):
|
||||
try:
|
||||
result = self.send_request('license-v2-list-info')
|
||||
except netapp_api.NaApiError as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE("Could not get licenses list. %s."), e)
|
||||
|
||||
return sorted(
|
||||
[l.get_child_content('package').lower()
|
||||
for l in result.get_child_by_name('licenses').get_children()])
|
||||
|
||||
def send_ems_log_message(self, message_dict):
|
||||
"""Sends a message to the Data ONTAP EMS log."""
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,972 @@
|
|||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2015 Clinton Knight. 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.
|
||||
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _, _LE, _LW
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.client import client_base
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NetAppCmodeClient(client_base.NetAppBaseClient):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(NetAppCmodeClient, self).__init__(**kwargs)
|
||||
self.vserver = kwargs.get('vserver')
|
||||
self.connection.set_vserver(self.vserver)
|
||||
|
||||
# Default values to run first api.
|
||||
self.connection.set_api_version(1, 15)
|
||||
(major, minor) = self.get_ontapi_version(cached=False)
|
||||
self.connection.set_api_version(major, minor)
|
||||
|
||||
# NOTE(vponomaryov): Different versions of Data ONTAP API has
|
||||
# different behavior for API call "nfs-exportfs-append-rules-2", that
|
||||
# can require prefix "/vol" or not for path to apply rules for.
|
||||
# Following attr used by "add_rules" method to handle setting up nfs
|
||||
# exports properly in long term.
|
||||
self.nfs_exports_with_prefix = False
|
||||
|
||||
def _invoke_vserver_api(self, na_element, vserver):
|
||||
server = copy.copy(self.connection)
|
||||
server.set_vserver(vserver)
|
||||
result = server.invoke_successfully(na_element, True)
|
||||
return result
|
||||
|
||||
def _has_records(self, api_result_element):
|
||||
if (not api_result_element.get_child_content('num-records') or
|
||||
api_result_element.get_child_content('num-records') == '0'):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def set_vserver(self, vserver):
|
||||
self.vserver = vserver
|
||||
self.connection.set_vserver(vserver)
|
||||
|
||||
@na_utils.trace
|
||||
def create_vserver(self, vserver_name, root_volume_aggregate_name,
|
||||
root_volume_name, aggregate_names):
|
||||
"""Creates new vserver and assigns aggregates."""
|
||||
create_args = {
|
||||
'vserver-name': vserver_name,
|
||||
'root-volume-security-style': 'unix',
|
||||
'root-volume-aggregate': root_volume_aggregate_name,
|
||||
'root-volume': root_volume_name,
|
||||
'name-server-switch': {
|
||||
'nsswitch': 'file',
|
||||
},
|
||||
}
|
||||
self.send_request('vserver-create', create_args)
|
||||
|
||||
aggr_list = [{'aggr-name': aggr_name} for aggr_name in aggregate_names]
|
||||
modify_args = {
|
||||
'aggr-list': aggr_list,
|
||||
'vserver-name': vserver_name,
|
||||
}
|
||||
self.send_request('vserver-modify', modify_args)
|
||||
|
||||
@na_utils.trace
|
||||
def vserver_exists(self, vserver_name):
|
||||
"""Checks if Vserver exists."""
|
||||
LOG.debug('Checking if Vserver %s exists' % vserver_name)
|
||||
|
||||
api_args = {
|
||||
'query': {
|
||||
'vserver-info': {
|
||||
'vserver-name': vserver_name,
|
||||
},
|
||||
},
|
||||
'desired-attributes': {
|
||||
'vserver-info': {
|
||||
'vserver-name': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('vserver-get-iter', api_args)
|
||||
return self._has_records(result)
|
||||
|
||||
@na_utils.trace
|
||||
def get_vserver_root_volume_name(self, vserver_name):
|
||||
"""Get the root volume name of the vserver."""
|
||||
api_args = {
|
||||
'query': {
|
||||
'vserver-info': {
|
||||
'vserver-name': vserver_name,
|
||||
},
|
||||
},
|
||||
'desired-attributes': {
|
||||
'vserver-info': {
|
||||
'root-volume': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
vserver_info = self.send_request('vserver-get-iter', api_args)
|
||||
|
||||
try:
|
||||
root_volume_name = vserver_info.get_child_by_name(
|
||||
'attributes-list').get_child_by_name('vserver-info')\
|
||||
.get_child_content('root-volume')
|
||||
except AttributeError:
|
||||
msg = _('Could not determine root volume name '
|
||||
'for Vserver %s.') % vserver_name
|
||||
raise exception.NetAppException(msg)
|
||||
return root_volume_name
|
||||
|
||||
@na_utils.trace
|
||||
def list_vservers(self, vserver_type='data'):
|
||||
"""Get the names of vservers present, optionally filtered by type."""
|
||||
query = {
|
||||
'vserver-info': {
|
||||
'vserver-type': vserver_type,
|
||||
}
|
||||
} if vserver_type else None
|
||||
|
||||
api_args = {
|
||||
'desired-attributes': {
|
||||
'vserver-info': {
|
||||
'vserver-name': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
if query:
|
||||
api_args['query'] = query
|
||||
|
||||
result = self.send_request('vserver-get-iter', api_args)
|
||||
vserver_info_list = result.get_child_by_name(
|
||||
'attributes-list') or netapp_api.NaElement('none')
|
||||
return [vserver_info.get_child_content('vserver-name')
|
||||
for vserver_info in vserver_info_list.get_children()]
|
||||
|
||||
@na_utils.trace
|
||||
def get_vserver_volume_count(self, max_records=20):
|
||||
"""Get the number of volumes present on a cluster or vserver.
|
||||
|
||||
Call this on a vserver client to see how many volumes exist
|
||||
on that vserver.
|
||||
"""
|
||||
api_args = {
|
||||
'max-records': max_records,
|
||||
'desired-attributes': {
|
||||
'volume-attributes': {
|
||||
'volume-id-attributes': {
|
||||
'name': None,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
volumes_data = self.send_request('volume-get-iter', api_args)
|
||||
return int(volumes_data.get_child_content('num-records'))
|
||||
|
||||
@na_utils.trace
|
||||
def delete_vserver(self, vserver_name, vserver_client,
|
||||
security_services=None):
|
||||
"""Delete Vserver.
|
||||
|
||||
Checks if Vserver exists and does not have active shares.
|
||||
Offlines and destroys root volumes. Deletes Vserver.
|
||||
"""
|
||||
if not self.vserver_exists(vserver_name):
|
||||
LOG.error(_LE("Vserver %s does not exist."), vserver_name)
|
||||
return
|
||||
|
||||
root_volume_name = self.get_vserver_root_volume_name(vserver_name)
|
||||
volumes_count = vserver_client.get_vserver_volume_count(max_records=2)
|
||||
|
||||
if volumes_count == 1:
|
||||
try:
|
||||
vserver_client.offline_volume(root_volume_name)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EVOLUMEOFFLINE:
|
||||
LOG.error(_LE("Volume %s is already offline."),
|
||||
root_volume_name)
|
||||
else:
|
||||
raise e
|
||||
vserver_client.delete_volume(root_volume_name)
|
||||
|
||||
elif volumes_count > 1:
|
||||
msg = _("Cannot delete Vserver. Vserver %s has shares.")
|
||||
raise exception.NetAppException(msg % vserver_name)
|
||||
|
||||
if security_services:
|
||||
self._terminate_vserver_services(vserver_name, vserver_client,
|
||||
security_services)
|
||||
|
||||
self.send_request('vserver-destroy', {'vserver-name': vserver_name})
|
||||
|
||||
@na_utils.trace
|
||||
def _terminate_vserver_services(self, vserver_name, vserver_client,
|
||||
security_services):
|
||||
for service in security_services:
|
||||
if service['type'] == 'active_directory':
|
||||
api_args = {
|
||||
'admin-password': service['password'],
|
||||
'admin-username': service['user'],
|
||||
}
|
||||
try:
|
||||
vserver_client.send_request('cifs-server-delete', api_args)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EOBJECTNOTFOUND:
|
||||
LOG.error(_LE('CIFS server does not exist for '
|
||||
'Vserver %s'), vserver_name)
|
||||
else:
|
||||
vserver_client.send_request('cifs-server-delete')
|
||||
|
||||
@na_utils.trace
|
||||
def list_cluster_nodes(self):
|
||||
"""Get all available cluster nodes."""
|
||||
api_args = {
|
||||
'desired-attributes': {
|
||||
'node-details-info': {
|
||||
'node': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('system-node-get-iter', api_args)
|
||||
nodes_info_list = result.get_child_by_name(
|
||||
'attributes-list') or netapp_api.NaElement('none')
|
||||
return [node_info.get_child_content('node') for node_info
|
||||
in nodes_info_list.get_children()]
|
||||
|
||||
@na_utils.trace
|
||||
def get_node_data_port(self, node):
|
||||
"""Get data port on the node."""
|
||||
api_args = {
|
||||
'query': {
|
||||
'net-port-info': {
|
||||
'node': node,
|
||||
'port-type': 'physical',
|
||||
'role': 'data',
|
||||
},
|
||||
},
|
||||
}
|
||||
port_info = self.send_request('net-port-get-iter', api_args)
|
||||
try:
|
||||
port = port_info.get_child_by_name('attributes-list')\
|
||||
.get_child_by_name('net-port-info')\
|
||||
.get_child_content('port')
|
||||
except AttributeError:
|
||||
msg = _("Data port does not exist for node %s.")
|
||||
raise exception.NetAppException(msg % node)
|
||||
return port
|
||||
|
||||
@na_utils.trace
|
||||
def list_aggregates(self):
|
||||
"""Get names of all aggregates."""
|
||||
try:
|
||||
api_args = {
|
||||
'desired-attributes': {
|
||||
'aggr-attributes': {
|
||||
'aggregate-name': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('aggr-get-iter', api_args)
|
||||
aggr_list = result.get_child_by_name(
|
||||
'attributes-list').get_children()
|
||||
except AttributeError:
|
||||
msg = _("Could not list aggregates.")
|
||||
raise exception.NetAppException(msg)
|
||||
return [aggr.get_child_content('aggregate-name') for aggr
|
||||
in aggr_list]
|
||||
|
||||
@na_utils.trace
|
||||
def create_network_interface(self, ip, netmask, vlan, node, port,
|
||||
vserver_name, allocation_id,
|
||||
lif_name_template):
|
||||
"""Creates LIF on VLAN port."""
|
||||
|
||||
self._create_vlan(node, port, vlan)
|
||||
|
||||
vlan_interface_name = '%(port)s-%(tag)s' % {'port': port, 'tag': vlan}
|
||||
interface_name = (lif_name_template %
|
||||
{'node': node, 'net_allocation_id': allocation_id})
|
||||
|
||||
LOG.debug('Creating LIF %(lif)s for Vserver %(vserver)s ',
|
||||
{'lif': interface_name, 'vserver': vserver_name})
|
||||
|
||||
api_args = {
|
||||
'address': ip,
|
||||
'administrative-status': 'up',
|
||||
'data-protocols': [
|
||||
{'data-protocol': 'nfs'},
|
||||
{'data-protocol': 'cifs'},
|
||||
],
|
||||
'home-node': node,
|
||||
'home-port': vlan_interface_name,
|
||||
'netmask': netmask,
|
||||
'interface-name': interface_name,
|
||||
'role': 'data',
|
||||
'vserver': vserver_name,
|
||||
}
|
||||
self.send_request('net-interface-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def _create_vlan(self, node, port, vlan):
|
||||
try:
|
||||
api_args = {
|
||||
'vlan-info': {
|
||||
'parent-interface': port,
|
||||
'node': node,
|
||||
'vlanid': vlan,
|
||||
},
|
||||
}
|
||||
self.send_request('net-vlan-create', api_args)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EDUPLICATEENTRY:
|
||||
LOG.debug('VLAN %(vlan)s already exists on port %(port)s',
|
||||
{'vlan': vlan, 'port': port})
|
||||
else:
|
||||
msg = _('Failed to create VLAN %(vlan)s on '
|
||||
'port %(port)s. %(err_msg)s')
|
||||
msg_args = {'vlan': vlan, 'port': port, 'err_msg': e.message}
|
||||
raise exception.NetAppException(msg % msg_args)
|
||||
|
||||
@na_utils.trace
|
||||
def network_interface_exists(self, vserver_name, node, port, ip, netmask,
|
||||
vlan):
|
||||
"""Checks if LIF exists."""
|
||||
vlan_interface_name = '%(port)s-%(tag)s' % {'port': port, 'tag': vlan}
|
||||
|
||||
api_args = {
|
||||
'query': {
|
||||
'net-interface-info': {
|
||||
'address': ip,
|
||||
'home-node': node,
|
||||
'home-port': vlan_interface_name,
|
||||
'netmask': netmask,
|
||||
'vserver': vserver_name,
|
||||
},
|
||||
},
|
||||
'desired-attributes': {
|
||||
'net-interface-info': {
|
||||
'interface-name': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('net-interface-get-iter', api_args)
|
||||
return self._has_records(result)
|
||||
|
||||
@na_utils.trace
|
||||
def list_network_interfaces(self):
|
||||
"""Get the names of available LIFs."""
|
||||
api_args = {
|
||||
'desired-attributes': {
|
||||
'net-interface-info': {
|
||||
'interface-name': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('net-interface-get-iter', api_args)
|
||||
lif_info_list = result.get_child_by_name(
|
||||
'attributes-list') or netapp_api.NaElement('none')
|
||||
return [lif_info.get_child_content('interface-name') for lif_info
|
||||
in lif_info_list.get_children()]
|
||||
|
||||
@na_utils.trace
|
||||
def get_network_interfaces(self):
|
||||
"""Get available LIFs."""
|
||||
result = self.send_request('net-interface-get-iter')
|
||||
lif_info_list = result.get_child_by_name(
|
||||
'attributes-list') or netapp_api.NaElement('none')
|
||||
|
||||
interfaces = []
|
||||
for lif_info in lif_info_list.get_children():
|
||||
lif = {
|
||||
'address': lif_info.get_child_content('address'),
|
||||
'home-node': lif_info.get_child_content('home-node'),
|
||||
'home-port': lif_info.get_child_content('home-port'),
|
||||
'interface-name': lif_info.get_child_content('interface-name'),
|
||||
'netmask': lif_info.get_child_content('netmask'),
|
||||
'role': lif_info.get_child_content('role'),
|
||||
'vserver': lif_info.get_child_content('vserver'),
|
||||
}
|
||||
interfaces.append(lif)
|
||||
|
||||
return interfaces
|
||||
|
||||
@na_utils.trace
|
||||
def delete_network_interface(self, interface_name):
|
||||
"""Deletes LIF."""
|
||||
api_args = {'vserver': None, 'interface-name': interface_name}
|
||||
self.send_request('net-interface-delete', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def calculate_aggregate_capacity(self, aggregate_names):
|
||||
"""Calculates capacity of one or more aggregates
|
||||
|
||||
Returns tuple (total, free) in bytes.
|
||||
"""
|
||||
desired_attributes = {
|
||||
'aggr-attributes': {
|
||||
'aggregate-name': None,
|
||||
'aggr-space-attributes': {
|
||||
'size-total': None,
|
||||
'size-available': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
aggrs = self._get_aggregates(aggregate_names=aggregate_names,
|
||||
desired_attributes=desired_attributes)
|
||||
aggr_space_attrs = [aggr.get_child_by_name('aggr-space-attributes')
|
||||
for aggr in aggrs]
|
||||
total = sum([int(aggr.get_child_content('size-total'))
|
||||
for aggr in aggr_space_attrs]) if aggr_space_attrs else 0
|
||||
free = max([int(aggr.get_child_content('size-available'))
|
||||
for aggr in aggr_space_attrs]) if aggr_space_attrs else 0
|
||||
return total, free
|
||||
|
||||
@na_utils.trace
|
||||
def get_aggregates_for_vserver(self, vserver_name):
|
||||
"""Returns aggregate list and size info for a Vserver.
|
||||
|
||||
Must be called against a Vserver API client.
|
||||
"""
|
||||
LOG.debug('Finding available aggregates for Vserver %s', vserver_name)
|
||||
|
||||
api_args = {
|
||||
'desired-attributes': {
|
||||
'vserver-info': {
|
||||
'vserver-aggr-info-list': {
|
||||
'vserver-aggr-info': {
|
||||
'aggr-name': None,
|
||||
'aggr-availsize': None,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('vserver-get', api_args)
|
||||
vserver_info = result.get_child_by_name(
|
||||
'attributes').get_child_by_name('vserver-info')
|
||||
vserver_aggr_info_element = vserver_info.get_child_by_name(
|
||||
'vserver-aggr-info-list') or netapp_api.NaElement('none')
|
||||
vserver_aggr_info_list = vserver_aggr_info_element.get_children()
|
||||
|
||||
if not vserver_aggr_info_list:
|
||||
msg = _("No aggregates assigned to Vserver %s")
|
||||
raise exception.NetAppException(msg % vserver_name)
|
||||
|
||||
# Return dict of key-value pair of aggr_name:aggr_size_available.
|
||||
aggr_dict = {}
|
||||
|
||||
for aggr_info in vserver_aggr_info_list:
|
||||
aggr_name = aggr_info.get_child_content('aggr-name')
|
||||
aggr_size = int(aggr_info.get_child_content('aggr-availsize'))
|
||||
aggr_dict[aggr_name] = aggr_size
|
||||
|
||||
LOG.debug('Found available aggregates: %s', aggr_dict)
|
||||
return aggr_dict
|
||||
|
||||
def _get_aggregates(self, aggregate_names=None, desired_attributes=None):
|
||||
|
||||
query = {
|
||||
'aggr-attributes': {
|
||||
'aggregate-name': '|'.join(aggregate_names),
|
||||
}
|
||||
} if aggregate_names else None
|
||||
|
||||
api_args = {}
|
||||
if query:
|
||||
api_args['query'] = query
|
||||
if desired_attributes:
|
||||
api_args['desired-attributes'] = desired_attributes
|
||||
|
||||
result = self.send_request('aggr-get-iter', api_args)
|
||||
if not self._has_records(result):
|
||||
return []
|
||||
else:
|
||||
return result.get_child_by_name('attributes-list').get_children()
|
||||
|
||||
@na_utils.trace
|
||||
def setup_security_services(self, security_services, vserver_client,
|
||||
vserver_name):
|
||||
api_args = {
|
||||
'name-mapping-switch': {
|
||||
'nmswitch': 'ldap,file',
|
||||
},
|
||||
'name-server-switch': {
|
||||
'nsswitch': 'ldap,file',
|
||||
},
|
||||
'vserver-name': vserver_name,
|
||||
}
|
||||
self.send_request('vserver-modify', api_args)
|
||||
|
||||
for security_service in security_services:
|
||||
if security_service['type'].lower() == 'ldap':
|
||||
vserver_client.configure_ldap(security_service)
|
||||
|
||||
elif security_service['type'].lower() == 'active_directory':
|
||||
vserver_client.configure_active_directory(security_service,
|
||||
vserver_name)
|
||||
|
||||
elif security_service['type'].lower() == 'kerberos':
|
||||
self.create_kerberos_realm(security_service)
|
||||
vserver_client.configure_kerberos(security_service,
|
||||
vserver_name)
|
||||
|
||||
else:
|
||||
msg = _('Unsupported security service type %s for '
|
||||
'Data ONTAP driver')
|
||||
raise exception.NetAppException(msg % security_service['type'])
|
||||
|
||||
@na_utils.trace
|
||||
def enable_nfs(self):
|
||||
"""Enables NFS on Vserver."""
|
||||
self.send_request('nfs-enable')
|
||||
self.send_request('nfs-service-modify', {'is-nfsv40-enabled': 'true'})
|
||||
|
||||
api_args = {
|
||||
'client-match': '0.0.0.0/0',
|
||||
'policy-name': 'default',
|
||||
'ro-rule': {
|
||||
'security-flavor': 'any',
|
||||
},
|
||||
'rw-rule': {
|
||||
'security-flavor': 'any',
|
||||
},
|
||||
}
|
||||
self.send_request('export-rule-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def configure_ldap(self, security_service):
|
||||
"""Configures LDAP on Vserver."""
|
||||
config_name = hashlib.md5(security_service['id']).hexdigest()
|
||||
api_args = {
|
||||
'ldap-client-config': config_name,
|
||||
'servers': {
|
||||
'ip-address': security_service['server'],
|
||||
},
|
||||
'tcp-port': '389',
|
||||
'schema': 'RFC-2307',
|
||||
'bind-password': security_service['password'],
|
||||
}
|
||||
self.send_request('ldap-client-create', api_args)
|
||||
|
||||
api_args = {'client-config': config_name, 'client-enabled': 'true'}
|
||||
self.send_request('ldap-config-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def configure_active_directory(self, security_service, vserver_name):
|
||||
"""Configures AD on Vserver."""
|
||||
self.configure_dns(security_service)
|
||||
|
||||
# 'cifs-server' is CIFS Server NetBIOS Name, max length is 15.
|
||||
# Should be unique within each domain (data['domain']).
|
||||
cifs_server = (vserver_name[0:7] + '..' + vserver_name[-6:]).upper()
|
||||
api_args = {
|
||||
'admin-username': security_service['user'],
|
||||
'admin-password': security_service['password'],
|
||||
'force-account-overwrite': 'true',
|
||||
'cifs-server': cifs_server,
|
||||
'domain': security_service['domain'],
|
||||
}
|
||||
try:
|
||||
LOG.debug("Trying to setup CIFS server with data: %s", api_args)
|
||||
self.send_request('cifs-server-create', api_args)
|
||||
except netapp_api.NaApiError as e:
|
||||
msg = _("Failed to create CIFS server entry. %s")
|
||||
raise exception.NetAppException(msg % e.message)
|
||||
|
||||
@na_utils.trace
|
||||
def create_kerberos_realm(self, security_service):
|
||||
"""Creates Kerberos realm on cluster."""
|
||||
|
||||
api_args = {
|
||||
'admin-server-ip': security_service['server'],
|
||||
'admin-server-port': '749',
|
||||
'clock-skew': '5',
|
||||
'comment': '',
|
||||
'config-name': security_service['id'],
|
||||
'kdc-ip': security_service['server'],
|
||||
'kdc-port': '88',
|
||||
'kdc-vendor': 'other',
|
||||
'password-server-ip': security_service['server'],
|
||||
'password-server-port': '464',
|
||||
'realm': security_service['domain'].upper(),
|
||||
}
|
||||
try:
|
||||
self.send_request('kerberos-realm-create', api_args)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EDUPLICATEENTRY:
|
||||
LOG.debug('Kerberos realm config already exists.')
|
||||
else:
|
||||
msg = _('Failed to create Kerberos realm. %s')
|
||||
raise exception.NetAppException(msg % e.message)
|
||||
|
||||
@na_utils.trace
|
||||
def configure_kerberos(self, security_service, vserver_name):
|
||||
"""Configures Kerberos for NFS on Vserver."""
|
||||
|
||||
self.configure_dns(security_service)
|
||||
spn = self._get_kerberos_service_principal_name(
|
||||
security_service, vserver_name)
|
||||
|
||||
lifs = self.list_network_interfaces()
|
||||
if not lifs:
|
||||
msg = _("Cannot set up Kerberos. There are no LIFs configured.")
|
||||
raise exception.NetAppException(msg)
|
||||
|
||||
for lif_name in lifs:
|
||||
api_args = {
|
||||
'admin-password': security_service['password'],
|
||||
'admin-user-name': security_service['user'],
|
||||
'interface-name': lif_name,
|
||||
'is-kerberos-enabled': 'true',
|
||||
'service-principal-name': spn,
|
||||
}
|
||||
self.send_request('kerberos-config-modify', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def _get_kerberos_service_principal_name(self, security_service,
|
||||
vserver_name):
|
||||
return 'nfs/' + vserver_name.replace('_', '-') + '.' + \
|
||||
security_service['domain'] + '@' + \
|
||||
security_service['domain'].upper()
|
||||
|
||||
@na_utils.trace
|
||||
def configure_dns(self, security_service):
|
||||
api_args = {
|
||||
'domains': {
|
||||
'string': security_service['domain'],
|
||||
},
|
||||
'name-servers': {
|
||||
'ip-address': security_service['dns_ip'],
|
||||
},
|
||||
'dns-state': 'enabled',
|
||||
}
|
||||
try:
|
||||
self.send_request('net-dns-create', api_args)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EDUPLICATEENTRY:
|
||||
LOG.error(_LE("DNS exists for Vserver."))
|
||||
else:
|
||||
msg = _("Failed to configure DNS. %s")
|
||||
raise exception.NetAppException(msg % e.message)
|
||||
|
||||
@na_utils.trace
|
||||
def create_volume(self, aggregate_name, volume_name, size_gb):
|
||||
"""Creates a volume."""
|
||||
api_args = {
|
||||
'containing-aggr-name': aggregate_name,
|
||||
'size': six.text_type(size_gb) + 'g',
|
||||
'volume': volume_name,
|
||||
'junction-path': '/%s' % volume_name,
|
||||
}
|
||||
self.send_request('volume-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def volume_exists(self, volume_name):
|
||||
"""Checks if volume exists."""
|
||||
LOG.debug('Checking if volume %s exists', volume_name)
|
||||
|
||||
api_args = {
|
||||
'query': {
|
||||
'volume-attributes': {
|
||||
'volume-id-attributes': {
|
||||
'name': volume_name,
|
||||
},
|
||||
},
|
||||
},
|
||||
'desired-attributes': {
|
||||
'volume-attributes': {
|
||||
'volume-id-attributes': {
|
||||
'name': None,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('volume-get-iter', api_args)
|
||||
return self._has_records(result)
|
||||
|
||||
@na_utils.trace
|
||||
def create_volume_clone(self, volume_name, parent_volume_name,
|
||||
parent_snapshot_name=None):
|
||||
"""Clones a volume."""
|
||||
api_args = {
|
||||
'volume': volume_name,
|
||||
'parent-volume': parent_volume_name,
|
||||
'parent-snapshot': parent_snapshot_name,
|
||||
'junction-path': '/%s' % volume_name,
|
||||
}
|
||||
self.send_request('volume-clone-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def get_volume_junction_path(self, volume_name, is_style_cifs=False):
|
||||
"""Gets a volume junction path."""
|
||||
api_args = {
|
||||
'volume': volume_name,
|
||||
'is-style-cifs': six.text_type(is_style_cifs).lower(),
|
||||
}
|
||||
result = self.send_request('volume-get-volume-path', api_args)
|
||||
return result.get_child_content('junction')
|
||||
|
||||
@na_utils.trace
|
||||
def offline_volume(self, volume_name):
|
||||
"""Offlines a volume."""
|
||||
self.send_request('volume-offline', {'name': volume_name})
|
||||
|
||||
@na_utils.trace
|
||||
def unmount_volume(self, volume_name, force=False):
|
||||
"""Unmounts a volume."""
|
||||
api_args = {
|
||||
'volume-name': volume_name,
|
||||
'force': six.text_type(force).lower(),
|
||||
}
|
||||
self.send_request('volume-unmount', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def delete_volume(self, volume_name):
|
||||
"""Deletes a volume."""
|
||||
self.send_request('volume-destroy', {'name': volume_name})
|
||||
|
||||
@na_utils.trace
|
||||
def create_snapshot(self, volume_name, snapshot_name):
|
||||
"""Creates a volume snapshot."""
|
||||
api_args = {'volume': volume_name, 'snapshot': snapshot_name}
|
||||
self.send_request('snapshot-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def is_snapshot_busy(self, volume_name, snapshot_name):
|
||||
"""Checks if volume snapshot is busy."""
|
||||
api_args = {
|
||||
'query': {
|
||||
'snapshot-info': {
|
||||
'name': snapshot_name,
|
||||
'volume': volume_name,
|
||||
},
|
||||
},
|
||||
'desired-attributes': {
|
||||
'snapshot-info': {
|
||||
'busy': None,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = self.send_request('snapshot-get-iter', api_args)
|
||||
|
||||
attributes_list = result.get_child_by_name(
|
||||
'attributes-list') or netapp_api.NaElement('none')
|
||||
snapshot_info_list = attributes_list.get_children()
|
||||
|
||||
if not self._has_records(result) or len(snapshot_info_list) != 1:
|
||||
msg = _('Could not find unique snapshot %(snap)s on '
|
||||
'volume %(vol)s.')
|
||||
msg_args = {'snap': snapshot_name, 'vol': volume_name}
|
||||
raise exception.NetAppException(msg % msg_args)
|
||||
|
||||
snapshot_info = snapshot_info_list[0]
|
||||
busy = snapshot_info.get_child_content('busy').lower()
|
||||
return busy == 'true'
|
||||
|
||||
@na_utils.trace
|
||||
def delete_snapshot(self, volume_name, snapshot_name):
|
||||
"""Deletes a volume snapshot."""
|
||||
api_args = {'volume': volume_name, 'snapshot': snapshot_name}
|
||||
self.send_request('snapshot-delete', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def create_cifs_share(self, share_name):
|
||||
share_path = '/%s' % share_name
|
||||
api_args = {'path': share_path, 'share-name': share_name}
|
||||
self.send_request('cifs-share-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def add_cifs_share_access(self, share_name, user_name):
|
||||
api_args = {
|
||||
'permission': 'full_control',
|
||||
'share': share_name,
|
||||
'user-or-group': user_name,
|
||||
}
|
||||
self.send_request('cifs-share-access-control-create', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def remove_cifs_share_access(self, share_name, user_name):
|
||||
api_args = {'user-or-group': user_name, 'share': share_name}
|
||||
self.send_request('cifs-share-access-control-delete', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def remove_cifs_share(self, share_name):
|
||||
self.send_request('cifs-share-delete', {'share-name': share_name})
|
||||
|
||||
@na_utils.trace
|
||||
def add_nfs_export_rules(self, export_path, rules):
|
||||
|
||||
# This method builds up a complicated structure needed by the
|
||||
# nfs-exportfs-append-rules-2 ZAPI. Here is how the end result
|
||||
# should appear:
|
||||
#
|
||||
# {
|
||||
# 'rules': {
|
||||
# 'exports-rule-info-2': {
|
||||
# 'pathname': <share pathname>,
|
||||
# 'security-rules': {
|
||||
# 'security-rule-info': {
|
||||
# 'read-write': [
|
||||
# {
|
||||
# 'exports-hostname-info': {
|
||||
# 'name': <ip address 1>,
|
||||
# }
|
||||
# },
|
||||
# {
|
||||
# 'exports-hostname-info': {
|
||||
# 'name': <ip address 2>,
|
||||
# }
|
||||
# }
|
||||
# ],
|
||||
# 'root': [
|
||||
# {
|
||||
# 'exports-hostname-info': {
|
||||
# 'name': <ip address 1>,
|
||||
# }
|
||||
# },
|
||||
# {
|
||||
# 'exports-hostname-info': {
|
||||
# 'name': <ip address 2>,
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# Default API request, some of which is overwritten below.
|
||||
request = {
|
||||
'rules': {
|
||||
'exports-rule-info-2': {
|
||||
'pathname': export_path,
|
||||
'security-rules': {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
allowed_hosts_xml = []
|
||||
for ip in rules:
|
||||
allowed_hosts_xml.append({'exports-hostname-info': {'name': ip}})
|
||||
|
||||
# Build security rules to be grafted into request.
|
||||
security_rule = {
|
||||
'security-rule-info': {
|
||||
'read-write': allowed_hosts_xml,
|
||||
'root': allowed_hosts_xml,
|
||||
},
|
||||
}
|
||||
|
||||
# Insert security rules section into request.
|
||||
request['rules']['exports-rule-info-2']['security-rules'] = (
|
||||
security_rule)
|
||||
|
||||
# Make a second copy of the request with /vol prefix on export path.
|
||||
request_with_prefix = copy.deepcopy(request)
|
||||
request_with_prefix['rules']['exports-rule-info-2']['pathname'] = (
|
||||
'/vol' + export_path)
|
||||
|
||||
LOG.debug('Appending NFS rules %r', rules)
|
||||
try:
|
||||
if self.nfs_exports_with_prefix:
|
||||
self.send_request('nfs-exportfs-append-rules-2',
|
||||
request_with_prefix)
|
||||
else:
|
||||
self.send_request('nfs-exportfs-append-rules-2', request)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EINTERNALERROR:
|
||||
# We expect getting here only in one case - when received first
|
||||
# call of this method per backend, that is not covered by
|
||||
# default value. Change its value, to send proper requests from
|
||||
# first time.
|
||||
self.nfs_exports_with_prefix = not self.nfs_exports_with_prefix
|
||||
LOG.warning(_LW("Data ONTAP API 'nfs-exportfs-append-rules-2' "
|
||||
"compatibility action: remember behavior to "
|
||||
"send proper values with first attempt next "
|
||||
"time. Now trying send another request with "
|
||||
"changed value for 'pathname'."))
|
||||
|
||||
if self.nfs_exports_with_prefix:
|
||||
self.send_request('nfs-exportfs-append-rules-2',
|
||||
request_with_prefix)
|
||||
else:
|
||||
self.send_request('nfs-exportfs-append-rules-2', request)
|
||||
else:
|
||||
raise
|
||||
|
||||
@na_utils.trace
|
||||
def get_nfs_export_rules(self, export_path):
|
||||
"""Returns available access rules for a given NFS share."""
|
||||
api_args = {'pathname': export_path}
|
||||
response = self.send_request('nfs-exportfs-list-rules-2', api_args)
|
||||
|
||||
rules = response.get_child_by_name('rules')
|
||||
allowed_hosts = []
|
||||
if rules and rules.get_child_by_name('exports-rule-info-2'):
|
||||
security_rule = rules.get_child_by_name(
|
||||
'exports-rule-info-2').get_child_by_name('security-rules')
|
||||
security_info = security_rule.get_child_by_name(
|
||||
'security-rule-info')
|
||||
if security_info:
|
||||
root_rules = security_info.get_child_by_name('root')
|
||||
if root_rules:
|
||||
allowed_hosts = root_rules.get_children()
|
||||
|
||||
existing_rules = []
|
||||
|
||||
for allowed_host in allowed_hosts:
|
||||
if 'exports-hostname-info' in allowed_host.get_name():
|
||||
existing_rules.append(allowed_host.get_child_content('name'))
|
||||
|
||||
return existing_rules
|
||||
|
||||
@na_utils.trace
|
||||
def remove_nfs_export_rules(self, export_path):
|
||||
api_args = {
|
||||
'pathnames': {
|
||||
'pathname-info': {
|
||||
'name': export_path,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.send_request('nfs-exportfs-delete-rules', api_args)
|
||||
|
||||
@na_utils.trace
|
||||
def _get_ems_log_destination_vserver(self):
|
||||
major, minor = self.get_ontapi_version(cached=True)
|
||||
if (major > 1) or (major == 1 and minor > 15):
|
||||
return self.list_vservers(vserver_type='admin')[0]
|
||||
else:
|
||||
return self.list_vservers(vserver_type='node')[0]
|
||||
|
||||
@na_utils.trace
|
||||
def send_ems_log_message(self, message_dict):
|
||||
"""Sends a message to the Data ONTAP EMS log."""
|
||||
|
||||
node_client = copy.deepcopy(self)
|
||||
node_client.connection.set_timeout(25)
|
||||
|
||||
try:
|
||||
node_client.set_vserver(self._get_ems_log_destination_vserver())
|
||||
node_client.send_request('ems-autosupport-log', message_dict)
|
||||
LOG.debug('EMS executed successfully.')
|
||||
except netapp_api.NaApiError as e:
|
||||
LOG.warning(_LW('Failed to invoke EMS. %s') % e)
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright (c) 2015 Clinton Knight. 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.
|
||||
"""
|
||||
NetApp Data ONTAP cDOT storage driver. Supports NFS & CIFS protocols.
|
||||
|
||||
This driver requires a Data ONTAP (Cluster-mode) storage system with
|
||||
installed CIFS and/or NFS licenses, as well as a FlexClone license.
|
||||
"""
|
||||
|
||||
from manila.share import driver
|
||||
from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base
|
||||
|
||||
|
||||
class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver):
|
||||
"""NetApp Cluster-mode multi-SVM share driver."""
|
||||
|
||||
DRIVER_NAME = 'NetApp_Cluster_MultiSVM'
|
||||
|
||||
def __init__(self, db, *args, **kwargs):
|
||||
super(NetAppCmodeMultiSvmShareDriver, self).__init__(True, *args,
|
||||
**kwargs)
|
||||
|
||||
self.library = lib_base.NetAppCmodeFileStorageLibrary(
|
||||
db, self.DRIVER_NAME, **kwargs)
|
||||
|
||||
def do_setup(self, context):
|
||||
self.library.do_setup(context)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
self.library.check_for_setup_error()
|
||||
|
||||
def create_share(self, context, share, **kwargs):
|
||||
return self.library.create_share(context, share, **kwargs)
|
||||
|
||||
def create_share_from_snapshot(self, context, share, snapshot, **kwargs):
|
||||
return self.library.create_share_from_snapshot(context, share,
|
||||
snapshot, **kwargs)
|
||||
|
||||
def create_snapshot(self, context, snapshot, **kwargs):
|
||||
self.library.create_snapshot(context, snapshot, **kwargs)
|
||||
|
||||
def delete_share(self, context, share, **kwargs):
|
||||
self.library.delete_share(context, share, **kwargs)
|
||||
|
||||
def delete_snapshot(self, context, snapshot, **kwargs):
|
||||
self.library.delete_snapshot(context, snapshot, **kwargs)
|
||||
|
||||
def ensure_share(self, context, share, **kwargs):
|
||||
pass
|
||||
|
||||
def allow_access(self, context, share, access, **kwargs):
|
||||
self.library.allow_access(context, share, access, **kwargs)
|
||||
|
||||
def deny_access(self, context, share, access, **kwargs):
|
||||
self.library.deny_access(context, share, access, **kwargs)
|
||||
|
||||
def _update_share_stats(self, data=None):
|
||||
data = self.library.get_share_stats()
|
||||
super(NetAppCmodeMultiSvmShareDriver, self)._update_share_stats(
|
||||
data=data)
|
||||
|
||||
def get_network_allocations_number(self):
|
||||
return self.library.get_network_allocations_number()
|
||||
|
||||
def _setup_server(self, network_info, metadata=None):
|
||||
return self.library.setup_server(network_info, metadata)
|
||||
|
||||
def _teardown_server(self, server_details, **kwargs):
|
||||
self.library.teardown_server(server_details, **kwargs)
|
|
@ -0,0 +1,469 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
NetApp Data ONTAP cDOT storage driver library. Supports NFS & CIFS protocols.
|
||||
|
||||
This driver requires a Data ONTAP (Cluster-mode) storage system with
|
||||
installed CIFS and/or NFS licenses.
|
||||
"""
|
||||
|
||||
import re
|
||||
import socket
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import units
|
||||
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila.i18n import _, _LE, _LI
|
||||
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.protocols import cifs_cmode
|
||||
from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode
|
||||
from manila.share.drivers.netapp import options as na_opts
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila import utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def ensure_vserver(f):
|
||||
def wrap(self, *args, **kwargs):
|
||||
server = kwargs.get('share_server')
|
||||
if not server:
|
||||
# For now cmode driver does not support flat networking.
|
||||
raise exception.NetAppException(_('Share server is not provided.'))
|
||||
vserver_name = server['backend_details'].get('vserver_name') if \
|
||||
server.get('backend_details') else None
|
||||
if not vserver_name:
|
||||
msg = _('Vserver name is absent in backend details. Please '
|
||||
'check whether Vserver was created properly or not.')
|
||||
raise exception.NetAppException(msg)
|
||||
if not self._client.vserver_exists(vserver_name):
|
||||
raise exception.VserverUnavailable(vserver=vserver_name)
|
||||
return f(self, *args, **kwargs)
|
||||
return wrap
|
||||
|
||||
|
||||
class NetAppCmodeFileStorageLibrary(object):
|
||||
"""NetApp specific ONTAP Cluster mode driver.
|
||||
|
||||
Supports NFS and CIFS protocols.
|
||||
Uses Data ONTAP as backend to create shares and snapshots.
|
||||
Sets up vServer for each share_network.
|
||||
Connectivity between storage and client VM is organized
|
||||
by plugging vServer's network interfaces into neutron subnet
|
||||
that VM is using.
|
||||
"""
|
||||
|
||||
AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly
|
||||
|
||||
def __init__(self, db, driver_name, **kwargs):
|
||||
na_utils.validate_instantiation(**kwargs)
|
||||
|
||||
self.db = db
|
||||
self.driver_name = driver_name
|
||||
|
||||
self._helpers = None
|
||||
self._licenses = []
|
||||
self._client = None
|
||||
self._clients = {}
|
||||
self._last_ems = timeutils.utcnow()
|
||||
|
||||
self.configuration = kwargs['configuration']
|
||||
self.configuration.append_config_values(na_opts.netapp_connection_opts)
|
||||
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
|
||||
self.configuration.append_config_values(na_opts.netapp_transport_opts)
|
||||
self.configuration.append_config_values(na_opts.netapp_support_opts)
|
||||
self.configuration.append_config_values(
|
||||
na_opts.netapp_provisioning_opts)
|
||||
|
||||
self._app_version = kwargs.get('app_version', 'unknown')
|
||||
|
||||
na_utils.setup_tracing(self.configuration.netapp_trace_flags)
|
||||
self.backend_name = self.configuration.safe_get(
|
||||
'share_backend_name') or driver_name
|
||||
|
||||
@na_utils.trace
|
||||
def do_setup(self, context):
|
||||
self._client = self._get_api_client()
|
||||
self._setup_helpers()
|
||||
|
||||
@na_utils.trace
|
||||
def check_for_setup_error(self):
|
||||
self._get_licenses()
|
||||
|
||||
@na_utils.trace
|
||||
def _get_api_client(self, vserver=None):
|
||||
# Use cached value to prevent calls to system-get-ontapi-version.
|
||||
client = self._clients.get(vserver)
|
||||
|
||||
if not client:
|
||||
client = client_cmode.NetAppCmodeClient(
|
||||
transport_type=self.configuration.netapp_transport_type,
|
||||
username=self.configuration.netapp_login,
|
||||
password=self.configuration.netapp_password,
|
||||
hostname=self.configuration.netapp_server_hostname,
|
||||
port=self.configuration.netapp_server_port,
|
||||
vserver=vserver,
|
||||
trace=na_utils.TRACE_API)
|
||||
self._clients[vserver] = client
|
||||
|
||||
return client
|
||||
|
||||
@na_utils.trace
|
||||
def _get_licenses(self):
|
||||
self._licenses = self._client.get_licenses()
|
||||
|
||||
log_data = {
|
||||
'backend': self.backend_name,
|
||||
'licenses': ', '.join(self._licenses),
|
||||
}
|
||||
LOG.info(_LI('Available licenses on %(backend)s '
|
||||
'are %(licenses)s.'), log_data)
|
||||
|
||||
if 'nfs' not in self._licenses and 'cifs' not in self._licenses:
|
||||
msg = _LE('Neither NFS nor CIFS is licensed on %(backend)s')
|
||||
msg_args = {'backend': self.backend_name}
|
||||
LOG.error(msg % msg_args)
|
||||
|
||||
return self._licenses
|
||||
|
||||
@na_utils.trace
|
||||
def _get_valid_share_name(self, share_id):
|
||||
"""Get share name according to share name template."""
|
||||
return self.configuration.netapp_volume_name_template % {
|
||||
'share_id': share_id.replace('-', '_')}
|
||||
|
||||
@na_utils.trace
|
||||
def _get_valid_snapshot_name(self, snapshot_id):
|
||||
"""Get snapshot name according to snapshot name template."""
|
||||
return 'share_snapshot_' + snapshot_id.replace('-', '_')
|
||||
|
||||
@na_utils.trace
|
||||
def get_share_stats(self):
|
||||
"""Retrieve stats info from Cluster Mode backend."""
|
||||
total, free = self._client.calculate_aggregate_capacity(
|
||||
self._find_matching_aggregates())
|
||||
|
||||
data = dict(
|
||||
share_backend_name=self.backend_name,
|
||||
driver_name=self.driver_name,
|
||||
vendor_name='NetApp',
|
||||
driver_version='1.0',
|
||||
storage_protocol='NFS_CIFS',
|
||||
total_capacity_gb=(total / units.Gi),
|
||||
free_capacity_gb=(free / units.Gi))
|
||||
|
||||
self._handle_ems_logging()
|
||||
|
||||
return data
|
||||
|
||||
@na_utils.trace
|
||||
def _handle_ems_logging(self):
|
||||
"""Send an EMS log message if one hasn't been sent recently."""
|
||||
if timeutils.is_older_than(self._last_ems,
|
||||
self.AUTOSUPPORT_INTERVAL_SECONDS):
|
||||
self._last_ems = timeutils.utcnow()
|
||||
self._client.send_ems_log_message(self._build_ems_log_message())
|
||||
|
||||
@na_utils.trace
|
||||
def _build_ems_log_message(self):
|
||||
"""Construct EMS Autosupport log message."""
|
||||
|
||||
ems_log = {
|
||||
'computer-name': socket.getfqdn() or 'Manila_node',
|
||||
'event-id': '0',
|
||||
'event-source': 'Manila driver %s' % self.driver_name,
|
||||
'app-version': self._app_version,
|
||||
'category': 'provisioning',
|
||||
'event-description': 'OpenStack Manila connected to cluster node',
|
||||
'log-level': '6',
|
||||
'auto-support': 'false',
|
||||
}
|
||||
|
||||
return ems_log
|
||||
|
||||
@na_utils.trace
|
||||
def _find_matching_aggregates(self):
|
||||
"""Find all aggregates match pattern."""
|
||||
pattern = self.configuration.netapp_aggregate_name_search_pattern
|
||||
all_aggr_names = self._client.list_aggregates()
|
||||
matching_aggr_names = [aggr_name for aggr_name in all_aggr_names
|
||||
if re.match(pattern, aggr_name)]
|
||||
return matching_aggr_names
|
||||
|
||||
@na_utils.trace
|
||||
def _setup_helpers(self):
|
||||
"""Initializes protocol-specific NAS drivers."""
|
||||
self._helpers = {'CIFS': cifs_cmode.NetAppCmodeCIFSHelper(),
|
||||
'NFS': nfs_cmode.NetAppCmodeNFSHelper()}
|
||||
|
||||
@na_utils.trace
|
||||
def _get_helper(self, share):
|
||||
"""Returns driver which implements share protocol."""
|
||||
share_protocol = share['share_proto']
|
||||
if share_protocol.lower() not in self._licenses:
|
||||
current_licenses = self._get_licenses()
|
||||
if share_protocol.lower() not in current_licenses:
|
||||
msg_args = {
|
||||
'protocol': share_protocol,
|
||||
'host': self.configuration.netapp_server_hostname,
|
||||
}
|
||||
msg = _('The protocol %(protocol)s is not licensed on '
|
||||
'controller %(host)s') % msg_args
|
||||
LOG.error(msg)
|
||||
raise exception.NetAppException(msg)
|
||||
|
||||
for protocol in self._helpers.keys():
|
||||
if share_protocol.upper().startswith(protocol):
|
||||
return self._helpers[protocol]
|
||||
|
||||
err_msg = _("Invalid NAS protocol supplied: %s. ") % share_protocol
|
||||
raise exception.NetAppException(err_msg)
|
||||
|
||||
@na_utils.trace
|
||||
def setup_server(self, network_info, metadata=None):
|
||||
"""Creates and configures new Vserver."""
|
||||
LOG.debug('Creating server %s', network_info['server_id'])
|
||||
vserver_name = self._create_vserver_if_nonexistent(network_info)
|
||||
return {'vserver_name': vserver_name}
|
||||
|
||||
@na_utils.trace
|
||||
def _create_vserver_if_nonexistent(self, network_info):
|
||||
"""Creates Vserver with given parameters if it doesn't exist."""
|
||||
vserver_name = (self.configuration.netapp_vserver_name_template %
|
||||
network_info['server_id'])
|
||||
context_adm = context.get_admin_context()
|
||||
self.db.share_server_backend_details_set(
|
||||
context_adm,
|
||||
network_info['server_id'],
|
||||
{'vserver_name': vserver_name},
|
||||
)
|
||||
|
||||
if self._client.vserver_exists(vserver_name):
|
||||
msg = _('Vserver %s already exists.')
|
||||
raise exception.NetAppException(msg % vserver_name)
|
||||
|
||||
LOG.debug('Vserver %s does not exist, creating.', vserver_name)
|
||||
self._client.create_vserver(
|
||||
vserver_name,
|
||||
self.configuration.netapp_root_volume_aggregate,
|
||||
self.configuration.netapp_root_volume,
|
||||
self._find_matching_aggregates())
|
||||
|
||||
vserver_client = self._get_api_client(vserver=vserver_name)
|
||||
try:
|
||||
self._create_vserver_lifs(vserver_name,
|
||||
vserver_client,
|
||||
network_info)
|
||||
except netapp_api.NaApiError:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE("Failed to create network interface(s)."))
|
||||
self._client.delete_vserver(vserver_name, vserver_client)
|
||||
|
||||
vserver_client.enable_nfs()
|
||||
|
||||
security_services = network_info.get('security_services')
|
||||
if security_services:
|
||||
self._client.setup_security_services(security_services,
|
||||
vserver_client,
|
||||
vserver_name)
|
||||
return vserver_name
|
||||
|
||||
@na_utils.trace
|
||||
def _create_vserver_lifs(self, vserver_name, vserver_client,
|
||||
network_info):
|
||||
|
||||
nodes = self._client.list_cluster_nodes()
|
||||
node_network_info = zip(nodes, network_info['network_allocations'])
|
||||
netmask = utils.cidr_to_netmask(network_info['cidr'])
|
||||
|
||||
for node, net_info in node_network_info:
|
||||
net_id = net_info['id']
|
||||
port = self._client.get_node_data_port(node)
|
||||
ip = net_info['ip_address']
|
||||
self._create_lif_if_nonexistent(vserver_name,
|
||||
net_id,
|
||||
network_info['segmentation_id'],
|
||||
node,
|
||||
port,
|
||||
ip,
|
||||
netmask,
|
||||
vserver_client)
|
||||
|
||||
@na_utils.trace
|
||||
def _create_lif_if_nonexistent(self, vserver_name, allocation_id, vlan,
|
||||
node, port, ip, netmask, vserver_client):
|
||||
"""Creates LIF for Vserver."""
|
||||
if not vserver_client.network_interface_exists(vserver_name, node,
|
||||
port, ip, netmask,
|
||||
vlan):
|
||||
self._client.create_network_interface(
|
||||
ip, netmask, vlan, node, port, vserver_name, allocation_id,
|
||||
self.configuration.netapp_lif_name_template)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def create_share(self, context, share, share_server):
|
||||
"""Creates new share."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
self._allocate_container(share, vserver, vserver_client)
|
||||
return self._create_export(share, vserver, vserver_client)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def create_share_from_snapshot(self, context, share, snapshot,
|
||||
share_server=None):
|
||||
"""Creates new share from snapshot."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
self._allocate_container_from_snapshot(share, snapshot, vserver_client)
|
||||
return self._create_export(share, vserver, vserver_client)
|
||||
|
||||
@na_utils.trace
|
||||
def _allocate_container(self, share, vserver, vserver_client):
|
||||
"""Create new share on aggregate."""
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
aggregates = vserver_client.get_aggregates_for_vserver(vserver)
|
||||
aggregate = max(aggregates, key=lambda m: aggregates[m])
|
||||
|
||||
LOG.debug('Creating volume %(share_name)s on aggregate %(aggregate)s',
|
||||
{'share_name': share_name, 'aggregate': aggregate})
|
||||
vserver_client.create_volume(aggregate, share_name, share['size'])
|
||||
|
||||
@na_utils.trace
|
||||
def _allocate_container_from_snapshot(self, share, snapshot,
|
||||
vserver_client):
|
||||
"""Clones existing share."""
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
parent_share_name = self._get_valid_share_name(snapshot['share_id'])
|
||||
parent_snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
|
||||
|
||||
LOG.debug('Creating volume from snapshot %s', snapshot['id'])
|
||||
vserver_client.create_volume_clone(share_name, parent_share_name,
|
||||
parent_snapshot_name)
|
||||
|
||||
def _share_exists(self, share_name, vserver_client):
|
||||
return vserver_client.volume_exists(share_name)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def delete_share(self, context, share, share_server=None):
|
||||
"""Deletes share."""
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
if self._share_exists(share_name, vserver_client):
|
||||
self._remove_export(share, vserver_client)
|
||||
self._deallocate_container(share_name, vserver_client)
|
||||
else:
|
||||
LOG.info(_LI("Share %s does not exist."), share['id'])
|
||||
|
||||
@na_utils.trace
|
||||
def _deallocate_container(self, share_name, vserver_client):
|
||||
"""Free share space."""
|
||||
vserver_client.unmount_volume(share_name, force=True)
|
||||
vserver_client.offline_volume(share_name)
|
||||
vserver_client.delete_volume(share_name)
|
||||
|
||||
@na_utils.trace
|
||||
def _create_export(self, share, vserver, vserver_client):
|
||||
"""Creates NAS storage."""
|
||||
helper = self._get_helper(share)
|
||||
helper.set_client(vserver_client)
|
||||
share_name = self._get_valid_share_name(share['id'])
|
||||
|
||||
interfaces = vserver_client.get_network_interfaces()
|
||||
if not interfaces:
|
||||
msg = _("Cannot find network interfaces for Vserver %s.")
|
||||
raise exception.NetAppException(msg % vserver)
|
||||
|
||||
ip_address = interfaces[0]['address']
|
||||
export_location = helper.create_share(share_name, ip_address)
|
||||
return export_location
|
||||
|
||||
@na_utils.trace
|
||||
def _remove_export(self, share, vserver_client):
|
||||
"""Deletes NAS storage."""
|
||||
helper = self._get_helper(share)
|
||||
helper.set_client(vserver_client)
|
||||
target = helper.get_target(share)
|
||||
# Share may be in error state, so there's no share and target.
|
||||
if target:
|
||||
helper.delete_share(share)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def create_snapshot(self, context, snapshot, share_server=None):
|
||||
"""Creates a snapshot of a share."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
share_name = self._get_valid_share_name(snapshot['share_id'])
|
||||
snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
|
||||
LOG.debug('Creating snapshot %s', snapshot_name)
|
||||
vserver_client.create_snapshot(share_name, snapshot_name)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def delete_snapshot(self, context, snapshot, share_server=None):
|
||||
"""Deletes a snapshot of a share."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
share_name = self._get_valid_share_name(snapshot['share_id'])
|
||||
snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
|
||||
|
||||
if vserver_client.is_snapshot_busy(share_name, snapshot_name):
|
||||
raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot_name)
|
||||
|
||||
LOG.debug('Deleting snapshot %(snap)s for share %(share)s.',
|
||||
{'snap': snapshot_name, 'share': share_name})
|
||||
vserver_client.delete_snapshot(share_name, snapshot_name)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def allow_access(self, context, share, access, share_server=None):
|
||||
"""Allows access to a given NAS storage."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
helper = self._get_helper(share)
|
||||
helper.set_client(vserver_client)
|
||||
helper.allow_access(context, share, access)
|
||||
|
||||
@ensure_vserver
|
||||
@na_utils.trace
|
||||
def deny_access(self, context, share, access, share_server=None):
|
||||
"""Denies access to a given NAS storage."""
|
||||
vserver = share_server['backend_details']['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
helper = self._get_helper(share)
|
||||
helper.set_client(vserver_client)
|
||||
helper.deny_access(context, share, access)
|
||||
|
||||
@na_utils.trace
|
||||
def get_network_allocations_number(self):
|
||||
"""Get number of network interfaces to be created."""
|
||||
return len(self._client.list_cluster_nodes())
|
||||
|
||||
@na_utils.trace
|
||||
def teardown_server(self, server_details, security_services=None):
|
||||
"""Teardown share network."""
|
||||
vserver = server_details['vserver_name']
|
||||
vserver_client = self._get_api_client(vserver=vserver)
|
||||
self._client.delete_vserver(vserver, vserver_client,
|
||||
security_services=security_services)
|
|
@ -0,0 +1,49 @@
|
|||
# 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.
|
||||
"""
|
||||
Abstract base class for NetApp NAS protocol helper classes.
|
||||
"""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class NetAppBaseHelper(object):
|
||||
"""Interface for protocol-specific NAS drivers."""
|
||||
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
|
||||
def set_client(self, client):
|
||||
self._client = client
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_share(self, share, export_ip):
|
||||
"""Creates NAS share."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_share(self, share):
|
||||
"""Deletes NAS share."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def allow_access(self, context, share, access):
|
||||
"""Allows new_rules to a given NAS storage in new_rules."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def deny_access(self, context, share, access):
|
||||
"""Denies new_rules to a given NAS storage in new_rules."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_target(self, share):
|
||||
"""Returns host where the share located."""
|
|
@ -0,0 +1,83 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
NetApp CIFS protocol helper class.
|
||||
"""
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _, _LE
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.protocols import base
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NetAppCmodeCIFSHelper(base.NetAppBaseHelper):
|
||||
"""Netapp specific cluster-mode CIFS sharing driver."""
|
||||
|
||||
def create_share(self, share_name, export_ip):
|
||||
"""Creates CIFS share on Data ONTAP Vserver."""
|
||||
self._client.create_cifs_share(share_name)
|
||||
self._client.remove_cifs_share_access(share_name, 'Everyone')
|
||||
return "//%s/%s" % (export_ip, share_name)
|
||||
|
||||
def delete_share(self, share):
|
||||
"""Deletes CIFS share on Data ONTAP Vserver."""
|
||||
host_ip, share_name = self._get_export_location(share)
|
||||
self._client.remove_cifs_share(share_name)
|
||||
|
||||
def allow_access(self, context, share, access):
|
||||
"""Allows access to the CIFS share for a given user."""
|
||||
if access['access_type'] != 'user':
|
||||
msg = _("Cluster Mode supports only 'user' type for share access"
|
||||
" rules with CIFS protocol.")
|
||||
raise exception.NetAppException(msg)
|
||||
|
||||
target, share_name = self._get_export_location(share)
|
||||
try:
|
||||
self._client.add_cifs_share_access(share_name, access['access_to'])
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EDUPLICATEENTRY:
|
||||
# Duplicate entry, so use specific exception.
|
||||
raise exception.ShareAccessExists(
|
||||
access_type=access['access_type'], access=access)
|
||||
raise e
|
||||
|
||||
def deny_access(self, context, share, access):
|
||||
"""Denies access to the CIFS share for a given user."""
|
||||
host_ip, share_name = self._get_export_location(share)
|
||||
user_name = access['access_to']
|
||||
try:
|
||||
self._client.remove_cifs_share_access(share_name, user_name)
|
||||
except netapp_api.NaApiError as e:
|
||||
if e.code == netapp_api.EONTAPI_EINVAL:
|
||||
LOG.error(_LE("User %s does not exist."), user_name)
|
||||
elif e.code == netapp_api.EOBJECTNOTFOUND:
|
||||
LOG.error(_LE("Rule %s does not exist."), user_name)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def get_target(self, share):
|
||||
"""Returns OnTap target IP based on share export location."""
|
||||
return self._get_export_location(share)[0]
|
||||
|
||||
@staticmethod
|
||||
def _get_export_location(share):
|
||||
"""Returns host ip and share name for a given CIFS share."""
|
||||
export_location = share['export_location'] or '///'
|
||||
_x, _x, host_ip, share_name = export_location.split('/')
|
||||
return host_ip, share_name
|
|
@ -0,0 +1,94 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
NetApp NFS protocol helper class.
|
||||
"""
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.protocols import base
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NetAppCmodeNFSHelper(base.NetAppBaseHelper):
|
||||
"""Netapp specific cluster-mode NFS sharing driver."""
|
||||
|
||||
def create_share(self, share_name, export_ip):
|
||||
"""Creates NFS share."""
|
||||
export_path = self._client.get_volume_junction_path(share_name)
|
||||
self._client.add_nfs_export_rules(export_path, ['localhost'])
|
||||
export_location = ':'.join([export_ip, export_path])
|
||||
return export_location
|
||||
|
||||
def delete_share(self, share):
|
||||
"""Deletes NFS share."""
|
||||
target, export_path = self._get_export_location(share)
|
||||
LOG.debug('Deleting NFS rules for share %s', share['id'])
|
||||
self._client.remove_nfs_export_rules(export_path)
|
||||
|
||||
def allow_access(self, context, share, access):
|
||||
"""Allows access to a given NFS storage."""
|
||||
new_rules = access['access_to']
|
||||
existing_rules = self._get_existing_rules(share)
|
||||
|
||||
if not isinstance(new_rules, list):
|
||||
new_rules = [new_rules]
|
||||
|
||||
rules = existing_rules + new_rules
|
||||
try:
|
||||
self._modify_rule(share, rules)
|
||||
except netapp_api.NaApiError:
|
||||
self._modify_rule(share, existing_rules)
|
||||
|
||||
def deny_access(self, context, share, access):
|
||||
"""Denies access to a given NFS storage."""
|
||||
access_to = access['access_to']
|
||||
existing_rules = self._get_existing_rules(share)
|
||||
|
||||
if not isinstance(access_to, list):
|
||||
access_to = [access_to]
|
||||
|
||||
for deny_rule in access_to:
|
||||
if deny_rule in existing_rules:
|
||||
existing_rules.remove(deny_rule)
|
||||
|
||||
self._modify_rule(share, existing_rules)
|
||||
|
||||
def get_target(self, share):
|
||||
"""Returns ID of target OnTap device based on export location."""
|
||||
return self._get_export_location(share)[0]
|
||||
|
||||
def _modify_rule(self, share, rules):
|
||||
"""Modifies access rule for a given NFS share."""
|
||||
target, export_path = self._get_export_location(share)
|
||||
self._client.add_nfs_export_rules(export_path, rules)
|
||||
|
||||
def _get_existing_rules(self, share):
|
||||
"""Returns available access rules for a given NFS share."""
|
||||
target, export_path = self._get_export_location(share)
|
||||
existing_rules = self._client.get_nfs_export_rules(export_path)
|
||||
|
||||
LOG.debug('Found existing rules %(rules)r for share %(share)s',
|
||||
{'rules': existing_rules, 'share': share['id']})
|
||||
|
||||
return existing_rules
|
||||
|
||||
@staticmethod
|
||||
def _get_export_location(share):
|
||||
"""Returns IP address and export location of a NFS share."""
|
||||
export_location = share['export_location'] or ':'
|
||||
return export_location.split(':')
|
|
@ -0,0 +1,99 @@
|
|||
# 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.
|
||||
|
||||
"""Contains configuration options for NetApp drivers.
|
||||
|
||||
Common place to hold configuration options for all NetApp drivers.
|
||||
Options need to be grouped into granular units to be able to be reused
|
||||
by different modules and classes. This does not restrict declaring options in
|
||||
individual modules. If options are not re usable then can be declared in
|
||||
individual modules. It is recommended to Keep options at a single
|
||||
place to ensure re usability and better management of configuration options.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
netapp_proxy_opts = [
|
||||
cfg.StrOpt('netapp_storage_family',
|
||||
default='ontap_cluster',
|
||||
help=('The storage family type used on the storage system; '
|
||||
'valid values include ontap_cluster for using '
|
||||
'clustered Data ONTAP.')), ]
|
||||
|
||||
netapp_connection_opts = [
|
||||
cfg.StrOpt('netapp_server_hostname',
|
||||
deprecated_name='netapp_nas_server_hostname',
|
||||
default=None,
|
||||
help='The hostname (or IP address) for the storage system.'),
|
||||
cfg.IntOpt('netapp_server_port',
|
||||
default=None,
|
||||
help=('The TCP port to use for communication with the storage '
|
||||
'system or proxy server. If not specified, Data ONTAP '
|
||||
'drivers will use 80 for HTTP and 443 for HTTPS.')), ]
|
||||
|
||||
netapp_transport_opts = [
|
||||
cfg.StrOpt('netapp_transport_type',
|
||||
deprecated_name='netapp_nas_transport_type',
|
||||
default='http',
|
||||
help=('The transport protocol used when communicating with '
|
||||
'the storage system or proxy server. Valid values are '
|
||||
'http or https.')), ]
|
||||
|
||||
netapp_basicauth_opts = [
|
||||
cfg.StrOpt('netapp_login',
|
||||
deprecated_name='netapp_nas_login',
|
||||
default=None,
|
||||
help=('Administrative user account name used to access the '
|
||||
'storage system.')),
|
||||
cfg.StrOpt('netapp_password',
|
||||
deprecated_name='netapp_nas_password',
|
||||
default=None,
|
||||
help=('Password for the administrative user account '
|
||||
'specified in the netapp_login option.'),
|
||||
secret=True), ]
|
||||
|
||||
netapp_provisioning_opts = [
|
||||
cfg.StrOpt('netapp_volume_name_template',
|
||||
deprecated_name='netapp_nas_volume_name_template',
|
||||
help='NetApp volume name template.',
|
||||
default='share_%(share_id)s'),
|
||||
cfg.StrOpt('netapp_vserver_name_template',
|
||||
default='os_%s',
|
||||
help='Name template to use for new vserver.'),
|
||||
cfg.StrOpt('netapp_lif_name_template',
|
||||
default='os_%(net_allocation_id)s',
|
||||
help='Logical interface (LIF) name template'),
|
||||
cfg.StrOpt('netapp_aggregate_name_search_pattern',
|
||||
default='(.*)',
|
||||
help='Pattern for searching available aggregates '
|
||||
'for provisioning.'),
|
||||
cfg.StrOpt('netapp_root_volume_aggregate',
|
||||
help='Name of aggregate to create root volume on.'),
|
||||
cfg.StrOpt('netapp_root_volume',
|
||||
deprecated_name='netapp_root_volume_name',
|
||||
default='root',
|
||||
help='Root volume name.'), ]
|
||||
|
||||
netapp_support_opts = [
|
||||
cfg.StrOpt('netapp_trace_flags',
|
||||
default=None,
|
||||
help=('Comma-separated list of options that control which '
|
||||
'trace info is written to the debug logs. Values '
|
||||
'include method and api.')), ]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(netapp_proxy_opts)
|
||||
CONF.register_opts(netapp_connection_opts)
|
||||
CONF.register_opts(netapp_transport_opts)
|
||||
CONF.register_opts(netapp_basicauth_opts)
|
||||
CONF.register_opts(netapp_provisioning_opts)
|
||||
CONF.register_opts(netapp_support_opts)
|
|
@ -15,21 +15,17 @@
|
|||
# under the License.
|
||||
"""Utilities for NetApp drivers."""
|
||||
|
||||
import copy
|
||||
import platform
|
||||
import socket
|
||||
|
||||
from oslo_concurrency import processutils as putils
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from manila import exception
|
||||
from manila.i18n import _, _LI, _LW
|
||||
from manila.share.drivers.netapp import api as na_api
|
||||
from manila import version
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
VALID_TRACE_FLAGS = ['method', 'api']
|
||||
TRACE_METHOD = False
|
||||
|
@ -53,10 +49,10 @@ def setup_tracing(trace_flags_string):
|
|||
def trace(f):
|
||||
def trace_wrapper(self, *args, **kwargs):
|
||||
if TRACE_METHOD:
|
||||
LOG.debug('Entering method %s' % f.__name__)
|
||||
LOG.debug('Entering method %s', f.__name__)
|
||||
result = f(self, *args, **kwargs)
|
||||
if TRACE_METHOD:
|
||||
LOG.debug('Leaving method %s' % f.__name__)
|
||||
LOG.debug('Leaving method %s', f.__name__)
|
||||
return result
|
||||
return trace_wrapper
|
||||
|
||||
|
@ -69,94 +65,17 @@ def check_flags(required_flags, configuration):
|
|||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
|
||||
def provide_ems(requester, server, netapp_backend, app_version,
|
||||
server_type="cluster"):
|
||||
"""Provide ems with volume stats for the requester.
|
||||
def validate_instantiation(**kwargs):
|
||||
"""Checks if a driver is instantiated other than by the unified driver.
|
||||
|
||||
Helps check direct instantiation of netapp drivers.
|
||||
Call this function in every netapp block driver constructor.
|
||||
"""
|
||||
# TODO(tbarron): rework provide_ems to not store timestamp in the caller.
|
||||
# This requires upcoming Manila NetApp refactoring work.
|
||||
|
||||
def _create_ems(netapp_backend, app_version, server_type):
|
||||
"""Create ems api request."""
|
||||
ems_log = na_api.NaElement('ems-autosupport-log')
|
||||
host = socket.getfqdn() or 'Manila_node'
|
||||
if server_type == "cluster":
|
||||
dest = "cluster node"
|
||||
else:
|
||||
dest = "7 mode controller"
|
||||
ems_log.add_new_child('computer-name', host)
|
||||
ems_log.add_new_child('event-id', '0')
|
||||
ems_log.add_new_child('event-source',
|
||||
'Manila driver %s' % netapp_backend)
|
||||
ems_log.add_new_child('app-version', app_version)
|
||||
ems_log.add_new_child('category', 'provisioning')
|
||||
ems_log.add_new_child('event-description',
|
||||
'OpenStack Manila connected to %s' % dest)
|
||||
ems_log.add_new_child('log-level', '6')
|
||||
ems_log.add_new_child('auto-support', 'false')
|
||||
return ems_log
|
||||
|
||||
def _create_vs_get():
|
||||
"""Create vs_get api request."""
|
||||
vs_get = na_api.NaElement('vserver-get-iter')
|
||||
vs_get.add_new_child('max-records', '1')
|
||||
query = na_api.NaElement('query')
|
||||
query.add_node_with_children('vserver-info',
|
||||
**{'vserver-type': 'node'})
|
||||
vs_get.add_child_elem(query)
|
||||
desired = na_api.NaElement('desired-attributes')
|
||||
desired.add_node_with_children(
|
||||
'vserver-info', **{'vserver-name': '', 'vserver-type': ''})
|
||||
vs_get.add_child_elem(desired)
|
||||
return vs_get
|
||||
|
||||
def _get_cluster_node(na_server):
|
||||
"""Get the cluster node for ems."""
|
||||
na_server.set_vserver(None)
|
||||
vs_get = _create_vs_get()
|
||||
res = na_server.invoke_successfully(vs_get)
|
||||
if (res.get_child_content('num-records') and
|
||||
int(res.get_child_content('num-records')) > 0):
|
||||
attr_list = res.get_child_by_name('attributes-list')
|
||||
vs_info = attr_list.get_child_by_name('vserver-info')
|
||||
vs_name = vs_info.get_child_content('vserver-name')
|
||||
return vs_name
|
||||
return None
|
||||
|
||||
do_ems = True
|
||||
if hasattr(requester, 'last_ems'):
|
||||
sec_limit = 3559
|
||||
if not (timeutils.is_older_than(requester.last_ems, sec_limit)):
|
||||
do_ems = False
|
||||
if do_ems:
|
||||
na_server = copy.copy(server)
|
||||
na_server.set_timeout(25)
|
||||
ems = _create_ems(netapp_backend, app_version, server_type)
|
||||
try:
|
||||
if server_type == "cluster":
|
||||
api_version = na_server.get_api_version()
|
||||
if api_version:
|
||||
major, minor = api_version
|
||||
else:
|
||||
raise na_api.NaApiError(code='Not found',
|
||||
message='No api version found')
|
||||
if major == 1 and minor > 15:
|
||||
node = getattr(requester, 'vserver', None)
|
||||
else:
|
||||
node = _get_cluster_node(na_server)
|
||||
if node is None:
|
||||
raise na_api.NaApiError(code='Not found',
|
||||
message='No vserver found')
|
||||
na_server.set_vserver(node)
|
||||
else:
|
||||
na_server.set_vfiler(None)
|
||||
na_server.invoke_successfully(ems, True)
|
||||
LOG.debug("ems executed successfully.")
|
||||
except na_api.NaApiError as e:
|
||||
LOG.warn(_LW("Failed to invoke ems. Message : %s") % e)
|
||||
finally:
|
||||
requester.last_ems = timeutils.utcnow()
|
||||
if kwargs and kwargs.get('netapp_mode') == 'proxy':
|
||||
return
|
||||
LOG.warning(_LW('Please use NetAppDriver in the configuration file '
|
||||
'to load the driver instead of directly specifying '
|
||||
'the driver module name.'))
|
||||
|
||||
|
||||
class OpenStackInfo(object):
|
||||
|
@ -173,58 +92,21 @@ class OpenStackInfo(object):
|
|||
self._vendor = 'unknown vendor'
|
||||
self._platform = 'unknown platform'
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, value):
|
||||
self._version = value
|
||||
|
||||
@property
|
||||
def release(self):
|
||||
return self._release
|
||||
|
||||
@release.setter
|
||||
def release(self, value):
|
||||
self._release = value
|
||||
|
||||
@property
|
||||
def vendor(self):
|
||||
return self._vendor
|
||||
|
||||
@vendor.setter
|
||||
def vendor(self, value):
|
||||
self._vendor = value
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
return self._platform
|
||||
|
||||
@platform.setter
|
||||
def platform(self, value):
|
||||
self._platform = value
|
||||
|
||||
# Because of the variety of platforms and OpenStack distributions that we
|
||||
# may run against, it is by no means a sure thing that these update methods
|
||||
# will work. We collect what information we can and deliberately ignore
|
||||
# exceptions of any kind in order to avoid fillling the manila share log
|
||||
# with noise.
|
||||
def _update_version_from_version_string(self):
|
||||
try:
|
||||
self.version = version.version_info.version_string()
|
||||
self._version = version.version_info.version_string()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _update_release_from_release_string(self):
|
||||
try:
|
||||
self.release = version.version_info.release_string()
|
||||
self._release = version.version_info.release_string()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _update_platform(self):
|
||||
try:
|
||||
self.platform = platform.platform()
|
||||
self._platform = platform.platform()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
@ -240,17 +122,17 @@ class OpenStackInfo(object):
|
|||
try:
|
||||
ver = self._get_version_info_version()
|
||||
if ver:
|
||||
self.version = ver
|
||||
self._version = ver
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
rel = self._get_version_info_release()
|
||||
if rel:
|
||||
self.release = rel
|
||||
self._release = rel
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# RDO, RHEL-OSP, Mirantis on Redhat, SUSE
|
||||
# RDO, RHEL-OSP, Mirantis on Redhat, SUSE.
|
||||
def _update_info_from_rpm(self):
|
||||
LOG.debug('Trying rpm command.')
|
||||
try:
|
||||
|
@ -262,16 +144,16 @@ class OpenStackInfo(object):
|
|||
'pkg': self.PACKAGE_NAME})
|
||||
return False
|
||||
parts = out.split()
|
||||
self.version = parts[0]
|
||||
self.release = parts[1]
|
||||
self.vendor = ' '.join(parts[2::])
|
||||
self._version = parts[0]
|
||||
self._release = parts[1]
|
||||
self._vendor = ' '.join(parts[2::])
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.info(_LI('Could not run rpm command: %(msg)s.') % {
|
||||
'msg': e})
|
||||
return False
|
||||
|
||||
# ubuntu, mirantis on ubuntu
|
||||
# Ubuntu, Mirantis on Ubuntu.
|
||||
def _update_info_from_dpkg(self):
|
||||
LOG.debug('Trying dpkg-query command.')
|
||||
try:
|
||||
|
@ -283,9 +165,9 @@ class OpenStackInfo(object):
|
|||
'No dpkg-query info found for %(pkg)s package.') % {
|
||||
'pkg': self.PACKAGE_NAME})
|
||||
return False
|
||||
# debian format: [epoch:]upstream_version[-debian_revision]
|
||||
# Debian format: [epoch:]upstream_version[-debian_revision]
|
||||
deb_version = out
|
||||
# in case epoch or revision is missing, copy entire string
|
||||
# In case epoch or revision is missing, copy entire string.
|
||||
_release = deb_version
|
||||
if ':' in deb_version:
|
||||
deb_epoch, upstream_version = deb_version.split(':')
|
||||
|
@ -293,9 +175,9 @@ class OpenStackInfo(object):
|
|||
if '-' in deb_version:
|
||||
deb_revision = deb_version.split('-')[1]
|
||||
_vendor = deb_revision
|
||||
self.release = _release
|
||||
self._release = _release
|
||||
if _vendor:
|
||||
self.vendor = _vendor
|
||||
self._vendor = _vendor
|
||||
return True
|
||||
except Exception as e:
|
||||
LOG.info(_LI('Could not run dpkg-query command: %(msg)s.') % {
|
||||
|
@ -306,9 +188,9 @@ class OpenStackInfo(object):
|
|||
self._update_version_from_version_string()
|
||||
self._update_release_from_release_string()
|
||||
self._update_platform()
|
||||
# some distributions override with more meaningful information
|
||||
# Some distributions override with more meaningful information.
|
||||
self._update_info_from_version_info()
|
||||
# see if we have still more targeted info from rpm or apt
|
||||
# See if we have still more targeted info from rpm or apt.
|
||||
found_package = self._update_info_from_rpm()
|
||||
if not found_package:
|
||||
self._update_info_from_dpkg()
|
||||
|
@ -316,5 +198,5 @@ class OpenStackInfo(object):
|
|||
def info(self):
|
||||
self._update_openstack_info()
|
||||
return '%(version)s|%(release)s|%(vendor)s|%(platform)s' % {
|
||||
'version': self.version, 'release': self.release,
|
||||
'vendor': self.vendor, 'platform': self.platform}
|
||||
'version': self._version, 'release': self._release,
|
||||
'vendor': self._vendor, 'platform': self._platform}
|
||||
|
|
|
@ -53,6 +53,8 @@ share_manager_opts = [
|
|||
CONF = cfg.CONF
|
||||
CONF.register_opts(share_manager_opts)
|
||||
|
||||
# Drivers that need to change module paths or class names can add their
|
||||
# old/new path here to maintain backward compatibility.
|
||||
MAPPING = {
|
||||
'manila.share.drivers.netapp.cluster_mode.NetAppClusteredShareDriver':
|
||||
'manila.share.drivers.netapp.common.NetAppDriver', }
|
||||
|
|
|
@ -0,0 +1,967 @@
|
|||
# Copyright (c) - 2014, Clinton Knight. 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.
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
CONNECTION_INFO = {
|
||||
'hostname': 'hostname',
|
||||
'transport_type': 'https',
|
||||
'port': 443,
|
||||
'username': 'admin',
|
||||
'password': 'passw0rd'
|
||||
}
|
||||
|
||||
NODE_NAME = 'fake_node'
|
||||
VSERVER_NAME = 'fake_vserver'
|
||||
ADMIN_VSERVER_NAME = 'fake_admin_vserver'
|
||||
NODE_VSERVER_NAME = 'fake_node_vserver'
|
||||
ROOT_VOLUME_AGGREGATE_NAME = 'fake_root_aggr'
|
||||
ROOT_VOLUME_NAME = 'fake_root_volume'
|
||||
SHARE_AGGREGATE_NAME = 'fake_aggr1'
|
||||
SHARE_AGGREGATE_NAMES = ['fake_aggr1', 'fake_aggr2']
|
||||
SHARE_NAME = 'fake_share'
|
||||
SNAPSHOT_NAME = 'fake_snapshot'
|
||||
PARENT_SHARE_NAME = 'fake_parent_share'
|
||||
PARENT_SNAPSHOT_NAME = 'fake_parent_snapshot'
|
||||
|
||||
USER_NAME = 'fake_user'
|
||||
|
||||
PORT = 'e0a'
|
||||
VLAN = '1001'
|
||||
VLAN_PORT = 'e0a-1001'
|
||||
IP_ADDRESS = '10.10.10.10'
|
||||
NETMASK = '255.255.255.0'
|
||||
NET_ALLOCATION_ID = 'fake_allocation_id'
|
||||
LIF_NAME_TEMPLATE = 'os_%(net_allocation_id)s'
|
||||
LIF_NAME = LIF_NAME_TEMPLATE % {'net_allocation_id': NET_ALLOCATION_ID}
|
||||
|
||||
EMS_MESSAGE = {
|
||||
'computer-name': 'fake_host',
|
||||
'event-id': '0',
|
||||
'event-source': 'fake driver',
|
||||
'app-version': 'fake app version',
|
||||
'category': 'fake category',
|
||||
'event-description': 'fake description',
|
||||
'log-level': '6',
|
||||
'auto-support': 'false',
|
||||
}
|
||||
|
||||
NO_RECORDS_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<num-records>0</num-records>
|
||||
</results>
|
||||
""")
|
||||
|
||||
VSERVER_GET_ITER_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<vserver-info>
|
||||
<vserver-name>%(fake_vserver)s</vserver-name>
|
||||
</vserver-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'fake_vserver': VSERVER_NAME})
|
||||
|
||||
VSERVER_GET_ROOT_VOLUME_NAME_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<vserver-info>
|
||||
<root-volume>%(root_volume)s</root-volume>
|
||||
<vserver-name>%(fake_vserver)s</vserver-name>
|
||||
</vserver-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'root_volume': ROOT_VOLUME_NAME, 'fake_vserver': VSERVER_NAME})
|
||||
|
||||
VSERVER_GET_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes>
|
||||
<vserver-info>
|
||||
<aggr-list>
|
||||
<aggr-name>aggr0</aggr-name>
|
||||
<aggr-name>manila</aggr-name>
|
||||
</aggr-list>
|
||||
<vserver-aggr-info-list>
|
||||
<vserver-aggr-info>
|
||||
<aggr-availsize>45678592</aggr-availsize>
|
||||
<aggr-name>aggr0</aggr-name>
|
||||
</vserver-aggr-info>
|
||||
<vserver-aggr-info>
|
||||
<aggr-availsize>6448431104</aggr-availsize>
|
||||
<aggr-name>manila</aggr-name>
|
||||
</vserver-aggr-info>
|
||||
</vserver-aggr-info-list>
|
||||
<vserver-name>%(vserver)s</vserver-name>
|
||||
</vserver-info>
|
||||
</attributes>
|
||||
</results>
|
||||
""" % {'vserver': VSERVER_NAME})
|
||||
|
||||
VSERVER_DATA_LIST_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<vserver-info>
|
||||
<vserver-name>%(vserver)s</vserver-name>
|
||||
<vserver-type>data</vserver-type>
|
||||
</vserver-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'vserver': VSERVER_NAME})
|
||||
|
||||
VSERVER_AGGREGATES = {'aggr0': 45678592, 'manila': 6448431104}
|
||||
|
||||
VSERVER_GET_RESPONSE_NO_AGGREGATES = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes>
|
||||
<vserver-info>
|
||||
<vserver-name>%(vserver)s</vserver-name>
|
||||
</vserver-info>
|
||||
</attributes>
|
||||
</results>
|
||||
""" % {'vserver': VSERVER_NAME})
|
||||
|
||||
ONTAPI_VERSION_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<major-version>1</major-version>
|
||||
<minor-version>19</minor-version>
|
||||
</results>
|
||||
""")
|
||||
|
||||
LICENSE_V2_LIST_INFO_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<licenses>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>Cluster Base License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3</owner>
|
||||
<package>base</package>
|
||||
<serial-number>1-80-000008</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>NFS License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>nfs</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>CIFS License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>cifs</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>iSCSI License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>iscsi</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>FCP License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>fcp</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>SnapRestore License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>snaprestore</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>SnapMirror License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>snapmirror</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>FlexClone License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>flexclone</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
<license-v2-info>
|
||||
<customer-id>none</customer-id>
|
||||
<description>SnapVault License</description>
|
||||
<legacy>false</legacy>
|
||||
<owner>cluster3-01</owner>
|
||||
<package>snapvault</package>
|
||||
<serial-number>1-81-0000000000000004082368507</serial-number>
|
||||
<type>license</type>
|
||||
</license-v2-info>
|
||||
</licenses>
|
||||
</results>
|
||||
""")
|
||||
|
||||
LICENSES = [
|
||||
'base', 'cifs', 'fcp', 'flexclone', 'iscsi', 'nfs', 'snapmirror',
|
||||
'snaprestore', 'snapvault'
|
||||
]
|
||||
|
||||
VOLUME_COUNT_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes>
|
||||
<name>vol0</name>
|
||||
<owning-vserver-name>cluster3-01</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
</volume-attributes>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes>
|
||||
<name>%(root_volume)s</name>
|
||||
<owning-vserver-name>%(fake_vserver)s</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
</volume-attributes>
|
||||
</attributes-list>
|
||||
<num-records>2</num-records>
|
||||
</results>
|
||||
""" % {'root_volume': ROOT_VOLUME_NAME, 'fake_vserver': VSERVER_NAME})
|
||||
|
||||
CIFS_SECURITY_SERVICE = {
|
||||
'type': 'active_directory',
|
||||
'password': 'fake_password',
|
||||
'user': 'fake_user',
|
||||
'domain': 'fake_domain',
|
||||
'dns_ip': 'fake_dns_ip',
|
||||
}
|
||||
|
||||
LDAP_SECURITY_SERVICE = {
|
||||
'type': 'ldap',
|
||||
'password': 'fake_password',
|
||||
'server': 'fake_server',
|
||||
'id': 'fake_id',
|
||||
}
|
||||
|
||||
KERBEROS_SECURITY_SERVICE = {
|
||||
'type': 'kerberos',
|
||||
'password': 'fake_password',
|
||||
'user': 'fake_user',
|
||||
'server': 'fake_server',
|
||||
'id': 'fake_id',
|
||||
'domain': 'fake_domain',
|
||||
'dns_ip': 'fake_dns_ip',
|
||||
}
|
||||
|
||||
KERBEROS_SERVICE_PRINCIPAL_NAME = 'nfs/fake-vserver.fake_domain@FAKE_DOMAIN'
|
||||
|
||||
INVALID_SECURITY_SERVICE = {
|
||||
'type': 'fake',
|
||||
}
|
||||
|
||||
SYSTEM_NODE_GET_ITER_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<node-details-info>
|
||||
<node>%s</node>
|
||||
</node-details-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % NODE_NAME)
|
||||
|
||||
NET_PORT_GET_ITER_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<net-port-info>
|
||||
<administrative-duplex>full</administrative-duplex>
|
||||
<administrative-flowcontrol>full</administrative-flowcontrol>
|
||||
<administrative-speed>auto</administrative-speed>
|
||||
<is-administrative-auto-negotiate>true</is-administrative-auto-negotiate>
|
||||
<is-administrative-up>true</is-administrative-up>
|
||||
<is-operational-auto-negotiate>true</is-operational-auto-negotiate>
|
||||
<link-status>up</link-status>
|
||||
<mac-address>00:0c:29:fc:04:d9</mac-address>
|
||||
<mtu>1500</mtu>
|
||||
<node>%(node_name)s</node>
|
||||
<operational-duplex>full</operational-duplex>
|
||||
<operational-flowcontrol>none</operational-flowcontrol>
|
||||
<operational-speed>1000</operational-speed>
|
||||
<port>e0a</port>
|
||||
<port-type>physical</port-type>
|
||||
<role>data</role>
|
||||
</net-port-info>
|
||||
<net-port-info>
|
||||
<administrative-duplex>full</administrative-duplex>
|
||||
<administrative-flowcontrol>full</administrative-flowcontrol>
|
||||
<administrative-speed>auto</administrative-speed>
|
||||
<is-administrative-auto-negotiate>true</is-administrative-auto-negotiate>
|
||||
<is-administrative-up>true</is-administrative-up>
|
||||
<is-operational-auto-negotiate>true</is-operational-auto-negotiate>
|
||||
<link-status>up</link-status>
|
||||
<mac-address>00:0c:29:fc:04:e3</mac-address>
|
||||
<mtu>1500</mtu>
|
||||
<node>%(node_name)s</node>
|
||||
<operational-duplex>full</operational-duplex>
|
||||
<operational-flowcontrol>none</operational-flowcontrol>
|
||||
<operational-speed>1000</operational-speed>
|
||||
<port>e0b</port>
|
||||
<port-type>physical</port-type>
|
||||
<role>data</role>
|
||||
</net-port-info>
|
||||
<net-port-info>
|
||||
<administrative-duplex>full</administrative-duplex>
|
||||
<administrative-flowcontrol>full</administrative-flowcontrol>
|
||||
<administrative-speed>auto</administrative-speed>
|
||||
<is-administrative-auto-negotiate>true</is-administrative-auto-negotiate>
|
||||
<is-administrative-up>true</is-administrative-up>
|
||||
<is-operational-auto-negotiate>true</is-operational-auto-negotiate>
|
||||
<link-status>up</link-status>
|
||||
<mac-address>00:0c:29:fc:04:ed</mac-address>
|
||||
<mtu>1500</mtu>
|
||||
<node>%(node_name)s</node>
|
||||
<operational-duplex>full</operational-duplex>
|
||||
<operational-flowcontrol>none</operational-flowcontrol>
|
||||
<operational-speed>1000</operational-speed>
|
||||
<port>e0c</port>
|
||||
<port-type>physical</port-type>
|
||||
<role>data</role>
|
||||
</net-port-info>
|
||||
<net-port-info>
|
||||
<administrative-duplex>full</administrative-duplex>
|
||||
<administrative-flowcontrol>full</administrative-flowcontrol>
|
||||
<administrative-speed>auto</administrative-speed>
|
||||
<is-administrative-auto-negotiate>true</is-administrative-auto-negotiate>
|
||||
<is-administrative-up>true</is-administrative-up>
|
||||
<is-operational-auto-negotiate>true</is-operational-auto-negotiate>
|
||||
<link-status>up</link-status>
|
||||
<mac-address>00:0c:29:fc:04:f7</mac-address>
|
||||
<mtu>1500</mtu>
|
||||
<node>%(node_name)s</node>
|
||||
<operational-duplex>full</operational-duplex>
|
||||
<operational-flowcontrol>none</operational-flowcontrol>
|
||||
<operational-speed>1000</operational-speed>
|
||||
<port>e0d</port>
|
||||
<port-type>physical</port-type>
|
||||
<role>data</role>
|
||||
</net-port-info>
|
||||
</attributes-list>
|
||||
<num-records>4</num-records>
|
||||
</results>
|
||||
""" % {'node_name': NODE_NAME})
|
||||
|
||||
PORTS = ['e0a', 'e0b', 'e0c', 'e0d']
|
||||
|
||||
NET_INTERFACE_GET_ITER_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<net-interface-info>
|
||||
<address>192.168.228.42</address>
|
||||
<address-family>ipv4</address-family>
|
||||
<administrative-status>up</administrative-status>
|
||||
<current-node>%(node)s</current-node>
|
||||
<current-port>e0c</current-port>
|
||||
<data-protocols>
|
||||
<data-protocol>none</data-protocol>
|
||||
</data-protocols>
|
||||
<dns-domain-name>none</dns-domain-name>
|
||||
<failover-group>system-defined</failover-group>
|
||||
<failover-policy>disabled</failover-policy>
|
||||
<firewall-policy>mgmt</firewall-policy>
|
||||
<home-node>%(node)s</home-node>
|
||||
<home-port>e0c</home-port>
|
||||
<interface-name>cluster_mgmt</interface-name>
|
||||
<is-auto-revert>true</is-auto-revert>
|
||||
<is-home>true</is-home>
|
||||
<lif-uuid>d3230112-7524-11e4-8608-123478563412</lif-uuid>
|
||||
<listen-for-dns-query>false</listen-for-dns-query>
|
||||
<netmask>%(netmask)s</netmask>
|
||||
<netmask-length>24</netmask-length>
|
||||
<operational-status>up</operational-status>
|
||||
<role>cluster_mgmt</role>
|
||||
<routing-group-name>c192.168.228.0/24</routing-group-name>
|
||||
<use-failover-group>system_defined</use-failover-group>
|
||||
<vserver>cluster3</vserver>
|
||||
</net-interface-info>
|
||||
<net-interface-info>
|
||||
<address>192.168.228.43</address>
|
||||
<address-family>ipv4</address-family>
|
||||
<administrative-status>up</administrative-status>
|
||||
<current-node>%(node)s</current-node>
|
||||
<current-port>e0d</current-port>
|
||||
<dns-domain-name>none</dns-domain-name>
|
||||
<failover-group>system-defined</failover-group>
|
||||
<failover-policy>nextavail</failover-policy>
|
||||
<firewall-policy>mgmt</firewall-policy>
|
||||
<home-node>%(node)s</home-node>
|
||||
<home-port>e0d</home-port>
|
||||
<interface-name>mgmt1</interface-name>
|
||||
<is-auto-revert>true</is-auto-revert>
|
||||
<is-home>true</is-home>
|
||||
<lif-uuid>0ccc57cc-7525-11e4-8608-123478563412</lif-uuid>
|
||||
<listen-for-dns-query>false</listen-for-dns-query>
|
||||
<netmask>%(netmask)s</netmask>
|
||||
<netmask-length>24</netmask-length>
|
||||
<operational-status>up</operational-status>
|
||||
<role>node_mgmt</role>
|
||||
<routing-group-name>n192.168.228.0/24</routing-group-name>
|
||||
<use-failover-group>system_defined</use-failover-group>
|
||||
<vserver>cluster3-01</vserver>
|
||||
</net-interface-info>
|
||||
<net-interface-info>
|
||||
<address>%(address)s</address>
|
||||
<address-family>ipv4</address-family>
|
||||
<administrative-status>up</administrative-status>
|
||||
<current-node>%(node)s</current-node>
|
||||
<current-port>%(vlan)s</current-port>
|
||||
<data-protocols>
|
||||
<data-protocol>nfs</data-protocol>
|
||||
<data-protocol>cifs</data-protocol>
|
||||
</data-protocols>
|
||||
<dns-domain-name>none</dns-domain-name>
|
||||
<failover-group>system-defined</failover-group>
|
||||
<failover-policy>nextavail</failover-policy>
|
||||
<firewall-policy>data</firewall-policy>
|
||||
<home-node>%(node)s</home-node>
|
||||
<home-port>%(vlan)s</home-port>
|
||||
<interface-name>%(lif)s</interface-name>
|
||||
<is-auto-revert>false</is-auto-revert>
|
||||
<is-home>true</is-home>
|
||||
<lif-uuid>db4d91b6-95d9-11e4-8608-123478563412</lif-uuid>
|
||||
<listen-for-dns-query>false</listen-for-dns-query>
|
||||
<netmask>%(netmask)s</netmask>
|
||||
<netmask-length>24</netmask-length>
|
||||
<operational-status>up</operational-status>
|
||||
<role>data</role>
|
||||
<routing-group-name>d10.0.0.0/24</routing-group-name>
|
||||
<use-failover-group>system_defined</use-failover-group>
|
||||
<vserver>%(vserver)s</vserver>
|
||||
</net-interface-info>
|
||||
</attributes-list>
|
||||
<num-records>3</num-records>
|
||||
</results>
|
||||
""" % {
|
||||
'lif': LIF_NAME,
|
||||
'vserver': VSERVER_NAME,
|
||||
'node': NODE_NAME,
|
||||
'address': IP_ADDRESS,
|
||||
'netmask': NETMASK,
|
||||
'vlan': VLAN_PORT
|
||||
})
|
||||
|
||||
LIF_NAMES = ['cluster_mgmt', 'mgmt1', LIF_NAME]
|
||||
|
||||
LIFS = [
|
||||
{'address': '192.168.228.42',
|
||||
'home-node': NODE_NAME,
|
||||
'home-port': 'e0c',
|
||||
'interface-name': 'cluster_mgmt',
|
||||
'netmask': NETMASK,
|
||||
'role': 'cluster_mgmt',
|
||||
'vserver': 'cluster3'
|
||||
},
|
||||
{'address': '192.168.228.43',
|
||||
'home-node': NODE_NAME,
|
||||
'home-port': 'e0d',
|
||||
'interface-name': 'mgmt1',
|
||||
'netmask': NETMASK,
|
||||
'role': 'node_mgmt',
|
||||
'vserver': 'cluster3-01'
|
||||
},
|
||||
{'address': IP_ADDRESS,
|
||||
'home-node': NODE_NAME,
|
||||
'home-port': VLAN_PORT,
|
||||
'interface-name': LIF_NAME,
|
||||
'netmask': NETMASK,
|
||||
'role': 'data',
|
||||
'vserver': VSERVER_NAME
|
||||
}
|
||||
]
|
||||
|
||||
NET_INTERFACE_GET_ONE_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<net-interface-info>
|
||||
<interface-name>%(lif)s</interface-name>
|
||||
<vserver>%(vserver)s</vserver>
|
||||
</net-interface-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'lif': LIF_NAME, 'vserver': VSERVER_NAME})
|
||||
|
||||
AGGR_GET_NAMES_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<aggr-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<plex-name>/aggr0/plex0</plex-name>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/aggr0/plex0/rg0</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
</aggr-raid-attributes>
|
||||
<aggregate-name>aggr0</aggregate-name>
|
||||
</aggr-attributes>
|
||||
<aggr-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<plex-name>/manila/plex0</plex-name>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/manila/plex0/rg0</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/manila/plex0/rg1</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
</aggr-raid-attributes>
|
||||
<aggregate-name>manila</aggregate-name>
|
||||
</aggr-attributes>
|
||||
</attributes-list>
|
||||
<num-records>2</num-records>
|
||||
</results>
|
||||
""")
|
||||
|
||||
AGGR_NAMES = ['aggr0', 'manila']
|
||||
|
||||
AGGR_GET_SPACE_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<aggr-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<plex-name>/aggr0/plex0</plex-name>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/aggr0/plex0/rg0</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
</aggr-raid-attributes>
|
||||
<aggr-space-attributes>
|
||||
<size-available>45678592</size-available>
|
||||
<size-total>943718400</size-total>
|
||||
</aggr-space-attributes>
|
||||
<aggregate-name>aggr0</aggregate-name>
|
||||
</aggr-attributes>
|
||||
<aggr-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<plex-name>/manila/plex0</plex-name>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/manila/plex0/rg0</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
<raidgroup-attributes>
|
||||
<raidgroup-name>/manila/plex0/rg1</raidgroup-name>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
</aggr-raid-attributes>
|
||||
<aggr-space-attributes>
|
||||
<size-available>6448435200</size-available>
|
||||
<size-total>7549747200</size-total>
|
||||
</aggr-space-attributes>
|
||||
<aggregate-name>manila</aggregate-name>
|
||||
</aggr-attributes>
|
||||
</attributes-list>
|
||||
<num-records>2</num-records>
|
||||
</results>
|
||||
""")
|
||||
|
||||
AGGR_GET_ITER_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<aggr-attributes>
|
||||
<aggr-64bit-upgrade-attributes>
|
||||
<aggr-status-attributes>
|
||||
<is-64-bit-upgrade-in-progress>false</is-64-bit-upgrade-in-progress>
|
||||
</aggr-status-attributes>
|
||||
</aggr-64bit-upgrade-attributes>
|
||||
<aggr-fs-attributes>
|
||||
<block-type>64_bit</block-type>
|
||||
<fsid>1758646411</fsid>
|
||||
<type>aggr</type>
|
||||
</aggr-fs-attributes>
|
||||
<aggr-inode-attributes>
|
||||
<files-private-used>512</files-private-used>
|
||||
<files-total>30384</files-total>
|
||||
<files-used>96</files-used>
|
||||
<inodefile-private-capacity>30384</inodefile-private-capacity>
|
||||
<inodefile-public-capacity>30384</inodefile-public-capacity>
|
||||
<maxfiles-available>30384</maxfiles-available>
|
||||
<maxfiles-possible>243191</maxfiles-possible>
|
||||
<maxfiles-used>96</maxfiles-used>
|
||||
<percent-inode-used-capacity>0</percent-inode-used-capacity>
|
||||
</aggr-inode-attributes>
|
||||
<aggr-ownership-attributes>
|
||||
<home-id>4082368507</home-id>
|
||||
<home-name>cluster3-01</home-name>
|
||||
<owner-id>4082368507</owner-id>
|
||||
<owner-name>cluster3-01</owner-name>
|
||||
</aggr-ownership-attributes>
|
||||
<aggr-performance-attributes>
|
||||
<free-space-realloc>off</free-space-realloc>
|
||||
<max-write-alloc-blocks>0</max-write-alloc-blocks>
|
||||
</aggr-performance-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<checksum-status>active</checksum-status>
|
||||
<checksum-style>block</checksum-style>
|
||||
<disk-count>3</disk-count>
|
||||
<ha-policy>cfo</ha-policy>
|
||||
<has-local-root>true</has-local-root>
|
||||
<has-partner-root>false</has-partner-root>
|
||||
<is-checksum-enabled>true</is-checksum-enabled>
|
||||
<is-hybrid>false</is-hybrid>
|
||||
<is-hybrid-enabled>false</is-hybrid-enabled>
|
||||
<is-inconsistent>false</is-inconsistent>
|
||||
<mirror-status>unmirrored</mirror-status>
|
||||
<mount-state>online</mount-state>
|
||||
<plex-count>1</plex-count>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<is-online>true</is-online>
|
||||
<is-resyncing>false</is-resyncing>
|
||||
<plex-name>/aggr0/plex0</plex-name>
|
||||
<plex-status>normal,active</plex-status>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<checksum-style>block</checksum-style>
|
||||
<is-cache-tier>false</is-cache-tier>
|
||||
<is-recomputing-parity>false</is-recomputing-parity>
|
||||
<is-reconstructing>false</is-reconstructing>
|
||||
<raidgroup-name>/aggr0/plex0/rg0</raidgroup-name>
|
||||
<recomputing-parity-percentage>0</recomputing-parity-percentage>
|
||||
<reconstruction-percentage>0</reconstruction-percentage>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
<resyncing-percentage>0</resyncing-percentage>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
<raid-lost-write-state>on</raid-lost-write-state>
|
||||
<raid-size>16</raid-size>
|
||||
<raid-status>raid_dp, normal</raid-status>
|
||||
<raid-type>raid_dp</raid-type>
|
||||
<state>online</state>
|
||||
</aggr-raid-attributes>
|
||||
<aggr-snaplock-attributes>
|
||||
<is-snaplock>false</is-snaplock>
|
||||
</aggr-snaplock-attributes>
|
||||
<aggr-snapshot-attributes>
|
||||
<files-total>0</files-total>
|
||||
<files-used>0</files-used>
|
||||
<is-snapshot-auto-create-enabled>true</is-snapshot-auto-create-enabled>
|
||||
<is-snapshot-auto-delete-enabled>true</is-snapshot-auto-delete-enabled>
|
||||
<maxfiles-available>0</maxfiles-available>
|
||||
<maxfiles-possible>0</maxfiles-possible>
|
||||
<maxfiles-used>0</maxfiles-used>
|
||||
<percent-inode-used-capacity>0</percent-inode-used-capacity>
|
||||
<percent-used-capacity>0</percent-used-capacity>
|
||||
<size-available>0</size-available>
|
||||
<size-total>0</size-total>
|
||||
<size-used>0</size-used>
|
||||
<snapshot-reserve-percent>0</snapshot-reserve-percent>
|
||||
</aggr-snapshot-attributes>
|
||||
<aggr-space-attributes>
|
||||
<aggregate-metadata>245760</aggregate-metadata>
|
||||
<hybrid-cache-size-total>0</hybrid-cache-size-total>
|
||||
<percent-used-capacity>95</percent-used-capacity>
|
||||
<size-available>45670400</size-available>
|
||||
<size-total>943718400</size-total>
|
||||
<size-used>898048000</size-used>
|
||||
<total-reserved-space>0</total-reserved-space>
|
||||
<used-including-snapshot-reserve>898048000</used-including-snapshot-reserve>
|
||||
<volume-footprints>897802240</volume-footprints>
|
||||
</aggr-space-attributes>
|
||||
<aggr-volume-count-attributes>
|
||||
<flexvol-count>1</flexvol-count>
|
||||
<flexvol-count-collective>0</flexvol-count-collective>
|
||||
<flexvol-count-striped>0</flexvol-count-striped>
|
||||
</aggr-volume-count-attributes>
|
||||
<aggregate-name>aggr0</aggregate-name>
|
||||
<aggregate-uuid>15863632-ea49-49a8-9c88-2bd2d57c6d7a</aggregate-uuid>
|
||||
<nodes>
|
||||
<node-name>cluster3-01</node-name>
|
||||
</nodes>
|
||||
<striping-type>unknown</striping-type>
|
||||
</aggr-attributes>
|
||||
<aggr-attributes>
|
||||
<aggr-64bit-upgrade-attributes>
|
||||
<aggr-status-attributes>
|
||||
<is-64-bit-upgrade-in-progress>false</is-64-bit-upgrade-in-progress>
|
||||
</aggr-status-attributes>
|
||||
</aggr-64bit-upgrade-attributes>
|
||||
<aggr-fs-attributes>
|
||||
<block-type>64_bit</block-type>
|
||||
<fsid>706602229</fsid>
|
||||
<type>aggr</type>
|
||||
</aggr-fs-attributes>
|
||||
<aggr-inode-attributes>
|
||||
<files-private-used>528</files-private-used>
|
||||
<files-total>31142</files-total>
|
||||
<files-used>96</files-used>
|
||||
<inodefile-private-capacity>31142</inodefile-private-capacity>
|
||||
<inodefile-public-capacity>31142</inodefile-public-capacity>
|
||||
<maxfiles-available>31142</maxfiles-available>
|
||||
<maxfiles-possible>1945584</maxfiles-possible>
|
||||
<maxfiles-used>96</maxfiles-used>
|
||||
<percent-inode-used-capacity>0</percent-inode-used-capacity>
|
||||
</aggr-inode-attributes>
|
||||
<aggr-ownership-attributes>
|
||||
<home-id>4082368507</home-id>
|
||||
<home-name>cluster3-01</home-name>
|
||||
<owner-id>4082368507</owner-id>
|
||||
<owner-name>cluster3-01</owner-name>
|
||||
</aggr-ownership-attributes>
|
||||
<aggr-performance-attributes>
|
||||
<free-space-realloc>off</free-space-realloc>
|
||||
<max-write-alloc-blocks>0</max-write-alloc-blocks>
|
||||
</aggr-performance-attributes>
|
||||
<aggr-raid-attributes>
|
||||
<checksum-status>active</checksum-status>
|
||||
<checksum-style>block</checksum-style>
|
||||
<disk-count>10</disk-count>
|
||||
<ha-policy>sfo</ha-policy>
|
||||
<has-local-root>false</has-local-root>
|
||||
<has-partner-root>false</has-partner-root>
|
||||
<is-checksum-enabled>true</is-checksum-enabled>
|
||||
<is-hybrid>false</is-hybrid>
|
||||
<is-hybrid-enabled>false</is-hybrid-enabled>
|
||||
<is-inconsistent>false</is-inconsistent>
|
||||
<mirror-status>unmirrored</mirror-status>
|
||||
<mount-state>online</mount-state>
|
||||
<plex-count>1</plex-count>
|
||||
<plexes>
|
||||
<plex-attributes>
|
||||
<is-online>true</is-online>
|
||||
<is-resyncing>false</is-resyncing>
|
||||
<plex-name>/manila/plex0</plex-name>
|
||||
<plex-status>normal,active</plex-status>
|
||||
<raidgroups>
|
||||
<raidgroup-attributes>
|
||||
<checksum-style>block</checksum-style>
|
||||
<is-cache-tier>false</is-cache-tier>
|
||||
<is-recomputing-parity>false</is-recomputing-parity>
|
||||
<is-reconstructing>false</is-reconstructing>
|
||||
<raidgroup-name>/manila/plex0/rg0</raidgroup-name>
|
||||
<recomputing-parity-percentage>0</recomputing-parity-percentage>
|
||||
<reconstruction-percentage>0</reconstruction-percentage>
|
||||
</raidgroup-attributes>
|
||||
<raidgroup-attributes>
|
||||
<checksum-style>block</checksum-style>
|
||||
<is-cache-tier>false</is-cache-tier>
|
||||
<is-recomputing-parity>false</is-recomputing-parity>
|
||||
<is-reconstructing>false</is-reconstructing>
|
||||
<raidgroup-name>/manila/plex0/rg1</raidgroup-name>
|
||||
<recomputing-parity-percentage>0</recomputing-parity-percentage>
|
||||
<reconstruction-percentage>0</reconstruction-percentage>
|
||||
</raidgroup-attributes>
|
||||
</raidgroups>
|
||||
<resyncing-percentage>0</resyncing-percentage>
|
||||
</plex-attributes>
|
||||
</plexes>
|
||||
<raid-lost-write-state>on</raid-lost-write-state>
|
||||
<raid-size>8</raid-size>
|
||||
<raid-status>raid4, normal</raid-status>
|
||||
<raid-type>raid4</raid-type>
|
||||
<state>online</state>
|
||||
</aggr-raid-attributes>
|
||||
<aggr-snaplock-attributes>
|
||||
<is-snaplock>false</is-snaplock>
|
||||
</aggr-snaplock-attributes>
|
||||
<aggr-snapshot-attributes>
|
||||
<files-total>0</files-total>
|
||||
<files-used>0</files-used>
|
||||
<is-snapshot-auto-create-enabled>true</is-snapshot-auto-create-enabled>
|
||||
<is-snapshot-auto-delete-enabled>true</is-snapshot-auto-delete-enabled>
|
||||
<maxfiles-available>0</maxfiles-available>
|
||||
<maxfiles-possible>0</maxfiles-possible>
|
||||
<maxfiles-used>0</maxfiles-used>
|
||||
<percent-inode-used-capacity>0</percent-inode-used-capacity>
|
||||
<percent-used-capacity>0</percent-used-capacity>
|
||||
<size-available>0</size-available>
|
||||
<size-total>0</size-total>
|
||||
<size-used>0</size-used>
|
||||
<snapshot-reserve-percent>0</snapshot-reserve-percent>
|
||||
</aggr-snapshot-attributes>
|
||||
<aggr-space-attributes>
|
||||
<aggregate-metadata>425984</aggregate-metadata>
|
||||
<hybrid-cache-size-total>0</hybrid-cache-size-total>
|
||||
<percent-used-capacity>15</percent-used-capacity>
|
||||
<size-available>6448431104</size-available>
|
||||
<size-total>7549747200</size-total>
|
||||
<size-used>1101316096</size-used>
|
||||
<total-reserved-space>0</total-reserved-space>
|
||||
<used-including-snapshot-reserve>1101316096</used-including-snapshot-reserve>
|
||||
<volume-footprints>1100890112</volume-footprints>
|
||||
</aggr-space-attributes>
|
||||
<aggr-volume-count-attributes>
|
||||
<flexvol-count>2</flexvol-count>
|
||||
<flexvol-count-collective>0</flexvol-count-collective>
|
||||
<flexvol-count-striped>0</flexvol-count-striped>
|
||||
</aggr-volume-count-attributes>
|
||||
<aggregate-name>manila</aggregate-name>
|
||||
<aggregate-uuid>2a741934-1aaf-42dd-93ca-aaf231be108a</aggregate-uuid>
|
||||
<nodes>
|
||||
<node-name>cluster3-01</node-name>
|
||||
</nodes>
|
||||
<striping-type>not_striped</striping-type>
|
||||
</aggr-attributes>
|
||||
</attributes-list>
|
||||
<num-records>2</num-records>
|
||||
</results>
|
||||
""")
|
||||
|
||||
VOLUME_GET_NAME_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes>
|
||||
<name>%(volume)s</name>
|
||||
<owning-vserver-name>%(vserver)s</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
</volume-attributes>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'volume': SHARE_NAME, 'vserver': VSERVER_NAME})
|
||||
|
||||
VOLUME_GET_VOLUME_PATH_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<junction>/%(volume)s</junction>
|
||||
</results>
|
||||
""" % {'volume': SHARE_NAME})
|
||||
|
||||
VOLUME_GET_VOLUME_PATH_CIFS_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<junction>\\%(volume)s</junction>
|
||||
</results>
|
||||
""" % {'volume': SHARE_NAME})
|
||||
|
||||
VOLUME_JUNCTION_PATH = '/' + SHARE_NAME
|
||||
VOLUME_JUNCTION_PATH_CIFS = '\\' + SHARE_NAME
|
||||
|
||||
SNAPSHOT_GET_ITER_NOT_BUSY_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<snapshot-info>
|
||||
<busy>false</busy>
|
||||
<name>%(snap)s</name>
|
||||
<volume>%(volume)s</volume>
|
||||
<vserver>%(vserver)s</vserver>
|
||||
</snapshot-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'snap': SNAPSHOT_NAME, 'volume': SHARE_NAME, 'vserver': VSERVER_NAME})
|
||||
|
||||
SNAPSHOT_GET_ITER_BUSY_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<attributes-list>
|
||||
<snapshot-info>
|
||||
<busy>true</busy>
|
||||
<name>%(snap)s</name>
|
||||
<volume>%(volume)s</volume>
|
||||
<vserver>%(vserver)s</vserver>
|
||||
</snapshot-info>
|
||||
</attributes-list>
|
||||
<num-records>1</num-records>
|
||||
</results>
|
||||
""" % {'snap': SNAPSHOT_NAME, 'volume': SHARE_NAME, 'vserver': VSERVER_NAME})
|
||||
|
||||
NFS_EXPORT_RULES = ['10.10.10.10', '10.10.10.20']
|
||||
|
||||
NFS_EXPORTFS_LIST_RULES_2_NO_RULES_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<rules />
|
||||
</results>
|
||||
""")
|
||||
|
||||
NFS_EXPORTFS_LIST_RULES_2_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<rules>
|
||||
<exports-rule-info-2>
|
||||
<pathname>%(path)s</pathname>
|
||||
<security-rules>
|
||||
<security-rule-info>
|
||||
<anon>65534</anon>
|
||||
<nosuid>false</nosuid>
|
||||
<read-only>
|
||||
<exports-hostname-info>
|
||||
<name>%(host1)s</name>
|
||||
</exports-hostname-info>
|
||||
<exports-hostname-info>
|
||||
<name>%(host2)s</name>
|
||||
</exports-hostname-info>
|
||||
</read-only>
|
||||
<read-write>
|
||||
<exports-hostname-info>
|
||||
<name>%(host1)s</name>
|
||||
</exports-hostname-info>
|
||||
<exports-hostname-info>
|
||||
<name>%(host2)s</name>
|
||||
</exports-hostname-info>
|
||||
</read-write>
|
||||
<root>
|
||||
<exports-hostname-info>
|
||||
<name>%(host1)s</name>
|
||||
</exports-hostname-info>
|
||||
<exports-hostname-info>
|
||||
<name>%(host2)s</name>
|
||||
</exports-hostname-info>
|
||||
</root>
|
||||
<sec-flavor>
|
||||
<sec-flavor-info>
|
||||
<flavor>sys</flavor>
|
||||
</sec-flavor-info>
|
||||
</sec-flavor>
|
||||
</security-rule-info>
|
||||
</security-rules>
|
||||
</exports-rule-info-2>
|
||||
</rules>
|
||||
</results>
|
||||
""" % {
|
||||
'path': VOLUME_JUNCTION_PATH,
|
||||
'host1': NFS_EXPORT_RULES[0],
|
||||
'host2': NFS_EXPORT_RULES[1],
|
||||
})
|
|
@ -0,0 +1,156 @@
|
|||
# Copyright (c) 2014 Ben Swartzlander. All rights reserved.
|
||||
# Copyright (c) 2014 Navneet Singh. All rights reserved.
|
||||
# Copyright (c) 2014 Clinton Knight. All rights reserved.
|
||||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2014 Bob Callaway. 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.
|
||||
"""
|
||||
Tests for NetApp API layer
|
||||
"""
|
||||
|
||||
from manila.share.drivers.netapp.dataontap.client import api
|
||||
from manila import test
|
||||
|
||||
|
||||
class NetAppApiElementTransTests(test.TestCase):
|
||||
"""Test case for NetApp API element translations."""
|
||||
|
||||
def test_translate_struct_dict_unique_key(self):
|
||||
"""Tests if dict gets properly converted to NaElements."""
|
||||
root = api.NaElement('root')
|
||||
child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'}
|
||||
|
||||
root.translate_struct(child)
|
||||
|
||||
self.assertEqual(3, len(root.get_children()))
|
||||
for key, value in child.items():
|
||||
self.assertEqual(value, root.get_child_content(key))
|
||||
|
||||
def test_translate_struct_dict_nonunique_key(self):
|
||||
"""Tests if list/dict gets properly converted to NaElements."""
|
||||
root = api.NaElement('root')
|
||||
child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}]
|
||||
|
||||
root.translate_struct(child)
|
||||
|
||||
children = root.get_children()
|
||||
self.assertEqual(3, len(children))
|
||||
for c in children:
|
||||
if c.get_name() == 'e1':
|
||||
self.assertIn(c.get_content(), ['v1', 'v3'])
|
||||
else:
|
||||
self.assertEqual('v2', c.get_content())
|
||||
|
||||
def test_translate_struct_list(self):
|
||||
"""Tests if list gets properly converted to NaElements."""
|
||||
root = api.NaElement('root')
|
||||
child = ['e1', 'e2']
|
||||
|
||||
root.translate_struct(child)
|
||||
|
||||
self.assertEqual(2, len(root.get_children()))
|
||||
self.assertIsNone(root.get_child_content('e1'))
|
||||
self.assertIsNone(root.get_child_content('e2'))
|
||||
|
||||
def test_translate_struct_tuple(self):
|
||||
"""Tests if tuple gets properly converted to NaElements."""
|
||||
root = api.NaElement('root')
|
||||
child = ('e1', 'e2')
|
||||
|
||||
root.translate_struct(child)
|
||||
|
||||
self.assertEqual(2, len(root.get_children()))
|
||||
self.assertIsNone(root.get_child_content('e1'))
|
||||
self.assertIsNone(root.get_child_content('e2'))
|
||||
|
||||
def test_translate_invalid_struct(self):
|
||||
"""Tests if invalid data structure raises exception."""
|
||||
root = api.NaElement('root')
|
||||
child = 'random child element'
|
||||
self.assertRaises(ValueError, root.translate_struct, child)
|
||||
|
||||
def test_setter_builtin_types(self):
|
||||
"""Tests str, int, float get converted to NaElement."""
|
||||
update = dict(e1='v1', e2='1', e3='2.0', e4='8')
|
||||
root = api.NaElement('root')
|
||||
|
||||
for key, value in update.items():
|
||||
root[key] = value
|
||||
|
||||
for key, value in update.items():
|
||||
self.assertEqual(value, root.get_child_content(key))
|
||||
|
||||
def test_setter_na_element(self):
|
||||
"""Tests na_element gets appended as child."""
|
||||
root = api.NaElement('root')
|
||||
root['e1'] = api.NaElement('nested')
|
||||
self.assertEqual(1, len(root.get_children()))
|
||||
e1 = root.get_child_by_name('e1')
|
||||
self.assertIsInstance(e1, api.NaElement)
|
||||
self.assertIsInstance(e1.get_child_by_name('nested'), api.NaElement)
|
||||
|
||||
def test_setter_child_dict(self):
|
||||
"""Tests dict is appended as child to root."""
|
||||
root = api.NaElement('root')
|
||||
root['d'] = {'e1': 'v1', 'e2': 'v2'}
|
||||
e1 = root.get_child_by_name('d')
|
||||
self.assertIsInstance(e1, api.NaElement)
|
||||
sub_ch = e1.get_children()
|
||||
self.assertEqual(2, len(sub_ch))
|
||||
for c in sub_ch:
|
||||
self.assertIn(c.get_name(), ['e1', 'e2'])
|
||||
if c.get_name() == 'e1':
|
||||
self.assertEqual('v1', c.get_content())
|
||||
else:
|
||||
self.assertEqual('v2', c.get_content())
|
||||
|
||||
def test_setter_child_list_tuple(self):
|
||||
"""Tests list/tuple are appended as child to root."""
|
||||
root = api.NaElement('root')
|
||||
|
||||
root['l'] = ['l1', 'l2']
|
||||
root['t'] = ('t1', 't2')
|
||||
|
||||
l = root.get_child_by_name('l')
|
||||
self.assertIsInstance(l, api.NaElement)
|
||||
t = root.get_child_by_name('t')
|
||||
self.assertIsInstance(t, api.NaElement)
|
||||
|
||||
self.assertEqual(2, len(l.get_children()))
|
||||
for le in l.get_children():
|
||||
self.assertIn(le.get_name(), ['l1', 'l2'])
|
||||
|
||||
self.assertEqual(2, len(t.get_children()))
|
||||
for te in t.get_children():
|
||||
self.assertIn(te.get_name(), ['t1', 't2'])
|
||||
|
||||
def test_setter_no_value(self):
|
||||
"""Tests key with None value."""
|
||||
root = api.NaElement('root')
|
||||
root['k'] = None
|
||||
self.assertIsNone(root.get_child_content('k'))
|
||||
|
||||
def test_setter_invalid_value(self):
|
||||
"""Tests invalid value raises exception."""
|
||||
self.assertRaises(TypeError,
|
||||
api.NaElement('root').__setitem__,
|
||||
'k',
|
||||
api.NaServer('localhost'))
|
||||
|
||||
def test_setter_invalid_key(self):
|
||||
"""Tests invalid value raises exception."""
|
||||
self.assertRaises(KeyError,
|
||||
api.NaElement('root').__setitem__,
|
||||
None,
|
||||
'value')
|
|
@ -0,0 +1,117 @@
|
|||
# Copyright (c) 2014 Alex Meade. All rights reserved.
|
||||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
|
||||
import mock
|
||||
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.client import client_base
|
||||
from manila import test
|
||||
from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake
|
||||
|
||||
|
||||
class NetAppBaseClientTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppBaseClientTestCase, self).setUp()
|
||||
self.mock_object(client_base, 'LOG')
|
||||
self.client = client_base.NetAppBaseClient(**fake.CONNECTION_INFO)
|
||||
self.client.connection = mock.MagicMock()
|
||||
self.connection = self.client.connection
|
||||
|
||||
def test_get_ontapi_version(self):
|
||||
version_response = netapp_api.NaElement(fake.ONTAPI_VERSION_RESPONSE)
|
||||
self.connection.invoke_successfully.return_value = version_response
|
||||
|
||||
major, minor = self.client.get_ontapi_version(cached=False)
|
||||
|
||||
self.assertEqual('1', major)
|
||||
self.assertEqual('19', minor)
|
||||
|
||||
def test_get_ontapi_version_cached(self):
|
||||
|
||||
self.connection.get_api_version.return_value = (1, 20)
|
||||
|
||||
major, minor = self.client.get_ontapi_version()
|
||||
|
||||
self.assertEqual(1, self.connection.get_api_version.call_count)
|
||||
self.assertEqual(1, major)
|
||||
self.assertEqual(20, minor)
|
||||
|
||||
def test_check_is_naelement(self):
|
||||
|
||||
element = netapp_api.NaElement('name')
|
||||
|
||||
self.assertIsNone(self.client.check_is_naelement(element))
|
||||
self.assertRaises(ValueError, self.client.check_is_naelement, None)
|
||||
|
||||
def test_send_request(self):
|
||||
|
||||
element = netapp_api.NaElement('fake-api')
|
||||
|
||||
self.client.send_request('fake-api')
|
||||
|
||||
self.assertEqual(
|
||||
element.to_string(),
|
||||
self.connection.invoke_successfully.call_args[0][0].to_string())
|
||||
self.assertTrue(self.connection.invoke_successfully.call_args[0][1])
|
||||
|
||||
def test_send_request_no_tunneling(self):
|
||||
|
||||
element = netapp_api.NaElement('fake-api')
|
||||
|
||||
self.client.send_request('fake-api', enable_tunneling=False)
|
||||
|
||||
self.assertEqual(
|
||||
element.to_string(),
|
||||
self.connection.invoke_successfully.call_args[0][0].to_string())
|
||||
self.assertFalse(self.connection.invoke_successfully.call_args[0][1])
|
||||
|
||||
def test_send_request_with_args(self):
|
||||
|
||||
element = netapp_api.NaElement('fake-api')
|
||||
api_args = {'arg1': 'data1', 'arg2': 'data2'}
|
||||
element.translate_struct(api_args)
|
||||
|
||||
self.client.send_request('fake-api', api_args=api_args)
|
||||
|
||||
self.assertEqual(
|
||||
element.to_string(),
|
||||
self.connection.invoke_successfully.call_args[0][0].to_string())
|
||||
self.assertTrue(self.connection.invoke_successfully.call_args[0][1])
|
||||
|
||||
def test_get_licenses(self):
|
||||
|
||||
api_response = netapp_api.NaElement(fake.LICENSE_V2_LIST_INFO_RESPONSE)
|
||||
self.mock_object(
|
||||
self.client, 'send_request', mock.Mock(return_value=api_response))
|
||||
|
||||
response = self.client.get_licenses()
|
||||
|
||||
self.assertListEqual(fake.LICENSES, response)
|
||||
|
||||
def test_get_licenses_api_error(self):
|
||||
|
||||
self.mock_object(self.client,
|
||||
'send_request',
|
||||
mock.Mock(side_effect=netapp_api.NaApiError))
|
||||
|
||||
self.assertRaises(netapp_api.NaApiError, self.client.get_licenses)
|
||||
self.assertEqual(1, client_base.LOG.error.call_count)
|
||||
|
||||
def test_send_ems_log_message(self):
|
||||
|
||||
self.assertRaises(NotImplementedError,
|
||||
self.client.send_ems_log_message,
|
||||
{})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,962 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
Mock unit tests for the NetApp Data ONTAP cDOT storage driver library.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import socket
|
||||
|
||||
import mock
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import units
|
||||
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.client import client_cmode
|
||||
from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base
|
||||
from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode
|
||||
from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila import test
|
||||
import manila.tests.share.drivers.netapp.dataontap.fakes as fake
|
||||
import manila.tests.share.drivers.netapp.fakes as na_fakes
|
||||
|
||||
|
||||
class EnsureVserverDecoratorTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(EnsureVserverDecoratorTestCase, self).setUp()
|
||||
self._client = mock.Mock()
|
||||
|
||||
@lib_base.ensure_vserver
|
||||
def ensure_vserver_test_method(*args, **kwargs):
|
||||
return 'OK'
|
||||
|
||||
def test_ensure_vserver(self):
|
||||
|
||||
self._client.vserver_exists.return_value = True
|
||||
kwargs = {'share_server': fake.SHARE_SERVER}
|
||||
|
||||
result = self.ensure_vserver_test_method(**kwargs)
|
||||
|
||||
self.assertEqual('OK', result)
|
||||
|
||||
def test_ensure_vserver_no_share_server(self):
|
||||
|
||||
self._client.vserver_exists.return_value = True
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.ensure_vserver_test_method)
|
||||
|
||||
def test_ensure_vserver_no_backend_details(self):
|
||||
|
||||
self._client.vserver_exists.return_value = True
|
||||
fake_share_server = copy.deepcopy(fake.SHARE_SERVER)
|
||||
fake_share_server['backend_details'] = None
|
||||
kwargs = {'share_server': fake_share_server}
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.ensure_vserver_test_method,
|
||||
**kwargs)
|
||||
|
||||
def test_ensure_vserver_no_vserver_name(self):
|
||||
|
||||
self._client.vserver_exists.return_value = True
|
||||
fake_share_server = copy.deepcopy(fake.SHARE_SERVER)
|
||||
fake_share_server['backend_details']['vserver_name'] = None
|
||||
kwargs = {'share_server': fake_share_server}
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.ensure_vserver_test_method,
|
||||
**kwargs)
|
||||
|
||||
def test_ensure_vserver_not_found(self):
|
||||
|
||||
self._client.vserver_exists.return_value = False
|
||||
kwargs = {'share_server': fake.SHARE_SERVER}
|
||||
|
||||
self.assertRaises(exception.VserverUnavailable,
|
||||
self.ensure_vserver_test_method,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class NetAppFileStorageLibraryTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppFileStorageLibraryTestCase, self).setUp()
|
||||
|
||||
self.mock_object(na_utils, 'validate_instantiation')
|
||||
self.mock_object(na_utils, 'setup_tracing')
|
||||
self.mock_object(lib_base, 'LOG')
|
||||
|
||||
self.mock_db = mock.Mock()
|
||||
kwargs = {
|
||||
'configuration': self._get_config_cmode(),
|
||||
'app_version': fake.APP_VERSION
|
||||
}
|
||||
self.library = lib_base.NetAppCmodeFileStorageLibrary(self.mock_db,
|
||||
fake.DRIVER_NAME,
|
||||
**kwargs)
|
||||
self.library._client = mock.Mock()
|
||||
self.client = self.library._client
|
||||
self.context = mock.Mock()
|
||||
|
||||
def _get_config_cmode(self):
|
||||
config = na_fakes.create_configuration_cmode()
|
||||
config.local_conf.set_override('share_backend_name',
|
||||
fake.BACKEND_NAME)
|
||||
config.netapp_login = fake.CLIENT_KWARGS['username']
|
||||
config.netapp_password = fake.CLIENT_KWARGS['password']
|
||||
config.netapp_server_hostname = fake.CLIENT_KWARGS['hostname']
|
||||
config.netapp_transport_type = fake.CLIENT_KWARGS['transport_type']
|
||||
config.netapp_server_port = fake.CLIENT_KWARGS['port']
|
||||
config.netapp_vserver = fake.VSERVER1
|
||||
config.netapp_volume_name_template = fake.VOLUME_NAME_TEMPLATE
|
||||
config.netapp_aggregate_name_search_pattern = \
|
||||
fake.AGGREGATE_NAME_SEARCH_PATTERN
|
||||
config.netapp_vserver_name_template = fake.VSERVER_NAME_TEMPLATE
|
||||
config.netapp_root_volume_aggregate = fake.ROOT_VOLUME_AGGREGATE
|
||||
config.netapp_root_volume = fake.ROOT_VOLUME
|
||||
config.netapp_lif_name_template = fake.LIF_NAME_TEMPLATE
|
||||
return config
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(fake.DRIVER_NAME, self.library.driver_name)
|
||||
self.assertEqual(self.mock_db, self.library.db)
|
||||
self.assertEqual(1, na_utils.validate_instantiation.call_count)
|
||||
self.assertEqual(1, na_utils.setup_tracing.call_count)
|
||||
self.assertIsNone(self.library._helpers)
|
||||
self.assertListEqual([], self.library._licenses)
|
||||
self.assertDictEqual({}, self.library._clients)
|
||||
self.assertIsNotNone(self.library._app_version)
|
||||
self.assertIsNotNone(self.library._last_ems)
|
||||
|
||||
def test_do_setup(self):
|
||||
mock_setup_helpers = self.mock_object(self.library, '_setup_helpers')
|
||||
mock_get_api_client = self.mock_object(self.library,
|
||||
'_get_api_client')
|
||||
self.library.do_setup(self.context)
|
||||
|
||||
mock_get_api_client.assert_called_once_with()
|
||||
mock_setup_helpers.assert_called_once_with()
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
mock_get_licenses = self.mock_object(self.library, '_get_licenses')
|
||||
|
||||
self.library.check_for_setup_error()
|
||||
|
||||
mock_get_licenses.assert_called_once_with()
|
||||
|
||||
def test_get_api_client(self):
|
||||
|
||||
client_kwargs = fake.CLIENT_KWARGS.copy()
|
||||
|
||||
# First call should proceed normally.
|
||||
mock_client_constructor = self.mock_object(client_cmode,
|
||||
'NetAppCmodeClient')
|
||||
client1 = self.library._get_api_client()
|
||||
self.assertIsNotNone(client1)
|
||||
mock_client_constructor.assert_called_once_with(**client_kwargs)
|
||||
|
||||
# Second call should yield the same object.
|
||||
mock_client_constructor = self.mock_object(client_cmode,
|
||||
'NetAppCmodeClient')
|
||||
client2 = self.library._get_api_client()
|
||||
self.assertEqual(client1, client2)
|
||||
self.assertFalse(mock_client_constructor.called)
|
||||
|
||||
def test_get_api_client_with_vserver(self):
|
||||
|
||||
client_kwargs = fake.CLIENT_KWARGS.copy()
|
||||
client_kwargs['vserver'] = fake.VSERVER1
|
||||
|
||||
# First call should proceed normally.
|
||||
mock_client_constructor = self.mock_object(client_cmode,
|
||||
'NetAppCmodeClient')
|
||||
client1 = self.library._get_api_client(vserver=fake.VSERVER1)
|
||||
self.assertIsNotNone(client1)
|
||||
mock_client_constructor.assert_called_once_with(**client_kwargs)
|
||||
|
||||
# Second call should yield the same object.
|
||||
mock_client_constructor = self.mock_object(client_cmode,
|
||||
'NetAppCmodeClient')
|
||||
client2 = self.library._get_api_client(vserver=fake.VSERVER1)
|
||||
self.assertEqual(client1, client2)
|
||||
self.assertFalse(mock_client_constructor.called)
|
||||
|
||||
# A different vserver should work normally without caching.
|
||||
mock_client_constructor = self.mock_object(client_cmode,
|
||||
'NetAppCmodeClient')
|
||||
client3 = self.library._get_api_client(vserver=fake.VSERVER2)
|
||||
self.assertNotEqual(client1, client3)
|
||||
client_kwargs['vserver'] = fake.VSERVER2
|
||||
mock_client_constructor.assert_called_once_with(**client_kwargs)
|
||||
|
||||
def test_get_licenses_both_protocols(self):
|
||||
self.mock_object(self.client,
|
||||
'get_licenses',
|
||||
mock.Mock(return_value=fake.LICENSES))
|
||||
|
||||
result = self.library._get_licenses()
|
||||
|
||||
self.assertListEqual(fake.LICENSES, result)
|
||||
self.assertEqual(0, lib_base.LOG.error.call_count)
|
||||
self.assertEqual(1, lib_base.LOG.info.call_count)
|
||||
|
||||
def test_get_licenses_one_protocol(self):
|
||||
licenses = list(fake.LICENSES)
|
||||
licenses.remove('nfs')
|
||||
self.mock_object(self.client,
|
||||
'get_licenses',
|
||||
mock.Mock(return_value=licenses))
|
||||
|
||||
result = self.library._get_licenses()
|
||||
|
||||
self.assertListEqual(licenses, result)
|
||||
self.assertEqual(0, lib_base.LOG.error.call_count)
|
||||
self.assertEqual(1, lib_base.LOG.info.call_count)
|
||||
|
||||
def test_get_licenses_no_protocols(self):
|
||||
licenses = list(fake.LICENSES)
|
||||
licenses.remove('nfs')
|
||||
licenses.remove('cifs')
|
||||
self.mock_object(self.client,
|
||||
'get_licenses',
|
||||
mock.Mock(return_value=licenses))
|
||||
|
||||
result = self.library._get_licenses()
|
||||
|
||||
self.assertListEqual(licenses, result)
|
||||
self.assertEqual(1, lib_base.LOG.error.call_count)
|
||||
self.assertEqual(1, lib_base.LOG.info.call_count)
|
||||
|
||||
def test_get_valid_share_name(self):
|
||||
|
||||
result = self.library._get_valid_share_name(fake.SHARE_ID)
|
||||
expected = (fake.VOLUME_NAME_TEMPLATE %
|
||||
{'share_id': fake.SHARE_ID.replace('-', '_')})
|
||||
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_get_valid_snapshot_name(self):
|
||||
|
||||
result = self.library._get_valid_snapshot_name(fake.SNAPSHOT_ID)
|
||||
expected = 'share_snapshot_' + fake.SNAPSHOT_ID.replace('-', '_')
|
||||
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_get_share_stats(self):
|
||||
|
||||
self.mock_object(self.library, '_find_matching_aggregates')
|
||||
self.mock_object(self.client,
|
||||
'calculate_aggregate_capacity',
|
||||
mock.Mock(return_value=(fake.TOTAL_CAPACITY,
|
||||
fake.FREE_CAPACITY)))
|
||||
mock_handle_ems_logging = self.mock_object(self.library,
|
||||
'_handle_ems_logging')
|
||||
|
||||
result = self.library.get_share_stats()
|
||||
|
||||
expected = {
|
||||
'share_backend_name': fake.BACKEND_NAME,
|
||||
'driver_name': fake.DRIVER_NAME,
|
||||
'vendor_name': 'NetApp',
|
||||
'driver_version': '1.0',
|
||||
'storage_protocol': 'NFS_CIFS',
|
||||
'total_capacity_gb': fake.TOTAL_CAPACITY / units.Gi,
|
||||
'free_capacity_gb': fake.FREE_CAPACITY / units.Gi
|
||||
}
|
||||
|
||||
self.assertDictEqual(expected, result)
|
||||
self.assertTrue(mock_handle_ems_logging.called)
|
||||
|
||||
def test_handle_ems_logging(self):
|
||||
|
||||
self.mock_object(self.library,
|
||||
'_build_ems_log_message',
|
||||
mock.Mock(return_value=fake.EMS_MESSAGE))
|
||||
test_now = timeutils.utcnow() - datetime.timedelta(
|
||||
seconds=(self.library.AUTOSUPPORT_INTERVAL_SECONDS + 1))
|
||||
self.library._last_ems = test_now
|
||||
|
||||
self.library._handle_ems_logging()
|
||||
|
||||
self.assertTrue(self.library._last_ems > test_now)
|
||||
self.library._client.send_ems_log_message.assert_called_with(
|
||||
fake.EMS_MESSAGE)
|
||||
|
||||
def test_handle_ems_logging_not_yet(self):
|
||||
|
||||
self.mock_object(self.library,
|
||||
'_build_ems_log_message',
|
||||
mock.Mock(return_value=fake.EMS_MESSAGE))
|
||||
test_now = timeutils.utcnow() - datetime.timedelta(
|
||||
seconds=(self.library.AUTOSUPPORT_INTERVAL_SECONDS - 1))
|
||||
self.library._last_ems = test_now
|
||||
|
||||
self.library._handle_ems_logging()
|
||||
|
||||
self.assertEqual(test_now, self.library._last_ems)
|
||||
self.assertFalse(self.library._client.send_ems_log_message.called)
|
||||
|
||||
def test_build_ems_log_message(self):
|
||||
|
||||
self.mock_object(socket,
|
||||
'getfqdn',
|
||||
mock.Mock(return_value=fake.HOST_NAME))
|
||||
|
||||
result = self.library._build_ems_log_message()
|
||||
|
||||
fake_ems_log = {
|
||||
'computer-name': fake.HOST_NAME,
|
||||
'event-id': '0',
|
||||
'event-source': 'Manila driver %s' % fake.DRIVER_NAME,
|
||||
'app-version': fake.APP_VERSION,
|
||||
'category': 'provisioning',
|
||||
'event-description': 'OpenStack Manila connected to cluster node',
|
||||
'log-level': '6',
|
||||
'auto-support': 'false'
|
||||
}
|
||||
self.assertDictEqual(fake_ems_log, result)
|
||||
|
||||
def test_find_matching_aggregates(self):
|
||||
|
||||
self.mock_object(self.client,
|
||||
'list_aggregates',
|
||||
mock.Mock(return_value=fake.AGGREGATES))
|
||||
|
||||
self.library.configuration.netapp_aggregate_name_search_pattern =\
|
||||
fake.AGGREGATE_NAME_SEARCH_PATTERN
|
||||
result = self.library._find_matching_aggregates()
|
||||
self.assertListEqual(result, fake.AGGREGATES)
|
||||
|
||||
self.library.configuration.netapp_aggregate_name_search_pattern =\
|
||||
'aggr.*'
|
||||
result = self.library._find_matching_aggregates()
|
||||
self.assertListEqual(result, ['aggr0'])
|
||||
|
||||
def test_setup_helpers(self):
|
||||
|
||||
self.mock_object(cifs_cmode,
|
||||
'NetAppCmodeCIFSHelper',
|
||||
mock.Mock(return_value='fake_cifs_helper'))
|
||||
self.mock_object(nfs_cmode,
|
||||
'NetAppCmodeNFSHelper',
|
||||
mock.Mock(return_value='fake_nfs_helper'))
|
||||
self.library._helpers = None
|
||||
|
||||
self.library._setup_helpers()
|
||||
|
||||
self.assertDictEqual({'CIFS': 'fake_cifs_helper',
|
||||
'NFS': 'fake_nfs_helper'},
|
||||
self.library._helpers)
|
||||
|
||||
def test_get_helper(self):
|
||||
|
||||
self.library._helpers = {'CIFS': 'fake_cifs_helper',
|
||||
'NFS': 'fake_nfs_helper'}
|
||||
self.library._licenses = fake.LICENSES
|
||||
fake_share = fake.SHARE.copy()
|
||||
fake_share['share_proto'] = 'NFS'
|
||||
|
||||
result = self.library._get_helper(fake_share)
|
||||
|
||||
self.assertEqual('fake_nfs_helper', result)
|
||||
|
||||
def test_get_helper_newly_licensed_protocol(self):
|
||||
|
||||
self.mock_object(self.library,
|
||||
'_get_licenses',
|
||||
mock.Mock(return_value=['base', 'nfs']))
|
||||
self.library._helpers = {'CIFS': 'fake_cifs_helper',
|
||||
'NFS': 'fake_nfs_helper'}
|
||||
self.library._licenses = ['base']
|
||||
fake_share = fake.SHARE.copy()
|
||||
fake_share['share_proto'] = 'NFS'
|
||||
|
||||
result = self.library._get_helper(fake_share)
|
||||
|
||||
self.assertEqual('fake_nfs_helper', result)
|
||||
self.assertTrue(self.library._get_licenses.called)
|
||||
|
||||
def test_get_helper_unlicensed_protocol(self):
|
||||
|
||||
self.mock_object(self.library,
|
||||
'_get_licenses',
|
||||
mock.Mock(return_value=['base']))
|
||||
self.library._helpers = {'CIFS': 'fake_cifs_helper',
|
||||
'NFS': 'fake_nfs_helper'}
|
||||
self.library._licenses = ['base']
|
||||
fake_share = fake.SHARE.copy()
|
||||
fake_share['share_proto'] = 'NFS'
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.library._get_helper,
|
||||
fake_share)
|
||||
|
||||
def test_get_helper_invalid_protocol(self):
|
||||
|
||||
self.mock_object(self.library,
|
||||
'_get_licenses',
|
||||
mock.Mock(return_value=['base', 'iscsi']))
|
||||
self.library._helpers = {'CIFS': 'fake_cifs_helper',
|
||||
'NFS': 'fake_nfs_helper'}
|
||||
self.library._licenses = ['base', 'iscsi']
|
||||
fake_share = fake.SHARE.copy()
|
||||
fake_share['share_proto'] = 'iSCSI'
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.library._get_helper,
|
||||
fake_share)
|
||||
|
||||
def test_setup_server(self):
|
||||
|
||||
mock_create_vserver = self.mock_object(
|
||||
self.library, '_create_vserver_if_nonexistent',
|
||||
mock.Mock(return_value=fake.VSERVER1))
|
||||
|
||||
result = self.library.setup_server(fake.NETWORK_INFO)
|
||||
|
||||
self.assertTrue(mock_create_vserver.called)
|
||||
self.assertDictEqual({'vserver_name': fake.VSERVER1}, result)
|
||||
|
||||
def test_create_vserver_if_nonexistent(self):
|
||||
|
||||
vserver_id = fake.NETWORK_INFO['server_id']
|
||||
vserver_name = fake.VSERVER_NAME_TEMPLATE % vserver_id
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.mock_object(context,
|
||||
'get_admin_context',
|
||||
mock.Mock(return_value='fake_admin_context'))
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
self.mock_object(self.library._client,
|
||||
'vserver_exists',
|
||||
mock.Mock(return_value=False))
|
||||
self.mock_object(self.library,
|
||||
'_find_matching_aggregates',
|
||||
mock.Mock(return_value=fake.AGGREGATES))
|
||||
self.mock_object(self.library, '_create_vserver_lifs')
|
||||
|
||||
result = self.library._create_vserver_if_nonexistent(
|
||||
fake.NETWORK_INFO)
|
||||
|
||||
self.assertEqual(vserver_name, result)
|
||||
self.library.db.share_server_backend_details_set.assert_called_with(
|
||||
'fake_admin_context',
|
||||
vserver_id,
|
||||
{'vserver_name': vserver_name})
|
||||
self.library._get_api_client.assert_called_with(vserver=vserver_name)
|
||||
self.library._client.create_vserver.assert_called_with(
|
||||
vserver_name,
|
||||
fake.ROOT_VOLUME_AGGREGATE,
|
||||
fake.ROOT_VOLUME,
|
||||
fake.AGGREGATES)
|
||||
self.library._create_vserver_lifs.assert_called_with(
|
||||
vserver_name,
|
||||
vserver_client,
|
||||
fake.NETWORK_INFO)
|
||||
self.assertTrue(vserver_client.enable_nfs.called)
|
||||
self.library._client.setup_security_services.assert_called_with(
|
||||
fake.NETWORK_INFO['security_services'],
|
||||
vserver_client,
|
||||
vserver_name)
|
||||
|
||||
def test_create_vserver_if_nonexistent_already_present(self):
|
||||
|
||||
vserver_id = fake.NETWORK_INFO['server_id']
|
||||
vserver_name = fake.VSERVER_NAME_TEMPLATE % vserver_id
|
||||
|
||||
self.mock_object(context,
|
||||
'get_admin_context',
|
||||
mock.Mock(return_value='fake_admin_context'))
|
||||
self.mock_object(self.library._client,
|
||||
'vserver_exists',
|
||||
mock.Mock(return_value=True))
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.library._create_vserver_if_nonexistent,
|
||||
fake.NETWORK_INFO)
|
||||
|
||||
self.library.db.share_server_backend_details_set.assert_called_with(
|
||||
'fake_admin_context',
|
||||
vserver_id,
|
||||
{'vserver_name': vserver_name})
|
||||
|
||||
def test_create_vserver_if_nonexistent_lif_creation_failure(self):
|
||||
|
||||
vserver_id = fake.NETWORK_INFO['server_id']
|
||||
vserver_name = fake.VSERVER_NAME_TEMPLATE % vserver_id
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.mock_object(context,
|
||||
'get_admin_context',
|
||||
mock.Mock(return_value='fake_admin_context'))
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
self.mock_object(self.library._client,
|
||||
'vserver_exists',
|
||||
mock.Mock(return_value=False))
|
||||
self.mock_object(self.library,
|
||||
'_find_matching_aggregates',
|
||||
mock.Mock(return_value=fake.AGGREGATES))
|
||||
self.mock_object(self.library,
|
||||
'_create_vserver_lifs',
|
||||
mock.Mock(side_effect=netapp_api.NaApiError))
|
||||
|
||||
self.assertRaises(netapp_api.NaApiError,
|
||||
self.library._create_vserver_if_nonexistent,
|
||||
fake.NETWORK_INFO)
|
||||
|
||||
self.library.db.share_server_backend_details_set.assert_called_with(
|
||||
'fake_admin_context',
|
||||
vserver_id,
|
||||
{'vserver_name': vserver_name})
|
||||
self.library._get_api_client.assert_called_with(vserver=vserver_name)
|
||||
self.assertTrue(self.library._client.create_vserver.called)
|
||||
self.library._create_vserver_lifs.assert_called_with(
|
||||
vserver_name,
|
||||
vserver_client,
|
||||
fake.NETWORK_INFO)
|
||||
self.library._client.delete_vserver.assert_called_once_with(
|
||||
vserver_name,
|
||||
vserver_client)
|
||||
self.assertFalse(vserver_client.enable_nfs.called)
|
||||
self.assertEqual(1, lib_base.LOG.error.call_count)
|
||||
|
||||
def test_create_vserver_lifs(self):
|
||||
|
||||
self.mock_object(self.library._client,
|
||||
'list_cluster_nodes',
|
||||
mock.Mock(return_value=fake.CLUSTER_NODES))
|
||||
self.mock_object(self.library._client,
|
||||
'get_node_data_port',
|
||||
mock.Mock(return_value=fake.NODE_DATA_PORT))
|
||||
self.mock_object(self.library, '_create_lif_if_nonexistent')
|
||||
|
||||
self.library._create_vserver_lifs(fake.VSERVER1,
|
||||
'fake_vserver_client',
|
||||
fake.NETWORK_INFO)
|
||||
|
||||
self.library._create_lif_if_nonexistent.assert_has_calls([
|
||||
mock.call(
|
||||
fake.VSERVER1,
|
||||
fake.NETWORK_INFO['network_allocations'][0]['id'],
|
||||
fake.NETWORK_INFO['segmentation_id'],
|
||||
fake.CLUSTER_NODES[0],
|
||||
fake.NODE_DATA_PORT,
|
||||
fake.NETWORK_INFO['network_allocations'][0]['ip_address'],
|
||||
fake.NETWORK_INFO_NETMASK,
|
||||
'fake_vserver_client'),
|
||||
mock.call(
|
||||
fake.VSERVER1,
|
||||
fake.NETWORK_INFO['network_allocations'][1]['id'],
|
||||
fake.NETWORK_INFO['segmentation_id'],
|
||||
fake.CLUSTER_NODES[1],
|
||||
fake.NODE_DATA_PORT,
|
||||
fake.NETWORK_INFO['network_allocations'][1]['ip_address'],
|
||||
fake.NETWORK_INFO_NETMASK,
|
||||
'fake_vserver_client')])
|
||||
|
||||
def test_create_lif_if_nonexistent(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.network_interface_exists = mock.Mock(
|
||||
return_value=False)
|
||||
|
||||
self.library._create_lif_if_nonexistent('fake_vserver',
|
||||
'fake_allocation_id',
|
||||
'fake_vlan',
|
||||
'fake_node',
|
||||
'fake_port',
|
||||
'fake_ip',
|
||||
'fake_netmask',
|
||||
vserver_client)
|
||||
|
||||
self.library._client.create_network_interface.assert_has_calls([
|
||||
mock.call(
|
||||
'fake_ip',
|
||||
'fake_netmask',
|
||||
'fake_vlan',
|
||||
'fake_node',
|
||||
'fake_port',
|
||||
'fake_vserver',
|
||||
'fake_allocation_id',
|
||||
fake.LIF_NAME_TEMPLATE)])
|
||||
|
||||
def test_create_lif_if_nonexistent_already_present(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.network_interface_exists = mock.Mock(
|
||||
return_value=True)
|
||||
|
||||
self.library._create_lif_if_nonexistent('fake_vserver',
|
||||
'fake_allocation_id',
|
||||
'fake_vlan',
|
||||
'fake_node',
|
||||
'fake_port',
|
||||
'fake_ip',
|
||||
'fake_netmask',
|
||||
vserver_client)
|
||||
|
||||
self.assertFalse(self.library._client.create_network_interface.called)
|
||||
|
||||
def test_create_share(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
mock_allocate_container = self.mock_object(self.library,
|
||||
'_allocate_container')
|
||||
mock_create_export = self.mock_object(
|
||||
self.library,
|
||||
'_create_export',
|
||||
mock.Mock(return_value='fake_export_location'))
|
||||
|
||||
result = self.library.create_share(self.context,
|
||||
fake.SHARE,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
mock_allocate_container.assert_called_once_with(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
mock_create_export.assert_called_once_with(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
self.assertEqual('fake_export_location', result)
|
||||
|
||||
def test_create_share_from_snapshot(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
mock_allocate_container_from_snapshot = self.mock_object(
|
||||
self.library,
|
||||
'_allocate_container_from_snapshot')
|
||||
mock_create_export = self.mock_object(
|
||||
self.library,
|
||||
'_create_export',
|
||||
mock.Mock(return_value='fake_export_location'))
|
||||
|
||||
result = self.library.create_share_from_snapshot(
|
||||
self.context,
|
||||
fake.SHARE,
|
||||
fake.SNAPSHOT,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
mock_allocate_container_from_snapshot.assert_called_once_with(
|
||||
fake.SHARE,
|
||||
fake.SNAPSHOT,
|
||||
vserver_client)
|
||||
mock_create_export.assert_called_once_with(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
self.assertEqual('fake_export_location', result)
|
||||
|
||||
def test_allocate_container(self):
|
||||
|
||||
aggregates = {'aggr0': 10000000000, 'aggr1': 20000000000}
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.get_aggregates_for_vserver.return_value = aggregates
|
||||
|
||||
self.library._allocate_container(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
vserver_client.create_volume.assert_called_with('aggr1',
|
||||
share_name,
|
||||
fake.SHARE['size'])
|
||||
|
||||
def test_allocate_container_from_snapshot(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.library._allocate_container_from_snapshot(fake.SHARE,
|
||||
fake.SNAPSHOT,
|
||||
vserver_client)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
parent_share_name = self.library._get_valid_share_name(
|
||||
fake.SNAPSHOT['share_id'])
|
||||
parent_snapshot_name = self.library._get_valid_snapshot_name(
|
||||
fake.SNAPSHOT['id'])
|
||||
vserver_client.create_volume_clone.assert_called_with(
|
||||
share_name,
|
||||
parent_share_name,
|
||||
parent_snapshot_name)
|
||||
|
||||
def test_share_exists(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
vserver_client.volume_exists.return_value = True
|
||||
result = self.library._share_exists(fake.SHARE_NAME, vserver_client)
|
||||
self.assertTrue(result)
|
||||
|
||||
vserver_client.volume_exists.return_value = False
|
||||
result = self.library._share_exists(fake.SHARE_NAME, vserver_client)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_delete_share(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
mock_share_exists = self.mock_object(self.library,
|
||||
'_share_exists',
|
||||
mock.Mock(return_value=True))
|
||||
mock_remove_export = self.mock_object(self.library, '_remove_export')
|
||||
mock_deallocate_container = self.mock_object(self.library,
|
||||
'_deallocate_container')
|
||||
|
||||
self.library.delete_share(self.context,
|
||||
fake.SHARE,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
mock_share_exists.assert_called_once_with(share_name, vserver_client)
|
||||
mock_remove_export.assert_called_once_with(fake.SHARE, vserver_client)
|
||||
mock_deallocate_container.assert_called_once_with(share_name,
|
||||
vserver_client)
|
||||
self.assertEqual(0, lib_base.LOG.info.call_count)
|
||||
|
||||
def test_delete_share_not_found(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
mock_share_exists = self.mock_object(self.library,
|
||||
'_share_exists',
|
||||
mock.Mock(return_value=False))
|
||||
mock_remove_export = self.mock_object(self.library, '_remove_export')
|
||||
mock_deallocate_container = self.mock_object(self.library,
|
||||
'_deallocate_container')
|
||||
|
||||
self.library.delete_share(self.context,
|
||||
fake.SHARE,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
mock_share_exists.assert_called_once_with(share_name, vserver_client)
|
||||
self.assertFalse(mock_remove_export.called)
|
||||
self.assertFalse(mock_deallocate_container.called)
|
||||
self.assertEqual(1, lib_base.LOG.info.call_count)
|
||||
|
||||
def test_deallocate_container(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.library._deallocate_container(fake.SHARE_NAME, vserver_client)
|
||||
|
||||
vserver_client.unmount_volume.assert_called_with(fake.SHARE_NAME,
|
||||
force=True)
|
||||
vserver_client.offline_volume.assert_called_with(fake.SHARE_NAME)
|
||||
vserver_client.delete_volume.assert_called_with(fake.SHARE_NAME)
|
||||
|
||||
def test_create_export(self):
|
||||
|
||||
protocol_helper = mock.Mock()
|
||||
protocol_helper.create_share.return_value = 'fake_export_location'
|
||||
self.mock_object(self.library,
|
||||
'_get_helper',
|
||||
mock.Mock(return_value=protocol_helper))
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.get_network_interfaces.return_value = fake.LIFS
|
||||
|
||||
result = self.library._create_export(fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
|
||||
share_name = self.library._get_valid_share_name(fake.SHARE['id'])
|
||||
self.assertEqual('fake_export_location', result)
|
||||
protocol_helper.create_share.assert_called_once_with(
|
||||
share_name,
|
||||
fake.LIFS[0]['address'])
|
||||
|
||||
def test_create_export_lifs_not_found(self):
|
||||
|
||||
self.mock_object(self.library, '_get_helper')
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.get_network_interfaces.return_value = []
|
||||
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.library._create_export,
|
||||
fake.SHARE,
|
||||
fake.VSERVER1,
|
||||
vserver_client)
|
||||
|
||||
def test_remove_export(self):
|
||||
|
||||
protocol_helper = mock.Mock()
|
||||
protocol_helper.get_target.return_value = 'fake_target'
|
||||
self.mock_object(self.library,
|
||||
'_get_helper',
|
||||
mock.Mock(return_value=protocol_helper))
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.library._remove_export(fake.SHARE, vserver_client)
|
||||
|
||||
protocol_helper.set_client.assert_called_once_with(vserver_client)
|
||||
protocol_helper.get_target.assert_called_once_with(fake.SHARE)
|
||||
protocol_helper.delete_share.assert_called_once_with(fake.SHARE)
|
||||
|
||||
def test_remove_export_target_not_found(self):
|
||||
|
||||
protocol_helper = mock.Mock()
|
||||
protocol_helper.get_target.return_value = None
|
||||
self.mock_object(self.library,
|
||||
'_get_helper',
|
||||
mock.Mock(return_value=protocol_helper))
|
||||
vserver_client = mock.Mock()
|
||||
|
||||
self.library._remove_export(fake.SHARE, vserver_client)
|
||||
|
||||
protocol_helper.set_client.assert_called_once_with(vserver_client)
|
||||
protocol_helper.get_target.assert_called_once_with(fake.SHARE)
|
||||
self.assertFalse(protocol_helper.delete_share.called)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.library.create_snapshot(self.context,
|
||||
fake.SNAPSHOT,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
share_name = self.library._get_valid_share_name(
|
||||
fake.SNAPSHOT['share_id'])
|
||||
snapshot_name = self.library._get_valid_snapshot_name(
|
||||
fake.SNAPSHOT['id'])
|
||||
vserver_client.create_snapshot.assert_called_once_with(
|
||||
share_name,
|
||||
snapshot_name)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.is_snapshot_busy.return_value = False
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.library.delete_snapshot(self.context,
|
||||
fake.SNAPSHOT,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
share_name = self.library._get_valid_share_name(
|
||||
fake.SNAPSHOT['share_id'])
|
||||
snapshot_name = self.library._get_valid_snapshot_name(
|
||||
fake.SNAPSHOT['id'])
|
||||
vserver_client.delete_snapshot.assert_called_once_with(
|
||||
share_name,
|
||||
snapshot_name)
|
||||
|
||||
def test_delete_snapshot_busy(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
vserver_client.is_snapshot_busy.return_value = True
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.assertRaises(exception.ShareSnapshotIsBusy,
|
||||
self.library.delete_snapshot,
|
||||
self.context,
|
||||
fake.SNAPSHOT,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
def test_allow_access(self):
|
||||
|
||||
protocol_helper = mock.Mock()
|
||||
protocol_helper.allow_access.return_value = None
|
||||
self.mock_object(self.library,
|
||||
'_get_helper',
|
||||
mock.Mock(return_value=protocol_helper))
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.library.allow_access(self.context,
|
||||
fake.SHARE,
|
||||
fake.SHARE_ACCESS,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
protocol_helper.set_client.assert_called_once_with(vserver_client)
|
||||
protocol_helper.allow_access.assert_called_once_with(
|
||||
self.context,
|
||||
fake.SHARE,
|
||||
fake.SHARE_ACCESS)
|
||||
|
||||
def test_deny_access(self):
|
||||
|
||||
protocol_helper = mock.Mock()
|
||||
protocol_helper.deny_access.return_value = None
|
||||
self.mock_object(self.library,
|
||||
'_get_helper',
|
||||
mock.Mock(return_value=protocol_helper))
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.library.deny_access(self.context,
|
||||
fake.SHARE,
|
||||
fake.SHARE_ACCESS,
|
||||
share_server=fake.SHARE_SERVER)
|
||||
|
||||
protocol_helper.set_client.assert_called_once_with(vserver_client)
|
||||
protocol_helper.deny_access.assert_called_once_with(
|
||||
self.context,
|
||||
fake.SHARE,
|
||||
fake.SHARE_ACCESS)
|
||||
|
||||
def test_get_network_allocations_number(self):
|
||||
|
||||
self.library._client.list_cluster_nodes.return_value = \
|
||||
fake.CLUSTER_NODES
|
||||
|
||||
result = self.library.get_network_allocations_number()
|
||||
|
||||
self.assertEqual(len(fake.CLUSTER_NODES), result)
|
||||
|
||||
def test_teardown_server(self):
|
||||
|
||||
vserver_client = mock.Mock()
|
||||
self.mock_object(self.library,
|
||||
'_get_api_client',
|
||||
mock.Mock(return_value=vserver_client))
|
||||
|
||||
self.library.teardown_server(
|
||||
fake.SHARE_SERVER['backend_details'],
|
||||
security_services=fake.NETWORK_INFO['security_services'])
|
||||
|
||||
self.library._client.delete_vserver.assert_called_once_with(
|
||||
fake.VSERVER1,
|
||||
vserver_client,
|
||||
security_services=fake.NETWORK_INFO['security_services'])
|
|
@ -0,0 +1,126 @@
|
|||
# Copyright (c) - 2014, Clinton Knight 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.
|
||||
|
||||
|
||||
BACKEND_NAME = 'fake_backend_name'
|
||||
DRIVER_NAME = 'fake_driver_name'
|
||||
APP_VERSION = 'fake_app_vsersion'
|
||||
HOST_NAME = 'fake_host'
|
||||
VSERVER1 = 'fake_vserver_1'
|
||||
VSERVER2 = 'fake_vserver_2'
|
||||
LICENSES = ['base', 'cifs', 'fcp', 'flexclone', 'iscsi', 'nfs', 'snapmirror',
|
||||
'snaprestore', 'snapvault']
|
||||
VOLUME_NAME_TEMPLATE = 'share_%(share_id)s'
|
||||
VSERVER_NAME_TEMPLATE = 'os_%s'
|
||||
AGGREGATE_NAME_SEARCH_PATTERN = '(.*)'
|
||||
SHARE_NAME = 'fake_share'
|
||||
SHARE_SIZE = 10
|
||||
TENANT_ID = '24cb2448-13d8-4f41-afd9-eff5c4fd2a57'
|
||||
SHARE_ID = '7cf7c200-d3af-4e05-b87e-9167c95dfcad'
|
||||
PARENT_SHARE_ID = '585c3935-2aa9-437c-8bad-5abae1076555'
|
||||
SNAPSHOT_ID = 'de4c9050-e2f9-4ce1-ade4-5ed0c9f26451'
|
||||
FREE_CAPACITY = 10000000000
|
||||
TOTAL_CAPACITY = 20000000000
|
||||
AGGREGATES = ['aggr0', 'manila']
|
||||
ROOT_VOLUME_AGGREGATE = 'manila'
|
||||
ROOT_VOLUME = 'root'
|
||||
CLUSTER_NODES = ['cluster1_01', 'cluster1_02']
|
||||
NODE_DATA_PORT = 'e0c'
|
||||
LIF_NAME_TEMPLATE = 'os_%(net_allocation_id)s'
|
||||
|
||||
CLIENT_KWARGS = {
|
||||
'username': 'admin',
|
||||
'trace': False,
|
||||
'hostname': '127.0.0.1',
|
||||
'vserver': None,
|
||||
'transport_type': 'https',
|
||||
'password': 'pass',
|
||||
'port': '443'
|
||||
}
|
||||
|
||||
SHARE = {
|
||||
'id': SHARE_ID,
|
||||
'project_id': TENANT_ID,
|
||||
'name': SHARE_NAME,
|
||||
'size': SHARE_SIZE,
|
||||
'share_proto': 'fake',
|
||||
'share_network_id': '5dfe0898-e2a1-4740-9177-81c7d26713b0',
|
||||
'share_server_id': '7e6a2cc8-871f-4b1d-8364-5aad0f98da86',
|
||||
'network_info': {
|
||||
'network_allocations': [{'ip_address': 'ip'}]
|
||||
}
|
||||
}
|
||||
|
||||
NETWORK_INFO = {
|
||||
'server_id': '56aafd02-4d44-43d7-b784-57fc88167224',
|
||||
'cidr': '10.0.0.0/24',
|
||||
'security_services': ['fake_ldap', 'fake_kerberos', 'fake_ad', ],
|
||||
'segmentation_id': '1000',
|
||||
'network_allocations': [
|
||||
{'id': '132dbb10-9a36-46f2-8d89-3d909830c356',
|
||||
'ip_address': '10.10.10.10'},
|
||||
{'id': '7eabdeed-bad2-46ea-bd0f-a33884c869e0',
|
||||
'ip_address': '10.10.10.20'}
|
||||
]
|
||||
}
|
||||
NETWORK_INFO_NETMASK = '255.255.255.0'
|
||||
|
||||
SHARE_SERVER = {
|
||||
'backend_details': {
|
||||
'vserver_name': VSERVER1
|
||||
}
|
||||
}
|
||||
|
||||
SNAPSHOT = {
|
||||
'id': SNAPSHOT_ID,
|
||||
'project_id': TENANT_ID,
|
||||
'share_id': PARENT_SHARE_ID
|
||||
}
|
||||
|
||||
LIF_NAMES = []
|
||||
LIF_ADDRESSES = ['10.10.10.10', '10.10.10.20']
|
||||
LIFS = [
|
||||
{'address': LIF_ADDRESSES[0],
|
||||
'home-node': CLUSTER_NODES[0],
|
||||
'home-port': 'e0c',
|
||||
'interface-name': 'os_132dbb10-9a36-46f2-8d89-3d909830c356',
|
||||
'netmask': NETWORK_INFO_NETMASK,
|
||||
'role': 'data',
|
||||
'vserver': VSERVER1
|
||||
},
|
||||
{'address': LIF_ADDRESSES[1],
|
||||
'home-node': CLUSTER_NODES[1],
|
||||
'home-port': 'e0c',
|
||||
'interface-name': 'os_7eabdeed-bad2-46ea-bd0f-a33884c869e0',
|
||||
'netmask': NETWORK_INFO_NETMASK,
|
||||
'role': 'data',
|
||||
'vserver': VSERVER1
|
||||
}
|
||||
]
|
||||
|
||||
SHARE_ACCESS = {
|
||||
'access_type': 'user',
|
||||
'access_to': [LIF_ADDRESSES[0]]
|
||||
}
|
||||
|
||||
EMS_MESSAGE = {
|
||||
'computer-name': 'fake_host',
|
||||
'event-id': '0',
|
||||
'event-source': 'fake_driver',
|
||||
'app-version': 'fake_app_version',
|
||||
'category': 'fake_category',
|
||||
'event-description': 'fake_description',
|
||||
'log-level': '6',
|
||||
'auto-support': 'false'
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# Copyright (c) - 2014, Clinton Knight 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.
|
||||
|
||||
|
||||
SHARE_NAME = 'fake_share'
|
||||
SHARE_ID = '9dba208c-9aa7-11e4-89d3-123b93f75cba'
|
||||
SHARE_ADDRESS = '10.10.10.10'
|
||||
CLIENT_ADDRESS_1 = '20.20.20.10'
|
||||
CLIENT_ADDRESS_2 = '20.20.20.20'
|
||||
|
||||
CIFS_SHARE = {
|
||||
'export_location': '//%s/%s' % (SHARE_ADDRESS, SHARE_NAME),
|
||||
'id': SHARE_ID
|
||||
}
|
||||
|
||||
NFS_SHARE_PATH = '/%s' % SHARE_NAME
|
||||
NFS_SHARE = {
|
||||
'export_location': '%s:%s' % (SHARE_ADDRESS, NFS_SHARE_PATH),
|
||||
'id': SHARE_ID
|
||||
}
|
||||
|
||||
NFS_ACCESS_HOSTS = [CLIENT_ADDRESS_1]
|
||||
|
||||
ACCESS = {
|
||||
'access_type': 'user',
|
||||
'access_to': NFS_ACCESS_HOSTS
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
Mock unit tests for the NetApp driver protocols base class module.
|
||||
"""
|
||||
|
||||
from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode
|
||||
from manila import test
|
||||
|
||||
|
||||
class NetAppNASHelperBaseTestCase(test.TestCase):
|
||||
|
||||
def test_set_client(self):
|
||||
# The base class is abstract, so we'll use a subclass to test
|
||||
# base class functionality.
|
||||
helper = cifs_cmode.NetAppCmodeCIFSHelper()
|
||||
self.assertIsNone(helper._client)
|
||||
|
||||
helper.set_client('fake_client')
|
||||
self.assertEqual('fake_client', helper._client)
|
|
@ -0,0 +1,165 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
Mock unit tests for the NetApp driver protocols CIFS class module.
|
||||
"""
|
||||
|
||||
import mock
|
||||
|
||||
from manila import exception
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode
|
||||
from manila import test
|
||||
from manila.tests.share.drivers.netapp.dataontap.protocols \
|
||||
import fakes as fake
|
||||
|
||||
|
||||
class NetAppClusteredCIFSHelperTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppClusteredCIFSHelperTestCase, self).setUp()
|
||||
self.mock_object(cifs_cmode, 'LOG')
|
||||
|
||||
self.mock_context = mock.Mock()
|
||||
|
||||
self.mock_client = mock.Mock()
|
||||
self.helper = cifs_cmode.NetAppCmodeCIFSHelper()
|
||||
self.helper.set_client(self.mock_client)
|
||||
|
||||
def test_create_share(self):
|
||||
|
||||
result = self.helper.create_share(fake.SHARE_NAME, fake.SHARE_ADDRESS)
|
||||
|
||||
self.mock_client.create_cifs_share.assert_called_once_with(
|
||||
fake.SHARE_NAME)
|
||||
self.mock_client.remove_cifs_share_access.assert_called_once_with(
|
||||
fake.SHARE_NAME, 'Everyone')
|
||||
self.assertEqual('//%s/%s' % (fake.SHARE_ADDRESS, fake.SHARE_NAME),
|
||||
result)
|
||||
|
||||
def test_delete_share(self):
|
||||
|
||||
self.helper.delete_share(fake.CIFS_SHARE)
|
||||
|
||||
self.mock_client.remove_cifs_share.assert_called_once_with(
|
||||
fake.SHARE_NAME)
|
||||
|
||||
def test_allow_access(self):
|
||||
|
||||
self.helper.allow_access(self.mock_context, fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
self.mock_client.add_cifs_share_access.assert_called_once_with(
|
||||
fake.SHARE_NAME, fake.ACCESS['access_to'])
|
||||
|
||||
def test_allow_access_preexisting(self):
|
||||
|
||||
self.mock_client.add_cifs_share_access.side_effect = \
|
||||
netapp_api.NaApiError(code=netapp_api.EDUPLICATEENTRY)
|
||||
|
||||
self.assertRaises(exception.ShareAccessExists,
|
||||
self.helper.allow_access,
|
||||
self.mock_context,
|
||||
fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
def test_allow_access_api_error(self):
|
||||
|
||||
self.mock_client.add_cifs_share_access.side_effect = \
|
||||
netapp_api.NaApiError()
|
||||
|
||||
self.assertRaises(netapp_api.NaApiError,
|
||||
self.helper.allow_access,
|
||||
self.mock_context,
|
||||
fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
def test_allow_access_invalid_type(self):
|
||||
|
||||
fake_access = fake.ACCESS.copy()
|
||||
fake_access['access_type'] = 'group'
|
||||
self.assertRaises(exception.NetAppException,
|
||||
self.helper.allow_access,
|
||||
self.mock_context,
|
||||
fake.CIFS_SHARE,
|
||||
fake_access)
|
||||
|
||||
def test_deny_access(self):
|
||||
|
||||
self.helper.deny_access(self.mock_context, fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
self.mock_client.remove_cifs_share_access.assert_called_once_with(
|
||||
fake.SHARE_NAME, fake.ACCESS['access_to'])
|
||||
|
||||
def test_deny_access_nonexistent_user(self):
|
||||
|
||||
self.mock_client.remove_cifs_share_access.side_effect = \
|
||||
netapp_api.NaApiError(code=netapp_api.EONTAPI_EINVAL)
|
||||
|
||||
self.helper.deny_access(self.mock_context, fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
self.mock_client.remove_cifs_share_access.assert_called_once_with(
|
||||
fake.SHARE_NAME, fake.ACCESS['access_to'])
|
||||
self.assertEqual(1, cifs_cmode.LOG.error.call_count)
|
||||
|
||||
def test_deny_access_nonexistent_rule(self):
|
||||
|
||||
self.mock_client.remove_cifs_share_access.side_effect = \
|
||||
netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND)
|
||||
|
||||
self.helper.deny_access(self.mock_context, fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
self.mock_client.remove_cifs_share_access.assert_called_once_with(
|
||||
fake.SHARE_NAME, fake.ACCESS['access_to'])
|
||||
self.assertEqual(1, cifs_cmode.LOG.error.call_count)
|
||||
|
||||
def test_deny_access_api_error(self):
|
||||
|
||||
self.mock_client.remove_cifs_share_access.side_effect = \
|
||||
netapp_api.NaApiError()
|
||||
|
||||
self.assertRaises(netapp_api.NaApiError,
|
||||
self.helper.deny_access,
|
||||
self.mock_context,
|
||||
fake.CIFS_SHARE,
|
||||
fake.ACCESS)
|
||||
|
||||
def test_get_target(self):
|
||||
|
||||
target = self.helper.get_target(fake.CIFS_SHARE)
|
||||
self.assertEqual(fake.SHARE_ADDRESS, target)
|
||||
|
||||
def test_get_target_missing_location(self):
|
||||
|
||||
target = self.helper.get_target({'export_location': ''})
|
||||
self.assertEqual('', target)
|
||||
|
||||
def test_get_export_location(self):
|
||||
|
||||
host_ip, share_name = self.helper._get_export_location(fake.CIFS_SHARE)
|
||||
self.assertEqual(fake.SHARE_ADDRESS, host_ip)
|
||||
self.assertEqual(fake.SHARE_NAME, share_name)
|
||||
|
||||
def test_get_export_location_missing_location(self):
|
||||
|
||||
fake_share = fake.CIFS_SHARE.copy()
|
||||
fake_share['export_location'] = ''
|
||||
|
||||
host_ip, share_name = self.helper._get_export_location(fake_share)
|
||||
|
||||
self.assertEqual('', host_ip)
|
||||
self.assertEqual('', share_name)
|
|
@ -0,0 +1,172 @@
|
|||
# Copyright (c) 2014 Clinton Knight. 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.
|
||||
"""
|
||||
Mock unit tests for the NetApp driver protocols NFS class module.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
|
||||
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode
|
||||
from manila import test
|
||||
from manila.tests.share.drivers.netapp.dataontap.protocols \
|
||||
import fakes as fake
|
||||
|
||||
|
||||
class NetAppClusteredNFSHelperTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppClusteredNFSHelperTestCase, self).setUp()
|
||||
self.mock_object(nfs_cmode, 'LOG')
|
||||
|
||||
self.mock_context = mock.Mock()
|
||||
|
||||
self.mock_client = mock.Mock()
|
||||
self.helper = nfs_cmode.NetAppCmodeNFSHelper()
|
||||
self.helper.set_client(self.mock_client)
|
||||
|
||||
def test_create_share(self):
|
||||
|
||||
self.mock_client.get_volume_junction_path.return_value = \
|
||||
fake.NFS_SHARE_PATH
|
||||
|
||||
result = self.helper.create_share(fake.SHARE_NAME, fake.SHARE_ADDRESS)
|
||||
|
||||
self.mock_client.add_nfs_export_rules.assert_called_once_with(
|
||||
fake.NFS_SHARE_PATH, ['localhost'])
|
||||
self.assertEqual(':'.join([fake.SHARE_ADDRESS, fake.NFS_SHARE_PATH]),
|
||||
result)
|
||||
|
||||
def test_delete_share(self):
|
||||
|
||||
self.helper.delete_share(fake.NFS_SHARE)
|
||||
|
||||
self.mock_client.remove_nfs_export_rules.assert_called_once_with(
|
||||
fake.NFS_SHARE_PATH)
|
||||
|
||||
def test_allow_access(self):
|
||||
|
||||
mock_modify_rule = self.mock_object(self.helper, '_modify_rule')
|
||||
self.mock_client.get_nfs_export_rules.return_value = ['localhost']
|
||||
|
||||
self.helper.allow_access(
|
||||
self.mock_context, fake.NFS_SHARE, fake.ACCESS)
|
||||
|
||||
mock_modify_rule.assert_called_once_with(
|
||||
fake.NFS_SHARE, ['localhost'] + fake.ACCESS['access_to'])
|
||||
|
||||
def test_allow_access_single_host(self):
|
||||
|
||||
mock_modify_rule = self.mock_object(self.helper, '_modify_rule')
|
||||
self.mock_client.get_nfs_export_rules.return_value = ['localhost']
|
||||
fake_access = copy.deepcopy(fake.ACCESS)
|
||||
fake_access['access_to'] = fake.CLIENT_ADDRESS_1
|
||||
|
||||
self.helper.allow_access(
|
||||
self.mock_context, fake.NFS_SHARE, fake_access)
|
||||
|
||||
mock_modify_rule.assert_called_once_with(
|
||||
fake.NFS_SHARE, ['localhost'] + fake.ACCESS['access_to'])
|
||||
|
||||
def test_allow_access_api_error(self):
|
||||
|
||||
mock_modify_rule = self.mock_object(self.helper, '_modify_rule')
|
||||
mock_modify_rule.side_effect = [netapp_api.NaApiError, None]
|
||||
self.mock_client.get_nfs_export_rules.return_value = ['localhost']
|
||||
|
||||
self.helper.allow_access(
|
||||
self.mock_context, fake.NFS_SHARE, fake.ACCESS)
|
||||
|
||||
mock_modify_rule.assert_has_calls([
|
||||
mock.call(
|
||||
fake.NFS_SHARE, ['localhost'] + fake.ACCESS['access_to']),
|
||||
mock.call(fake.NFS_SHARE, ['localhost'])
|
||||
])
|
||||
|
||||
def test_deny_access(self):
|
||||
|
||||
mock_modify_rule = self.mock_object(self.helper, '_modify_rule')
|
||||
existing_hosts = [fake.CLIENT_ADDRESS_1, fake.CLIENT_ADDRESS_2]
|
||||
self.mock_client.get_nfs_export_rules.return_value = existing_hosts
|
||||
|
||||
fake_access = fake.ACCESS.copy()
|
||||
fake_access['access_to'] = [fake.CLIENT_ADDRESS_2]
|
||||
self.helper.deny_access(
|
||||
self.mock_context, fake.NFS_SHARE, fake_access)
|
||||
|
||||
mock_modify_rule.assert_called_once_with(
|
||||
fake.NFS_SHARE, [fake.CLIENT_ADDRESS_1])
|
||||
|
||||
def test_deny_access_single_host(self):
|
||||
|
||||
mock_modify_rule = self.mock_object(self.helper, '_modify_rule')
|
||||
existing_hosts = [fake.CLIENT_ADDRESS_1, fake.CLIENT_ADDRESS_2]
|
||||
self.mock_client.get_nfs_export_rules.return_value = existing_hosts
|
||||
|
||||
fake_access = fake.ACCESS.copy()
|
||||
fake_access['access_to'] = fake.CLIENT_ADDRESS_2
|
||||
self.helper.deny_access(
|
||||
self.mock_context, fake.NFS_SHARE, fake_access)
|
||||
|
||||
mock_modify_rule.assert_called_once_with(
|
||||
fake.NFS_SHARE, [fake.CLIENT_ADDRESS_1])
|
||||
|
||||
def test_get_target(self):
|
||||
|
||||
target = self.helper.get_target(fake.NFS_SHARE)
|
||||
self.assertEqual(fake.SHARE_ADDRESS, target)
|
||||
|
||||
def test_get_target_missing_location(self):
|
||||
|
||||
target = self.helper.get_target({'export_location': ''})
|
||||
self.assertEqual('', target)
|
||||
|
||||
def test_modify_rule(self):
|
||||
|
||||
access_rules = [fake.CLIENT_ADDRESS_1, fake.CLIENT_ADDRESS_2]
|
||||
|
||||
self.helper._modify_rule(fake.NFS_SHARE, access_rules)
|
||||
|
||||
self.mock_client.add_nfs_export_rules.assert_called_once_with(
|
||||
fake.NFS_SHARE_PATH, access_rules)
|
||||
|
||||
def test_get_existing_rules(self):
|
||||
|
||||
self.mock_client.get_nfs_export_rules.return_value = \
|
||||
fake.NFS_ACCESS_HOSTS
|
||||
|
||||
result = self.helper._get_existing_rules(fake.NFS_SHARE)
|
||||
|
||||
self.mock_client.get_nfs_export_rules.assert_called_once_with(
|
||||
fake.NFS_SHARE_PATH)
|
||||
self.assertEqual(fake.NFS_ACCESS_HOSTS, result)
|
||||
|
||||
def test_get_export_location(self):
|
||||
|
||||
host_ip, export_path = self.helper._get_export_location(
|
||||
fake.NFS_SHARE)
|
||||
self.assertEqual(fake.SHARE_ADDRESS, host_ip)
|
||||
self.assertEqual('/' + fake.SHARE_NAME, export_path)
|
||||
|
||||
def test_get_export_location_missing_location(self):
|
||||
|
||||
fake_share = fake.NFS_SHARE.copy()
|
||||
fake_share['export_location'] = ''
|
||||
|
||||
host_ip, export_path = self.helper._get_export_location(fake_share)
|
||||
|
||||
self.assertEqual('', host_ip)
|
||||
self.assertEqual('', export_path)
|
|
@ -15,16 +15,20 @@
|
|||
|
||||
from manila.share import configuration as conf
|
||||
from manila.share import driver as manila_opts
|
||||
from manila.share.drivers.netapp import cluster_mode
|
||||
from manila.share.drivers.netapp import options as na_opts
|
||||
|
||||
|
||||
def create_configuration():
|
||||
config = conf.Configuration(None)
|
||||
config.append_config_values(manila_opts.share_opts)
|
||||
config.append_config_values(cluster_mode.NETAPP_NAS_OPTS)
|
||||
config.append_config_values(na_opts.netapp_connection_opts)
|
||||
config.append_config_values(na_opts.netapp_transport_opts)
|
||||
config.append_config_values(na_opts.netapp_basicauth_opts)
|
||||
config.append_config_values(na_opts.netapp_provisioning_opts)
|
||||
return config
|
||||
|
||||
|
||||
def create_configuration_cmode():
|
||||
config = create_configuration()
|
||||
config.append_config_values(na_opts.netapp_support_opts)
|
||||
return config
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -16,8 +16,9 @@ import mock
|
|||
import six
|
||||
|
||||
from manila import exception
|
||||
from manila.share.drivers.netapp import cluster_mode
|
||||
from manila.share.drivers.netapp import common as na_common
|
||||
from manila.share.drivers.netapp.dataontap.cluster_mode import drv_multi_svm
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila import test
|
||||
from manila.tests.share.drivers.netapp import fakes as na_fakes
|
||||
|
||||
|
@ -30,8 +31,13 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
|
||||
def test_new(self):
|
||||
|
||||
self.mock_object(na_utils.OpenStackInfo, 'info',
|
||||
mock.Mock(return_value='fake_info'))
|
||||
mock_get_driver_mode = self.mock_object(
|
||||
na_common.NetAppDriver, '_get_driver_mode',
|
||||
mock.Mock(return_value='fake_mode'))
|
||||
mock_create_driver = self.mock_object(na_common.NetAppDriver,
|
||||
'create_driver')
|
||||
'_create_driver')
|
||||
|
||||
config = na_fakes.create_configuration()
|
||||
config.netapp_storage_family = 'fake_family'
|
||||
|
@ -40,18 +46,23 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
kwargs = {'configuration': config}
|
||||
na_common.NetAppDriver(**kwargs)
|
||||
|
||||
mock_create_driver.assert_called_with('fake_family', True,
|
||||
*(), **kwargs)
|
||||
kwargs['app_version'] = 'fake_info'
|
||||
mock_get_driver_mode.assert_called_once_with('fake_family', True)
|
||||
mock_create_driver.assert_called_once_with('fake_family', 'fake_mode',
|
||||
*(), **kwargs)
|
||||
|
||||
def test_new_missing_config(self):
|
||||
|
||||
self.mock_object(na_common.NetAppDriver, 'create_driver')
|
||||
self.mock_object(na_utils.OpenStackInfo, 'info')
|
||||
self.mock_object(na_common.NetAppDriver, '_create_driver')
|
||||
|
||||
self.assertRaises(exception.InvalidInput, na_common.NetAppDriver, **{})
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
na_common.NetAppDriver, **{})
|
||||
|
||||
def test_new_missing_family(self):
|
||||
|
||||
self.mock_object(na_common.NetAppDriver, 'create_driver')
|
||||
self.mock_object(na_utils.OpenStackInfo, 'info')
|
||||
self.mock_object(na_common.NetAppDriver, '_create_driver')
|
||||
|
||||
config = na_fakes.create_configuration()
|
||||
config.driver_handles_share_servers = True
|
||||
|
@ -64,6 +75,9 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
|
||||
def test_new_missing_mode(self):
|
||||
|
||||
self.mock_object(na_utils.OpenStackInfo, 'info')
|
||||
self.mock_object(na_common.NetAppDriver, '_create_driver')
|
||||
|
||||
config = na_fakes.create_configuration()
|
||||
config.driver_handles_share_servers = None
|
||||
config.netapp_storage_family = 'fake_family'
|
||||
|
@ -73,6 +87,28 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
na_common.NetAppDriver,
|
||||
**kwargs)
|
||||
|
||||
def test_get_driver_mode_missing_mode_good_default(self):
|
||||
|
||||
result = na_common.NetAppDriver._get_driver_mode('ONTAP_CLUSTER', None)
|
||||
self.assertEqual(na_common.MULTI_SVM, result)
|
||||
|
||||
def test_create_driver_missing_mode_no_default(self):
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
na_common.NetAppDriver._get_driver_mode,
|
||||
'fake_family', None)
|
||||
|
||||
def test_get_driver_mode_multi_svm(self):
|
||||
|
||||
result = na_common.NetAppDriver._get_driver_mode('ONTAP_CLUSTER', True)
|
||||
self.assertEqual(na_common.MULTI_SVM, result)
|
||||
|
||||
def test_get_driver_mode_single_svm(self):
|
||||
|
||||
result = na_common.NetAppDriver._get_driver_mode('ONTAP_CLUSTER',
|
||||
False)
|
||||
self.assertEqual(na_common.SINGLE_SVM, result)
|
||||
|
||||
def test_create_driver(self):
|
||||
|
||||
def get_full_class_name(obj):
|
||||
|
@ -81,14 +117,14 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
config = na_fakes.create_configuration()
|
||||
config.local_conf.set_override('driver_handles_share_servers', True)
|
||||
|
||||
kwargs = {'configuration': config}
|
||||
kwargs = {'configuration': config, 'app_version': 'fake_info'}
|
||||
|
||||
registry = na_common.NETAPP_UNIFIED_DRIVER_REGISTRY
|
||||
mock_db = mock.Mock()
|
||||
|
||||
for family in six.iterkeys(registry):
|
||||
for mode, full_class_name in six.iteritems(registry[family]):
|
||||
driver = na_common.NetAppDriver.create_driver(
|
||||
driver = na_common.NetAppDriver._create_driver(
|
||||
family, mode, mock_db, **kwargs)
|
||||
self.assertEqual(full_class_name, get_full_class_name(driver))
|
||||
|
||||
|
@ -97,48 +133,38 @@ class NetAppDriverFactoryTestCase(test.TestCase):
|
|||
config = na_fakes.create_configuration()
|
||||
config.local_conf.set_override('driver_handles_share_servers', True)
|
||||
|
||||
kwargs = {'configuration': config}
|
||||
|
||||
kwargs = {'configuration': config, 'app_version': 'fake_info'}
|
||||
mock_db = mock.Mock()
|
||||
|
||||
driver = na_common.NetAppDriver.create_driver('ONTAP_CLUSTER',
|
||||
True,
|
||||
mock_db,
|
||||
**kwargs)
|
||||
driver = na_common.NetAppDriver._create_driver('ONTAP_CLUSTER',
|
||||
na_common.MULTI_SVM,
|
||||
mock_db,
|
||||
**kwargs)
|
||||
|
||||
self.assertIsInstance(driver,
|
||||
cluster_mode.NetAppClusteredShareDriver)
|
||||
drv_multi_svm.NetAppCmodeMultiSvmShareDriver)
|
||||
|
||||
def test_create_driver_invalid_family(self):
|
||||
|
||||
kwargs = {'configuration': na_fakes.create_configuration()}
|
||||
kwargs = {
|
||||
'configuration': na_fakes.create_configuration(),
|
||||
'app_version': 'fake_info',
|
||||
}
|
||||
mock_db = mock.Mock()
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
na_common.NetAppDriver.create_driver,
|
||||
'fake_family', 'iscsi', mock_db, **kwargs)
|
||||
na_common.NetAppDriver._create_driver,
|
||||
'fake_family', na_common.MULTI_SVM,
|
||||
mock_db, **kwargs)
|
||||
|
||||
def test_create_driver_missing_mode_good_default(self):
|
||||
def test_create_driver_invalid_mode(self):
|
||||
|
||||
config = na_fakes.create_configuration()
|
||||
config.local_conf.set_override('driver_handles_share_servers', True)
|
||||
|
||||
kwargs = {'configuration': config}
|
||||
mock_db = mock.Mock()
|
||||
|
||||
driver = na_common.NetAppDriver.create_driver('ONTAP_CLUSTER',
|
||||
None,
|
||||
mock_db,
|
||||
**kwargs)
|
||||
|
||||
self.assertIsInstance(driver,
|
||||
cluster_mode.NetAppClusteredShareDriver)
|
||||
|
||||
def test_create_driver_missing_mode_no_default(self):
|
||||
|
||||
kwargs = {'configuration': na_fakes.create_configuration()}
|
||||
kwargs = {
|
||||
'configuration': na_fakes.create_configuration(),
|
||||
'app_version': 'fake_info',
|
||||
}
|
||||
mock_db = mock.Mock()
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
na_common.NetAppDriver.create_driver,
|
||||
'fake_family', None, mock_db, **kwargs)
|
||||
na_common.NetAppDriver._create_driver,
|
||||
'ontap_cluster', 'fake_mode', mock_db, **kwargs)
|
||||
|
|
|
@ -21,6 +21,7 @@ import platform
|
|||
import mock
|
||||
from oslo_concurrency import processutils as putils
|
||||
|
||||
from manila import exception
|
||||
from manila.share.drivers.netapp import utils as na_utils
|
||||
from manila import test
|
||||
from manila import version
|
||||
|
@ -83,6 +84,39 @@ class NetAppDriverUtilsTestCase(test.TestCase):
|
|||
self.assertEqual('OK', result)
|
||||
self.assertEqual(2, na_utils.LOG.debug.call_count)
|
||||
|
||||
def test_validate_instantiation_proxy(self):
|
||||
kwargs = {'netapp_mode': 'proxy'}
|
||||
|
||||
na_utils.validate_instantiation(**kwargs)
|
||||
|
||||
self.assertEqual(0, na_utils.LOG.warning.call_count)
|
||||
|
||||
def test_validate_instantiation_no_proxy(self):
|
||||
self.mock_object(na_utils, 'LOG')
|
||||
kwargs = {'netapp_mode': 'asdf'}
|
||||
|
||||
na_utils.validate_instantiation(**kwargs)
|
||||
|
||||
self.assertEqual(1, na_utils.LOG.warning.call_count)
|
||||
|
||||
def test_check_flags(self):
|
||||
configuration = type('Fake',
|
||||
(object,),
|
||||
{'flag1': 'value1', 'flag2': 'value2'})
|
||||
|
||||
self.assertIsNone(na_utils.check_flags(['flag1', 'flag2'],
|
||||
configuration))
|
||||
|
||||
def test_check_flags_missing_flag(self):
|
||||
configuration = type('Fake',
|
||||
(object,),
|
||||
{'flag1': 'value1', 'flag3': 'value3'})
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
na_utils.check_flags,
|
||||
['flag1', 'flag2'],
|
||||
configuration)
|
||||
|
||||
|
||||
class OpenstackInfoTestCase(test.TestCase):
|
||||
|
||||
|
|
Loading…
Reference in New Issue