From 12eeedb6392462c8aa2a0afa5763e0a4612cc475 Mon Sep 17 00:00:00 2001 From: Clinton Knight Date: Thu, 8 Jan 2015 18:01:08 -0500 Subject: [PATCH] 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 --- doc/source/adminref/multi_backends.rst | 9 +- doc/source/devref/index.rst | 2 +- ...ver.rst => netapp_cluster_mode_driver.rst} | 15 +- manila/opts.py | 10 +- manila/share/drivers/netapp/cluster_mode.py | 1277 ------------ manila/share/drivers/netapp/common.py | 53 +- .../drivers/netapp/dataontap/__init__.py | 0 .../netapp/dataontap/client/__init__.py | 0 .../netapp/{ => dataontap/client}/api.py | 156 +- .../netapp/dataontap/client/client_base.py | 76 + .../netapp/dataontap/client/client_cmode.py | 972 ++++++++++ .../netapp/dataontap/cluster_mode/__init__.py | 0 .../dataontap/cluster_mode/drv_multi_svm.py | 80 + .../netapp/dataontap/cluster_mode/lib_base.py | 469 +++++ .../netapp/dataontap/protocols/__init__.py | 0 .../netapp/dataontap/protocols/base.py | 49 + .../netapp/dataontap/protocols/cifs_cmode.py | 83 + .../netapp/dataontap/protocols/nfs_cmode.py | 94 + manila/share/drivers/netapp/options.py | 99 + manila/share/drivers/netapp/utils.py | 178 +- manila/share/manager.py | 2 + .../drivers/netapp/dataontap/__init__.py | 0 .../netapp/dataontap/client/__init__.py | 0 .../drivers/netapp/dataontap/client/fakes.py | 967 +++++++++ .../netapp/dataontap/client/test_api.py | 156 ++ .../dataontap/client/test_client_base.py | 117 ++ .../dataontap/client/test_client_cmode.py | 1726 +++++++++++++++++ .../netapp/dataontap/cluster_mode/__init__.py | 0 .../dataontap/cluster_mode/test_lib_base.py | 962 +++++++++ .../share/drivers/netapp/dataontap/fakes.py | 126 ++ .../netapp/dataontap/protocols/__init__.py | 0 .../netapp/dataontap/protocols/fakes.py | 38 + .../netapp/dataontap/protocols/test_base.py | 31 + .../dataontap/protocols/test_cifs_cmode.py | 165 ++ .../dataontap/protocols/test_nfs_cmode.py | 172 ++ manila/tests/share/drivers/netapp/fakes.py | 8 +- .../share/drivers/netapp/test_cluster_mode.py | 1006 ---------- .../tests/share/drivers/netapp/test_common.py | 104 +- .../tests/share/drivers/netapp/test_utils.py | 34 + 39 files changed, 6696 insertions(+), 2540 deletions(-) rename doc/source/devref/{cluster_mode_driver.rst => netapp_cluster_mode_driver.rst} (82%) delete mode 100644 manila/share/drivers/netapp/cluster_mode.py create mode 100644 manila/share/drivers/netapp/dataontap/__init__.py create mode 100644 manila/share/drivers/netapp/dataontap/client/__init__.py rename manila/share/drivers/netapp/{ => dataontap/client}/api.py (77%) create mode 100644 manila/share/drivers/netapp/dataontap/client/client_base.py create mode 100644 manila/share/drivers/netapp/dataontap/client/client_cmode.py create mode 100644 manila/share/drivers/netapp/dataontap/cluster_mode/__init__.py create mode 100644 manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py create mode 100644 manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py create mode 100644 manila/share/drivers/netapp/dataontap/protocols/__init__.py create mode 100644 manila/share/drivers/netapp/dataontap/protocols/base.py create mode 100644 manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py create mode 100644 manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py create mode 100644 manila/share/drivers/netapp/options.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/__init__.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/client/__init__.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/client/fakes.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/client/test_api.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/cluster_mode/__init__.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/fakes.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/protocols/__init__.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/protocols/fakes.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/protocols/test_base.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py create mode 100644 manila/tests/share/drivers/netapp/dataontap/protocols/test_nfs_cmode.py delete mode 100644 manila/tests/share/drivers/netapp/test_cluster_mode.py diff --git a/doc/source/adminref/multi_backends.rst b/doc/source/adminref/multi_backends.rst index 30ed93b057..b06acd6134 100644 --- a/doc/source/adminref/multi_backends.rst +++ b/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 diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index 8e7fd78d98..c67fde9687 100644 --- a/doc/source/devref/index.rst +++ b/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 diff --git a/doc/source/devref/cluster_mode_driver.rst b/doc/source/devref/netapp_cluster_mode_driver.rst similarity index 82% rename from doc/source/devref/cluster_mode_driver.rst rename to doc/source/devref/netapp_cluster_mode_driver.rst index 6f54b7bde7..88be3fb9eb 100644 --- a/doc/source/devref/cluster_mode_driver.rst +++ b/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: diff --git a/manila/opts.py b/manila/opts.py index 29eb42a3dc..b6d785751b 100644 --- a/manila/opts.py +++ b/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, diff --git a/manila/share/drivers/netapp/cluster_mode.py b/manila/share/drivers/netapp/cluster_mode.py deleted file mode 100644 index 6a6ee9d4de..0000000000 --- a/manila/share/drivers/netapp/cluster_mode.py +++ /dev/null @@ -1,1277 +0,0 @@ -# Copyright (c) 2014 NetApp, Inc. -# Copyright (c) 2015 Tom Barron. -# 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 specific NAS storage driver. Supports NFS and CIFS protocols. - -This driver requires ONTAP Cluster mode storage system -with installed CIFS and NFS licenses. -""" -import abc -import copy -import hashlib -import re - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import excutils -from oslo_utils import units -import six - -from manila import context -from manila import exception -from manila.i18n import _ -from manila.i18n import _LE -from manila.i18n import _LI -from manila.share import driver -from manila.share.drivers.netapp import api as naapi -from manila.share.drivers.netapp import utils as na_utils -from manila import utils - - -NETAPP_NAS_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.')), - cfg.StrOpt('netapp_nas_login', - default='admin', - help='User name for the ONTAP controller.'), - cfg.StrOpt('netapp_nas_password', - help='Password for the ONTAP controller.', - secret=True), - cfg.StrOpt('netapp_nas_server_hostname', - help='Hostname for the ONTAP controller.'), - cfg.StrOpt('netapp_nas_transport_type', - default='http', - help='Transport type protocol.'), - cfg.StrOpt('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='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_name', - default='root', - help='Root volume name.'), - 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_NAS_OPTS) - -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._vserver_exists(vserver_name): - raise exception.VserverUnavailable(vserver=vserver_name) - return f(self, *args, **kwargs) - return wrap - - -class NetAppApiClient(object): - - def __init__(self, version, vserver=None, *args, **kwargs): - self.configuration = kwargs.get('configuration', None) - if not self.configuration: - raise exception.NetAppException(_("NetApp configuration missing.")) - self._client = naapi.NaServer( - host=self.configuration.netapp_nas_server_hostname, - username=self.configuration.netapp_nas_login, - password=self.configuration.netapp_nas_password, - transport_type=self.configuration.netapp_nas_transport_type, - trace=na_utils.TRACE_API - ) - self._client.set_api_version(*version) - if vserver: - self._client.set_vserver(vserver) - - def send_request(self, api_name, args=None): - """Sends request to Ontapi.""" - elem = naapi.NaElement(api_name) - if args: - elem.translate_struct(args) - LOG.debug("NaElement: %s", elem.to_string(pretty=True)) - return self._client.invoke_successfully(elem, enable_tunneling=True) - - -class NetAppClusteredShareDriver(driver.ShareDriver): - """NetApp specific ONTAP Cluster mode driver. - - Supports NFS and CIFS protocols. - Uses Ontap devices 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. - """ - - def __init__(self, db, *args, **kwargs): - super(NetAppClusteredShareDriver, self).__init__(True, *args, **kwargs) - self._app_version = na_utils.OpenStackInfo().info() - LOG.info(_LI("OpenStack OS Version Info: %(info)s") % { - 'info': self._app_version}) - self.db = db - self._helpers = None - self._licenses = [] - self._client = None - if self.configuration: - self.configuration.append_config_values(NETAPP_NAS_OPTS) - self.api_version = (1, 15) - self.backend_name = self.configuration.safe_get( - 'share_backend_name') or "NetApp_Cluster_Mode" - na_utils.setup_tracing(self.configuration.netapp_trace_flags) - - @na_utils.trace - def do_setup(self, context): - """Prepare once the driver. - - Called once by the manager after the driver is loaded. - Sets up clients, check licenses, sets up protocol - specific helpers. - """ - self._client = NetAppApiClient(self.api_version, - configuration=self.configuration) - self._setup_helpers() - - @na_utils.trace - def ensure_share(self, context, share, share_server=None): - """Invoked to ensure that share is exported.""" - pass - - @na_utils.trace - def _check_licenses(self): - self._licenses = [] - try: - licenses = self._client.send_request('license-v2-list-info') - except naapi.NaApiError as e: - LOG.error(_LE("Could not get licenses list. %s."), e) - else: - self._licenses = sorted([ - l.get_child_content('package').lower() - for l in licenses.get_child_by_name('licenses').get_children() - ]) - log_data = { - 'backend': self.backend_name, - 'licenses': ', '.join(self._licenses), - } - LOG.info(_LI("Available licenses on '%(backend)s' " - "are %(licenses)s."), log_data) - return self._licenses - - def _get_valid_share_name(self, share_id): - """Get share name according to share name template.""" - return self.configuration.netapp_nas_volume_name_template % { - 'share_id': share_id.replace('-', '_')} - - 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 _update_share_stats(self): - """Retrieve stats info from Cluster Mode backend.""" - total, free = self._calculate_capacity() - data = dict( - share_backend_name=self.backend_name, - vendor_name='NetApp', - storage_protocol='NFS_CIFS', - total_capacity_gb=(total / units.Gi), - free_capacity_gb=(free / units.Gi)) - super(NetAppClusteredShareDriver, self)._update_share_stats(data) - na_utils.provide_ems(self, self._client._client, self.backend_name, - self._app_version) - - def check_for_setup_error(self): - """Raises error if prerequisites are not met.""" - self._check_licenses() - - @na_utils.trace - def _calculate_capacity(self): - """Calculates capacity - - Returns tuple (total, free) in bytes. - """ - aggrs = self._find_match_aggregates() - 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]) - free = max([int(aggr.get_child_content('size-available')) - for aggr in aggr_space_attrs]) - return total, free - - @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._vserver_create_if_not_exists(network_info) - return {'vserver_name': vserver_name} - - @na_utils.trace - def _get_cluster_nodes(self): - """Get all available cluster nodes.""" - response = self._client.send_request('system-node-get-iter') - nodes_info_list = response.get_child_by_name('attributes-list')\ - .get_children() if response.get_child_by_name('attributes-list') \ - else [] - nodes = [node_info.get_child_content('node') for node_info - in nodes_info_list] - return nodes - - @na_utils.trace - def _get_node_data_port(self, node): - """Get data port on the node.""" - args = { - 'query': { - 'net-port-info': { - 'node': node, - 'port-type': 'physical', - 'role': 'data' - } - } - } - port_info = self._client.send_request('net-port-get-iter', 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.") % node - LOG.error(msg) - raise exception.NetAppException(msg) - return port - - @na_utils.trace - def _create_vserver(self, vserver_name): - """Creates new vserver and assigns aggregates.""" - create_args = {'vserver-name': vserver_name, - 'root-volume-security-style': 'unix', - 'root-volume-aggregate': - self.configuration.netapp_root_volume_aggregate, - 'root-volume': - self.configuration.netapp_root_volume_name, - 'name-server-switch': {'nsswitch': 'file'}} - self._client.send_request('vserver-create', create_args) - aggrs = self._find_match_aggregates() - aggr_list = [{'aggr-name': aggr.get_child_content('aggregate-name')} - for aggr in aggrs] - modify_args = {'aggr-list': aggr_list, - 'vserver-name': vserver_name} - self._client.send_request('vserver-modify', modify_args) - - @na_utils.trace - def _find_match_aggregates(self): - """Find all aggregates match pattern.""" - pattern = self.configuration.netapp_aggregate_name_search_pattern - try: - aggrs = self._client.send_request('aggr-get-iter')\ - .get_child_by_name('attributes-list').get_children() - except AttributeError: - msg = _("Have not found aggregates match pattern %s") % pattern - LOG.error(msg) - raise exception.NetAppException(msg) - aggr_list = [aggr for aggr in aggrs if re.match( - pattern, aggr.get_child_content('aggregate-name'))] - return aggr_list - - @na_utils.trace - def get_network_allocations_number(self): - """Get number of network interfaces to be created.""" - return int(self._client.send_request( - 'system-node-get-iter').get_child_content('num-records')) - - @na_utils.trace - def _create_net_iface(self, ip, netmask, vlan, node, port, vserver_name, - allocation_id): - """Creates lif on vlan port.""" - vlan_iface_name = "%(port)s-%(tag)s" % {'port': port, 'tag': vlan} - try: - args = { - 'vlan-info': { - 'parent-interface': port, - 'node': node, - 'vlanid': vlan - } - } - self._client.send_request('net-vlan-create', args) - except naapi.NaApiError as e: - if e.code == '13130': - LOG.debug("Vlan %(vlan)s already exists on port %(port)s", - {'vlan': vlan, 'port': port}) - else: - raise exception.NetAppException( - _("Failed to create vlan %(vlan)s on " - "port %(port)s. %(err_msg)") % - {'vlan': vlan, 'port': port, 'err_msg': e.message}) - iface_name = (self.configuration.netapp_lif_name_template % - {'node': node, 'net_allocation_id': allocation_id}) - LOG.debug('Creating LIF %(lif)r for vserver %(vserver)s ', - {'lif': iface_name, 'vserver': vserver_name}) - args = {'address': ip, - 'administrative-status': 'up', - 'data-protocols': [ - {'data-protocol': 'nfs'}, - {'data-protocol': 'cifs'} - ], - 'home-node': node, - 'home-port': vlan_iface_name, - 'netmask': netmask, - 'interface-name': iface_name, - 'role': 'data', - 'vserver': vserver_name, - } - self._client.send_request('net-interface-create', args) - - @na_utils.trace - def _delete_net_iface(self, iface_name): - """Deletes lif.""" - args = {'vserver': None, - 'interface-name': iface_name} - self._client.send_request('net-interface-delete', args) - - @na_utils.trace - def _setup_helpers(self): - """Initializes protocol-specific NAS drivers.""" - self._helpers = {'CIFS': NetAppClusteredCIFSHelper(), - 'NFS': NetAppClusteredNFSHelper()} - - @na_utils.trace - def _get_helper(self, share): - """Returns driver which implements share protocol.""" - share_proto = share['share_proto'] - if share_proto.lower() not in self._licenses: - current_licenses = self._check_licenses() - if share_proto not in current_licenses: - msg = _("There is no license for %s at Ontap") % share_proto - LOG.error(msg) - raise exception.NetAppException(msg) - - for proto in self._helpers.keys(): - if share_proto == proto: - return self._helpers[proto] - - err_msg = _("Invalid NAS protocol supplied: %s.") % share_proto - - raise exception.NetAppException(err_msg) - - @na_utils.trace - def _vserver_exists(self, vserver_name): - args = {'query': {'vserver-info': {'vserver-name': vserver_name}}} - - LOG.debug('Checking if vserver exists') - vserver_info = self._client.send_request('vserver-get-iter', args) - if int(vserver_info.get_child_content('num-records')): - return True - else: - return False - - @na_utils.trace - def _vserver_create_if_not_exists(self, network_info): - """Creates vserver if not exists with given parameters.""" - 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}, - ) - vserver_client = NetAppApiClient( - self.api_version, vserver=vserver_name, - configuration=self.configuration) - if not self._vserver_exists(vserver_name): - LOG.debug('Vserver %s does not exist, creating', vserver_name) - self._create_vserver(vserver_name) - nodes = self._get_cluster_nodes() - - node_network_info = zip(nodes, network_info['network_allocations']) - netmask = utils.cidr_to_netmask(network_info['cidr']) - try: - for node, net_info in node_network_info: - port = self._get_node_data_port(node) - ip = net_info['ip_address'] - self._create_lif_if_not_exists( - vserver_name, net_info['id'], - network_info['segmentation_id'], node, port, - ip, netmask, vserver_client) - except naapi.NaApiError: - with excutils.save_and_reraise_exception(): - LOG.error(_LE("Failed to create network interface")) - self._delete_vserver(vserver_name, vserver_client) - - self._enable_nfs(vserver_client) - - security_services = network_info.get('security_services') - if security_services: - self._setup_security_services(security_services, vserver_client, - vserver_name) - return vserver_name - - @na_utils.trace - def _setup_security_services(self, security_services, vserver_client, - vserver_name): - modify_args = { - 'name-mapping-switch': { - 'nmswitch': 'ldap,file'}, - 'name-server-switch': { - 'nsswitch': 'ldap,file'}, - 'vserver-name': vserver_name} - self._client.send_request('vserver-modify', modify_args) - for security_service in security_services: - if security_service['type'].lower() == "ldap": - self._configure_ldap(security_service, vserver_client) - elif security_service['type'].lower() == "active_directory": - self._configure_active_directory(security_service, - vserver_client, - vserver_name) - elif security_service['type'].lower() == "kerberos": - self._configure_kerberos(vserver_name, security_service, - vserver_client) - else: - raise exception.NetAppException( - _('Unsupported protocol %s for NetApp driver') - % security_service['type']) - - @na_utils.trace - def _enable_nfs(self, vserver_client): - """Enables NFS on vserver.""" - vserver_client.send_request('nfs-enable') - args = {'is-nfsv40-enabled': 'true'} - vserver_client.send_request('nfs-service-modify', args) - args = { - 'client-match': '0.0.0.0/0', - 'policy-name': 'default', - 'ro-rule': { - 'security-flavor': 'any' - }, - 'rw-rule': { - 'security-flavor': 'any' - } - } - vserver_client.send_request('export-rule-create', args) - - @na_utils.trace - def _configure_ldap(self, data, vserver_client): - """Configures LDAP on vserver.""" - config_name = hashlib.md5(data['id']).hexdigest() - args = {'ldap-client-config': config_name, - 'servers': { - 'ip-address': data['server'] - }, - 'tcp-port': '389', - 'schema': 'RFC-2307', - 'bind-password': data['password']} - vserver_client.send_request('ldap-client-create', args) - args = {'client-config': config_name, - 'client-enabled': 'true'} - vserver_client.send_request('ldap-config-create', args) - - @na_utils.trace - def _configure_dns(self, data, vserver_client): - args = { - 'domains': { - 'string': data['domain'] - }, - 'name-servers': { - 'ip-address': data['dns_ip'] - }, - 'dns-state': 'enabled' - } - try: - vserver_client.send_request('net-dns-create', args) - except naapi.NaApiError as e: - if e.code == '13130': - LOG.error(_LE("DNS exists for vserver.")) - else: - raise exception.NetAppException( - _("Failed to configure DNS. %s") % e.message) - - @na_utils.trace - def _configure_kerberos(self, vserver, data, vserver_client): - """Configures Kerberos for NFS on vServer.""" - args = {'admin-server-ip': data['server'], - 'admin-server-port': '749', - 'clock-skew': '5', - 'comment': '', - 'config-name': data['id'], - 'kdc-ip': data['server'], - 'kdc-port': '88', - 'kdc-vendor': 'other', - 'password-server-ip': data['server'], - 'password-server-port': '464', - 'realm': data['domain'].upper()} - try: - self._client.send_request('kerberos-realm-create', args) - except naapi.NaApiError as e: - if e.code == '13130': - LOG.debug("Kerberos realm config already exists") - else: - raise exception.NetAppException( - _("Failed to configure Kerberos. %s") % e.message) - - self._configure_dns(data, vserver_client) - spn = 'nfs/' + vserver.replace('_', '-') + '.' + data['domain'] + '@'\ - + data['domain'].upper() - lifs = self._get_lifs(vserver_client) - if not lifs: - msg = _("Cannot set up kerberos. There are no lifs configured") - LOG.error(msg) - raise Exception(msg) - for lif_name in lifs: - args = { - 'admin-password': data['password'], - 'admin-user-name': data['user'], - 'interface-name': lif_name, - 'is-kerberos-enabled': 'true', - 'service-principal-name': spn - } - vserver_client.send_request('kerberos-config-modify', args) - - @na_utils.trace - def _configure_active_directory(self, sec_service_data, vserver_client, - vserver_name): - """Configures AD on vserver.""" - self._configure_dns(sec_service_data, vserver_client) - # '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() - data = { - 'admin-username': sec_service_data['user'], - 'admin-password': sec_service_data['password'], - 'force-account-overwrite': 'true', - 'cifs-server': cifs_server, - 'domain': sec_service_data['domain'], - } - try: - LOG.debug("Trying to setup cifs server with data: %s", data) - vserver_client.send_request('cifs-server-create', data) - except naapi.NaApiError as e: - msg = _("Failed to create CIFS server entry. %s.") % e.message - raise exception.NetAppException(msg) - - @na_utils.trace - def _get_lifs(self, vserver_client): - lifs_info = vserver_client.send_request('net-interface-get-iter') - try: - lif_names = [lif.get_child_content('interface-name') for lif in - lifs_info.get_child_by_name('attributes-list') - .get_children()] - except AttributeError: - lif_names = [] - return lif_names - - @na_utils.trace - def _create_lif_if_not_exists(self, vserver_name, allocation_id, vlan, - node, port, ip, netmask, vserver_client): - """Creates lif for vserver.""" - args = { - 'query': { - 'net-interface-info': { - 'address': ip, - 'home-node': node, - 'home-port': port, - 'netmask': netmask, - 'vserver': vserver_name} - } - } - ifaces = vserver_client.send_request('net-interface-get-iter', - args) - if (not ifaces.get_child_content('num_records') or - ifaces.get_child_content('num_records') == '0'): - self._create_net_iface(ip, netmask, vlan, node, port, vserver_name, - allocation_id) - - @na_utils.trace - def get_available_aggregates_for_vserver(self, vserver, vserver_client): - """Returns aggregate list for the vserver.""" - LOG.debug('Finding available aggreagates for vserver %s', vserver) - response = vserver_client.send_request('vserver-get') - vserver_info = response.get_child_by_name('attributes')\ - .get_child_by_name('vserver-info') - aggr_list_elements = vserver_info\ - .get_child_by_name('vserver-aggr-info-list').get_children() - - if not aggr_list_elements: - msg = _("No aggregate assigned to vserver %s") - raise exception.NetAppException(msg % vserver) - - # return dict of key-value pair of aggr_name:si$ - aggr_dict = {} - - for aggr_elem in aggr_list_elements: - aggr_name = aggr_elem.get_child_content('aggr-name') - aggr_size = int(aggr_elem.get_child_content('aggr-availsize')) - aggr_dict[aggr_name] = aggr_size - LOG.debug("Found available aggregates: %r", aggr_dict) - return aggr_dict - - @ensure_vserver - @na_utils.trace - def create_share(self, context, share, share_server=None): - """Creates new share.""" - vserver = share_server['backend_details']['vserver_name'] - vserver_client = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - 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 form snapshot.""" - vserver = share_server['backend_details']['vserver_name'] - vserver_client = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - - self._allocate_container_from_snapshot(share, snapshot, vserver, - 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 = self.get_available_aggregates_for_vserver(vserver, - vserver_client) - 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}) - args = {'containing-aggr-name': aggregate, - 'size': str(share['size']) + 'g', - 'volume': share_name, - 'junction-path': '/%s' % share_name - } - vserver_client.send_request('volume-create', args) - - @na_utils.trace - def _allocate_container_from_snapshot(self, share, snapshot, vserver, - 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']) - args = {'volume': share_name, - 'parent-volume': parent_share_name, - 'parent-snapshot': parent_snapshot_name, - 'junction-path': '/%s' % share_name - } - - vserver_client.send_request('volume-clone-create', args) - - @na_utils.trace - def _share_exists(self, share_name, vserver_client): - args = { - 'query': { - 'volume-attributes': { - 'volume-id-attributes': { - 'name': share_name - } - } - } - } - response = vserver_client.send_request('volume-get-iter', args) - if int(response.get_child_content('num-records')): - return True - - @na_utils.trace - def _deallocate_container(self, share, vserver_client): - """Free share space.""" - self._share_unmount(share, vserver_client) - self._offline_share(share, vserver_client) - self._delete_share(share, vserver_client) - - @na_utils.trace - def _offline_share(self, share, vserver_client): - """Sends share offline. Required before deleting a share.""" - share_name = self._get_valid_share_name(share['id']) - args = {'name': share_name} - LOG.debug('Offline volume %s', share_name) - vserver_client.send_request('volume-offline', args) - - @na_utils.trace - def _delete_share(self, share, vserver_client): - """Destroys share on a target OnTap device.""" - share_name = self._get_valid_share_name(share['id']) - args = {'name': share_name} - LOG.debug('Deleting share %s', share_name) - vserver_client.send_request('volume-destroy', args) - - @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 = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - if self._share_exists(share_name, vserver_client): - self._remove_export(share, vserver_client) - self._deallocate_container(share, vserver_client) - else: - LOG.info(_LI("Share %s does not exist."), share['id']) - - @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']) - args = { - 'query': { - 'net-interface-info': {'vserver': vserver} - } - } - ifaces = vserver_client.send_request('net-interface-get-iter', args) - if not int(ifaces.get_child_content('num-records')): - raise exception.NetAppException( - _("Cannot find network interfaces for vserver %s.") % vserver) - ifaces_list = ifaces.get_child_by_name('attributes-list')\ - .get_children() - ip_address = ifaces_list[0].get_child_content('address') - export_location = helper.create_share(share_name, ip_address) - return export_location - - @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 = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - share_name = self._get_valid_share_name(snapshot['share_id']) - snapshot_name = self._get_valid_snapshot_name(snapshot['id']) - args = {'volume': share_name, - 'snapshot': snapshot_name} - LOG.debug('Creating snapshot %s', snapshot_name) - vserver_client.send_request('snapshot-create', args) - - @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 delete_snapshot(self, context, snapshot, share_server=None): - """Deletes a snapshot of a share.""" - vserver = share_server['backend_details']['vserver_name'] - vserver_client = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - share_name = self._get_valid_share_name(snapshot['share_id']) - snapshot_name = self._get_valid_snapshot_name(snapshot['id']) - - self._is_snapshot_busy(share_name, snapshot_name, vserver_client) - args = {'snapshot': snapshot_name, - 'volume': share_name} - LOG.debug('Deleting snapshot %s', snapshot_name) - vserver_client.send_request('snapshot-delete', args) - - @na_utils.trace - def _is_snapshot_busy(self, share_name, snapshot_name, vserver_client): - """Raises ShareSnapshotIsBusy if snapshot is busy.""" - args = {'volume': share_name} - snapshots = vserver_client.send_request('snapshot-list-info', - args) - for snap in snapshots.get_child_by_name('snapshots').get_children(): - if (snap.get_child_by_name('name').get_content() == snapshot_name - and (snap.get_child_by_name('busy').get_content() - == 'true')): - return True - - @na_utils.trace - def _share_unmount(self, share, vserver_client): - """Unmounts share (required before deleting).""" - share_name = self._get_valid_share_name(share['id']) - args = {'volume-name': share_name} - LOG.debug('Unmounting volume %s', share_name) - vserver_client.send_request('volume-unmount', args) - - @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 = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - helper = self._get_helper(share) - helper.set_client(vserver_client) - return 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 = NetAppApiClient( - self.api_version, vserver=vserver, - configuration=self.configuration) - helper = self._get_helper(share) - helper.set_client(vserver_client) - return helper.deny_access(context, share, access) - - @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 - volumes_data = vserver_client.send_request('volume-get-iter') - volumes_count = int(volumes_data.get_child_content('num-records')) - if volumes_count == 1: - try: - vserver_client.send_request( - 'volume-offline', - {'name': self.configuration.netapp_root_volume_name}) - except naapi.NaApiError as e: - if e.code == '13042': - LOG.error(_LE("Volume %s is already offline."), - self.configuration.netapp_root_volume_name) - else: - raise e - vserver_client.send_request( - 'volume-destroy', - {'name': self.configuration.netapp_root_volume_name}) - elif volumes_count > 1: - msg = _("Error deleting vserver. " - "Vserver %s has shares.") % vserver_name - LOG.error(msg) - raise exception.NetAppException(msg) - if security_services: - for service in security_services: - if service['type'] == 'active_directory': - args = { - 'admin-password': service['password'], - 'admin-username': service['user'], - } - try: - vserver_client.send_request('cifs-server-delete', - args) - except naapi.NaApiError as e: - if e.code == "15661": - LOG.error(_LE("CIFS server does not exist for" - " vserver %s"), vserver_name) - else: - vserver_client.send_request('cifs-server-delete') - self._client.send_request('vserver-destroy', - {'vserver-name': vserver_name}) - - @na_utils.trace - def _teardown_server(self, server_details, security_services=None): - """Teardown share network.""" - vserver_name = server_details['vserver_name'] - vserver_client = NetAppApiClient( - self.api_version, vserver=vserver_name, - configuration=self.configuration) - self._delete_vserver(vserver_name, vserver_client, - security_services=security_services) - - -@six.add_metaclass(abc.ABCMeta) -class NetAppNASHelperBase(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.""" - - -class NetAppClusteredNFSHelper(NetAppNASHelperBase): - """Netapp specific cluster-mode NFS sharing driver.""" - - def __init__(self): - super(NetAppClusteredNFSHelper, self).__init__() - # 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 - - @na_utils.trace - def create_share(self, share_name, export_ip): - """Creates NFS share.""" - arguments = {'is-style-cifs': 'false', 'volume': share_name} - export_pathname = self._client.send_request( - 'volume-get-volume-path', arguments).get_child_by_name( - 'junction').get_content() - self.add_rules(export_pathname, ['localhost']) - export_location = ':'.join([export_ip, export_pathname]) - return export_location - - @na_utils.trace - def allow_access_by_user(self, share, user): - user, _x, group = user.partition(':') - args = { - 'attributes': { - 'volume-attributes': { - 'volume-security-attributes': { - 'volume-security-unix-attributes': { - 'user-id': user, - 'group-id': group or 'root' - } - } - } - }, - 'query': { - 'volume-attributes': { - 'volume-id-attributes': { - 'junction-path': self._get_export_path(share) - } - } - } - } - self._client.send_request('volume-modify-iter', args) - - @na_utils.trace - def deny_access_by_user(self, share, user): - args = { - 'attributes': { - 'volume-security-attributes': { - 'volume-security-unix-attributes': { - 'user': 'root' - } - } - }, - 'query': { - 'volume-attributes': { - 'volume-id-attributes': { - 'junction-path': self._get_export_path(share) - } - } - } - } - self._client.send_request('volume-modify-iter', args) - - @na_utils.trace - def add_rules(self, volume_path, rules): - req_bodies = [] - security_rule_args = { - 'security-rule-info': { - 'read-write': { - 'exports-hostname-info': { - 'name': 'localhost', - } - }, - 'root': { - 'exports-hostname-info': { - 'all-hosts': 'false', - 'name': 'localhost', - } - } - } - } - hostname_info_args = { - 'exports-hostname-info': { - 'name': 'localhost', - } - } - req_bodies.insert(0, { - 'rules': { - 'exports-rule-info-2': { - 'pathname': volume_path, - 'security-rules': { - 'security-rule-info': { - 'read-write': { - 'exports-hostname-info': { - 'name': 'localhost', - } - }, - 'root': { - 'exports-hostname-info': { - 'all-hosts': 'false', - 'name': 'localhost', - } - } - } - } - } - } - }) - allowed_hosts_xml = [] - - for ip in rules: - hostname_info = hostname_info_args.copy() - hostname_info['exports-hostname-info'] = {'name': ip} - allowed_hosts_xml.append(hostname_info) - - security_rule = security_rule_args.copy() - security_rule['security-rule-info']['read-write'] = allowed_hosts_xml - security_rule['security-rule-info']['root'] = allowed_hosts_xml - req_bodies[0]['rules']['exports-rule-info-2']['security-rules'] = ( - security_rule) - req_bodies.insert(1, copy.deepcopy(req_bodies[0])) - req_bodies[1]['rules']['exports-rule-info-2']['pathname'] = ( - '/vol' + volume_path) - - LOG.debug('Appending nfs rules %r', rules) - try: - if self.nfs_exports_with_prefix: - self._client.send_request( - 'nfs-exportfs-append-rules-2', req_bodies.pop(1)) - else: - self._client.send_request( - 'nfs-exportfs-append-rules-2', req_bodies.pop(0)) - except naapi.NaApiError as e: - if e.code == "13114": - # 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.debug("Data ONTAP API 'nfs-exportfs-append-rules-2' " - "compatibility action: remember behavior to send " - "proper values with first attempt next times. " - "Now trying send another request with changed value " - "for 'pathname'.") - self._client.send_request( - 'nfs-exportfs-append-rules-2', req_bodies.pop(0)) - else: - raise - - @na_utils.trace - def delete_share(self, share): - """Deletes NFS share.""" - target, export_path = self._get_export_path(share) - args = { - 'pathnames': { - 'pathname-info': { - 'name': export_path, - } - } - } - LOG.debug('Deleting NFS rules for share %s', share['id']) - self._client.send_request('nfs-exportfs-delete-rules', args) - - @na_utils.trace - def allow_access(self, context, share, access): - """Allows access to a given NFS storage.""" - new_rules = access['access_to'] - existing_rules = self._get_exisiting_rules(share) - - if not isinstance(new_rules, list): - new_rules = [new_rules] - - rules = existing_rules + new_rules - try: - self._modify_rule(share, rules) - except naapi.NaApiError: - self._modify_rule(share, existing_rules) - - @na_utils.trace - def deny_access(self, context, share, access): - """Denies access to a given NFS storage.""" - access_to = access['access_to'] - existing_rules = self._get_exisiting_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) - - @na_utils.trace - def get_target(self, share): - """Returns ID of target OnTap device based on export location.""" - return self._get_export_path(share)[0] - - @na_utils.trace - def _modify_rule(self, share, rules): - """Modifies access rule for a given NFS share.""" - target, export_path = self._get_export_path(share) - self.add_rules(export_path, rules) - - @na_utils.trace - def _get_exisiting_rules(self, share): - """Returns available access rules for a given NFS share.""" - target, export_path = self._get_export_path(share) - - args = {'pathname': export_path} - response = self._client.send_request('nfs-exportfs-list-rules-2', 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')) - LOG.debug('Found existing rules %(rules)r for share %(share)s', - {'rules': existing_rules, 'share': share['id']}) - - return existing_rules - - @staticmethod - def _get_export_path(share): - """Returns IP address and export location of a NFS share.""" - export_location = share['export_location'] or ':' - return export_location.split(':') - - -class NetAppClusteredCIFSHelper(NetAppNASHelperBase): - """Netapp specific cluster-mode CIFS sharing driver.""" - - @na_utils.trace - def create_share(self, share_name, export_ip): - """Creates CIFS share on target OnTap host.""" - share_path = '/%s' % share_name - args = {'path': share_path, 'share-name': share_name} - self._client.send_request('cifs-share-create', args) - self._restrict_access('Everyone', share_name) - return "//%s/%s" % (export_ip, share_name) - - @na_utils.trace - def delete_share(self, share): - """Deletes CIFS share on target OnTap host.""" - host_ip, share_name = self._get_export_location(share) - args = {'share-name': share_name} - self._client.send_request('cifs-share-delete', args) - - @na_utils.trace - 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) - args = { - 'permission': 'full_control', - 'share': share_name, - 'user-or-group': access['access_to'], - } - try: - self._client.send_request('cifs-share-access-control-create', args) - except naapi.NaApiError as e: - if e.code == "13130": - # duplicate entry - raise exception.ShareAccessExists( - access_type=access['access_type'], access=access) - raise e - - @na_utils.trace - 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 = access['access_to'] - try: - self._restrict_access(user, share_name) - except naapi.NaApiError as e: - if e.code == "22": - LOG.error(_LE("User %s does not exist."), user) - elif e.code == "15661": - LOG.error(_LE("Rule %s does not exist."), user) - else: - raise e - - def get_target(self, share): - """Returns OnTap target IP based on share export location.""" - return self._get_export_location(share)[0] - - @na_utils.trace - def _restrict_access(self, user_name, share_name): - args = {'user-or-group': user_name, 'share': share_name} - self._client.send_request('cifs-share-access-control-delete', args) - - @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 diff --git a/manila/share/drivers/netapp/common.py b/manila/share/drivers/netapp/common.py index b93980e152..0a024078c3 100644 --- a/manila/share/drivers/netapp/common.py +++ b/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 + + 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 ' diff --git a/manila/share/drivers/netapp/dataontap/__init__.py b/manila/share/drivers/netapp/dataontap/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/netapp/dataontap/client/__init__.py b/manila/share/drivers/netapp/dataontap/client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/netapp/api.py b/manila/share/drivers/netapp/dataontap/client/api.py similarity index 77% rename from manila/share/drivers/netapp/api.py rename to manila/share/drivers/netapp/dataontap/client/api.py index 1c7ad8d42a..ef61552d7e 100644 --- a/manila/share/drivers/netapp/api.py +++ b/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. @@ -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 diff --git a/manila/share/drivers/netapp/dataontap/client/client_base.py b/manila/share/drivers/netapp/dataontap/client/client_base.py new file mode 100644 index 0000000000..4a90ae0c14 --- /dev/null +++ b/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() diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py new file mode 100644 index 0000000000..8ecb9c4e32 --- /dev/null +++ b/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': , + # 'security-rules': { + # 'security-rule-info': { + # 'read-write': [ + # { + # 'exports-hostname-info': { + # 'name': , + # } + # }, + # { + # 'exports-hostname-info': { + # 'name': , + # } + # } + # ], + # 'root': [ + # { + # 'exports-hostname-info': { + # 'name': , + # } + # }, + # { + # 'exports-hostname-info': { + # 'name': , + # } + # } + # ] + # } + # } + # } + # } + # } + + # 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) diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/__init__.py b/manila/share/drivers/netapp/dataontap/cluster_mode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py new file mode 100644 index 0000000000..2c068d32b7 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/drv_multi_svm.py @@ -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) diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py new file mode 100644 index 0000000000..3f46a7f696 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -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) diff --git a/manila/share/drivers/netapp/dataontap/protocols/__init__.py b/manila/share/drivers/netapp/dataontap/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/netapp/dataontap/protocols/base.py b/manila/share/drivers/netapp/dataontap/protocols/base.py new file mode 100644 index 0000000000..3634ceab68 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/protocols/base.py @@ -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.""" diff --git a/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py new file mode 100644 index 0000000000..437784ac96 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py @@ -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 diff --git a/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py b/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py new file mode 100644 index 0000000000..cbef43f31d --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py @@ -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(':') diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py new file mode 100644 index 0000000000..2c5fc3144e --- /dev/null +++ b/manila/share/drivers/netapp/options.py @@ -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) diff --git a/manila/share/drivers/netapp/utils.py b/manila/share/drivers/netapp/utils.py index 913d6bd638..3faf22b09a 100644 --- a/manila/share/drivers/netapp/utils.py +++ b/manila/share/drivers/netapp/utils.py @@ -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} diff --git a/manila/share/manager.py b/manila/share/manager.py index 7ffc619b55..c8d4f3470a 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -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', } diff --git a/manila/tests/share/drivers/netapp/dataontap/__init__.py b/manila/tests/share/drivers/netapp/dataontap/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/netapp/dataontap/client/__init__.py b/manila/tests/share/drivers/netapp/dataontap/client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py new file mode 100644 index 0000000000..19287e25a5 --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -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(""" + + 0 + +""") + +VSERVER_GET_ITER_RESPONSE = etree.XML(""" + + + + %(fake_vserver)s + + + 1 + +""" % {'fake_vserver': VSERVER_NAME}) + +VSERVER_GET_ROOT_VOLUME_NAME_RESPONSE = etree.XML(""" + + + + %(root_volume)s + %(fake_vserver)s + + + 1 + +""" % {'root_volume': ROOT_VOLUME_NAME, 'fake_vserver': VSERVER_NAME}) + +VSERVER_GET_RESPONSE = etree.XML(""" + + + + + aggr0 + manila + + + + 45678592 + aggr0 + + + 6448431104 + manila + + + %(vserver)s + + + +""" % {'vserver': VSERVER_NAME}) + +VSERVER_DATA_LIST_RESPONSE = etree.XML(""" + + + + %(vserver)s + data + + + 1 + +""" % {'vserver': VSERVER_NAME}) + +VSERVER_AGGREGATES = {'aggr0': 45678592, 'manila': 6448431104} + +VSERVER_GET_RESPONSE_NO_AGGREGATES = etree.XML(""" + + + + %(vserver)s + + + +""" % {'vserver': VSERVER_NAME}) + +ONTAPI_VERSION_RESPONSE = etree.XML(""" + + 1 + 19 + +""") + +LICENSE_V2_LIST_INFO_RESPONSE = etree.XML(""" + + + + none + Cluster Base License + false + cluster3 + base + 1-80-000008 + license + + + none + NFS License + false + cluster3-01 + nfs + 1-81-0000000000000004082368507 + license + + + none + CIFS License + false + cluster3-01 + cifs + 1-81-0000000000000004082368507 + license + + + none + iSCSI License + false + cluster3-01 + iscsi + 1-81-0000000000000004082368507 + license + + + none + FCP License + false + cluster3-01 + fcp + 1-81-0000000000000004082368507 + license + + + none + SnapRestore License + false + cluster3-01 + snaprestore + 1-81-0000000000000004082368507 + license + + + none + SnapMirror License + false + cluster3-01 + snapmirror + 1-81-0000000000000004082368507 + license + + + none + FlexClone License + false + cluster3-01 + flexclone + 1-81-0000000000000004082368507 + license + + + none + SnapVault License + false + cluster3-01 + snapvault + 1-81-0000000000000004082368507 + license + + + +""") + +LICENSES = [ + 'base', 'cifs', 'fcp', 'flexclone', 'iscsi', 'nfs', 'snapmirror', + 'snaprestore', 'snapvault' +] + +VOLUME_COUNT_RESPONSE = etree.XML(""" + + + + + vol0 + cluster3-01 + + + + + %(root_volume)s + %(fake_vserver)s + + + + 2 + +""" % {'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(""" + + + + %s + + + 1 + +""" % NODE_NAME) + +NET_PORT_GET_ITER_RESPONSE = etree.XML(""" + + + + full + full + auto + true + true + true + up + 00:0c:29:fc:04:d9 + 1500 + %(node_name)s + full + none + 1000 + e0a + physical + data + + + full + full + auto + true + true + true + up + 00:0c:29:fc:04:e3 + 1500 + %(node_name)s + full + none + 1000 + e0b + physical + data + + + full + full + auto + true + true + true + up + 00:0c:29:fc:04:ed + 1500 + %(node_name)s + full + none + 1000 + e0c + physical + data + + + full + full + auto + true + true + true + up + 00:0c:29:fc:04:f7 + 1500 + %(node_name)s + full + none + 1000 + e0d + physical + data + + + 4 + +""" % {'node_name': NODE_NAME}) + +PORTS = ['e0a', 'e0b', 'e0c', 'e0d'] + +NET_INTERFACE_GET_ITER_RESPONSE = etree.XML(""" + + + +
192.168.228.42
+ ipv4 + up + %(node)s + e0c + + none + + none + system-defined + disabled + mgmt + %(node)s + e0c + cluster_mgmt + true + true + d3230112-7524-11e4-8608-123478563412 + false + %(netmask)s + 24 + up + cluster_mgmt + c192.168.228.0/24 + system_defined + cluster3 +
+ +
192.168.228.43
+ ipv4 + up + %(node)s + e0d + none + system-defined + nextavail + mgmt + %(node)s + e0d + mgmt1 + true + true + 0ccc57cc-7525-11e4-8608-123478563412 + false + %(netmask)s + 24 + up + node_mgmt + n192.168.228.0/24 + system_defined + cluster3-01 +
+ +
%(address)s
+ ipv4 + up + %(node)s + %(vlan)s + + nfs + cifs + + none + system-defined + nextavail + data + %(node)s + %(vlan)s + %(lif)s + false + true + db4d91b6-95d9-11e4-8608-123478563412 + false + %(netmask)s + 24 + up + data + d10.0.0.0/24 + system_defined + %(vserver)s +
+
+ 3 +
+""" % { + '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(""" + + + + %(lif)s + %(vserver)s + + + 1 + +""" % {'lif': LIF_NAME, 'vserver': VSERVER_NAME}) + +AGGR_GET_NAMES_RESPONSE = etree.XML(""" + + + + + + + /aggr0/plex0 + + + /aggr0/plex0/rg0 + + + + + + aggr0 + + + + + + /manila/plex0 + + + /manila/plex0/rg0 + + + /manila/plex0/rg1 + + + + + + manila + + + 2 + +""") + +AGGR_NAMES = ['aggr0', 'manila'] + +AGGR_GET_SPACE_RESPONSE = etree.XML(""" + + + + + + + /aggr0/plex0 + + + /aggr0/plex0/rg0 + + + + + + + 45678592 + 943718400 + + aggr0 + + + + + + /manila/plex0 + + + /manila/plex0/rg0 + + + /manila/plex0/rg1 + + + + + + + 6448435200 + 7549747200 + + manila + + + 2 + +""") + +AGGR_GET_ITER_RESPONSE = etree.XML(""" + + + + + + false + + + + 64_bit + 1758646411 + aggr + + + 512 + 30384 + 96 + 30384 + 30384 + 30384 + 243191 + 96 + 0 + + + 4082368507 + cluster3-01 + 4082368507 + cluster3-01 + + + off + 0 + + + active + block + 3 + cfo + true + false + true + false + false + false + unmirrored + online + 1 + + + true + false + /aggr0/plex0 + normal,active + + + block + false + false + false + /aggr0/plex0/rg0 + 0 + 0 + + + 0 + + + on + 16 + raid_dp, normal + raid_dp + online + + + false + + + 0 + 0 + true + true + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + 245760 + 0 + 95 + 45670400 + 943718400 + 898048000 + 0 + 898048000 + 897802240 + + + 1 + 0 + 0 + + aggr0 + 15863632-ea49-49a8-9c88-2bd2d57c6d7a + + cluster3-01 + + unknown + + + + + false + + + + 64_bit + 706602229 + aggr + + + 528 + 31142 + 96 + 31142 + 31142 + 31142 + 1945584 + 96 + 0 + + + 4082368507 + cluster3-01 + 4082368507 + cluster3-01 + + + off + 0 + + + active + block + 10 + sfo + false + false + true + false + false + false + unmirrored + online + 1 + + + true + false + /manila/plex0 + normal,active + + + block + false + false + false + /manila/plex0/rg0 + 0 + 0 + + + block + false + false + false + /manila/plex0/rg1 + 0 + 0 + + + 0 + + + on + 8 + raid4, normal + raid4 + online + + + false + + + 0 + 0 + true + true + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + 425984 + 0 + 15 + 6448431104 + 7549747200 + 1101316096 + 0 + 1101316096 + 1100890112 + + + 2 + 0 + 0 + + manila + 2a741934-1aaf-42dd-93ca-aaf231be108a + + cluster3-01 + + not_striped + + + 2 + +""") + +VOLUME_GET_NAME_RESPONSE = etree.XML(""" + + + + + %(volume)s + %(vserver)s + + + + 1 + +""" % {'volume': SHARE_NAME, 'vserver': VSERVER_NAME}) + +VOLUME_GET_VOLUME_PATH_RESPONSE = etree.XML(""" + + /%(volume)s + +""" % {'volume': SHARE_NAME}) + +VOLUME_GET_VOLUME_PATH_CIFS_RESPONSE = etree.XML(""" + + \\%(volume)s + +""" % {'volume': SHARE_NAME}) + +VOLUME_JUNCTION_PATH = '/' + SHARE_NAME +VOLUME_JUNCTION_PATH_CIFS = '\\' + SHARE_NAME + +SNAPSHOT_GET_ITER_NOT_BUSY_RESPONSE = etree.XML(""" + + + + false + %(snap)s + %(volume)s + %(vserver)s + + + 1 + +""" % {'snap': SNAPSHOT_NAME, 'volume': SHARE_NAME, 'vserver': VSERVER_NAME}) + +SNAPSHOT_GET_ITER_BUSY_RESPONSE = etree.XML(""" + + + + true + %(snap)s + %(volume)s + %(vserver)s + + + 1 + +""" % {'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(""" + + + +""") + +NFS_EXPORTFS_LIST_RULES_2_RESPONSE = etree.XML(""" + + + + %(path)s + + + 65534 + false + + + %(host1)s + + + %(host2)s + + + + + %(host1)s + + + %(host2)s + + + + + %(host1)s + + + %(host2)s + + + + + sys + + + + + + + +""" % { + 'path': VOLUME_JUNCTION_PATH, + 'host1': NFS_EXPORT_RULES[0], + 'host2': NFS_EXPORT_RULES[1], +}) diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_api.py b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py new file mode 100644 index 0000000000..1051e79cdc --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py @@ -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') diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py new file mode 100644 index 0000000000..1244bed68e --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py @@ -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, + {}) \ No newline at end of file diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py new file mode 100644 index 0000000000..02365211d6 --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py @@ -0,0 +1,1726 @@ +# 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 copy +import hashlib + +import mock + +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_base +from manila.share.drivers.netapp.dataontap.client import client_cmode +from manila import test +from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake + + +class NetAppClientCmodeTestCase(test.TestCase): + + def setUp(self): + super(NetAppClientCmodeTestCase, self).setUp() + + self.mock_object(client_cmode, 'LOG') + self.mock_object(client_base.NetAppBaseClient, + 'get_ontapi_version', + mock.Mock(return_value=(1, 20))) + + self.client = client_cmode.NetAppCmodeClient(**fake.CONNECTION_INFO) + self.client.connection = mock.MagicMock() + + self.vserver_client = client_cmode.NetAppCmodeClient( + **fake.CONNECTION_INFO) + self.vserver_client.set_vserver(fake.VSERVER_NAME) + self.vserver_client.connection = mock.MagicMock() + + def _mock_api_error(self, code='fake'): + return mock.Mock(side_effect=netapp_api.NaApiError(code=code)) + + def test_invoke_vserver_api(self): + + self.client._invoke_vserver_api('fake-api', 'fake_vserver') + + self.client.connection.set_vserver.assert_has_calls( + [mock.call('fake_vserver')]) + self.client.connection.invoke_successfully.assert_has_calls( + [mock.call('fake-api', True)]) + + def test_has_records(self): + self.assertTrue(self.client._has_records( + netapp_api.NaElement(fake.VSERVER_GET_ITER_RESPONSE))) + + def test_has_records_not_found(self): + self.assertFalse(self.client._has_records( + netapp_api.NaElement(fake.NO_RECORDS_RESPONSE))) + + def test_set_vserver(self): + self.client.set_vserver(fake.VSERVER_NAME) + self.client.connection.set_vserver.assert_has_calls( + [mock.call('fake_vserver')]) + + def test_vserver_exists(self): + + api_response = netapp_api.NaElement(fake.VSERVER_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + vserver_get_args = { + 'query': {'vserver-info': {'vserver-name': fake.VSERVER_NAME}}, + 'desired-attributes': {'vserver-info': {'vserver-name': None}} + } + + response = self.client.vserver_exists(fake.VSERVER_NAME) + + self.client.send_request.assert_has_calls([ + mock.call('vserver-get-iter', vserver_get_args)]) + self.assertTrue(response) + + def test_vserver_exists_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.vserver_exists(fake.VSERVER_NAME) + + self.assertFalse(result) + + def test_create_vserver(self): + + self.mock_object(self.client, 'send_request') + + vserver_create_args = { + 'vserver-name': fake.VSERVER_NAME, + 'root-volume-security-style': 'unix', + 'root-volume-aggregate': fake.ROOT_VOLUME_AGGREGATE_NAME, + 'root-volume': fake.ROOT_VOLUME_NAME, + 'name-server-switch': {'nsswitch': 'file'} + } + vserver_modify_args = { + 'aggr-list': [{'aggr-name': aggr_name} for aggr_name + in fake.SHARE_AGGREGATE_NAMES], + 'vserver-name': fake.VSERVER_NAME + } + + self.client.create_vserver(fake.VSERVER_NAME, + fake.ROOT_VOLUME_AGGREGATE_NAME, + fake.ROOT_VOLUME_NAME, + fake.SHARE_AGGREGATE_NAMES) + + self.client.send_request.assert_has_calls([ + mock.call('vserver-create', vserver_create_args), + mock.call('vserver-modify', vserver_modify_args)]) + + def test_get_vserver_root_volume_name(self): + + api_response = netapp_api.NaElement( + fake.VSERVER_GET_ROOT_VOLUME_NAME_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + vserver_get_args = { + 'query': {'vserver-info': {'vserver-name': fake.VSERVER_NAME}}, + 'desired-attributes': {'vserver-info': {'root-volume': None}} + } + + response = self.client.get_vserver_root_volume_name(fake.VSERVER_NAME) + + self.client.send_request.assert_has_calls([ + mock.call('vserver-get-iter', vserver_get_args)]) + self.assertEqual(fake.ROOT_VOLUME_NAME, response) + + def test_get_vserver_root_volume_name_not_found(self): + + api_response = netapp_api.NaElement( + fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.client.get_vserver_root_volume_name, + fake.VSERVER_NAME) + + def test_list_vservers(self): + + api_response = netapp_api.NaElement( + fake.VSERVER_DATA_LIST_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_vservers() + + vserver_get_iter_args = { + 'query': { + 'vserver-info': { + 'vserver-type': 'data' + } + }, + 'desired-attributes': { + 'vserver-info': { + 'vserver-name': None + } + } + } + self.client.send_request.assert_has_calls([ + mock.call('vserver-get-iter', vserver_get_iter_args)]) + self.assertListEqual([fake.VSERVER_NAME], result) + + def test_list_vservers_node_type(self): + + api_response = netapp_api.NaElement( + fake.VSERVER_DATA_LIST_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_vservers(vserver_type='node') + + vserver_get_iter_args = { + 'query': { + 'vserver-info': { + 'vserver-type': 'node' + } + }, + 'desired-attributes': { + 'vserver-info': { + 'vserver-name': None + } + } + } + self.client.send_request.assert_has_calls([ + mock.call('vserver-get-iter', vserver_get_iter_args)]) + self.assertListEqual([fake.VSERVER_NAME], result) + + def test_list_vservers_not_found(self): + + api_response = netapp_api.NaElement( + fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_vservers(vserver_type='data') + + self.assertListEqual([], result) + + def test_get_vserver_volume_count(self): + + api_response = netapp_api.NaElement(fake.VOLUME_COUNT_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_vserver_volume_count() + + self.assertEqual(2, result) + + def test_delete_vserver_no_volumes(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=True)) + self.mock_object(self.client, + 'get_vserver_root_volume_name', + mock.Mock(return_value=fake.ROOT_VOLUME_NAME)) + self.mock_object(self.client, + 'get_vserver_volume_count', + mock.Mock(return_value=0)) + self.mock_object(self.client, '_terminate_vserver_services') + self.mock_object(self.client, 'send_request') + + self.client.delete_vserver( + fake.VSERVER_NAME, + self.vserver_client, + security_services=[fake.CIFS_SECURITY_SERVICE]) + + self.client._terminate_vserver_services.assert_called_with( + fake.VSERVER_NAME, self.vserver_client, + [fake.CIFS_SECURITY_SERVICE]) + + vserver_destroy_args = {'vserver-name': fake.VSERVER_NAME} + self.client.send_request.assert_has_calls([ + mock.call('vserver-destroy', vserver_destroy_args)]) + + def test_delete_vserver_one_volume(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=True)) + self.mock_object(self.client, + 'get_vserver_root_volume_name', + mock.Mock(return_value=fake.ROOT_VOLUME_NAME)) + self.mock_object(self.vserver_client, + 'get_vserver_volume_count', + mock.Mock(return_value=1)) + self.mock_object(self.client, 'send_request') + self.mock_object(self.vserver_client, 'offline_volume') + self.mock_object(self.vserver_client, 'delete_volume') + + self.client.delete_vserver(fake.VSERVER_NAME, + self.vserver_client) + + self.vserver_client.offline_volume.assert_called_with( + fake.ROOT_VOLUME_NAME) + self.vserver_client.delete_volume.assert_called_with( + fake.ROOT_VOLUME_NAME) + + vserver_destroy_args = {'vserver-name': fake.VSERVER_NAME} + self.client.send_request.assert_has_calls([ + mock.call('vserver-destroy', vserver_destroy_args)]) + + def test_delete_vserver_one_volume_already_offline(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=True)) + self.mock_object(self.client, + 'get_vserver_root_volume_name', + mock.Mock(return_value=fake.ROOT_VOLUME_NAME)) + self.mock_object(self.vserver_client, + 'get_vserver_volume_count', + mock.Mock(return_value=1)) + self.mock_object(self.client, 'send_request') + self.mock_object(self.vserver_client, + 'offline_volume', + self._mock_api_error(code=netapp_api.EVOLUMEOFFLINE)) + + self.mock_object(self.vserver_client, 'delete_volume') + + self.client.delete_vserver(fake.VSERVER_NAME, + self.vserver_client) + + self.vserver_client.offline_volume.assert_called_with( + fake.ROOT_VOLUME_NAME) + self.vserver_client.delete_volume.assert_called_with( + fake.ROOT_VOLUME_NAME) + + vserver_destroy_args = {'vserver-name': fake.VSERVER_NAME} + self.client.send_request.assert_has_calls([ + mock.call('vserver-destroy', vserver_destroy_args)]) + self.assertEqual(1, client_cmode.LOG.error.call_count) + + def test_delete_vserver_one_volume_api_error(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=True)) + self.mock_object(self.client, + 'get_vserver_root_volume_name', + mock.Mock(return_value=fake.ROOT_VOLUME_NAME)) + self.mock_object(self.vserver_client, + 'get_vserver_volume_count', + mock.Mock(return_value=1)) + self.mock_object(self.client, 'send_request') + self.mock_object(self.vserver_client, + 'offline_volume', + self._mock_api_error()) + self.mock_object(self.vserver_client, 'delete_volume') + + self.assertRaises(netapp_api.NaApiError, + self.client.delete_vserver, + fake.VSERVER_NAME, + self.vserver_client) + + def test_delete_vserver_multiple_volumes(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=True)) + self.mock_object(self.client, + 'get_vserver_root_volume_name', + mock.Mock(return_value=fake.ROOT_VOLUME_NAME)) + self.mock_object(self.vserver_client, + 'get_vserver_volume_count', + mock.Mock(return_value=2)) + + self.assertRaises(exception.NetAppException, + self.client.delete_vserver, + fake.VSERVER_NAME, + self.vserver_client) + + def test_delete_vserver_not_found(self): + + self.mock_object(self.client, + 'vserver_exists', + mock.Mock(return_value=False)) + + self.client.delete_vserver(fake.VSERVER_NAME, + self.vserver_client) + + self.assertEqual(1, client_cmode.LOG.error.call_count) + + def test_terminate_vserver_services(self): + + self.mock_object(self.vserver_client, 'send_request') + + self.client._terminate_vserver_services(fake.VSERVER_NAME, + self.vserver_client, + [fake.CIFS_SECURITY_SERVICE]) + + cifs_server_delete_args = { + 'admin-password': fake.CIFS_SECURITY_SERVICE['password'], + 'admin-username': fake.CIFS_SECURITY_SERVICE['user'], + } + self.vserver_client.send_request.assert_has_calls([ + mock.call('cifs-server-delete', cifs_server_delete_args)]) + + def test_terminate_vserver_services_cifs_not_found(self): + + self.mock_object(self.vserver_client, + 'send_request', + self._mock_api_error( + code=netapp_api.EOBJECTNOTFOUND)) + + self.client._terminate_vserver_services(fake.VSERVER_NAME, + self.vserver_client, + [fake.CIFS_SECURITY_SERVICE]) + + cifs_server_delete_args = { + 'admin-password': fake.CIFS_SECURITY_SERVICE['password'], + 'admin-username': fake.CIFS_SECURITY_SERVICE['user'], + } + self.vserver_client.send_request.assert_has_calls([ + mock.call('cifs-server-delete', cifs_server_delete_args)]) + self.assertEqual(1, client_cmode.LOG.error.call_count) + + def test_terminate_vserver_services_api_error(self): + + side_effects = [netapp_api.NaApiError(code='fake'), None] + self.mock_object(self.vserver_client, + 'send_request', + mock.Mock(side_effect=side_effects)) + + self.client._terminate_vserver_services(fake.VSERVER_NAME, + self.vserver_client, + [fake.CIFS_SECURITY_SERVICE]) + + cifs_server_delete_args = { + 'admin-password': fake.CIFS_SECURITY_SERVICE['password'], + 'admin-username': fake.CIFS_SECURITY_SERVICE['user'], + } + self.vserver_client.send_request.assert_has_calls([ + mock.call('cifs-server-delete', cifs_server_delete_args), + mock.call('cifs-server-delete')]) + self.assertEqual(0, client_cmode.LOG.error.call_count) + + def test_list_cluster_nodes(self): + + api_response = netapp_api.NaElement( + fake.SYSTEM_NODE_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_cluster_nodes() + + self.assertListEqual([fake.NODE_NAME], result) + + def test_list_cluster_nodes_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_cluster_nodes() + + self.assertListEqual([], result) + + def test_get_node_data_port(self): + + api_response = netapp_api.NaElement(fake.NET_PORT_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_node_data_port(fake.NODE_NAME) + + self.assertEqual(fake.PORTS[0], result) + + def test_get_node_data_port_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.client.get_node_data_port, + fake.NODE_NAME) + + def test_list_aggregates(self): + + api_response = netapp_api.NaElement(fake.AGGR_GET_NAMES_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_aggregates() + + self.assertListEqual(fake.AGGR_NAMES, result) + + def test_list_aggregates_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.client.list_aggregates) + + def test_create_network_interface(self): + + self.mock_object(self.client, '_create_vlan') + self.mock_object(self.client, 'send_request') + + lif_create_args = { + 'address': fake.IP_ADDRESS, + 'administrative-status': 'up', + 'data-protocols': [ + {'data-protocol': 'nfs'}, + {'data-protocol': 'cifs'} + ], + 'home-node': fake.NODE_NAME, + 'home-port': fake.VLAN_PORT, + 'netmask': fake.NETMASK, + 'interface-name': fake.LIF_NAME, + 'role': 'data', + 'vserver': fake.VSERVER_NAME, + } + self.client.create_network_interface(fake.IP_ADDRESS, fake.NETMASK, + fake.VLAN, fake.NODE_NAME, + fake.PORT, fake.VSERVER_NAME, + fake.NET_ALLOCATION_ID, + fake.LIF_NAME_TEMPLATE) + + self.client._create_vlan.assert_called_with(fake.NODE_NAME, fake.PORT, + fake.VLAN) + + self.client.send_request.assert_has_calls([ + mock.call('net-interface-create', lif_create_args)]) + + def test_create_vlan(self): + + self.mock_object(self.client, 'send_request') + + vlan_create_args = { + 'vlan-info': { + 'parent-interface': fake.PORT, + 'node': fake.NODE_NAME, + 'vlanid': fake.VLAN + } + } + self.client._create_vlan(fake.NODE_NAME, fake.PORT, fake.VLAN) + + self.client.send_request.assert_has_calls([ + mock.call('net-vlan-create', vlan_create_args)]) + + def test_create_vlan_already_present(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error( + code=netapp_api.EDUPLICATEENTRY)) + + vlan_create_args = { + 'vlan-info': { + 'parent-interface': fake.PORT, + 'node': fake.NODE_NAME, + 'vlanid': fake.VLAN + } + } + self.client._create_vlan(fake.NODE_NAME, fake.PORT, fake.VLAN) + + self.client.send_request.assert_has_calls([ + mock.call('net-vlan-create', vlan_create_args)]) + self.assertEqual(1, client_cmode.LOG.debug.call_count) + + def test_create_vlan_api_error(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + + self.assertRaises(exception.NetAppException, + self.client._create_vlan, + fake.NODE_NAME, + fake.PORT, + fake.VLAN) + + def test_network_interface_exists(self): + + api_response = netapp_api.NaElement( + fake.NET_INTERFACE_GET_ONE_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + net_interface_get_args = { + 'query': { + 'net-interface-info': { + 'address': fake.IP_ADDRESS, + 'home-node': fake.NODE_NAME, + 'home-port': fake.VLAN_PORT, + 'netmask': fake.NETMASK, + 'vserver': fake.VSERVER_NAME} + }, + 'desired-attributes': { + 'net-interface-info': { + 'interface-name': None, + } + } + } + response = self.client.network_interface_exists( + fake.VSERVER_NAME, fake.NODE_NAME, fake.PORT, fake.IP_ADDRESS, + fake.NETMASK, fake.VLAN) + + self.client.send_request.assert_has_calls([ + mock.call('net-interface-get-iter', net_interface_get_args)]) + self.assertTrue(response) + + def test_network_interface_exists_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + response = self.client.network_interface_exists( + fake.VSERVER_NAME, fake.NODE_NAME, fake.PORT, fake.IP_ADDRESS, + fake.NETMASK, fake.VLAN) + + self.assertFalse(response) + + def test_list_network_interfaces(self): + + api_response = netapp_api.NaElement( + fake.NET_INTERFACE_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + net_interface_get_args = { + 'desired-attributes': { + 'net-interface-info': { + 'interface-name': None, + } + } + } + + result = self.client.list_network_interfaces() + + self.client.send_request.assert_has_calls([ + mock.call('net-interface-get-iter', net_interface_get_args)]) + self.assertListEqual(fake.LIF_NAMES, result) + + def test_list_network_interfaces_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.list_network_interfaces() + + self.assertListEqual([], result) + + def test_get_network_interfaces(self): + + api_response = netapp_api.NaElement( + fake.NET_INTERFACE_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_network_interfaces() + + self.client.send_request.assert_has_calls([ + mock.call('net-interface-get-iter')]) + self.assertListEqual(fake.LIFS, result) + + def test_delete_network_interface(self): + + self.mock_object(self.client, 'send_request') + + self.client.delete_network_interface(fake.LIF_NAME) + + net_interface_get_args = { + 'vserver': None, + 'interface-name': fake.LIF_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('net-interface-delete', net_interface_get_args)]) + + def test_calculate_aggregate_capacity(self): + + api_response = netapp_api.NaElement( + fake.AGGR_GET_SPACE_RESPONSE).get_child_by_name( + 'attributes-list').get_children() + self.mock_object(self.client, + '_get_aggregates', + mock.Mock(return_value=api_response)) + + result = self.client.calculate_aggregate_capacity(fake.AGGR_NAMES) + + desired_attributes = { + 'aggr-attributes': { + 'aggregate-name': None, + 'aggr-space-attributes': { + 'size-total': None, + 'size-available': None, + } + } + } + + self.client._get_aggregates.assert_has_calls([ + mock.call( + aggregate_names=fake.AGGR_NAMES, + desired_attributes=desired_attributes)]) + self.assertEqual((8493465600, 6448435200), result) + + def test_calculate_aggregate_capacity_not_found(self): + + api_response = netapp_api.NaElement('none').get_children() + self.mock_object(self.client, + '_get_aggregates', + mock.Mock(return_value=api_response)) + + result = self.client.calculate_aggregate_capacity(fake.AGGR_NAMES) + + self.assertEqual((0, 0), result) + + def test_get_aggregates_for_vserver(self): + + api_response = netapp_api.NaElement(fake.VSERVER_GET_RESPONSE) + self.mock_object(self.vserver_client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.vserver_client.get_aggregates_for_vserver( + fake.VSERVER_NAME) + + vserver_args = { + 'desired-attributes': { + 'vserver-info': { + 'vserver-aggr-info-list': { + 'vserver-aggr-info': { + 'aggr-name': None, + 'aggr-availsize': None + } + } + } + } + } + + self.vserver_client.send_request.assert_has_calls([ + mock.call('vserver-get', vserver_args)]) + self.assertDictEqual(fake.VSERVER_AGGREGATES, result) + + def test_get_aggregates_for_vserver_not_found(self): + + api_response = netapp_api.NaElement( + fake.VSERVER_GET_RESPONSE_NO_AGGREGATES) + self.mock_object(self.vserver_client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.vserver_client.get_aggregates_for_vserver, + fake.VSERVER_NAME) + + def test_get_aggregates(self): + + api_response = netapp_api.NaElement(fake.AGGR_GET_ITER_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client._get_aggregates() + + self.client.send_request.assert_has_calls([ + mock.call('aggr-get-iter', {})]) + self.assertListEqual( + [aggr.to_string() for aggr in api_response.get_child_by_name( + 'attributes-list').get_children()], + [aggr.to_string() for aggr in result]) + + def test_get_aggregates_with_filters(self): + + api_response = netapp_api.NaElement(fake.AGGR_GET_SPACE_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + desired_attributes = { + 'aggr-attributes': { + 'aggregate-name': None, + 'aggr-space-attributes': { + 'size-total': None, + 'size-available': None, + } + } + } + + result = self.client._get_aggregates( + aggregate_names=fake.AGGR_NAMES, + desired_attributes=desired_attributes) + + aggr_get_iter_args = { + 'query': { + 'aggr-attributes': { + 'aggregate-name': '|'.join(fake.AGGR_NAMES), + } + }, + 'desired-attributes': desired_attributes + } + + self.client.send_request.assert_has_calls([ + mock.call('aggr-get-iter', aggr_get_iter_args)]) + self.assertListEqual( + [aggr.to_string() for aggr in api_response.get_child_by_name( + 'attributes-list').get_children()], + [aggr.to_string() for aggr in result]) + + def test_get_aggregates_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client._get_aggregates() + + self.client.send_request.assert_has_calls([ + mock.call('aggr-get-iter', {})]) + self.assertListEqual([], result) + + def test_setup_security_services_ldap(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.vserver_client, 'configure_ldap') + + self.client.setup_security_services([fake.LDAP_SECURITY_SERVICE], + self.vserver_client, + fake.VSERVER_NAME) + + vserver_modify_args = { + 'name-mapping-switch': {'nmswitch': 'ldap,file'}, + 'name-server-switch': {'nsswitch': 'ldap,file'}, + 'vserver-name': fake.VSERVER_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('vserver-modify', vserver_modify_args)]) + self.vserver_client.configure_ldap.assert_has_calls([ + mock.call(fake.LDAP_SECURITY_SERVICE)]) + + def test_setup_security_services_active_directory(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.vserver_client, 'configure_active_directory') + + self.client.setup_security_services([fake.CIFS_SECURITY_SERVICE], + self.vserver_client, + fake.VSERVER_NAME) + + vserver_modify_args = { + 'name-mapping-switch': {'nmswitch': 'ldap,file'}, + 'name-server-switch': {'nsswitch': 'ldap,file'}, + 'vserver-name': fake.VSERVER_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('vserver-modify', vserver_modify_args)]) + self.vserver_client.configure_active_directory.assert_has_calls([ + mock.call(fake.CIFS_SECURITY_SERVICE, fake.VSERVER_NAME)]) + + def test_setup_security_services_kerberos(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.client, 'create_kerberos_realm') + self.mock_object(self.vserver_client, 'configure_kerberos') + + self.client.setup_security_services([fake.KERBEROS_SECURITY_SERVICE], + self.vserver_client, + fake.VSERVER_NAME) + + vserver_modify_args = { + 'name-mapping-switch': {'nmswitch': 'ldap,file'}, + 'name-server-switch': {'nsswitch': 'ldap,file'}, + 'vserver-name': fake.VSERVER_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('vserver-modify', vserver_modify_args)]) + self.client.create_kerberos_realm.assert_has_calls([ + mock.call(fake.KERBEROS_SECURITY_SERVICE)]) + self.vserver_client.configure_kerberos.assert_has_calls([ + mock.call(fake.KERBEROS_SECURITY_SERVICE, fake.VSERVER_NAME)]) + + def test_setup_security_services_invalid(self): + + self.mock_object(self.client, 'send_request') + + self.assertRaises(exception.NetAppException, + self.client.setup_security_services, + [fake.INVALID_SECURITY_SERVICE], + self.vserver_client, + fake.VSERVER_NAME) + + vserver_modify_args = { + 'name-mapping-switch': {'nmswitch': 'ldap,file'}, + 'name-server-switch': {'nsswitch': 'ldap,file'}, + 'vserver-name': fake.VSERVER_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('vserver-modify', vserver_modify_args)]) + + def test_enable_nfs(self): + + self.mock_object(self.client, 'send_request') + + self.client.enable_nfs() + + nfs_service_modify_args = {'is-nfsv40-enabled': 'true'} + export_rule_create_args = { + 'client-match': '0.0.0.0/0', + 'policy-name': 'default', + 'ro-rule': {'security-flavor': 'any'}, + 'rw-rule': {'security-flavor': 'any'}} + + self.client.send_request.assert_has_calls([ + mock.call('nfs-enable'), + mock.call('nfs-service-modify', nfs_service_modify_args), + mock.call('export-rule-create', export_rule_create_args)]) + + def test_configure_ldap(self): + + self.mock_object(self.client, 'send_request') + + self.client.configure_ldap(fake.LDAP_SECURITY_SERVICE) + + config_name = hashlib.md5( + fake.LDAP_SECURITY_SERVICE['id']).hexdigest() + + ldap_client_create_args = { + 'ldap-client-config': config_name, + 'servers': {'ip-address': fake.LDAP_SECURITY_SERVICE['server']}, + 'tcp-port': '389', + 'schema': 'RFC-2307', + 'bind-password': fake.LDAP_SECURITY_SERVICE['password'] + } + ldap_config_create_args = { + 'client-config': config_name, + 'client-enabled': 'true' + } + + self.client.send_request.assert_has_calls([ + mock.call('ldap-client-create', ldap_client_create_args), + mock.call('ldap-config-create', ldap_config_create_args)]) + + def test_configure_active_directory(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.client, 'configure_dns') + + self.client.configure_active_directory(fake.CIFS_SECURITY_SERVICE, + fake.VSERVER_NAME) + + cifs_server = ( + fake.VSERVER_NAME[0:7] + '..' + fake.VSERVER_NAME[-6:]).upper() + cifs_server_create_args = { + 'admin-username': fake.CIFS_SECURITY_SERVICE['user'], + 'admin-password': fake.CIFS_SECURITY_SERVICE['password'], + 'force-account-overwrite': 'true', + 'cifs-server': cifs_server, + 'domain': fake.CIFS_SECURITY_SERVICE['domain'], + } + + self.client.configure_dns.assert_called_with( + fake.CIFS_SECURITY_SERVICE) + self.client.send_request.assert_has_calls([ + mock.call('cifs-server-create', cifs_server_create_args)]) + + def test_configure_active_directory_api_error(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + self.mock_object(self.client, 'configure_dns') + + self.assertRaises(exception.NetAppException, + self.client.configure_active_directory, + fake.CIFS_SECURITY_SERVICE, + fake.VSERVER_NAME) + + def test_create_kerberos_realm(self): + + self.mock_object(self.client, 'send_request') + + self.client.create_kerberos_realm(fake.KERBEROS_SECURITY_SERVICE) + + kerberos_realm_create_args = { + 'admin-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'admin-server-port': '749', + 'clock-skew': '5', + 'comment': '', + 'config-name': fake.KERBEROS_SECURITY_SERVICE['id'], + 'kdc-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'kdc-port': '88', + 'kdc-vendor': 'other', + 'password-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'password-server-port': '464', + 'realm': fake.KERBEROS_SECURITY_SERVICE['domain'].upper() + } + + self.client.send_request.assert_has_calls([ + mock.call('kerberos-realm-create', kerberos_realm_create_args)]) + + def test_create_kerberos_realm_already_present(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error( + code=netapp_api.EDUPLICATEENTRY)) + + self.client.create_kerberos_realm(fake.KERBEROS_SECURITY_SERVICE) + + kerberos_realm_create_args = { + 'admin-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'admin-server-port': '749', + 'clock-skew': '5', + 'comment': '', + 'config-name': fake.KERBEROS_SECURITY_SERVICE['id'], + 'kdc-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'kdc-port': '88', + 'kdc-vendor': 'other', + 'password-server-ip': fake.KERBEROS_SECURITY_SERVICE['server'], + 'password-server-port': '464', + 'realm': fake.KERBEROS_SECURITY_SERVICE['domain'].upper() + } + + self.client.send_request.assert_has_calls([ + mock.call('kerberos-realm-create', kerberos_realm_create_args)]) + self.assertEqual(1, client_cmode.LOG.debug.call_count) + + def test_create_kerberos_realm_api_error(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + + self.assertRaises(exception.NetAppException, + self.client.create_kerberos_realm, + fake.KERBEROS_SECURITY_SERVICE) + + def test_configure_kerberos(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.client, 'configure_dns') + self.mock_object(self.client, + 'list_network_interfaces', + mock.Mock(return_value=['lif1', 'lif2'])) + + self.client.configure_kerberos( + fake.KERBEROS_SECURITY_SERVICE, fake.VSERVER_NAME) + + spn = self.client._get_kerberos_service_principal_name( + fake.KERBEROS_SECURITY_SERVICE, fake.VSERVER_NAME) + + kerberos_config_modify_args1 = { + 'admin-password': fake.KERBEROS_SECURITY_SERVICE['password'], + 'admin-user-name': fake.KERBEROS_SECURITY_SERVICE['user'], + 'interface-name': 'lif1', + 'is-kerberos-enabled': 'true', + 'service-principal-name': spn + } + kerberos_config_modify_args2 = { + 'admin-password': fake.KERBEROS_SECURITY_SERVICE['password'], + 'admin-user-name': fake.KERBEROS_SECURITY_SERVICE['user'], + 'interface-name': 'lif2', + 'is-kerberos-enabled': 'true', + 'service-principal-name': spn + } + + self.client.configure_dns.assert_called_with( + fake.KERBEROS_SECURITY_SERVICE) + self.client.send_request.assert_has_calls([ + mock.call('kerberos-config-modify', + kerberos_config_modify_args1), + mock.call('kerberos-config-modify', + kerberos_config_modify_args2)]) + + def test_configure_kerberos_no_network_interfaces(self): + + self.mock_object(self.client, 'send_request') + self.mock_object(self.client, 'configure_dns') + self.mock_object(self.client, + 'list_network_interfaces', + mock.Mock(return_value=[])) + + self.assertRaises(exception.NetAppException, + self.client.configure_kerberos, + fake.KERBEROS_SECURITY_SERVICE, + fake.VSERVER_NAME) + + self.client.configure_dns.assert_called_with( + fake.KERBEROS_SECURITY_SERVICE) + + def test_get_kerberos_service_principal_name(self): + + spn = self.client._get_kerberos_service_principal_name( + fake.KERBEROS_SECURITY_SERVICE, fake.VSERVER_NAME + ) + self.assertEqual(fake.KERBEROS_SERVICE_PRINCIPAL_NAME, spn) + + def test_configure_dns_for_active_directory(self): + + self.mock_object(self.client, 'send_request') + + self.client.configure_dns(fake.CIFS_SECURITY_SERVICE) + + net_dns_create_args = { + 'domains': {'string': fake.CIFS_SECURITY_SERVICE['domain']}, + 'name-servers': { + 'ip-address': fake.CIFS_SECURITY_SERVICE['dns_ip'] + }, + 'dns-state': 'enabled' + } + + self.client.send_request.assert_has_calls([ + mock.call('net-dns-create', net_dns_create_args)]) + + def test_configure_dns_for_kerberos(self): + + self.mock_object(self.client, 'send_request') + + self.client.configure_dns(fake.KERBEROS_SECURITY_SERVICE) + + net_dns_create_args = { + 'domains': {'string': fake.KERBEROS_SECURITY_SERVICE['domain']}, + 'name-servers': { + 'ip-address': fake.KERBEROS_SECURITY_SERVICE['dns_ip'] + }, + 'dns-state': 'enabled' + } + + self.client.send_request.assert_has_calls([ + mock.call('net-dns-create', net_dns_create_args)]) + + def test_configure_dns_already_present(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error( + code=netapp_api.EDUPLICATEENTRY)) + + self.client.configure_dns(fake.KERBEROS_SECURITY_SERVICE) + + net_dns_create_args = { + 'domains': {'string': fake.KERBEROS_SECURITY_SERVICE['domain']}, + 'name-servers': { + 'ip-address': fake.KERBEROS_SECURITY_SERVICE['dns_ip'] + }, + 'dns-state': 'enabled' + } + + self.client.send_request.assert_has_calls([ + mock.call('net-dns-create', net_dns_create_args)]) + self.assertEqual(1, client_cmode.LOG.error.call_count) + + def test_configure_dns_api_error(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + + self.assertRaises(exception.NetAppException, + self.client.configure_dns, + fake.KERBEROS_SECURITY_SERVICE) + + def test_create_volume(self): + + self.mock_object(self.client, 'send_request') + + self.client.create_volume( + fake.SHARE_AGGREGATE_NAME, fake.SHARE_NAME, 100) + + volume_create_args = { + 'containing-aggr-name': fake.SHARE_AGGREGATE_NAME, + 'size': '100g', + 'volume': fake.SHARE_NAME, + 'junction-path': '/%s' % fake.SHARE_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-create', volume_create_args)]) + + def test_volume_exists(self): + + api_response = netapp_api.NaElement(fake.VOLUME_GET_NAME_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + response = self.client.volume_exists(fake.SHARE_NAME) + + volume_get_iter_args = { + 'query': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': fake.SHARE_NAME + } + } + }, + 'desired-attributes': { + 'volume-attributes': { + 'volume-id-attributes': { + 'name': None + } + } + } + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-get-iter', volume_get_iter_args)]) + self.assertTrue(response) + + def test_volume_exists_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertFalse(self.client.volume_exists(fake.SHARE_NAME)) + + def test_create_volume_clone(self): + + self.mock_object(self.client, 'send_request') + + self.client.create_volume_clone(fake.SHARE_NAME, + fake.PARENT_SHARE_NAME, + fake.PARENT_SNAPSHOT_NAME) + + volume_clone_create_args = { + 'volume': fake.SHARE_NAME, + 'parent-volume': fake.PARENT_SHARE_NAME, + 'parent-snapshot': fake.PARENT_SNAPSHOT_NAME, + 'junction-path': '/%s' % fake.SHARE_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-clone-create', volume_clone_create_args)]) + + def test_get_volume_junction_path(self): + + api_response = netapp_api.NaElement( + fake.VOLUME_GET_VOLUME_PATH_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_volume_junction_path(fake.SHARE_NAME) + + volume_get_volume_path_args = { + 'volume': fake.SHARE_NAME, + 'is-style-cifs': 'false' + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-get-volume-path', volume_get_volume_path_args)]) + self.assertEqual(fake.VOLUME_JUNCTION_PATH, result) + + def test_get_volume_junction_path_cifs(self): + + api_response = netapp_api.NaElement( + fake.VOLUME_GET_VOLUME_PATH_CIFS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_volume_junction_path(fake.SHARE_NAME, + is_style_cifs=True) + + volume_get_volume_path_args = { + 'volume': fake.SHARE_NAME, + 'is-style-cifs': 'true' + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-get-volume-path', volume_get_volume_path_args)]) + self.assertEqual(fake.VOLUME_JUNCTION_PATH_CIFS, result) + + def test_offline_volume(self): + + self.mock_object(self.client, 'send_request') + + self.client.offline_volume(fake.SHARE_NAME) + + volume_offline_args = {'name': fake.SHARE_NAME} + + self.client.send_request.assert_has_calls([ + mock.call('volume-offline', volume_offline_args)]) + + def test_unmount_volume(self): + + self.mock_object(self.client, 'send_request') + + self.client.unmount_volume(fake.SHARE_NAME) + + volume_unmount_args = { + 'volume-name': fake.SHARE_NAME, + 'force': 'false' + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-unmount', volume_unmount_args)]) + + def test_unmount_volume_force(self): + + self.mock_object(self.client, 'send_request') + + self.client.unmount_volume(fake.SHARE_NAME, force=True) + + volume_unmount_args = { + 'volume-name': fake.SHARE_NAME, + 'force': 'true' + } + + self.client.send_request.assert_has_calls([ + mock.call('volume-unmount', volume_unmount_args)]) + + def test_delete_volume(self): + + self.mock_object(self.client, 'send_request') + + self.client.delete_volume(fake.SHARE_NAME) + + volume_destroy_args = {'name': fake.SHARE_NAME} + + self.client.send_request.assert_has_calls([ + mock.call('volume-destroy', volume_destroy_args)]) + + def test_create_snapshot(self): + + self.mock_object(self.client, 'send_request') + + self.client.create_snapshot(fake.SHARE_NAME, fake.SNAPSHOT_NAME) + + snapshot_create_args = { + 'volume': fake.SHARE_NAME, + 'snapshot': fake.SNAPSHOT_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('snapshot-create', snapshot_create_args)]) + + def test_is_snapshot_busy(self): + + api_response = netapp_api.NaElement( + fake.SNAPSHOT_GET_ITER_BUSY_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.is_snapshot_busy(fake.SHARE_NAME, + fake.SNAPSHOT_NAME) + + snapshot_get_iter_args = { + 'query': { + 'snapshot-info': { + 'name': fake.SNAPSHOT_NAME, + 'volume': fake.SHARE_NAME + } + }, + 'desired-attributes': { + 'snapshot-info': { + 'busy': None + } + } + } + + self.client.send_request.assert_has_calls([ + mock.call('snapshot-get-iter', snapshot_get_iter_args)]) + self.assertTrue(result) + + def test_is_snapshot_busy_not_busy(self): + + api_response = netapp_api.NaElement( + fake.SNAPSHOT_GET_ITER_NOT_BUSY_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.is_snapshot_busy(fake.SHARE_NAME, + fake.SNAPSHOT_NAME) + + self.assertFalse(result) + + def test_is_snapshot_busy_not_found(self): + + api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + self.assertRaises(exception.NetAppException, + self.client.is_snapshot_busy, + fake.SHARE_NAME, + fake.SNAPSHOT_NAME) + + def test_delete_snapshot(self): + + self.mock_object(self.client, 'send_request') + + self.client.delete_snapshot(fake.SHARE_NAME, fake.SNAPSHOT_NAME) + + snapshot_delete_args = { + 'volume': fake.SHARE_NAME, + 'snapshot': fake.SNAPSHOT_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('snapshot-delete', snapshot_delete_args)]) + + def test_create_cifs_share(self): + + self.mock_object(self.client, 'send_request') + + self.client.create_cifs_share(fake.SHARE_NAME) + + cifs_share_create_args = { + 'path': '/%s' % fake.SHARE_NAME, + 'share-name': fake.SHARE_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call('cifs-share-create', cifs_share_create_args)]) + + def test_add_cifs_share_access(self): + + self.mock_object(self.client, 'send_request') + + self.client.add_cifs_share_access(fake.SHARE_NAME, fake.USER_NAME) + + cifs_share_access_control_create_args = { + 'permission': 'full_control', + 'share': fake.SHARE_NAME, + 'user-or-group': fake.USER_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call( + 'cifs-share-access-control-create', + cifs_share_access_control_create_args)]) + + def test_remove_cifs_share_access(self): + + self.mock_object(self.client, 'send_request') + + self.client.remove_cifs_share_access(fake.SHARE_NAME, fake.USER_NAME) + + cifs_share_access_control_delete_args = { + 'user-or-group': fake.USER_NAME, + 'share': fake.SHARE_NAME + } + + self.client.send_request.assert_has_calls([ + mock.call( + 'cifs-share-access-control-delete', + cifs_share_access_control_delete_args)]) + + def test_remove_cifs_share(self): + + self.mock_object(self.client, 'send_request') + + self.client.remove_cifs_share(fake.SHARE_NAME) + + cifs_share_delete_args = {'share-name': fake.SHARE_NAME} + + self.client.send_request.assert_has_calls([ + mock.call('cifs-share-delete', cifs_share_delete_args)]) + + def _get_add_nfs_export_rules_request(self, export_path, rules): + + return { + 'rules': { + 'exports-rule-info-2': { + 'pathname': fake.VOLUME_JUNCTION_PATH, + 'security-rules': { + 'security-rule-info': { + 'read-write': [ + { + 'exports-hostname-info': { + 'name': fake.NFS_EXPORT_RULES[0], + } + }, + { + 'exports-hostname-info': { + 'name': fake.NFS_EXPORT_RULES[1], + } + } + ], + 'root': [ + { + 'exports-hostname-info': { + 'name': fake.NFS_EXPORT_RULES[0], + } + }, + { + 'exports-hostname-info': { + 'name': fake.NFS_EXPORT_RULES[1], + } + } + ] + } + } + } + } + } + + def test_add_nfs_export_rules(self): + + self.mock_object(self.client, 'send_request') + self.client.nfs_exports_with_prefix = False + + self.client.add_nfs_export_rules(fake.VOLUME_JUNCTION_PATH, + fake.NFS_EXPORT_RULES) + + api_args = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-append-rules-2', api_args)]) + + def test_add_nfs_export_rules_with_vol_prefix(self): + + self.mock_object(self.client, 'send_request') + self.client.nfs_exports_with_prefix = True + + self.client.add_nfs_export_rules(fake.VOLUME_JUNCTION_PATH, + fake.NFS_EXPORT_RULES) + + api_args = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + api_args['rules']['exports-rule-info-2']['pathname'] = ( + '/vol' + fake.VOLUME_JUNCTION_PATH) + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-append-rules-2', api_args)]) + + def test_add_nfs_export_rules_retry_without_vol_prefix(self): + + side_effects = [netapp_api.NaApiError(code=netapp_api.EINTERNALERROR), + None] + self.mock_object(self.client, + 'send_request', + mock.Mock(side_effect=side_effects)) + self.client.nfs_exports_with_prefix = True + + self.client.add_nfs_export_rules(fake.VOLUME_JUNCTION_PATH, + fake.NFS_EXPORT_RULES) + + args_without_prefix = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + + args_with_prefix = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + args_with_prefix['rules']['exports-rule-info-2']['pathname'] = ( + '/vol' + fake.VOLUME_JUNCTION_PATH) + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-append-rules-2', args_with_prefix), + mock.call('nfs-exportfs-append-rules-2', args_without_prefix)]) + self.assertEqual(1, client_cmode.LOG.warning.call_count) + + # Test side effect of setting the prefix flag to false. + self.assertFalse(self.client.nfs_exports_with_prefix) + + def test_add_nfs_export_rules_retry_with_vol_prefix(self): + + side_effects = [netapp_api.NaApiError(code=netapp_api.EINTERNALERROR), + None] + self.mock_object(self.client, + 'send_request', + mock.Mock(side_effect=side_effects)) + self.client.nfs_exports_with_prefix = False + + self.client.add_nfs_export_rules(fake.VOLUME_JUNCTION_PATH, + fake.NFS_EXPORT_RULES) + + args_without_prefix = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + + args_with_prefix = self._get_add_nfs_export_rules_request( + fake.VOLUME_JUNCTION_PATH, fake.NFS_EXPORT_RULES) + args_with_prefix['rules']['exports-rule-info-2']['pathname'] = ( + '/vol' + fake.VOLUME_JUNCTION_PATH) + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-append-rules-2', args_without_prefix), + mock.call('nfs-exportfs-append-rules-2', args_with_prefix)]) + self.assertEqual(1, client_cmode.LOG.warning.call_count) + + # Test side effect of setting the prefix flag to false. + self.assertTrue(self.client.nfs_exports_with_prefix) + + def test_add_nfs_export_rules_api_error(self): + + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + + self.assertRaises(netapp_api.NaApiError, + self.client.add_nfs_export_rules, + fake.VOLUME_JUNCTION_PATH, + fake.NFS_EXPORT_RULES) + + def test_get_nfs_export_rules(self): + + api_response = netapp_api.NaElement( + fake.NFS_EXPORTFS_LIST_RULES_2_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_nfs_export_rules(fake.VOLUME_JUNCTION_PATH) + + nfs_exportfs_list_rules_2_args = { + 'pathname': fake.VOLUME_JUNCTION_PATH + } + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-list-rules-2', + nfs_exportfs_list_rules_2_args)]) + self.assertListEqual(fake.NFS_EXPORT_RULES, result) + + def test_get_nfs_export_rules_not_found(self): + + api_response = netapp_api.NaElement( + fake.NFS_EXPORTFS_LIST_RULES_2_NO_RULES_RESPONSE) + self.mock_object(self.client, + 'send_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_nfs_export_rules(fake.VOLUME_JUNCTION_PATH) + + self.assertListEqual([], result) + + def test_remove_nfs_export_rules(self): + + self.mock_object(self.client, 'send_request') + + self.client.remove_nfs_export_rules(fake.VOLUME_JUNCTION_PATH) + + nfs_exportfs_delete_rules_args = { + 'pathnames': { + 'pathname-info': { + 'name': fake.VOLUME_JUNCTION_PATH, + } + } + } + + self.client.send_request.assert_has_calls([ + mock.call('nfs-exportfs-delete-rules', + nfs_exportfs_delete_rules_args)]) + + def test_get_ems_log_destination_vserver(self): + + self.mock_object(self.client, + 'get_ontapi_version', + mock.Mock(return_value=(1, 21))) + mock_list_vservers = self.mock_object( + self.client, + 'list_vservers', + mock.Mock(return_value=[fake.ADMIN_VSERVER_NAME])) + + result = self.client._get_ems_log_destination_vserver() + + mock_list_vservers.assert_called_once_with(vserver_type='admin') + self.assertEqual(fake.ADMIN_VSERVER_NAME, result) + + def test_get_ems_log_destination_vserver_future(self): + + self.mock_object(self.client, + 'get_ontapi_version', + mock.Mock(return_value=(2, 0))) + mock_list_vservers = self.mock_object( + self.client, + 'list_vservers', + mock.Mock(return_value=[fake.ADMIN_VSERVER_NAME])) + + result = self.client._get_ems_log_destination_vserver() + + mock_list_vservers.assert_called_once_with(vserver_type='admin') + self.assertEqual(fake.ADMIN_VSERVER_NAME, result) + + def test_get_ems_log_destination_vserver_legacy(self): + + self.mock_object(self.client, + 'get_ontapi_version', + mock.Mock(return_value=(1, 15))) + mock_list_vservers = self.mock_object( + self.client, + 'list_vservers', + mock.Mock(return_value=[fake.NODE_VSERVER_NAME])) + + result = self.client._get_ems_log_destination_vserver() + + mock_list_vservers.assert_called_once_with(vserver_type='node') + self.assertEqual(fake.NODE_VSERVER_NAME, result) + + def test_send_ems_log_message(self): + + # Mock client lest we not be able to see calls on its copy. + self.mock_object(copy, + 'deepcopy', + mock.Mock(return_value=self.client)) + self.mock_object(self.client, + '_get_ems_log_destination_vserver', + mock.Mock(return_value=fake.ADMIN_VSERVER_NAME)) + self.mock_object(self.client, 'send_request') + + self.client.send_ems_log_message(fake.EMS_MESSAGE) + + self.client.send_request.assert_has_calls([ + mock.call('ems-autosupport-log', fake.EMS_MESSAGE)]) + self.assertEqual(1, client_cmode.LOG.debug.call_count) + + def test_send_ems_log_message_api_error(self): + + # Mock client lest we not be able to see calls on its copy. + self.mock_object(copy, + 'deepcopy', + mock.Mock(return_value=self.client)) + self.mock_object(self.client, + '_get_ems_log_destination_vserver', + mock.Mock(return_value=fake.ADMIN_VSERVER_NAME)) + self.mock_object(self.client, + 'send_request', + self._mock_api_error()) + + self.client.send_ems_log_message(fake.EMS_MESSAGE) + + self.client.send_request.assert_has_calls([ + mock.call('ems-autosupport-log', fake.EMS_MESSAGE)]) + self.assertEqual(1, client_cmode.LOG.warning.call_count) diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/__init__.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py new file mode 100644 index 0000000000..50b711af52 --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py @@ -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']) diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py new file mode 100644 index 0000000000..1b7bcf0aad --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -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' +} diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/__init__.py b/manila/tests/share/drivers/netapp/dataontap/protocols/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/fakes.py b/manila/tests/share/drivers/netapp/dataontap/protocols/fakes.py new file mode 100644 index 0000000000..abbfa7230c --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/protocols/fakes.py @@ -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 +} diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/test_base.py b/manila/tests/share/drivers/netapp/dataontap/protocols/test_base.py new file mode 100644 index 0000000000..6dcb281a2d --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/protocols/test_base.py @@ -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) diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py b/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py new file mode 100644 index 0000000000..51f3044d51 --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py @@ -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) \ No newline at end of file diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/test_nfs_cmode.py b/manila/tests/share/drivers/netapp/dataontap/protocols/test_nfs_cmode.py new file mode 100644 index 0000000000..dd6b0038ed --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/protocols/test_nfs_cmode.py @@ -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) \ No newline at end of file diff --git a/manila/tests/share/drivers/netapp/fakes.py b/manila/tests/share/drivers/netapp/fakes.py index 1324e9ff09..6ad24e8aab 100644 --- a/manila/tests/share/drivers/netapp/fakes.py +++ b/manila/tests/share/drivers/netapp/fakes.py @@ -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 diff --git a/manila/tests/share/drivers/netapp/test_cluster_mode.py b/manila/tests/share/drivers/netapp/test_cluster_mode.py deleted file mode 100644 index 0ace2251da..0000000000 --- a/manila/tests/share/drivers/netapp/test_cluster_mode.py +++ /dev/null @@ -1,1006 +0,0 @@ -# Copyright (c) 2014 NetApp, Inc. -# Copyright (c) 2015 Tom Barron. -# 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 - -import ddt -import mock - -from manila import context -from manila import exception -from manila.share import configuration -from manila.share.drivers.netapp import api as naapi -from manila.share.drivers.netapp import cluster_mode as driver -from manila.share.drivers.netapp import utils as na_utils -from manila import test -from manila.tests import fake_share -from manila import utils - - -@ddt.ddt -class NetAppClusteredDrvTestCase(test.TestCase): - """Test suite for NetApp Cluster Mode driver.""" - - @mock.patch.object(na_utils.OpenStackInfo, '_update_info_from_rpm', - mock.Mock()) - @mock.patch.object(na_utils.OpenStackInfo, '_update_info_from_dpkg', - mock.Mock()) - def setUp(self): - super(NetAppClusteredDrvTestCase, self).setUp() - self._context = context.get_admin_context() - self._db = mock.Mock() - driver.CONF.set_default('driver_handles_share_servers', True) - self.driver = driver.NetAppClusteredShareDriver( - self._db, configuration=configuration.Configuration(None)) - self.driver._client = mock.Mock() - self.driver._client.send_request = mock.Mock() - self._vserver_client = mock.Mock() - self._vserver_client.send_request = mock.Mock() - driver.NetAppApiClient = mock.Mock(return_value=self._vserver_client) - self.share = fake_share.fake_share(share_proto='NFS') - self.snapshot = fake_share.fake_snapshot() - self.security_service = {'id': 'fake_id', - 'domain': 'FAKE', - 'server': 'fake_server', - 'user': 'fake_user', - 'password': 'fake_password'} - self.share_server = { - 'backend_details': { - 'vserver_name': 'fake_vserver' - } - } - self.helper = mock.Mock() - self.driver._helpers = { - 'NFS': self.helper, - 'CIFS': self.helper, - } - self.driver._licenses = ['nfs', 'cifs'] - self.network_info = { - 'server_id': 'fake_server_id', - 'cidr': '10.0.0.0/24', - 'security_services': ['fake_ldap', 'fake_kerberos', 'fake_ad', ], - 'segmentation_id': '1000', - 'network_allocations': [ - {'id': 'fake_na_id_1', 'ip_address': 'fake_ip_1', }, - {'id': 'fake_na_id_2', 'ip_address': 'fake_ip_2', }, - ], - } - - def test_create_vserver(self): - res = naapi.NaElement('fake') - res.add_new_child('aggregate-name', 'aggr') - self.driver.configuration.netapp_root_volume_aggregate = 'root' - fake_aggrs = mock.Mock() - fake_aggrs.get_child_by_name.return_value = fake_aggrs - fake_aggrs.get_children.return_value = [res] - self.driver._client.send_request = mock.Mock(return_value=fake_aggrs) - vserver_create_args = { - 'vserver-name': 'os_fake_net_id', - 'root-volume-security-style': 'unix', - 'root-volume-aggregate': 'root', - 'root-volume': 'root', - 'name-server-switch': { - 'nsswitch': 'file' - } - } - vserver_modify_args = { - 'aggr-list': [ - {'aggr-name': 'aggr'} - ], - 'vserver-name': 'os_fake_net_id'} - self.driver._create_vserver('os_fake_net_id') - self.driver._client.send_request.assert_has_calls([ - mock.call('vserver-create', vserver_create_args), - mock.call('aggr-get-iter'), - mock.call('vserver-modify', vserver_modify_args), - ] - ) - - @mock.patch.object(na_utils, 'provide_ems', mock.Mock()) - def test_update_share_stats(self): - """Retrieve status info from share volume group.""" - fake_aggr1_struct = { - 'aggr-space-attributes': { - 'size-total': '3774873600', - 'size-available': '3688566784' - } - } - fake_aggr2_struct = { - 'aggr-space-attributes': { - 'size-total': '943718400', - 'size-available': '45506560' - } - } - - fake_aggr1 = naapi.NaElement('root') - fake_aggr1.translate_struct(fake_aggr1_struct) - - fake_aggr2 = naapi.NaElement('root') - fake_aggr2.translate_struct(fake_aggr2_struct) - self.driver._find_match_aggregates = mock.Mock( - return_value=[fake_aggr1, fake_aggr2]) - self.driver._update_share_stats() - res = self.driver._stats - - expected = {} - expected["share_backend_name"] = self.driver.backend_name - expected["driver_handles_share_servers"] = True - expected["vendor_name"] = 'NetApp' - expected["driver_version"] = '1.0' - expected["storage_protocol"] = 'NFS_CIFS' - expected['total_capacity_gb'] = 4 - expected['free_capacity_gb'] = 3 - expected['reserved_percentage'] = 0 - expected['QoS_support'] = False - self.assertDictMatch(expected, res) - - def test_setup_server(self): - self.driver._vserver_create_if_not_exists = mock.Mock( - return_value='fake_vserver') - result = self.driver.setup_server({'server_id': 'fake_vserver'}) - self.assertEqual(result, {'vserver_name': 'fake_vserver'}) - - def test_vserver_create_if_not_exists_1(self): - # server exists, lifs ok, all sec services - nodes = ['fake_node_1', 'fake_node_2', ] - port = 'fake_port' - ip1 = self.network_info['network_allocations'][0]['ip_address'] - ip2 = self.network_info['network_allocations'][1]['ip_address'] - vserver_name = \ - self.driver.configuration.netapp_vserver_name_template % \ - self.network_info['server_id'] - self.mock_object(self.driver.db, 'share_server_backend_details_set') - self.mock_object(self.driver, '_vserver_exists', - mock.Mock(return_value=True)) - self.mock_object(self.driver, '_get_cluster_nodes', - mock.Mock(return_value=nodes)) - self.mock_object(self.driver, '_get_node_data_port', - mock.Mock(return_value=port)) - self.mock_object(self.driver, '_create_lif_if_not_exists') - self.mock_object(self.driver, '_enable_nfs') - self.mock_object(self.driver, '_setup_security_services') - - returned_data = self.driver._vserver_create_if_not_exists( - self.network_info) - - self.assertEqual(returned_data, vserver_name) - self.driver.db.share_server_backend_details_set.assert_has_calls([ - mock.call(utils.IsAMatcher(context.RequestContext), - self.network_info['server_id'], - {'vserver_name': vserver_name}), - ]) - self.driver._vserver_exists.assert_has_calls([mock.call(vserver_name)]) - self.driver._get_cluster_nodes.assert_has_calls([]) - self.driver._get_node_data_port.assert_has_calls([ - mock.call(nodes[0]), - mock.call(nodes[1]), - ]) - self.driver._create_lif_if_not_exists.assert_has_calls([ - mock.call(vserver_name, - self.network_info['network_allocations'][0]['id'], - self.network_info['segmentation_id'], - nodes[0], port, ip1, '255.255.255.0', mock.ANY), - mock.call(vserver_name, - self.network_info['network_allocations'][1]['id'], - self.network_info['segmentation_id'], - nodes[1], port, ip2, '255.255.255.0', mock.ANY), - ]) - self.driver._enable_nfs.assert_has_calls([mock.call(mock.ANY)]) - self.driver._setup_security_services.assert_has_calls([ - mock.call(self.network_info.get('security_services'), - mock.ANY, vserver_name), - ]) - - def test_vserver_create_if_not_exists_2(self): - # server does not exist, lifs ok, no sec services - network_info = copy.deepcopy(self.network_info) - network_info['security_services'] = [] - nodes = ['fake_node_1', 'fake_node_2', ] - port = 'fake_port' - ip1 = network_info['network_allocations'][0]['ip_address'] - ip2 = network_info['network_allocations'][1]['ip_address'] - vserver_name = \ - self.driver.configuration.netapp_vserver_name_template % \ - network_info['server_id'] - self.mock_object(self.driver.db, 'share_server_backend_details_set') - self.mock_object(self.driver, '_vserver_exists', - mock.Mock(return_value=False)) - self.mock_object(self.driver, '_create_vserver') - self.mock_object(self.driver, '_get_cluster_nodes', - mock.Mock(return_value=nodes)) - self.mock_object(self.driver, '_get_node_data_port', - mock.Mock(return_value=port)) - self.mock_object(self.driver, '_create_lif_if_not_exists') - self.mock_object(self.driver, '_enable_nfs') - - returned_data = self.driver._vserver_create_if_not_exists(network_info) - - self.assertEqual(returned_data, vserver_name) - self.driver.db.share_server_backend_details_set.assert_has_calls([ - mock.call(utils.IsAMatcher(context.RequestContext), - self.network_info['server_id'], - {'vserver_name': vserver_name}), - ]) - self.driver._vserver_exists.assert_has_calls([mock.call(vserver_name)]) - self.driver._create_vserver.assert_has_calls([mock.call(vserver_name)]) - self.driver._get_cluster_nodes.assert_has_calls([]) - self.driver._get_node_data_port.assert_has_calls([ - mock.call(nodes[0]), - mock.call(nodes[1]), - ]) - self.driver._create_lif_if_not_exists.assert_has_calls([ - mock.call(vserver_name, - network_info['network_allocations'][0]['id'], - network_info['segmentation_id'], - nodes[0], port, ip1, '255.255.255.0', mock.ANY), - mock.call(vserver_name, - network_info['network_allocations'][1]['id'], - network_info['segmentation_id'], - nodes[1], port, ip2, '255.255.255.0', mock.ANY), - ]) - self.driver._enable_nfs.assert_has_calls([mock.call(mock.ANY)]) - - def test_vserver_create_if_not_exists_3(self): - # server does not exist, lifs ok, one sec service - network_info = copy.deepcopy(self.network_info) - network_info['security_services'] = self.network_info[ - 'security_services'][0] - nodes = ['fake_node_1', 'fake_node_2', ] - port = 'fake_port' - ip1 = network_info['network_allocations'][0]['ip_address'] - ip2 = network_info['network_allocations'][1]['ip_address'] - vserver_name = \ - self.driver.configuration.netapp_vserver_name_template % \ - network_info['server_id'] - self.mock_object(self.driver.db, 'share_server_backend_details_set') - self.mock_object(self.driver, '_vserver_exists', - mock.Mock(return_value=False)) - self.mock_object(self.driver, '_create_vserver') - self.mock_object(self.driver, '_get_cluster_nodes', - mock.Mock(return_value=nodes)) - self.mock_object(self.driver, '_get_node_data_port', - mock.Mock(return_value=port)) - self.mock_object(self.driver, '_create_lif_if_not_exists') - self.mock_object(self.driver, '_enable_nfs') - self.mock_object(self.driver, '_setup_security_services') - - returned_data = self.driver._vserver_create_if_not_exists(network_info) - - self.assertEqual(returned_data, vserver_name) - self.driver.db.share_server_backend_details_set.assert_has_calls([ - mock.call(utils.IsAMatcher(context.RequestContext), - self.network_info['server_id'], - {'vserver_name': vserver_name}), - ]) - self.driver._vserver_exists.assert_has_calls([mock.call(vserver_name)]) - self.driver._create_vserver.assert_has_calls([mock.call(vserver_name)]) - self.driver._get_cluster_nodes.assert_has_calls([]) - self.driver._get_node_data_port.assert_has_calls([ - mock.call(nodes[0]), - mock.call(nodes[1]), - ]) - self.driver._create_lif_if_not_exists.assert_has_calls([ - mock.call(vserver_name, - network_info['network_allocations'][0]['id'], - network_info['segmentation_id'], - nodes[0], port, ip1, '255.255.255.0', mock.ANY), - mock.call(vserver_name, - network_info['network_allocations'][1]['id'], - network_info['segmentation_id'], - nodes[1], port, ip2, '255.255.255.0', mock.ANY), - ]) - self.driver._enable_nfs.assert_has_calls([mock.call(mock.ANY)]) - self.driver._setup_security_services.assert_has_calls([ - mock.call(network_info.get('security_services'), - mock.ANY, vserver_name), - ]) - - def test_setup_security_services(self): - fake_sevice_ldap = {'type': 'ldap'} - fake_sevice_krb = {'type': 'kerberos'} - fake_sevice_ad = {'type': 'active_directory'} - vserver_name = 'fake_vserver' - modify_args = { - 'name-mapping-switch': {'nmswitch': 'ldap,file'}, - 'name-server-switch': {'nsswitch': 'ldap,file'}, - 'vserver-name': vserver_name, - } - self.driver._configure_kerberos = mock.Mock() - self.driver._configure_ldap = mock.Mock() - self.driver._configure_active_directory = mock.Mock() - - self.driver._setup_security_services( - [fake_sevice_ad, fake_sevice_krb, fake_sevice_ldap], - self._vserver_client, vserver_name) - - self.driver._client.send_request.assert_called_once_with( - 'vserver-modify', modify_args) - self.driver._configure_active_directory.assert_called_once_with( - fake_sevice_ad, self._vserver_client, vserver_name) - self.driver._configure_kerberos.assert_called_once_with( - vserver_name, - fake_sevice_krb, - self._vserver_client, - ) - self.driver._configure_ldap.assert_called_once_with( - fake_sevice_ldap, self._vserver_client) - - def test_get_network_allocations_number(self): - res = mock.Mock() - res.get_child_content.return_value = '5' - self.driver._client.send_request = mock.Mock(return_value=res) - self.assertEqual(self.driver.get_network_allocations_number(), 5) - - def test_delete_vserver_without_net_info(self): - el = naapi.NaElement('fake') - el['num-records'] = 1 - self.driver._vserver_exists = mock.Mock(return_value=True) - self._vserver_client.send_request = mock.Mock(return_value=el) - self.driver._delete_vserver('fake', self._vserver_client) - self._vserver_client.send_request.assert_has_calls([ - mock.call('volume-offline', {'name': 'root'}), - mock.call('volume-destroy', {'name': 'root'}) - ]) - self.driver._client.send_request.assert_called_once_with( - 'vserver-destroy', {'vserver-name': 'fake'}) - - def test_delete_vserver_with_net_info(self): - el = naapi.NaElement('fake') - el['num-records'] = 1 - self.driver._vserver_exists = mock.Mock(return_value=True) - self._vserver_client.send_request = mock.Mock(return_value=el) - security_services = [ - {'user': 'admin', - 'password': 'pass', - 'type': 'active_directory'} - ] - self.driver._delete_vserver('fake', - self._vserver_client, - security_services=security_services) - self._vserver_client.send_request.assert_has_calls([ - mock.call('volume-get-iter'), - mock.call('volume-offline', {'name': 'root'}), - mock.call('volume-destroy', {'name': 'root'}), - mock.call('cifs-server-delete', {'admin-username': 'admin', - 'admin-password': 'pass'}) - ]) - self.driver._client.send_request.assert_called_once_with( - 'vserver-destroy', {'vserver-name': 'fake'}) - - def test_delete_vserver_has_shares(self): - el = naapi.NaElement('fake') - el['num-records'] = 3 - self.driver._vserver_exists = mock.Mock(return_value=True) - self._vserver_client.send_request = mock.Mock(return_value=el) - self.assertRaises(exception.NetAppException, - self.driver._delete_vserver, 'fake', - self._vserver_client) - - def test_delete_vserver_without_root_volume(self): - el = naapi.NaElement('fake') - el['num-records'] = '0' - self.driver._vserver_exists = mock.Mock(return_value=True) - self._vserver_client.send_request = mock.Mock(return_value=el) - self.driver._delete_vserver('fake', self._vserver_client) - self.driver._client.send_request.assert_called_once_with( - 'vserver-destroy', {'vserver-name': 'fake'}) - - def test_delete_vserver_does_not_exists(self): - self.driver._vserver_exists = mock.Mock(return_value=False) - self.driver._delete_vserver('fake', self._vserver_client) - self.assertEqual(self.driver._client.send_request.called, False) - - def test_vserver_exists_true(self): - el = naapi.NaElement('fake') - el['num-records'] = '0' - self.driver._client.send_request = mock.Mock(return_value=el) - self.assertEqual(self.driver._vserver_exists('fake_vserver'), False) - self.driver._client.send_request.assert_called_once_with( - 'vserver-get-iter', {'query': { - 'vserver-info': { - 'vserver-name': 'fake_vserver' - } - } - } - ) - - def test_vserver_exists_false(self): - el = naapi.NaElement('fake') - el['num-records'] = '1' - self.driver._client.send_request = mock.Mock(return_value=el) - self.assertEqual(self.driver._vserver_exists('fake_vserver'), True) - self.driver._client.send_request.assert_called_once_with( - 'vserver-get-iter', {'query': { - 'vserver-info': { - 'vserver-name': 'fake_vserver' - } - } - } - ) - - def test_create_net_iface(self): - self.driver._create_net_iface('1.1.1.1', '255.255.255.0', '200', - 'node', 'port', 'vserver-name', 'all_id') - vlan_args = { - 'vlan-info': { - 'parent-interface': 'port', - 'node': 'node', - 'vlanid': '200'} - } - interface_args = { - 'address': '1.1.1.1', - 'administrative-status': 'up', - 'data-protocols': [ - {'data-protocol': 'nfs'}, - {'data-protocol': 'cifs'} - ], - 'home-node': 'node', - 'home-port': 'port-200', - 'netmask': '255.255.255.0', - 'interface-name': 'os_all_id', - 'role': 'data', - 'vserver': 'vserver-name', - } - self.driver._client.send_request.assert_has_calls([ - mock.call('net-vlan-create', vlan_args), - mock.call('net-interface-create', interface_args), - ]) - - @ddt.data(fake_share.fake_share(), - fake_share.fake_share(share_proto='NFSBOGUS'), - fake_share.fake_share(share_proto='CIFSBOGUS')) - def test_get_helper_with_wrong_proto(self, share): - self.mock_object(self.driver, - '_check_licenses', - mock.Mock(return_value=share['share_proto'])) - self.assertRaises(exception.NetAppException, - self.driver._get_helper, share) - - def test_enable_nfs(self): - self.driver._enable_nfs(self._vserver_client) - export_args = { - 'client-match': '0.0.0.0/0', - 'policy-name': 'default', - 'ro-rule': { - 'security-flavor': 'any' - }, - 'rw-rule': { - 'security-flavor': 'any' - } - } - self._vserver_client.send_request.assert_has_calls( - [mock.call('nfs-enable'), - mock.call('nfs-service-modify', {'is-nfsv40-enabled': 'true'}), - mock.call('export-rule-create', export_args)] - ) - - def test_configure_ldap(self): - conf_name = hashlib.md5('fake_id').hexdigest() - client_args = { - 'ldap-client-config': conf_name, - 'servers': { - 'ip-address': 'fake_server' - }, - 'tcp-port': '389', - 'schema': 'RFC-2307', - 'bind-password': 'fake_password' - } - config_args = {'client-config': conf_name, - 'client-enabled': 'true'} - self.driver._configure_ldap(self.security_service, - self._vserver_client) - self._vserver_client.send_request.assert_has_calls([ - mock.call('ldap-client-create', client_args), - mock.call('ldap-config-create', config_args)]) - - def test_configure_kerberos(self): - kerberos_args = {'admin-server-ip': 'fake_server', - 'admin-server-port': '749', - 'clock-skew': '5', - 'comment': '', - 'config-name': 'fake_id', - 'kdc-ip': 'fake_server', - 'kdc-port': '88', - 'kdc-vendor': 'other', - 'password-server-ip': 'fake_server', - 'password-server-port': '464', - 'realm': 'FAKE'} - spn = 'nfs/fake-vserver.FAKE@FAKE' - kerberos_modify_args = {'admin-password': 'fake_password', - 'admin-user-name': 'fake_user', - 'interface-name': 'fake_lif', - 'is-kerberos-enabled': 'true', - 'service-principal-name': spn - } - self.driver._get_lifs = mock.Mock(return_value=['fake_lif']) - self.driver._configure_dns = mock.Mock(return_value=['fake_lif']) - self.driver._configure_kerberos('fake_vserver', self.security_service, - self._vserver_client) - self.driver._client.send_request.assert_called_once_with( - 'kerberos-realm-create', kerberos_args) - self._vserver_client.send_request.assert_called_once_with( - 'kerberos-config-modify', kerberos_modify_args) - - def test_configure_active_directory(self): - vserver_name = 'fake_cifs_server_name' - # cifs_server is made of first seven symbols and end six symbols - # separated by two dots. - cifs_server = 'FAKE_CI..R_NAME' - self.driver._configure_dns = mock.Mock() - self.driver._configure_active_directory( - self.security_service, self._vserver_client, vserver_name) - arguments = { - 'admin-username': 'fake_user', - 'admin-password': 'fake_password', - 'force-account-overwrite': 'true', - 'cifs-server': cifs_server, - 'domain': 'FAKE', - } - self.driver._configure_dns.assert_called_with( - self.security_service, self._vserver_client) - self._vserver_client.send_request.assert_called_with( - 'cifs-server-create', arguments) - - def test_configure_active_directory_error_configuring(self): - vserver_name = 'fake_cifs_server_name' - # cifs_server is made of first seven symbols and end six symbols - # separated by two dots. - cifs_server = 'FAKE_CI..R_NAME' - arguments = { - 'admin-username': 'fake_user', - 'admin-password': 'fake_password', - 'force-account-overwrite': 'true', - 'cifs-server': cifs_server, - 'domain': 'FAKE', - } - self.driver._configure_dns = mock.Mock() - self.mock_object(self._vserver_client, 'send_request', - mock.Mock(side_effect=naapi.NaApiError())) - - self.assertRaises( - exception.NetAppException, - self.driver._configure_active_directory, - self.security_service, - self._vserver_client, - vserver_name, - ) - - self.driver._configure_dns.assert_called_with( - self.security_service, self._vserver_client) - self._vserver_client.send_request.assert_called_once_with( - 'cifs-server-create', arguments) - - def test_allocate_container(self): - root = naapi.NaElement('root') - attributes = naapi.NaElement('attributes') - vserver_info = naapi.NaElement('vserver-info') - vserver_aggr_info_list = naapi.NaElement('vserver-aggr-info-list') - for i in range(1, 4): - vserver_aggr_info_list.add_node_with_children( - 'aggr-attributes', **{'aggr-name': 'fake%s' % i, - 'aggr-availsize': '%s' % i}) - vserver_info.add_child_elem(vserver_aggr_info_list) - attributes.add_child_elem(vserver_info) - root.add_child_elem(attributes) - root.add_new_child('attributes', None) - self._vserver_client.send_request = mock.Mock(return_value=root) - self.driver._allocate_container(self.share, 'vserver', - self._vserver_client) - args = {'containing-aggr-name': 'fake3', - 'size': '1g', - 'volume': 'share_fakeid', - 'junction-path': '/share_fakeid' - } - self._vserver_client.send_request.assert_called_with( - 'volume-create', args) - - def test_allocate_container_from_snapshot(self): - self.driver._allocate_container_from_snapshot(self.share, - self.snapshot, - 'vserver', - self._vserver_client) - args = {'volume': 'share_fakeid', - 'parent-volume': 'share_fakeid', - 'parent-snapshot': 'share_snapshot_fakesnapshotid', - 'junction-path': '/share_fakeid'} - self._vserver_client.send_request.assert_called_with( - 'volume-clone-create', args) - - def test_deallocate_container(self): - self.driver._deallocate_container(self.share, self._vserver_client) - self._vserver_client.send_request.assert_has_calls([ - mock.call('volume-unmount', - {'volume-name': 'share_fakeid'}), - mock.call('volume-offline', - {'name': 'share_fakeid'}), - mock.call('volume-destroy', - {'name': 'share_fakeid'}) - ]) - - def test_create_export(self): - self.helper.create_share = mock.Mock(return_value="fake-location") - net_info = { - 'attributes-list': { - 'net-interface-info': {'address': 'ip'}}, - 'num-records': '1'} - ifaces = naapi.NaElement('root') - ifaces.translate_struct(net_info) - self._vserver_client.send_request = mock.Mock(return_value=ifaces) - export_location = self.driver._create_export( - self.share, 'vserver', self._vserver_client) - self.helper.create_share.assert_called_once_with( - "share_%s" % self.share['id'], 'ip') - self.assertEqual(export_location, "fake-location") - - def test_create_share(self): - self.driver._vserver_exists = mock.Mock(return_value=True) - self.driver._allocate_container = allocate = mock.Mock() - self.driver._create_export = create_export = mock.Mock() - self.driver.create_share(self._context, self.share, - share_server=self.share_server) - allocate.assert_called_once_with(self.share, - 'fake_vserver', - self._vserver_client) - create_export.assert_called_once_with(self.share, - 'fake_vserver', - self._vserver_client) - - def test_create_share_fails_without_server(self): - self.driver._vserver_exists = mock.Mock(return_value=True) - self.assertRaises(exception.NetAppException, - self.driver.create_share, - self._context, - self.share) - - def test_create_share_fails_vserver_unavailable(self): - self.driver._vserver_exists = mock.Mock(return_value=False) - self.assertRaises(exception.VserverUnavailable, - self.driver.create_share, - self._context, - self.share, - share_server=self.share_server) - - def test_create_share_fails_vserver_name_missing(self): - self.driver._vserver_exists = mock.Mock(return_value=False) - self.assertRaises(exception.NetAppException, - self.driver.create_share, - self._context, - self.share, - share_server={'backend_details': None}) - - def test_create_snapshot(self): - self.driver._vserver_exists = mock.Mock(return_value=True) - self.driver.create_snapshot(self._context, self.snapshot, - share_server=self.share_server) - self._vserver_client.send_request.assert_called_once_with( - 'snapshot-create', - {'volume': 'share_fakeid', - 'snapshot': 'share_snapshot_fakesnapshotid'}) - - def test_delete_share(self): - resp = mock.Mock() - resp.get_child_content.return_value = 1 - self._vserver_client.send_request = mock.Mock(return_value=resp) - self.driver._vserver_exists = mock.Mock(return_value=True) - self.driver.delete_share(self._context, self.share, - share_server=self.share_server) - self.helper.delete_share.assert_called_once_with(self.share) - - def test_allow_access(self): - access = "1.2.3.4" - self.driver._vserver_exists = mock.Mock(return_value=True) - self.driver.allow_access(self._context, self.share, access, - share_server=self.share_server) - self.helper.allow_access.assert_called_ince_with(self._context, - self.share, access) - - def test_deny_access(self): - access = "1.2.3.4" - self.driver._vserver_exists = mock.Mock(return_value=True) - self.driver.deny_access(self._context, self.share, access, - share_server=self.share_server) - self.helper.deny_access.assert_called_ince_with(self._context, - self.share, access) - - def test_teardown_server(self): - self.driver._delete_vserver = mock.Mock() - sec_services = [{'fake': 'fake'}] - self.driver.teardown_server(server_details={'vserver_name': 'fake'}, - security_services=sec_services) - self.driver._delete_vserver.assert_called_once_with( - 'fake', self._vserver_client, security_services=sec_services) - - def test_ensure_share(self): - self.driver.ensure_share( - self._context, self.share, share_server=self.share_server) - - def test_licenses(self): - licenses_dict = { - 'licenses': { - 'fake_license_1': { - 'package': 'Fake_License_1', - }, - 'fake_license_2': { - 'package': 'Fake_License_2', - }, - }, - } - licenses = naapi.NaElement('fake_licenses_as_response') - licenses.translate_struct(licenses_dict) - - self.mock_object(self.driver._client, 'send_request', - mock.Mock(return_value=licenses)) - self.mock_object(driver.LOG, 'info') - - response = self.driver._check_licenses() - - self.driver._client.send_request.assert_called_once_with( - 'license-v2-list-info') - driver.LOG.info.assert_called_once_with(mock.ANY, mock.ANY) - self.assertEqual(response, ['fake_license_1', 'fake_license_2']) - - def test_licenses_exception_raise(self): - self.mock_object(self.driver._client, 'send_request', - mock.Mock(side_effect=naapi.NaApiError())) - self.mock_object(driver.LOG, 'error') - - self.driver._check_licenses() - - self.driver._client.send_request.assert_called_once_with( - 'license-v2-list-info') - driver.LOG.error.assert_called_once_with(mock.ANY, mock.ANY) - - -class NetAppNFSHelperTestCase(test.TestCase): - """Test suite for NetApp Cluster Mode NFS helper.""" - - def setUp(self): - super(NetAppNFSHelperTestCase, self).setUp() - self._context = context.get_admin_context() - self._db = mock.Mock() - self.client = mock.Mock() - self.name = 'fake_share_name' - export_location = 'location:/%s' % self.name - self.share = fake_share.fake_share( - share_proto='NFS', name=self.name, export_location=export_location) - self.helper = driver.NetAppClusteredNFSHelper() - self.helper._client = mock.Mock() - self.helper._client.send_request = mock.Mock() - - def test_create_share(self): - export_ip = 'fake_export_ip' - junction = 'fake-vserver-location' - self.mock_object(self.helper._client, 'send_request') - self.helper._client.send_request().get_child_by_name = mock.Mock() - self.helper._client.send_request().get_child_by_name().get_content = ( - mock.Mock(side_effect=lambda: junction)) - self.mock_object(self.helper, 'add_rules') - - location = self.helper.create_share(self.share['name'], export_ip) - - self.helper._client.send_request.has_calls( - mock.call( - 'volume-get-volume-path', - {'is-style-cifs': 'false', 'volume': self.share['name']}, - ), - ) - self.helper.add_rules.assert_called_once_with( - junction, ['localhost']) - self.assertEqual(location, export_ip + ':' + junction) - - def test_add_rules(self): - volume_path = "fake_volume_path" - rules = ['1.2.3.4', '4.3.2.1'] - self.helper.nfs_exports_with_prefix = False - - self.helper.add_rules(volume_path, rules) - - self.helper._client.send_request.assert_called_once_with( - 'nfs-exportfs-append-rules-2', mock.ANY) - self.assertEqual(self.helper.nfs_exports_with_prefix, False) - - def test_add_rules_changed_pathname(self): - volume_path = "fake_volume_path" - rules = ['1.2.3.4', '4.3.2.1'] - self.helper.nfs_exports_with_prefix = False - - def raise_exception_13114(*args, **kwargs): - pathname = args[1]['rules']['exports-rule-info-2']['pathname'] - if pathname.startswith(volume_path): - raise naapi.NaApiError('13114') - - self.mock_object(self.helper._client, 'send_request', - mock.Mock(side_effect=raise_exception_13114)) - - self.helper.add_rules(volume_path, rules) - - self.helper._client.send_request.has_calls( - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - ) - self.assertEqual(self.helper.nfs_exports_with_prefix, True) - - def test_add_rules_verify_behavior_remembering(self): - volume_path = "fake_volume_path" - rules = ['1.2.3.4', '4.3.2.1'] - self.helper.nfs_exports_with_prefix = False - - def raise_exception_13114(*args, **kwargs): - pathname = args[1]['rules']['exports-rule-info-2']['pathname'] - if (pathname.startswith(volume_path) and - not self.helper.nfs_exports_with_prefix): - raise naapi.NaApiError('13114') - - self.mock_object(self.helper._client, 'send_request', - mock.Mock(side_effect=raise_exception_13114)) - - self.helper.add_rules(volume_path, rules) - - self.helper._client.send_request.has_calls( - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - ) - self.assertEqual(self.helper.nfs_exports_with_prefix, True) - - self.helper.add_rules(volume_path, rules) - - self.helper._client.send_request.has_calls( - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY), - ) - self.assertEqual(self.helper.nfs_exports_with_prefix, True) - - def test_delete_share(self): - self.helper.delete_share(self.share) - self.helper._client.send_request.assert_called_once_with( - 'nfs-exportfs-delete-rules', mock.ANY) - - def test_allow_access(self): - access = {'access_to': '1.2.3.4', - 'access_type': 'ip'} - root = naapi.NaElement('root') - rules = naapi.NaElement('rules') - root.add_child_elem(rules) - self.helper._client.send_request = mock.Mock(return_value=root) - self.helper.allow_access(self._context, self.share, access) - self.helper._client.send_request.assert_has_calls([ - mock.call('nfs-exportfs-list-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY) - ]) - - def test_deny_access(self): - access = {'access_to': '1.2.3.4', - 'access_type': 'ip'} - root = naapi.NaElement('root') - rules = naapi.NaElement('rules') - root.add_child_elem(rules) - self.helper._client.send_request = mock.Mock(return_value=root) - self.helper.allow_access(self._context, self.share, access) - self.helper._client.send_request.assert_has_calls([ - mock.call('nfs-exportfs-list-rules-2', mock.ANY), - mock.call('nfs-exportfs-append-rules-2', mock.ANY) - ]) - - -class NetAppCIFSHelperTestCase(test.TestCase): - """Test suite for NetApp Cluster Mode CIFS helper.""" - - def setUp(self): - super(NetAppCIFSHelperTestCase, self).setUp() - self._context = context.get_admin_context() - self.name = 'fake_share_name' - export_location = '//location/%s' % self.name - self.share = fake_share.fake_share( - share_proto='CIFS', - name=self.name, - export_location=export_location) - self.helper = driver.NetAppClusteredCIFSHelper() - self.mock_object(self.helper, '_client') - self.mock_object(self.helper._client, 'send_request') - - def test_create_share(self): - self.helper.create_share(self.name, '1.1.1.1') - self.helper._client.send_request.assert_has_calls([ - mock.call( - 'cifs-share-create', - {'path': '/%s' % self.name, 'share-name': self.name}, - ), - mock.call( - 'cifs-share-access-control-delete', - {'user-or-group': 'Everyone', 'share': self.name}, - ) - ]) - - def test_delete_share(self): - self.helper.delete_share(self.share) - self.helper._client.send_request.assert_called_with( - 'cifs-share-delete', {'share-name': self.name}) - - def test_allow_access_user_type(self): - access = {'access_to': 'fake_access', 'access_type': 'user'} - self.helper.allow_access(self._context, self.share, access) - self.helper._client.send_request.assert_called_once_with( - 'cifs-share-access-control-create', - { - 'permission': 'full_control', - 'share': self.name, - 'user-or-group': access['access_to'], - }, - ) - - def test_allow_access_user_type_rule_already_present(self): - self.mock_object(self.helper._client, 'send_request', - mock.Mock(side_effect=naapi.NaApiError('13130'))) - access = {'access_to': 'fake_access', 'access_type': 'user'} - self.assertRaises( - exception.ShareAccessExists, - self.helper.allow_access, - self._context, - self.share, - access, - ) - self.helper._client.send_request.assert_called_once_with( - 'cifs-share-access-control-create', - { - 'permission': 'full_control', - 'share': self.name, - 'user-or-group': access['access_to'], - }, - ) - - def test_allow_access_ip_type(self): - # IP rules are not supported by Cluster Mode driver - self.assertRaises( - exception.NetAppException, - self.helper.allow_access, - self._context, - self.share, - {'access_to': 'fake_access', 'access_type': 'ip'}, - ) - - def test_allow_access_fake_type(self): - self.assertRaises( - exception.NetAppException, - self.helper.allow_access, - self._context, - self.share, - {'access_to': 'fake_access', 'access_type': 'fake_type'}, - ) - - def test_deny_access(self): - access = {'access_to': 'fake_access', 'access_type': 'user'} - self.helper.deny_access(self._context, self.share, access) - self.helper._client.send_request.assert_called_once_with( - 'cifs-share-access-control-delete', - {'user-or-group': access['access_to'], 'share': self.name}, - ) - - def test_deny_access_exception_raised(self): - self.mock_object(self.helper, '_restrict_access', - mock.Mock(side_effect=naapi.NaApiError())) - self.assertRaises( - naapi.NaApiError, - self.helper.deny_access, - self._context, - self.share, - {'access_to': 'fake_access'}, - ) - - def test_get_target(self): - result = self.helper.get_target(self.share) - self.assertEqual(result, 'location') diff --git a/manila/tests/share/drivers/netapp/test_common.py b/manila/tests/share/drivers/netapp/test_common.py index 77f84e2cd8..ec0b1f127b 100644 --- a/manila/tests/share/drivers/netapp/test_common.py +++ b/manila/tests/share/drivers/netapp/test_common.py @@ -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) diff --git a/manila/tests/share/drivers/netapp/test_utils.py b/manila/tests/share/drivers/netapp/test_utils.py index 0d3a38006c..ae01e5d47d 100644 --- a/manila/tests/share/drivers/netapp/test_utils.py +++ b/manila/tests/share/drivers/netapp/test_utils.py @@ -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):