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:
Clinton Knight 2015-01-08 18:01:08 -05:00
parent c65fbe533a
commit 12eeedb639
39 changed files with 6696 additions and 2540 deletions

View File

@ -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

View File

@ -77,7 +77,7 @@ Share backends
.. toctree::
:maxdepth: 3
cluster_mode_driver
netapp_cluster_mode_driver
emc_vnx_driver
generic_driver
huawei_nas_driver

View File

@ -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:

View File

@ -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

View File

@ -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 '

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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(':')

View File

@ -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)

View File

@ -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}

View File

@ -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', }

View File

@ -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],
})

View File

@ -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')

View File

@ -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

View File

@ -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'])

View File

@ -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'
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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):