Browse Source

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
changes/22/153622/5
Clinton Knight 7 years ago
parent
commit
12eeedb639
  1. 9
      doc/source/adminref/multi_backends.rst
  2. 2
      doc/source/devref/index.rst
  3. 15
      doc/source/devref/netapp_cluster_mode_driver.rst
  4. 10
      manila/opts.py
  5. 1277
      manila/share/drivers/netapp/cluster_mode.py
  6. 53
      manila/share/drivers/netapp/common.py
  7. 0
      manila/share/drivers/netapp/dataontap/__init__.py
  8. 0
      manila/share/drivers/netapp/dataontap/client/__init__.py
  9. 156
      manila/share/drivers/netapp/dataontap/client/api.py
  10. 76
      manila/share/drivers/netapp/dataontap/client/client_base.py
  11. 972
      manila/share/drivers/netapp/dataontap/client/client_cmode.py
  12. 0
      manila/share/drivers/netapp/dataontap/cluster_mode/__init__.py
  13. 80
      manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py
  14. 469
      manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py
  15. 0
      manila/share/drivers/netapp/dataontap/protocols/__init__.py
  16. 49
      manila/share/drivers/netapp/dataontap/protocols/base.py
  17. 83
      manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py
  18. 94
      manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py
  19. 99
      manila/share/drivers/netapp/options.py
  20. 178
      manila/share/drivers/netapp/utils.py
  21. 2
      manila/share/manager.py
  22. 0
      manila/tests/share/drivers/netapp/dataontap/__init__.py
  23. 0
      manila/tests/share/drivers/netapp/dataontap/client/__init__.py
  24. 967
      manila/tests/share/drivers/netapp/dataontap/client/fakes.py
  25. 156
      manila/tests/share/drivers/netapp/dataontap/client/test_api.py
  26. 117
      manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py
  27. 1726
      manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py
  28. 0
      manila/tests/share/drivers/netapp/dataontap/cluster_mode/__init__.py
  29. 962
      manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py
  30. 126
      manila/tests/share/drivers/netapp/dataontap/fakes.py
  31. 0
      manila/tests/share/drivers/netapp/dataontap/protocols/__init__.py
  32. 38
      manila/tests/share/drivers/netapp/dataontap/protocols/fakes.py
  33. 31
      manila/tests/share/drivers/netapp/dataontap/protocols/test_base.py
  34. 165
      manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py
  35. 172
      manila/tests/share/drivers/netapp/dataontap/protocols/test_nfs_cmode.py
  36. 8
      manila/tests/share/drivers/netapp/fakes.py
  37. 1006
      manila/tests/share/drivers/netapp/test_cluster_mode.py
  38. 104
      manila/tests/share/drivers/netapp/test_common.py
  39. 34
      manila/tests/share/drivers/netapp/test_utils.py

9
doc/source/adminref/multi_backends.rst

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

2
doc/source/devref/index.rst

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

15
doc/source/devref/cluster_mode_driver.rst → doc/source/devref/netapp_cluster_mode_driver.rst

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

10
manila/opts.py

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

1277
manila/share/drivers/netapp/cluster_mode.py
File diff suppressed because it is too large
View File

53
manila/share/drivers/netapp/common.py

@ -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
@staticmethod
def create_driver(storage_family, driver_handles_share_servers, *args,
**kwargs):
""""Creates an appropriate driver based on family and mode."""
driver_mode = NetAppDriver._get_driver_mode(
config.netapp_storage_family, config.driver_handles_share_servers)
storage_family = storage_family.lower()
return NetAppDriver._create_driver(config.netapp_storage_family,
driver_mode,
*args, **kwargs)
@staticmethod
def _get_driver_mode(storage_family, driver_handles_share_servers):
# 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 '

0
manila/share/drivers/netapp/dataontap/__init__.py

0
manila/share/drivers/netapp/dataontap/client/__init__.py

156
manila/share/drivers/netapp/api.py → manila/share/drivers/netapp/dataontap/client/api.py

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

76
manila/share/drivers/netapp/dataontap/client/client_base.py

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

972
manila/share/drivers/netapp/dataontap/client/client_cmode.py

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