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