diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py
index f112aedfeb..bc84b831a5 100644
--- a/manila/db/sqlalchemy/api.py
+++ b/manila/db/sqlalchemy/api.py
@@ -1097,7 +1097,7 @@ def share_data_get_for_project(context, project_id, user_id, session=None):
else:
result = query.first()
- return (result[0] or 0, result[1] or 0)
+ return (result[1] or 0, result[2] or 0)
@require_context
@@ -1259,7 +1259,7 @@ def snapshot_data_get_for_project(context, project_id, user_id, session=None):
else:
result = query.first()
- return (result[0] or 0, result[1] or 0)
+ return (result[1] or 0, result[2] or 0)
@require_context
diff --git a/manila/exception.py b/manila/exception.py
index a49216a344..c3471fd72d 100644
--- a/manila/exception.py
+++ b/manila/exception.py
@@ -541,3 +541,7 @@ class BridgeDoesNotExist(ManilaException):
class ServiceInstanceException(ManilaException):
message = _("Exception in service instance manager occurred.")
+
+
+class NetAppException(ManilaException):
+ message = _("Exception due to NetApp failure.")
diff --git a/manila/share/drivers/netapp/__init__.py b/manila/share/drivers/netapp/__init__.py
index 4ab071d281..e69de29bb2 100644
--- a/manila/share/drivers/netapp/__init__.py
+++ b/manila/share/drivers/netapp/__init__.py
@@ -1,15 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright 2013 Openstack Foundation
-# 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.
diff --git a/manila/share/drivers/netapp/api.py b/manila/share/drivers/netapp/api.py
index d0d2ef0eed..3d52f31722 100644
--- a/manila/share/drivers/netapp/api.py
+++ b/manila/share/drivers/netapp/api.py
@@ -1,4 +1,4 @@
-# Copyright 2012 NetApp
+# Copyright (c) 2014 NetApp, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -12,116 +12,489 @@
# 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 suds
-from suds.sax import text
+"""
+NetApp api for ONTAP and OnCommand DFM.
+
+Contains classes required to issue api calls to ONTAP and OnCommand DFM.
+"""
+
+from lxml import etree
+import urllib2
-from manila import exception
from manila.openstack.common import log
-from oslo.config import cfg
-
LOG = log.getLogger(__name__)
-CONF = cfg.CONF
+
+URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
+NETAPP_NS = 'http://www.netapp.com/filer/admin'
-class NetAppApiClient(object):
- """Wrapper around DFM commands."""
+class NaServer(object):
+ """Encapsulates server connection logic."""
- REQUIRED_FLAGS = ['netapp_nas_wsdl_url',
- 'netapp_nas_login',
- 'netapp_nas_password',
- 'netapp_nas_server_hostname',
- 'netapp_nas_server_port']
+ TRANSPORT_TYPE_HTTP = 'http'
+ TRANSPORT_TYPE_HTTPS = 'https'
+ SERVER_TYPE_FILER = 'filer'
+ SERVER_TYPE_DFM = 'dfm'
+ URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
+ URL_DFM = 'apis/XMLrequest'
+ NETAPP_NS = 'http://www.netapp.com/filer/admin'
+ STYLE_LOGIN_PASSWORD = 'basic_auth'
+ STYLE_CERTIFICATE = 'certificate_auth'
- def __init__(self, configuration):
- self.configuration = configuration
- self._client = None
+ def __init__(self, host, server_type=SERVER_TYPE_FILER,
+ transport_type=TRANSPORT_TYPE_HTTP,
+ style=STYLE_LOGIN_PASSWORD, username=None,
+ password=None):
+ self._host = host
+ self.set_server_type(server_type)
+ self.set_transport_type(transport_type)
+ self.set_style(style)
+ self._username = username
+ self._password = password
+ self._refresh_conn = True
- def do_setup(self):
- """Setup suds (web services) client."""
- protocol = 'https' if self.configuration.netapp_nas_server_secure \
- else 'http'
- soap_url = ('%s://%s:%s/apis/soap/v1' %
- (protocol,
- self.configuration.netapp_nas_server_hostname,
- self.configuration.netapp_nas_server_port))
+ def get_transport_type(self):
+ """Get the transport type protocol."""
+ return self._protocol
- self._client = \
- suds.client.Client(self.configuration.netapp_nas_wsdl_url,
- username=self.configuration.netapp_nas_login,
- password=self.configuration.netapp_nas_password,
- location=soap_url)
+ def set_transport_type(self, transport_type):
+ """Set the transport type protocol for api.
- LOG.info('NetApp RPC client started')
-
- def send_request_to(self, target, request, xml_args=None,
- do_response_check=True):
+ Supports http and https transport types.
"""
- Sends RPC :request: to :target:.
- :param target: IP address, ID or network name of OnTap device
- :param request: API name
- :param xml_args: call arguments
- :param do_response_check: if set to True and RPC call has failed,
- raises exception.
+ if transport_type.lower() not in (
+ NaServer.TRANSPORT_TYPE_HTTP,
+ NaServer.TRANSPORT_TYPE_HTTPS):
+ raise ValueError('Unsupported transport type')
+ self._protocol = transport_type.lower()
+ if self._protocol == NaServer.TRANSPORT_TYPE_HTTP:
+ if self._server_type == NaServer.SERVER_TYPE_FILER:
+ self.set_port(80)
+ else:
+ self.set_port(8088)
+ else:
+ if self._server_type == NaServer.SERVER_TYPE_FILER:
+ self.set_port(443)
+ else:
+ self.set_port(8488)
+ self._refresh_conn = True
+
+ def get_style(self):
+ """Get the authorization style for communicating with the server."""
+ return self._auth_style
+
+ def set_style(self, style):
+ """Set the authorization style for communicating with the server.
+
+ Supports basic_auth for now. Certificate_auth mode to be done.
"""
- client = self._client
- srv = client.service
+ if style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD,
+ NaServer.STYLE_CERTIFICATE):
+ raise ValueError('Unsupported authentication style')
+ self._auth_style = style.lower()
- rpc = client.factory.create('Request')
- rpc.Name = request
- rpc.Args = text.Raw(xml_args)
- response = srv.ApiProxy(Request=rpc, Target=target)
+ def get_server_type(self):
+ """Get the target server type."""
+ return self._server_type
- if do_response_check:
- _check_response(rpc, response)
+ def set_server_type(self, server_type):
+ """Set the target server type.
- return response
-
- def get_available_aggregates(self):
- """Returns list of aggregates known by DFM."""
- srv = self._client.service
- resp = srv.AggregateListInfoIterStart()
- tag = resp.Tag
+ Supports filer and dfm server types.
+ """
+ if server_type.lower() not in (NaServer.SERVER_TYPE_FILER,
+ NaServer.SERVER_TYPE_DFM):
+ raise ValueError('Unsupported server type')
+ self._server_type = server_type.lower()
+ if self._server_type == NaServer.SERVER_TYPE_FILER:
+ self._url = NaServer.URL_FILER
+ else:
+ self._url = NaServer.URL_DFM
+ self._ns = NaServer.NETAPP_NS
+ self._refresh_conn = True
+ def set_api_version(self, major, minor):
+ """Set the api version."""
try:
- avail_aggrs = srv.AggregateListInfoIterNext(Tag=tag,
- Maximum=resp.Records)
- finally:
- srv.AggregateListInfoIterEnd(tag)
+ self._api_major_version = int(major)
+ self._api_minor_version = int(minor)
+ self._api_version = str(major) + "." + str(minor)
+ except ValueError:
+ raise ValueError('Major and minor versions must be integers')
+ self._refresh_conn = True
- return avail_aggrs
-
- def get_host_ip_by(self, host_id):
- """Returns IP address of a host known by DFM."""
- if (type(host_id) is str or type(host_id) is unicode) and \
- len(host_id.split('.')) == 4:
- # already IP
- return host_id
-
- client = self._client
- srv = client.service
-
- filer_filter = client.factory.create('HostListInfoIterStart')
- filer_filter.ObjectNameOrId = host_id
- resp = srv.HostListInfoIterStart(HostListInfoIterStart=filer_filter)
- tag = resp.Tag
+ def get_api_version(self):
+ """Gets the api version tuple."""
+ if hasattr(self, '_api_version'):
+ return (self._api_major_version, self._api_minor_version)
+ return None
+ def set_port(self, port):
+ """Set the server communication port."""
try:
- filers = srv.HostListInfoIterNext(Tag=tag, Maximum=resp.Records)
- finally:
- srv.HostListInfoIterEnd(Tag=tag)
+ int(port)
+ except ValueError:
+ raise ValueError('Port must be integer')
+ self._port = str(port)
+ self._refresh_conn = True
- ip = None
- for host in filers.Hosts.HostInfo:
- if int(host.HostId) == int(host_id):
- ip = host.HostAddress
+ def get_port(self):
+ """Get the server communication port."""
+ return self._port
- return ip
+ def set_timeout(self, seconds):
+ """Sets the timeout in seconds."""
+ try:
+ self._timeout = int(seconds)
+ except ValueError:
+ raise ValueError('timeout in seconds must be integer')
+
+ def get_timeout(self):
+ """Gets the timeout in seconds if set."""
+ if hasattr(self, '_timeout'):
+ return self._timeout
+ return None
+
+ def get_vfiler(self):
+ """Get the vfiler to use in tunneling."""
+ return self._vfiler
+
+ def set_vfiler(self, vfiler):
+ """Set the vfiler to use if tunneling gets enabled."""
+ self._vfiler = vfiler
+
+ def get_vserver(self):
+ """Get the vserver to use in tunneling."""
+ return self._vserver
+
+ def set_vserver(self, vserver):
+ """Set the vserver to use if tunneling gets enabled."""
+ self._vserver = vserver
+
+ def set_username(self, username):
+ """Set the user name for authentication."""
+ self._username = username
+ self._refresh_conn = True
+
+ def set_password(self, password):
+ """Set the password for authentication."""
+ self._password = password
+ self._refresh_conn = True
+
+ def invoke_elem(self, na_element, enable_tunneling=False):
+ """Invoke the api on the server."""
+ if na_element and not isinstance(na_element, NaElement):
+ ValueError('NaElement must be supplied to invoke api')
+ request = self._create_request(na_element, enable_tunneling)
+ if not hasattr(self, '_opener') or not self._opener \
+ or self._refresh_conn:
+ self._build_opener()
+ try:
+ if hasattr(self, '_timeout'):
+ response = self._opener.open(request, timeout=self._timeout)
+ else:
+ response = self._opener.open(request)
+ except urllib2.HTTPError as e:
+ raise NaApiError(e.code, e.msg)
+ except Exception as e:
+ raise NaApiError('Unexpected error', e)
+ xml = response.read()
+ return self._get_result(xml)
+
+ def invoke_successfully(self, na_element, enable_tunneling=False):
+ """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
+ tunneling. The vserver or vfiler should be set before this call
+ otherwise tunneling remains disabled.
+ """
+ result = self.invoke_elem(na_element, enable_tunneling)
+ if result.has_attr('status') and result.get_attr('status') == 'passed':
+ return result
+ 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'
+ raise NaApiError(code, msg)
+
+ def _create_request(self, na_element, enable_tunneling=False):
+ """Creates request in the desired format."""
+ netapp_elem = NaElement('netapp')
+ netapp_elem.add_attr('xmlns', self._ns)
+ if hasattr(self, '_api_version'):
+ netapp_elem.add_attr('version', self._api_version)
+ if enable_tunneling:
+ self._enable_tunnel_request(netapp_elem)
+ netapp_elem.add_child_elem(na_element)
+ request_d = netapp_elem.to_string()
+ request = urllib2.Request(
+ self._get_url(), data=request_d,
+ headers={'Content-Type': 'text/xml', 'charset': 'utf-8'})
+ return request
+
+ def _enable_tunnel_request(self, netapp_elem):
+ """Enables vserver or vfiler tunneling."""
+ if hasattr(self, '_vfiler') and self._vfiler:
+ if hasattr(self, '_api_major_version') and \
+ hasattr(self, '_api_minor_version') and \
+ self._api_major_version >= 1 and \
+ self._api_minor_version >= 7:
+ netapp_elem.add_attr('vfiler', self._vfiler)
+ else:
+ raise ValueError('ontapi version has to be atleast 1.7'
+ ' to send request to vfiler')
+ if hasattr(self, '_vserver') and self._vserver:
+ if hasattr(self, '_api_major_version') and \
+ hasattr(self, '_api_minor_version') and \
+ self._api_major_version >= 1 and \
+ self._api_minor_version >= 15:
+ netapp_elem.add_attr('vfiler', self._vserver)
+ else:
+ raise ValueError('ontapi version has to be atleast 1.15'
+ ' to send request to vserver')
+
+ def _parse_response(self, response):
+ """Get the NaElement for the response."""
+ if not response:
+ raise NaApiError('No response received')
+ xml = etree.XML(response)
+ return NaElement(xml)
+
+ def _get_result(self, response):
+ """Gets the call result."""
+ processed_response = self._parse_response(response)
+ return processed_response.get_child_by_name('results')
+
+ def _get_url(self):
+ return '%s://%s:%s/%s' % (self._protocol, self._host, self._port,
+ self._url)
+
+ def _build_opener(self):
+ if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD:
+ auth_handler = self._create_basic_auth_handler()
+ else:
+ auth_handler = self._create_certificate_auth_handler()
+ opener = urllib2.build_opener(auth_handler)
+ self._opener = opener
+
+ def _create_basic_auth_handler(self):
+ password_man = urllib2.HTTPPasswordMgrWithDefaultRealm()
+ password_man.add_password(None, self._get_url(), self._username,
+ self._password)
+ auth_handler = urllib2.HTTPBasicAuthHandler(password_man)
+ return auth_handler
+
+ def _create_certificate_auth_handler(self):
+ raise NotImplementedError()
+
+ def __str__(self):
+ return "server: %s" % (self._host)
+
+
+class NaElement(object):
+ """Class wraps basic building block for NetApp api request."""
+
+ def __init__(self, name):
+ """Name of the element or etree.Element."""
+ if isinstance(name, etree._Element):
+ self._element = name
+ else:
+ self._element = etree.Element(name)
+
+ def get_name(self):
+ """Returns the tag name of the element."""
+ return self._element.tag
+
+ def set_content(self, text):
+ """Set the text string for the element."""
+ self._element.text = text
+
+ def get_content(self):
+ """Get the text for the element."""
+ return self._element.text
+
+ def add_attr(self, name, value):
+ """Add the attribute to the element."""
+ self._element.set(name, value)
+
+ def add_attrs(self, **attrs):
+ """Add multiple attributes to the element."""
+ for attr in attrs.keys():
+ self._element.set(attr, attrs.get(attr))
+
+ def add_child_elem(self, na_element):
+ """Add the child element to the element."""
+ if isinstance(na_element, NaElement):
+ self._element.append(na_element._element)
+ return
+ raise
+
+ def get_child_by_name(self, name):
+ """Get the child element by the tag name."""
+ for child in self._element.iterchildren():
+ if child.tag == name or etree.QName(child.tag).localname == name:
+ return NaElement(child)
+ return None
+
+ def get_child_content(self, name):
+ """Get the content of the child."""
+ for child in self._element.iterchildren():
+ if child.tag == name or etree.QName(child.tag).localname == name:
+ return child.text
+ return None
+
+ def get_children(self):
+ """Get the children for the element."""
+ return [NaElement(el) for el in self._element.iterchildren()]
+
+ def has_attr(self, name):
+ """Checks whether element has attribute."""
+ attributes = self._element.attrib or {}
+ return name in attributes.keys()
+
+ def get_attr(self, name):
+ """Get the attribute with the given name."""
+ attributes = self._element.attrib or {}
+ return attributes.get(name)
+
+ def get_attr_names(self):
+ """Returns the list of attribute names."""
+ attributes = self._element.attrib or {}
+ return attributes.keys()
+
+ def add_new_child(self, name, content, convert=False):
+ """Add child with tag name and context.
+
+ Convert replaces entity refs to chars.
+ """
+ child = NaElement(name)
+ if convert:
+ content = NaElement._convert_entity_refs(content)
+ child.set_content(content)
+ self.add_child_elem(child)
@staticmethod
- def check_configuration(config_object):
- """Ensure that the flags we care about are set."""
- for flag in NetAppApiClient.REQUIRED_FLAGS:
- if not getattr(config_object, flag, None):
- raise exception.Error(_('%s is not set') % flag)
+ def _convert_entity_refs(text):
+ """Converts entity refs to chars to handle etree auto conversions."""
+ text = text.replace("<", "<")
+ text = text.replace(">", ">")
+ return text
+
+ @staticmethod
+ def create_node_with_children(node, **children):
+ """Creates and returns named node with children."""
+ parent = NaElement(node)
+ for child in children.keys():
+ parent.add_new_child(child, children.get(child, None))
+ return parent
+
+ def add_node_with_children(self, node, **children):
+ """Creates named node with children."""
+ parent = NaElement.create_node_with_children(node, **children)
+ self.add_child_elem(parent)
+
+ def to_string(self, pretty=False, method='xml', encoding='UTF-8'):
+ """Prints the element to string."""
+ return etree.tostring(self._element, method=method, encoding=encoding,
+ pretty_print=pretty)
+
+ 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.
+ """
+
+ child = self.get_child_by_name(key)
+ if child:
+ if child.get_children():
+ return child
+ else:
+ return child.get_content()
+ elif self.has_attr(key):
+ return self.get_attr(key)
+ raise KeyError(_('No element by given name %s.') % (key))
+
+ def __setitem__(self, key, value):
+ """Dict setter method for NaElement.
+
+ Accepts dict, list, tuple, str, int, float and long as valid value.
+ """
+ if key:
+ if value:
+ if isinstance(value, NaElement):
+ child = NaElement(key)
+ child.add_child_elem(value)
+ self.add_child_elem(child)
+ elif isinstance(value, (str, int, float, long)):
+ self.add_new_child(key, str(value))
+ elif isinstance(value, (list, tuple, dict)):
+ child = NaElement(key)
+ child.translate_struct(value)
+ self.add_child_elem(child)
+ else:
+ raise TypeError(_('Not a valid value for NaElement.'))
+ else:
+ self.add_child_elem(NaElement(key))
+ else:
+ raise KeyError(_('NaElement name cannot be null.'))
+
+ def translate_struct(self, data_struct):
+ """Convert list, tuple, dict to NaElement and appends.
+
+ Example usage:
+ 1.
+
+ vl1
+ vl2
+ vl3
+
+ The above can be achieved by doing
+ root = NaElement('root')
+ root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2',
+ 'elem3': 'vl3'})
+ 2.
+
+ vl1
+ vl2
+ vl3
+
+ The above can be achieved by doing
+ root = NaElement('root')
+ root.translate_struct([{'elem1': 'vl1', 'elem2': 'vl2'},
+ {'elem1': 'vl3'}])
+ """
+ if isinstance(data_struct, (list, tuple)):
+ for el in data_struct:
+ if isinstance(el, (list, tuple, dict)):
+ self.translate_struct(el)
+ else:
+ self.add_child_elem(NaElement(el))
+ elif isinstance(data_struct, dict):
+ for k in data_struct.keys():
+ child = NaElement(k)
+ if isinstance(data_struct[k], (dict, list, tuple)):
+ child.translate_struct(data_struct[k])
+ else:
+ if data_struct[k]:
+ child.set_content(str(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."""
+
+ 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)
diff --git a/manila/share/drivers/netapp/cluster_mode.py b/manila/share/drivers/netapp/cluster_mode.py
new file mode 100644
index 0000000000..09fc8b391a
--- /dev/null
+++ b/manila/share/drivers/netapp/cluster_mode.py
@@ -0,0 +1,717 @@
+# Copyright (c) 2014 NetApp, Inc.
+# 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 hashlib
+import os
+import re
+
+from oslo.config import cfg
+
+from manila import exception
+from manila.openstack.common import excutils
+from manila.openstack.common import log
+from manila.share.drivers.netapp import api as naapi
+from manila.share.drivers.netapp import driver
+from manila import utils
+
+
+NETAPP_NAS_OPTS = [
+ cfg.StrOpt('netapp_vserver_name_template',
+ default='os_%(net_id)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.')
+]
+
+
+CONF = cfg.CONF
+CONF.register_opts(NETAPP_NAS_OPTS)
+
+LOG = log.getLogger(__name__)
+
+
+class NetAppClusteredShareDriver(driver.NetAppShareDriver):
+ """
+ NetApp specific ONTAP C-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__(db, *args, **kwargs)
+ if self.configuration:
+ self.configuration.append_config_values(NETAPP_NAS_OPTS)
+ self.api_version = (1, 15)
+
+ 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 = driver.NetAppApiClient(self.api_version,
+ configuration=self.configuration)
+ self._setup_helpers()
+
+ def check_for_setup_error(self):
+ """Raises error if prerequisites are not met."""
+ self._check_licenses()
+
+ def setup_network(self, network_info, metadata=None):
+ """Creates and configures new vserver."""
+ LOG.debug(_('Configuring network %s') % network_info['id'])
+ self._vserver_create_if_not_exists(network_info)
+
+ 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
+
+ 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 exists for node %s") % node
+ LOG.error(msg)
+ raise exception.NetAppException(msg)
+ return port
+
+ 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)
+ aggr_list = self._find_match_aggregates()
+ modify_args = {'aggr-list': aggr_list,
+ 'vserver-name': vserver_name}
+ self._client.send_request('vserver-modify', modify_args)
+
+ 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-name': aggr} for aggr in
+ map(lambda x: x.get_child_content('aggregate-name'), aggrs)
+ if re.match(pattern, aggr)
+ ]
+ return aggr_list
+
+ 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'))
+
+ def _delete_vserver(self, vserver_name, vserver_client):
+ """Deletes vserver."""
+ vserver_client.send_request(
+ 'volume-offline',
+ {'name': self.configuration.netapp_root_volume_name})
+ vserver_client.send_request(
+ 'volume-destroy',
+ {'name': self.configuration.netapp_root_volume_name})
+ args = {'vserver-name': vserver_name}
+ self._client.send_request('vserver-destroy', args)
+
+ 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)
+
+ def _delete_net_iface(self, iface_name):
+ """Deletes lif."""
+ args = {'vserver': None,
+ 'interface-name': iface_name}
+ self._client.send_request('net-interface-delete', args)
+
+ def _setup_helpers(self):
+ """Initializes protocol-specific NAS drivers."""
+ self._helpers = {'CIFS': NetAppClusteredCIFSHelper(),
+ 'NFS': NetAppClusteredNFSHelper()}
+
+ def _get_vserver_name(self, net_id):
+ return self.configuration.netapp_vserver_name_template \
+ % {'net_id': net_id}
+
+ def _vserver_create_if_not_exists(self, network_info):
+ """Creates vserver if not exists with given parameters."""
+ vserver_name = self._get_vserver_name(network_info['id'])
+ vserver_client = driver.NetAppApiClient(
+ self.api_version, vserver=vserver_name,
+ configuration=self.configuration)
+ args = {'query': {'vserver-info': {'vserver-name': vserver_name}}}
+
+ LOG.debug(_('Checking if vserver is configured'))
+ vserver_info = self._client.send_request('vserver-get-iter', args)
+ if not int(vserver_info.get_child_content('num-records')):
+ 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(_("Failed to create network interface"))
+ self._delete_vserver(vserver_name, vserver_client)
+
+ self._enable_nfs(vserver_client)
+ if network_info.get('security_services'):
+ for security_service in network_info['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)
+ 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'])
+ return vserver_name
+
+ 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)
+
+ 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)
+
+ 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(_("Dns exists for vserver"))
+ else:
+ raise exception.NetAppException(
+ _("Failed to configure DNS. %s") % e.message)
+
+ 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['sid'],
+ 'interface-name': lif_name,
+ 'is-kerberos-enabled': 'true',
+ 'service-principal-name': spn
+ }
+ vserver_client.send_request('kerberos-config-modify', args)
+
+ def _configure_active_directory(self, data, vserver_client):
+ """Configures AD on vserver."""
+ self._configure_dns(data, vserver_client)
+ args = {'admin-username': data['sid'],
+ 'admin-password': data['password'],
+ 'force-account-overwrite': 'true',
+ 'cifs-server': data['server'],
+ 'domain': data['domain']}
+ try:
+ vserver_client.send_request('cifs-server-create', args)
+ except naapi.NaApiError as e:
+ if e.code == '13001':
+ LOG.debug(_("CIFS server entry already exists"))
+ else:
+ raise exception.NetAppException(
+ _("Failed to create CIFS server entry. %s") % e.message)
+
+ 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
+
+ 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)
+
+ 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
+
+ def create_share(self, context, share):
+ """Creates new share."""
+ if not share.get('network_info'):
+ msg = _("Cannot create share %s. "
+ "No share network provided") % share['id']
+ LOG.error(msg)
+ raise exception.NetAppException(message=msg)
+ vserver = self._get_vserver_name(share['share_network_id'])
+ vserver_client = driver.NetAppApiClient(
+ self.api_version, vserver=vserver,
+ configuration=self.configuration)
+ self._allocate_container(share, vserver, vserver_client)
+ return self._create_export(share, vserver, vserver_client)
+
+ def create_share_from_snapshot(self, context, share, snapshot,
+ net_details=None):
+ """Creates new share form snapshot."""
+ vserver = self._get_vserver_name(share['share_network_id'])
+ vserver_client = driver.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)
+
+ 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)
+
+ 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)
+
+ 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
+
+ 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)
+
+ 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)
+
+ 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)
+
+ def delete_share(self, context, share):
+ """Deletes share."""
+ share_name = self._get_valid_share_name(share['id'])
+ vserver = self._get_vserver_name(share['share_network_id'])
+ vserver_client = driver.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(_("Share %s does not exists") % share['id'])
+
+ 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'])
+ network_allocations = share['network_info']['network_allocations']
+ ip_address = network_allocations[0]['ip_address']
+ export_location = helper.create_share(share_name, ip_address)
+ return export_location
+
+ def create_snapshot(self, context, snapshot):
+ """Creates a snapshot of a share."""
+ vserver = self._get_vserver_name(snapshot['share']['share_network_id'])
+ vserver_client = driver.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)
+
+ 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)
+
+ def delete_snapshot(self, context, snapshot):
+ """Deletes a snapshot of a share."""
+ vserver = self._get_vserver_name(snapshot['share']['share_network_id'])
+ vserver_client = driver.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)
+
+ 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
+
+ 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)
+
+ def allow_access(self, context, share, access):
+ """Allows access to a given NAS storage for IPs in access."""
+ vserver = self._get_vserver_name(share['share_network_id'])
+ vserver_client = driver.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)
+
+ def deny_access(self, context, share, access):
+ """Denies access to a given NAS storage for IPs in access."""
+ vserver = self._get_vserver_name(share['share_network_id'])
+ vserver_client = driver.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)
+
+
+class NetAppClusteredNFSHelper(driver.NetAppNFSHelper):
+ """Netapp specific cluster-mode NFS sharing driver."""
+ def create_share(self, share_name, export_ip):
+ """Creates NFS share."""
+ export_pathname = os.path.join('/', share_name)
+ self.add_rules(export_pathname, ['localhost'])
+ export_location = ':'.join([export_ip, export_pathname])
+ return export_location
+
+ def allow_access_by_sid(self, share, sid):
+ user, _x, group = sid.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)
+
+ def deny_access_by_sid(self, share, sid):
+ 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)
+
+
+class NetAppClusteredCIFSHelper(driver.NetAppCIFSHelper):
+ """Netapp specific cluster-mode CIFS sharing driver."""
+
+ def create_share(self, share_name, export_ip):
+
+ self._add_share(share_name)
+
+ cifs_location = self._set_export_location(export_ip, share_name)
+ self._restrict_access('Everyone', share_name)
+
+ return cifs_location
+
+ def _add_share(self, share_name):
+ """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)
+
+ def delete_share(self, share):
+ """Deletes CIFS storage."""
+ host_ip, share_name = self._get_export_location(share)
+ args = {'share-name': share_name}
+ self._client.send_request('cifs-share-delete', args)
+
+ def _allow_access_for(self, username, share_name):
+ """Allows access to the CIFS share for a given user."""
+ args = {'permission': 'full_control',
+ 'share': share_name,
+ 'user-or-group': username}
+ self._client.send_request('cifs-share-access-control-create', args)
+
+ 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)
diff --git a/manila/share/drivers/netapp/driver.py b/manila/share/drivers/netapp/driver.py
index 84f06e91c2..dca380be64 100644
--- a/manila/share/drivers/netapp/driver.py
+++ b/manila/share/drivers/netapp/driver.py
@@ -1,5 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright 2012 NetApp
+# Copyright (c) 2014 NetApp, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -16,105 +15,183 @@
"""
NetApp specific NAS storage driver. Supports NFS and CIFS protocols.
-This driver requires NetApp OnCommand 5.0 and one or more Data
-ONTAP 7-mode storage systems with installed CIFS and NFS licenses.
+This driver requires one or more Data ONTAP 7-mode
+storage systems with installed CIFS and NFS licenses.
"""
+
+import os
+
+from oslo.config import cfg
+
from manila import exception
from manila.openstack.common import log
from manila.share import driver
-from manila.share.drivers.netapp.api import NetAppApiClient
-from oslo.config import cfg
-
-
-LOG = log.getLogger(__name__)
+from manila.share.drivers.netapp import api as naapi
NETAPP_NAS_OPTS = [
- cfg.StrOpt('netapp_nas_wsdl_url',
- default=None,
- help='URL of the WSDL file for the DFM server'),
+ cfg.StrOpt('netapp_nas_transport_type',
+ default='http',
+ help='Transport type protocol.'),
cfg.StrOpt('netapp_nas_login',
- default=None,
- help='User name for the DFM server'),
+ default='admin',
+ help='User name for the ONTAP controller.'),
cfg.StrOpt('netapp_nas_password',
- default=None,
- help='Password for the DFM server'),
+ help='Password for the ONTAP controller.',
+ secret=True),
cfg.StrOpt('netapp_nas_server_hostname',
- default=None,
- help='Hostname for the DFM server'),
- cfg.IntOpt('netapp_nas_server_port',
- default=8088,
- help='Port number for the DFM server'),
- cfg.BoolOpt('netapp_nas_server_secure',
- default=True,
- help='Use secure connection to server.'),
+ help='Hostname for the ONTAP controller.'),
+ cfg.FloatOpt('netapp_nas_size_multiplier',
+ default=1.2,
+ help='Volume size multiplier to ensure while creation.'),
+ cfg.StrOpt('netapp_nas_vfiler',
+ help='Vfiler to use for provisioning.'),
+ cfg.StrOpt('netapp_nas_volume_name_template',
+ help='Netapp volume name template.',
+ default='share_%(share_id)s'),
]
CONF = cfg.CONF
CONF.register_opts(NETAPP_NAS_OPTS)
+LOG = log.getLogger(__name__)
+
+
+class NetAppApiClient(object):
+
+ def __init__(self, version, vfiler=None, 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)
+ self._client.set_api_version(*version)
+ if vfiler:
+ self._client.set_vfiler(vfiler)
+ 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 NetAppShareDriver(driver.ShareDriver):
"""
- NetApp specific NAS driver. Allows for NFS and CIFS NAS storage usage.
+ NetApp specific ONTAP 7-mode driver.
+
+ Supports NFS and CIFS protocols.
+ Uses Ontap devices as backend to create shares
+ and snapshots.
+ Does not support multi-tenancy.
"""
+ ONTAP_LICENSES = ('NFS', 'CIFS', 'FlexClone')
+
def __init__(self, db, *args, **kwargs):
super(NetAppShareDriver, self).__init__(*args, **kwargs)
- self.db = db
- self._helpers = None
- self._share_table = {}
self.configuration.append_config_values(NETAPP_NAS_OPTS)
- self._client = NetAppApiClient(self.configuration)
+ self.db = db
+ self.api_version = (1, 7)
+ self._helpers = None
+ self._licenses = None
+ self._client = None
- def allocate_container(self, context, share):
- """Allocate space for the share on aggregates."""
- aggregate = self._find_best_aggregate()
- filer = aggregate.FilerId
- self._allocate_share_space(aggregate, share)
- self._remember_share(share['id'], filer)
+ def do_setup(self, context):
+ """Prepare once the driver.
- def allocate_container_from_snapshot(self, context, share, snapshot):
- """Creates a share from a snapshot."""
- share_name = _get_valid_share_name(share['id'])
- parent_share_name = _get_valid_share_name(snapshot['share_id'])
- parent_snapshot_name = _get_valid_snapshot_name(snapshot['id'])
+ 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, vfiler=self.configuration.netapp_nas_vfiler,
+ configuration=self.configuration)
+ self._setup_helpers()
- filer = self._get_filer(snapshot['share_id'])
-
- xml_args = ('%s'
- '%s'
- '%s') % \
- (share_name, parent_share_name, parent_snapshot_name)
- self._client.send_request_to(filer, 'volume-clone-create', xml_args)
- self._remember_share(share['id'], filer)
-
- def deallocate_container(self, context, share):
- """Free share space."""
- target = self._get_filer(share['id'])
- if target:
- self._share_offline(target, share)
- self._delete_share(target, share)
- self._forget_share(share['id'])
+ def check_for_setup_error(self):
+ """Check if vfiler form config exists."""
+ self._check_licenses()
+ self._check_vfiler_exists()
def create_share(self, context, share):
- """Creates NAS storage."""
+ """Creates container for new share and exports it."""
+ self._allocate_container(share)
+ return self._create_export(share)
+
+ def create_share_from_snapshot(self, context, share, snapshot):
+ self._allocate_container_from_snapshot(share, snapshot)
+ return self._create_export(share)
+
+ def ensure_share(self, context, share):
+ """"""
+ pass
+
+ def _allocate_container(self, share):
+ """Allocate space for the share on aggregates."""
+ self._allocate_share_space(share)
+
+ def _allocate_container_from_snapshot(self, share, snapshot):
+ """Creates clone from 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}
+
+ self._client.send_request('volume-clone-create', args)
+
+ def delete_share(self, context, share):
+ """Deletes share."""
+ share_name = self._get_valid_share_name(share['id'])
+ if self._share_exists(share_name):
+ self._remove_export(share)
+ self._deallocate_container(share)
+ else:
+ LOG.info(_("Share %s does not exists") % share['id'])
+
+ def _share_exists(self, share_name):
+ args = {'volume': share_name}
+ try:
+ self._client.send_request('volume-list-info', args)
+ return True
+ except naapi.NaApiError as e:
+ if e.code == "13040":
+ LOG.debug(_("Share %s does not exists") % share_name)
+ return False
+
+ def _deallocate_container(self, share):
+ """Free share space."""
+ self._offline_share(share)
+ self._delete_share(share)
+
+ def _create_export(self, share):
+ """Creates export accordingly to share protocol."""
helper = self._get_helper(share)
- filer = self._get_filer(share['id'])
- export_location = helper.create_share(filer, share)
+ share_name = self._get_valid_share_name(share['id'])
+ export_location = helper.create_share(
+ share_name, self.configuration.netapp_nas_server_hostname)
return export_location
def create_snapshot(self, context, snapshot):
"""Creates a snapshot of a share."""
- share_name = _get_valid_share_name(snapshot['share_id'])
- snapshot_name = _get_valid_snapshot_name(snapshot['id'])
+ 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)
+ self._client.send_request('snapshot-create', args)
- filer = self._get_filer(snapshot['share_id'])
-
- xml_args = ('%s'
- '%s') % (share_name, snapshot_name)
- self._client.send_request_to(filer, 'snapshot-create', xml_args)
-
- def delete_share(self, context, share):
+ def _remove_export(self, share):
"""Deletes NAS storage."""
helper = self._get_helper(share)
target = helper.get_target(share)
@@ -124,150 +201,140 @@ class NetAppShareDriver(driver.ShareDriver):
def delete_snapshot(self, context, snapshot):
"""Deletes a snapshot of a share."""
- share_name = _get_valid_share_name(snapshot['share_id'])
- snapshot_name = _get_valid_snapshot_name(snapshot['id'])
+ share_name = self._get_valid_share_name(snapshot['share_id'])
+ snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
- filer = self._get_filer(snapshot['share_id'])
+ if self._is_snapshot_busy(share_name, snapshot_name):
+ raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot_name)
+ args = {'snapshot': snapshot_name,
+ 'volume': share_name}
- self._is_snapshot_busy(filer, share_name, snapshot_name)
- xml_args = ('%s'
- '%s') % (snapshot_name, share_name)
- self._client.send_request_to(filer, 'snapshot-delete', xml_args)
-
- def create_export(self, context, share):
- """Share already exported."""
- pass
-
- def remove_export(self, context, share):
- """Share already removed."""
- pass
-
- def ensure_share(self, context, share):
- """Remember previously created shares."""
- helper = self._get_helper(share)
- filer = helper.get_target(share)
- self._remember_share(share['id'], filer)
+ LOG.debug(_('Deleting snapshot %s') % snapshot_name)
+ self._client.send_request('snapshot-delete', args)
def allow_access(self, context, share, access):
- """Allows access to a given NAS storage for IPs in :access:"""
+ """Allows access to a given NAS storage for IPs in access."""
helper = self._get_helper(share)
return helper.allow_access(context, share, access)
def deny_access(self, context, share, access):
- """Denies access to a given NAS storage for IPs in :access:"""
+ """Denies access to a given NAS storage for IPs in access."""
helper = self._get_helper(share)
return helper.deny_access(context, share, access)
- def do_setup(self, context):
- """Prepare once the driver.
+ def _check_vfiler_exists(self):
+ vfiler_status = self._client.send_request('vfiler-get-status',
+ {'vfiler': self.configuration.netapp_nas_vfiler})
+ if vfiler_status.get_child_content('status') != 'running':
+ msg = _("Vfiler %s is not running") \
+ % self.configuration.netapp_nas_vfiler
+ LOG.error(msg)
+ raise exception.NetAppException(msg)
- Called once by the manager after the driver is loaded.
- Validate the flags we care about and setup the suds (web
- services) client.
- """
- self._client.do_setup()
- self._setup_helpers()
-
- def check_for_setup_error(self):
- """Raises error if prerequisites are not met."""
- self._client.check_configuration(self.configuration)
-
- def _get_filer(self, share_id):
- """Returns filer name for the share_id."""
+ def _check_licenses(self):
try:
- return self._share_table[share_id]
- except KeyError:
- return
+ licenses = self._client.send_request('license-v2-list-info')
+ except naapi.NaApiError:
+ licenses = self._client.send_request('license-list-info')
+ self._licenses = [l.get_child_content('package').lower() for l in
+ licenses.get_child_by_name('licenses').get_children()]
+ LOG.info(_("Available licenses: %s") % ', '.join(self._licenses))
+ return self._licenses
- def _remember_share(self, share_id, filer):
- """Stores required share info in local state."""
- self._share_table[share_id] = filer
-
- def _forget_share(self, share_id):
- """Remove share info about share."""
- try:
- self._share_table.pop(share_id)
- except KeyError:
- pass
-
- def _share_offline(self, target, share):
+ def _offline_share(self, share):
"""Sends share offline. Required before deleting a share."""
- share_name = _get_valid_share_name(share['id'])
- xml_args = ('%s') % share_name
- self._client.send_request_to(target, 'volume-offline', xml_args)
+ share_name = self._get_valid_share_name(share['id'])
+ args = {'name': share_name}
+ LOG.debug(_('Offline volume %s') % share_name)
+ self._client.send_request('volume-offline', args)
- def _delete_share(self, target, share):
+ def _delete_share(self, share):
"""Destroys share on a target OnTap device."""
- share_name = _get_valid_share_name(share['id'])
- xml_args = ('true'
- '%s') % share_name
- self._client.send_request_to(target, 'volume-destroy', xml_args)
+ share_name = self._get_valid_share_name(share['id'])
+ args = {'name': share_name}
+ LOG.debug(_('Deleting share %s') % share_name)
+ self._client.send_request('volume-destroy', args)
def _setup_helpers(self):
"""Initializes protocol-specific NAS drivers."""
- #TODO(rushiagr): better way to handle configuration instead of just
- # passing to the helper
- self._helpers = {
- 'CIFS': NetAppCIFSHelper(self._client,
- self.configuration),
- 'NFS': NetAppNFSHelper(self._client,
- self.configuration),
- }
+ self._helpers = {'CIFS': NetAppCIFSHelper(),
+ 'NFS': NetAppNFSHelper()}
+ for helper in self._helpers.values():
+ helper.set_client(self._client)
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.upper().startswith(proto):
return self._helpers[proto]
- err_msg = _("Invalid NAS protocol supplied: %s. ") % (share_proto)
+ err_msg = _("Invalid NAS protocol supplied: %s. ") % share_proto
- raise exception.Error(err_msg)
+ raise exception.NetAppException(err_msg)
- def _find_best_aggregate(self):
- """Returns aggregate with the most free space left."""
- aggrs = self._client.get_available_aggregates()
- if aggrs is None:
- raise exception.Error(_("No aggregates available"))
+ def get_available_aggregates(self):
+ """Returns aggregate list for the vfiler."""
+ LOG.debug(_('Finding available aggreagates for vfiler'))
+ response = self._client.send_request('aggr-list-info')
+ aggr_list_elements = response.get_child_by_name('aggregates')\
+ .get_children()
- best_aggregate = max(aggrs.Aggregates.AggregateInfo,
- key=lambda ai: ai.AggregateSize.SizeAvailable)
- return best_aggregate
+ if not aggr_list_elements:
+ msg = _("No aggregate assigned to vfiler %s")\
+ % self.configuration.netapp_nas_vfiler
+ LOG.error(msg)
+ raise exception.NetAppException(msg)
- def _allocate_share_space(self, aggregate, share):
+ # return dict of key-value pair of aggr_name:size
+ aggr_dict = {}
+
+ for aggr_elem in aggr_list_elements:
+ aggr_name = aggr_elem.get_child_content('name')
+ aggr_size = int(aggr_elem.get_child_content('size-available'))
+ aggr_dict[aggr_name] = aggr_size
+ LOG.debug(_("Found available aggregates: %r") % aggr_dict)
+ return aggr_dict
+
+ def _allocate_share_space(self, share):
"""Create new share on aggregate."""
- filer_id = aggregate.FilerId
- aggr_name = aggregate.AggregateName.split(':')[1]
- share_name = _get_valid_share_name(share['id'])
- args_xml = ('%s'
- '%dg'
- '%s') % (aggr_name, share['size'],
- share_name)
- self._client.send_request_to(filer_id, 'volume-create', args_xml)
+ share_name = self._get_valid_share_name(share['id'])
+ aggregates = self.get_available_aggregates()
+ aggregate = max(aggregates, key=lambda m: aggregates[m])
+ LOG.debug(_('Creating volume %(share)s on aggregate %(aggr)s')
+ % {'share': share_name, 'aggr': aggregate})
+ args = {'containing-aggr-name': aggregate,
+ 'size': str(share['size']) + 'g',
+ 'volume': share_name,
+ }
+ self._client.send_request('volume-create', args)
- def _is_snapshot_busy(self, filer, share_name, snapshot_name):
+ def _is_snapshot_busy(self, share_name, snapshot_name):
"""Raises ShareSnapshotIsBusy if snapshot is busy."""
- xml_args = ('%s') % share_name
- snapshots = self._client.send_request_to(filer,
- 'snapshot-list-info',
- xml_args,
- do_response_check=False)
+ args = {'volume': share_name}
+ snapshots = self._client.send_request('snapshot-list-info', args)
+ snapshots = snapshots.get_child_by_name('snapshots')
+ if snapshots:
+ for snap in snapshots.get_children():
+ if snap.get_child_content('name') == snapshot_name \
+ and snap.get_child_content('busy') == 'true':
+ return True
- for snap in snapshots.Results.snapshots[0]['snapshot-info']:
- if snap['name'][0] == snapshot_name and snap['busy'][0] == 'true':
- raise exception.ShareSnapshotIsBusy(
- snapshot_name=snapshot_name)
+ 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_share_stats(self, refresh=False):
- """Get share status.
-
- If 'refresh' is True, run update the stats first."""
- if refresh:
- self._update_share_status()
-
- return self._stats
+ def _get_valid_snapshot_name(self, snapshot_id):
+ """Get snapshot name according to snapshot name template."""
+ return 'share_snapshot_' + snapshot_id.replace('-', '_')
def _update_share_status(self):
"""Retrieve status info from share volume group."""
@@ -292,44 +359,23 @@ class NetAppShareDriver(driver.ShareDriver):
self._stats = data
def get_network_allocations_number(self):
- """7mode driver does not need to create VIFS"""
+ """7mode driver does not need to create VIFS."""
return 0
- def setup_network(self, network_info):
+ def setup_network(self, network_info, metadata=None):
"""Nothing to set up"""
pass
-def _check_response(request, response):
- """Checks RPC responses from NetApp devices."""
- if response.Status == 'failed':
- name = request.Name
- reason = response.Reason
- msg = _('API %(name)s failed: %(reason)s')
- raise exception.Error(msg % locals())
-
-
-def _get_valid_share_name(share_id):
- """The name can contain letters, numbers, and the underscore
- character (_). The first character must be a letter or an
- underscore."""
- return 'share_' + share_id.replace('-', '_')
-
-
-def _get_valid_snapshot_name(snapshot_id):
- """The name can contain letters, numbers, and the underscore
- character (_). The first character must be a letter or an
- underscore."""
- return 'share_snapshot_' + snapshot_id.replace('-', '_')
-
-
class NetAppNASHelperBase(object):
"""Interface for protocol-specific NAS drivers."""
- def __init__(self, suds_client, config_object):
- self.configuration = config_object
- self._client = suds_client
+ def __init__(self):
+ self._client = None
- def create_share(self, target_id, share):
+ def set_client(self, client):
+ self._client = client
+
+ def create_share(self, share, export_ip):
"""Creates NAS share."""
raise NotImplementedError()
@@ -338,56 +384,85 @@ class NetAppNASHelperBase(object):
raise NotImplementedError()
def allow_access(self, context, share, new_rules):
- """Allows new_rules to a given NAS storage for IPs in :new_rules."""
+ """Allows new_rules to a given NAS storage for IPs in new_rules."""
raise NotImplementedError()
def deny_access(self, context, share, new_rules):
- """Denies new_rules to a given NAS storage for IPs in :new_rules:."""
+ """Denies new_rules to a given NAS storage for IPs in new_rules."""
raise NotImplementedError()
def get_target(self, share):
- """Returns host where the share located.."""
+ """Returns host where the share located."""
raise NotImplementedError()
class NetAppNFSHelper(NetAppNASHelperBase):
"""Netapp specific NFS sharing driver."""
+ def add_rules(self, volume_path, rules):
+ 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'
+ }
+ }
+ args = {
+ '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 = []
- def __init__(self, suds_client, config_object):
- self.configuration = config_object
- super(NetAppNFSHelper, self).__init__(suds_client, config_object)
+ for ip in rules:
+ hostname_info = hostname_info_args.copy()
+ hostname_info['exports-hostname-info'] = {'name': ip}
+ allowed_hosts_xml.append(hostname_info)
- def create_share(self, target_id, share):
- """Creates NFS share"""
- args_xml = (''
- ''
- '%s'
- ''
- ''
- ''
- ''
- 'localhost'
- ''
- ''
- ''
- ''
- 'false'
- 'localhost'
- ''
- ''
- ''
- ''
- ''
- '')
+ 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
- client = self._client
- valid_share_name = _get_valid_share_name(share['id'])
- export_pathname = '/vol/' + valid_share_name
+ args['rules']['exports-rule-info-2']['security-rules'] = security_rule
- client.send_request_to(target_id, 'nfs-exportfs-append-rules-2',
- args_xml % export_pathname)
+ LOG.debug(_('Appending nfs rules %r') % rules)
+ self._client.send_request('nfs-exportfs-append-rules-2',
+ args)
+
+ def create_share(self, share_name, export_ip):
+ """Creates NFS share."""
+ export_pathname = os.path.join('/vol', share_name)
+ self.add_rules(export_pathname, ['127.0.0.1'])
- export_ip = client.get_host_ip_by(target_id)
export_location = ':'.join([export_ip, export_pathname])
return export_location
@@ -395,30 +470,34 @@ class NetAppNFSHelper(NetAppNASHelperBase):
"""Deletes NFS share."""
target, export_path = self._get_export_path(share)
- xml_args = (''
- ''
- '%s'
- ''
- '') % export_path
-
- self._client.send_request_to(target, 'nfs-exportfs-delete-rules',
- xml_args)
+ 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)
def allow_access(self, context, share, access):
- """Allows access to a given NFS storage for IPs in :access:."""
+ """Allows access to a given NFS storage for IPs in access."""
if access['access_type'] != 'ip':
- raise exception.Error(('Invalid access type supplied. '
- 'Only \'ip\' type is supported'))
-
- ips = access['access_to']
+ raise exception.NetAppException(_('7mode driver supports only'
+ ' \'ip\' type'))
+ new_rules = access['access_to']
existing_rules = self._get_exisiting_rules(share)
- new_rules_xml = self._append_new_rules_to(existing_rules, ips)
-
- self._modify_rule(share, new_rules_xml)
+ 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)
def deny_access(self, context, share, access):
- """Denies access to a given NFS storage for IPs in :access:."""
+ """Denies access to a given NFS storage for IPs in access."""
denied_ips = access['access_to']
existing_rules = self._get_exisiting_rules(share)
@@ -431,78 +510,45 @@ class NetAppNFSHelper(NetAppNASHelperBase):
except ValueError:
pass
- new_rules_xml = self._append_new_rules_to([], existing_rules)
- self._modify_rule(share, new_rules_xml)
+ 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_path(share)[0]
- def _modify_rule(self, share, rw_rules):
+ def _modify_rule(self, share, rules):
"""Modifies access rule for a share."""
target, export_path = self._get_export_path(share)
-
- xml_args = ('true'
- ''
- ''
- '%s'
- '%s'
- ''
- ''
- '') % (export_path, ''.join(rw_rules))
-
- self._client.send_request_to(target, 'nfs-exportfs-append-rules-2',
- xml_args)
+ self.add_rules(export_path, rules)
def _get_exisiting_rules(self, share):
"""Returns available access rules for the share."""
target, export_path = self._get_export_path(share)
- xml_args = '%s' % export_path
- response = self._client.send_request_to(target,
- 'nfs-exportfs-list-rules-2',
- xml_args)
-
- rules = response.Results.rules[0]
- security_rule = rules['exports-rule-info-2'][0]['security-rules'][0]
- security_info = security_rule['security-rule-info'][0]
- root_rules = security_info['root'][0]
- allowed_hosts = root_rules['exports-hostname-info']
+ 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 'name' in allowed_host:
- existing_rules.append(allowed_host['name'][0])
+ 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 _append_new_rules_to(existing_rules, new_rules):
- """Adds new rules to existing."""
- security_rule_xml = (''
- '%s'
- ''
- '%s'
- ''
- '')
-
- hostname_info_xml = (''
- '%s'
- '')
-
- allowed_hosts_xml = []
-
- if type(new_rules) is not list:
- new_rules = [new_rules]
-
- all_rules = existing_rules + new_rules
-
- for ip in all_rules:
- allowed_hosts_xml.append(hostname_info_xml % ip)
-
- return security_rule_xml % (allowed_hosts_xml, allowed_hosts_xml)
-
@staticmethod
def _get_export_path(share):
"""Returns IP address and export location of a share."""
@@ -515,90 +561,84 @@ class NetAppNFSHelper(NetAppNASHelperBase):
class NetAppCIFSHelper(NetAppNASHelperBase):
- """Netapp specific NFS sharing driver."""
+ """Netapp specific CIFS sharing driver."""
CIFS_USER_GROUP = 'Administrators'
- def __init__(self, suds_client, config_object):
- self.configuration = config_object
- super(NetAppCIFSHelper, self).__init__(suds_client, config_object)
-
- def create_share(self, target_id, share):
+ def create_share(self, share_name, export_ip):
"""Creates CIFS storage."""
- cifs_status = self._get_cifs_status(target_id)
+ cifs_status = self._get_cifs_status()
if cifs_status == 'stopped':
- self._start_cifs_service(target_id)
+ self._start_cifs_service()
- share_name = _get_valid_share_name(share['id'])
+ self._set_qtree_security(share_name)
+ self._add_share(share_name)
+ self._restrict_access('everyone', share_name)
- self._set_qtree_security(target_id, share)
- self._add_share(target_id, share_name)
- self._restrict_access(target_id, 'everyone', share_name)
-
- ip_address = self._client.get_host_ip_by(target_id)
-
- cifs_location = self._set_export_location(ip_address, share_name)
+ cifs_location = self._set_export_location(
+ export_ip, share_name)
return cifs_location
def delete_share(self, share):
"""Deletes CIFS storage."""
host_ip, share_name = self._get_export_location(share)
- xml_args = '%s' % share_name
- self._client.send_request_to(host_ip, 'cifs-share-delete', xml_args)
+ args = {'share-name': share_name}
+ self._client.send_request('cifs-share-delete', args)
def allow_access(self, context, share, access):
- """Allows access to a given CIFS storage for IPs in :access:."""
- if access['access_type'] != 'passwd':
- ex_text = ('NetApp only supports "passwd" access type for CIFS.')
- raise exception.Error(ex_text)
+ """Allows access to a given CIFS storage for IPs in access."""
+ if access['access_type'] != 'sid':
+ msg = _('NetApp only supports "sid" access type for CIFS.')
+ raise exception.NetAppException(msg)
user = access['access_to']
target, share_name = self._get_export_location(share)
- if self._user_exists(target, user):
- self._allow_access_for(target, user, share_name)
- else:
- exc_text = ('User "%s" does not exist on %s OnTap.') % (user,
- target)
- raise exception.Error(exc_text)
+ self._allow_access_for(user, share_name)
def deny_access(self, context, share, access):
"""Denies access to a given CIFS storage for IPs in access."""
host_ip, share_name = self._get_export_location(share)
user = access['access_to']
- self._restrict_access(host_ip, user, share_name)
+ try:
+ self._restrict_access(user, share_name)
+ except naapi.NaApiError as e:
+ if e.code == "22":
+ LOG.error(_("User %s does not exist") % user)
+ elif e.code == "15661":
+ LOG.error(_("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]
- def _set_qtree_security(self, target, share):
- client = self._client
- share_name = '/vol/' + _get_valid_share_name(share['id'])
+ def _set_qtree_security(self, share_name):
+ share_name = '/vol/%s' % share_name
- xml_args = (''
- 'qtree'
- 'security'
- '%s'
- 'mixed'
- '') % share_name
+ args = {
+ 'args': [
+ {'arg': 'qtree'},
+ {'arg': 'security'},
+ {'arg': share_name},
+ {'arg': 'mixed'}
+ ]
+ }
- client.send_request_to(target, 'system-cli', xml_args)
+ self._client.send_request('system-cli', args)
- def _restrict_access(self, target, user_name, share_name):
- xml_args = ('%s'
- '%s') % (user_name, share_name)
- self._client.send_request_to(target, 'cifs-share-ace-delete',
- xml_args)
+ def _restrict_access(self, user_name, share_name):
+ args = {'user-name': user_name,
+ 'share-name': share_name}
+ self._client.send_request('cifs-share-ace-delete', args)
- def _start_cifs_service(self, target_id):
+ def _start_cifs_service(self):
"""Starts CIFS service on OnTap target."""
- client = self._client
- return client.send_request_to(target_id, 'cifs-start',
- do_response_check=False)
+ self._client.send_request('cifs-start')
@staticmethod
def _get_export_location(share):
@@ -608,7 +648,7 @@ class NetAppCIFSHelper(NetAppNASHelperBase):
if export_location is None:
export_location = '///'
- _, _, host_ip, share_name = export_location.split('/')
+ _x, _x, host_ip, share_name = export_location.split('/')
return host_ip, share_name
@staticmethod
@@ -616,32 +656,21 @@ class NetAppCIFSHelper(NetAppNASHelperBase):
"""Returns export location of a share."""
return "//%s/%s" % (ip, share_name)
- def _get_cifs_status(self, target_id):
+ def _get_cifs_status(self):
"""Returns status of a CIFS service on target OnTap."""
- client = self._client
- response = client.send_request_to(target_id, 'cifs-status')
- return response.Status
+ response = self._client.send_request('cifs-status')
+ return response.get_child_content('status')
- def _allow_access_for(self, target, username, share_name):
+ def _allow_access_for(self, username, share_name):
"""Allows access to the CIFS share for a given user."""
- xml_args = ('rwx'
- '%s'
- '%s') % (share_name, username)
- self._client.send_request_to(target, 'cifs-share-ace-set', xml_args)
+ args = {'access-rights': 'rwx',
+ 'share-name': share_name,
+ 'user-name': username}
+ self._client.send_request('cifs-share-ace-set', args)
- def _user_exists(self, target, user):
- """Returns True if user already exists on a target OnTap."""
- xml_args = ('%s') % user
- resp = self._client.send_request_to(target,
- 'useradmin-user-list',
- xml_args,
- do_response_check=False)
-
- return (resp.Status == 'passed')
-
- def _add_share(self, target_id, share_name):
+ def _add_share(self, share_name):
"""Creates CIFS share on target OnTap host."""
- client = self._client
- xml_args = ('/vol/%s'
- '%s') % (share_name, share_name)
- client.send_request_to(target_id, 'cifs-share-add', xml_args)
+ share_path = '/vol/%s' % share_name
+ args = {'path': share_path,
+ 'share-name': share_name}
+ self._client.send_request('cifs-share-add', args)
diff --git a/manila/test.py b/manila/test.py
index b4ffba30b1..58408b3f91 100644
--- a/manila/test.py
+++ b/manila/test.py
@@ -32,7 +32,6 @@ import nose.plugins.skip
from oslo.config import cfg
import stubout
-from manila.common import config
from manila.openstack.common import importutils
from manila.openstack.common import log as logging
from manila.openstack.common import timeutils
diff --git a/manila/tests/netapp/__init__.py b/manila/tests/netapp/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/manila/tests/netapp/test_7mode_drv.py b/manila/tests/netapp/test_7mode_drv.py
new file mode 100644
index 0000000000..e88a64fc1c
--- /dev/null
+++ b/manila/tests/netapp/test_7mode_drv.py
@@ -0,0 +1,275 @@
+# Copyright (c) 2014 NetApp, Inc.
+# 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 oslo.config import cfg
+
+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 driver
+from manila import test
+
+CONF = cfg.CONF
+
+
+class NetApp7modeDrvTestCase(test.TestCase):
+ """Tests for NetApp 7mode driver."""
+
+ def setUp(self):
+ super(NetApp7modeDrvTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+ self.driver = driver.NetAppShareDriver(
+ self._db, configuration=configuration.Configuration(None))
+ self.driver._client = mock.Mock()
+ self.driver._client.send_request = mock.Mock()
+
+ self.share = {'id': 'fake_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'share_proto': 'fake'}
+ self.snapshot = {'id': 'fake_snapshot_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'share_id': 'fake_share_id'}
+ self.helper = mock.Mock()
+ self.driver._helpers = {'FAKE': self.helper}
+ self.driver._licenses = ['fake']
+
+ def test_check_vfiler_exists(self):
+ elem = naapi.NaElement('fake')
+ elem['status'] = 'running'
+ self.driver._client.send_request = mock.Mock(return_value=elem)
+ self.driver._check_vfiler_exists()
+
+ def test_check_vfiler_exists_error(self):
+ elem = naapi.NaElement('fake')
+ elem['status'] = 'error'
+ self.driver._client.send_request = mock.Mock(return_value=elem)
+ self.assertRaises(exception.NetAppException,
+ self.driver._check_vfiler_exists)
+
+ def test_check_licenses(self):
+ root = naapi.NaElement('fake')
+ elem = naapi.NaElement('licenses')
+ licenses = ['l1', 'l2']
+ for license in licenses:
+ el = naapi.NaElement('license')
+ el['package'] = license
+ elem.add_child_elem(el)
+ root.add_child_elem(elem)
+ self.driver._client.send_request = mock.Mock(return_value=root)
+ self.driver._check_licenses()
+ self.assertEqual(self.driver._licenses, licenses)
+
+ def test_create_share(self):
+ self.driver.configuration.netapp_nas_server_hostname\
+ = 'fake-netapp-location'
+ root = naapi.NaElement('root')
+ aggregates = naapi.NaElement('aggregates')
+ for i in range(1, 4):
+ aggregates.add_node_with_children('aggr-attributes',
+ **{'name': 'fake%s' % i,
+ 'size-available': '%s' % i})
+ root.add_child_elem(aggregates)
+
+ self.driver._client.send_request = mock.Mock(return_value=root)
+ self.helper.create_share = mock.Mock(return_value="fake-location")
+ export_location = self.driver.create_share(self._context, self.share)
+
+ args = {'containing-aggr-name': 'fake3',
+ 'size': '1g',
+ 'volume': 'share_fake_uuid'}
+ self.driver._client.send_request.assert_called_with('volume-create',
+ args)
+ self.helper.create_share.assert_called_once_with(
+ "share_%s" % self.share['id'], 'fake-netapp-location')
+ self.assertEqual(export_location, "fake-location")
+
+ def test_create_share_from_snapshot(self):
+ self.helper.create_share = mock.Mock(return_value="fake-location")
+ export_location = self.driver.create_share_from_snapshot(self._context,
+ self.share,
+ self.snapshot)
+ args = {'volume': 'share_fake_uuid',
+ 'parent-volume': 'share_fake_share_id',
+ 'parent-snapshot': 'share_snapshot_fake_snapshot_uuid'}
+ self.driver._client.send_request.assert_called_once_with(
+ 'volume-clone-create', args)
+ self.assertEqual(export_location, "fake-location")
+
+ def test_delete_share(self):
+ self.driver.delete_share(self._context, self.share)
+ self.driver._client.send_request.assert_has_calls([
+ mock.call('volume-list-info', {'volume': 'share_fake_uuid'}),
+ mock.call('volume-offline', {'name': 'share_fake_uuid'}),
+ mock.call('volume-destroy', {'name': 'share_fake_uuid'})
+ ])
+ self.helper.get_target.assert_called_once_with(self.share)
+ self.helper.delete_share.assert_called_once_with(self.share)
+
+ def test_delete_share_not_exists(self):
+ self.driver._client.send_request = mock.Mock(
+ side_effect=naapi.NaApiError)
+ self.driver.delete_share(self._context, self.share)
+ self.driver._client.send_request.assert_has_calls([
+ mock.call('volume-list-info', {'volume': 'share_fake_uuid'})
+ ])
+
+ def test_create_snapshot(self):
+ self.driver.create_snapshot(self._context, self.snapshot)
+ self.driver._client.send_request.assert_called_once_with(
+ 'snapshot-create',
+ {'volume': 'share_fake_share_id',
+ 'snapshot': 'share_snapshot_fake_snapshot_uuid'})
+
+ def test_delete_snapshot(self):
+ res = mock.Mock()
+ res.get_child_by_name.return_value = res
+ snap = naapi.NaElement('snap')
+ snap.add_new_child('busy', 'true')
+ snap.add_new_child('name', 'share_snapshot_fake_snapshot_uuid')
+ res.get_children = mock.Mock(return_value=[snap])
+ self.driver._client.send_request = mock.Mock(return_value=res)
+ self.assertRaises(exception.ShareSnapshotIsBusy,
+ self.driver.delete_snapshot, self._context,
+ self.snapshot)
+
+ def test_delete_snapshot_busy(self):
+ res = mock.Mock()
+ res.get_child_by_name.return_value = res
+ snap = naapi.NaElement('snap')
+ snap.add_new_child('busy', 'false')
+ snap.add_new_child('name', 'share_fake_uuid')
+ res.get_children = mock.Mock(return_value=[snap])
+ self.driver._client.send_request = mock.Mock(return_value=res)
+ self.driver.delete_snapshot(self._context, self.snapshot)
+ self.driver._client.send_request.assert_called_with(
+ 'snapshot-delete',
+ {'volume': 'share_fake_share_id',
+ 'snapshot': 'share_snapshot_fake_snapshot_uuid'})
+
+ def test_allow_access(self):
+ access = "1.2.3.4"
+ self.driver.allow_access(self._context, self.share, access)
+ 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.deny_access(self._context, self.share, access)
+ self.helper.deny_access.assert_called_ince_with(self._context,
+ self.share, access)
+
+
+class NetAppNFSHelperTestCase(test.TestCase):
+ """Tests for NetApp 7mode driver.
+ """
+ def setUp(self):
+ super(NetAppNFSHelperTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+ self.client = mock.Mock()
+
+ self.share = {'id': 'fake_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'export_location': 'location:/path',
+ 'share_proto': 'fake'}
+ self.helper = driver.NetAppNFSHelper()
+ self.helper._client = mock.Mock()
+ self.helper._client.send_request = mock.Mock()
+
+ def test_create_share(self):
+ location = self.helper.create_share('share_name', 'location')
+ self.helper._client.send_request.assert_called_once_with(
+ 'nfs-exportfs-append-rules-2', mock.ANY)
+ self.assertEqual(location, 'location:/vol/share_name')
+
+ 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):
+ """Tests for NetApp 7mode driver.
+ """
+ def setUp(self):
+ super(NetAppCIFSHelperTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+ self.share = {'id': 'fake_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'export_location': None,
+ 'share_proto': 'fake'}
+ self.share_name = 'fake_share_name'
+ self.helper = driver.NetAppCIFSHelper()
+ self.helper._client = mock.Mock()
+ self.helper._client.send_request = mock.Mock()
+
+ def test_create_share(self):
+ self.helper.create_share(self.share_name, 'location')
+ self.helper._client.send_request.assert_has_calls([
+ mock.call('cifs-status'),
+ mock.call().get_child_content('status'),
+ mock.call('system-cli', mock.ANY),
+ mock.call('cifs-share-add', mock.ANY),
+ mock.call('cifs-share-ace-delete', mock.ANY),
+ ])
+
+ def test_delete_share(self):
+ self.helper.delete_share(self.share)
+ self.helper._client.send_request.assert_called_once_with(
+ 'cifs-share-delete', mock.ANY)
+
+ def test_allow_access(self):
+ access = {'access_to': 'user',
+ 'access_type': 'sid'}
+ self.helper.allow_access(self._context, self.share, access)
+ self.helper._client.send_request.assert_called_once_with(
+ 'cifs-share-ace-set', mock.ANY)
diff --git a/manila/tests/netapp/test_cmode_drv.py b/manila/tests/netapp/test_cmode_drv.py
new file mode 100644
index 0000000000..1195cad53a
--- /dev/null
+++ b/manila/tests/netapp/test_cmode_drv.py
@@ -0,0 +1,397 @@
+# Copyright (c) 2014 NetApp, Inc.
+# 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 hashlib
+import mock
+
+from manila import context
+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 import test
+
+
+class NetAppClusteredDrvTestCase(test.TestCase):
+ """Tests for NetApp cmode driver.
+ """
+ def setUp(self):
+ super(NetAppClusteredDrvTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+ driver.driver.NetAppApiClient = mock.Mock()
+ 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.driver.NetAppApiClient = mock.Mock(
+ return_value=self._vserver_client)
+
+ self.share = {'id': 'fake_uuid',
+ 'project_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'share_proto': 'fake',
+ 'share_network_id': 'fake_net_id',
+ 'network_info': {
+ 'network_allocations': [
+ {'ip_address': 'ip'}
+ ]
+ }
+ }
+ self.snapshot = {'id': 'fake_snapshot_uuid',
+ 'project_id': 'fake_tenant_id',
+ 'share_id': 'fake_share_id',
+ 'share': self.share
+ }
+ self.security_service = {'id': 'fake_id',
+ 'domain': 'FAKE',
+ 'server': 'fake_server',
+ 'sid': 'fake_sid',
+ 'password': 'fake_password'}
+ self.helper = mock.Mock()
+ self.driver._helpers = {'FAKE': self.helper}
+ self.driver._licenses = ['fake']
+
+ 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),
+ ]
+ )
+
+ 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(self):
+ 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_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),
+ ])
+
+ 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_sid',
+ '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):
+ self.driver._configure_dns = mock.Mock()
+ self.driver._configure_active_directory(self.security_service,
+ self._vserver_client)
+ args = {'admin-username': 'fake_sid',
+ 'admin-password': 'fake_password',
+ 'force-account-overwrite': 'true',
+ 'cifs-server': 'fake_server',
+ 'domain': 'FAKE'}
+ self._vserver_client.send_request.assert_called_with(
+ 'cifs-server-create', args)
+
+ 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_fake_uuid',
+ 'junction-path': '/share_fake_uuid'
+ }
+ 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_fake_uuid',
+ 'parent-volume': 'share_fake_share_id',
+ 'parent-snapshot': 'share_snapshot_fake_snapshot_uuid',
+ 'junction-path': '/share_fake_uuid'}
+ 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_fake_uuid'}),
+ mock.call('volume-offline',
+ {'name': 'share_fake_uuid'}),
+ mock.call('volume-destroy',
+ {'name': 'share_fake_uuid'})
+ ])
+
+ def test_create_export(self):
+ self.helper.create_share = mock.Mock(return_value="fake-location")
+ 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_snapshot(self):
+ self.driver.create_snapshot(self._context, self.snapshot)
+ self._vserver_client.send_request.assert_called_once_with(
+ 'snapshot-create',
+ {'volume': 'share_fake_share_id',
+ 'snapshot': 'share_snapshot_fake_snapshot_uuid'})
+
+ 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.delete_share(self._context, self.share)
+ self.helper.delete_share.assert_called_once_with(self.share)
+
+ def test_allow_access(self):
+ access = "1.2.3.4"
+ self.driver.allow_access(self._context, self.share, access)
+ 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.deny_access(self._context, self.share, access)
+ self.helper.deny_access.assert_called_ince_with(self._context,
+ self.share, access)
+
+
+class NetAppNFSHelperTestCase(test.TestCase):
+ """Tests for NetApp 7mode driver.
+ """
+ def setUp(self):
+ super(NetAppNFSHelperTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+ self.client = mock.Mock()
+
+ self.share = {'id': 'fake_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'export_location': 'location:/path',
+ 'share_proto': 'fake'}
+ self.helper = driver.NetAppClusteredNFSHelper()
+ self.helper._client = mock.Mock()
+ self.helper._client.send_request = mock.Mock()
+
+ def test_create_share(self):
+ location = self.helper.create_share('share_name',
+ 'fake-vserver-location')
+ self.helper._client.send_request.assert_called_once_with(
+ 'nfs-exportfs-append-rules-2', mock.ANY)
+ self.assertEqual(location, 'fake-vserver-location:/share_name')
+
+ 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):
+ """Tests for NetApp 7mode driver.
+ """
+ def setUp(self):
+ super(NetAppCIFSHelperTestCase, self).setUp()
+ self._context = context.get_admin_context()
+ self._db = mock.Mock()
+
+ self.share = {'id': 'fake_uuid',
+ 'tenant_id': 'fake_tenant_id',
+ 'name': 'fake_name',
+ 'size': 1,
+ 'export_location': 'location:/path',
+ 'share_proto': 'fake'}
+ self.helper = driver.NetAppClusteredCIFSHelper()
+ self.helper._client = mock.Mock()
+ self.helper._client.send_request = mock.Mock()
+
+ def test_create_share(self):
+ self.helper.create_share('fake_name', '1.1.1.1')
+ self.helper._client.send_request.assert_has_calls([
+ mock.call('cifs-share-create', {'path': '/fake_name',
+ 'share-name': 'fake_name'}),
+ mock.call('cifs-share-access-control-delete',
+ {'user-or-group': 'Everyone', 'share': 'fake_name'})
+ ])
+
+ def test_delete_share(self):
+ self.share['export_location'] = "nfs://host/fake_name"
+ self.helper.delete_share(self.share)
+ self.helper._client.send_request.assert_called_with(
+ 'cifs-share-delete', {'share-name': 'fake_name'})
+
+ def test_allow_access(self):
+ self.helper._allow_access_for('fake_name', 'fake_share')
+ self.helper._client.send_request.assert_called_with(
+ 'cifs-share-access-control-create', {'permission': 'full_control',
+ 'share': 'fake_share',
+ 'user-or-group': 'fake_name'})
diff --git a/manila/tests/test_share_netapp.py b/manila/tests/test_share_netapp.py
deleted file mode 100644
index 5fb4e172a2..0000000000
--- a/manila/tests/test_share_netapp.py
+++ /dev/null
@@ -1,690 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright 2012 NetApp
-# 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.
-
-"""Unit tests for the NetApp NAS driver module."""
-
-from mox import IgnoreArg
-import random
-import suds
-
-from manila import context
-from manila import exception
-from manila.share.configuration import Configuration
-from manila.share.drivers.netapp import driver as netapp
-from manila import test
-
-
-class FakeObject(object):
- pass
-
-
-class FakeRequest(object):
- def __init__(self, name=None, args=None):
- self.Name = name
- self.Args = args
-
-
-class FakeStartResp(object):
- def __init__(self):
- self.Tag = random.randint(1, 100)
- self.Records = random.randint(1, 10)
-
-
-class FakeStatus(object):
- def __init__(self, status):
- self.Status = status
-
-
-class FakeAggregates(object):
- def __init__(self, max_aggr_id):
- class AggrSizeAvail(object):
- def __init__(self, filer_id, avail):
- self.AggregateSize = FakeObject()
- self.FilerId = filer_id
- self.AggregateName = 'filer%d:aggr0' % filer_id
- setattr(self.AggregateSize, 'SizeAvailable', avail)
-
- class AggregateInfo(object):
- def __init__(self):
- self.AggregateInfo = [AggrSizeAvail(1, 10),
- AggrSizeAvail(2, 20),
- AggrSizeAvail(3, 1),
- AggrSizeAvail(max_aggr_id, 50),
- AggrSizeAvail(5, 15)]
-
- self.Aggregates = AggregateInfo()
-
-
-class FakeSnapshots(object):
- def __init__(self, snapshot_name, is_busy='false'):
- class Result(object):
- def __init__(self):
- self.snapshots = [{}]
- self.snapshots[0]['snapshot-info'] = [
- {'name': [snapshot_name], 'busy': [is_busy]},
- {'name': ['fakesnapname1'], 'busy': [is_busy]},
- {'name': ['fakesnapname2'], 'busy': ['true']},
- ]
-
- self.Results = Result()
-
-
-class FakeNfsRules(object):
- def __init__(self):
- class Rules(object):
- def __init__(self):
- self.rules = [
- {'exports-rule-info-2': [
- {'security-rules': [
- {'security-rule-info': [
- {'root': [
- {'exports-hostname-info': [
- {'name': 'allowed_host'},
- {'name': 'disallowed_host'}]}
- ]}
- ]}
- ]}
- ]}
- ]
-
- self.Results = Rules()
-
-
-class FakeHost(object):
- def __init__(self, id):
- self.HostId = id
-
-
-class FakeHostInfo(object):
- def __init__(self):
- self.Hosts = FakeObject()
- setattr(self.Hosts, 'HostInfo', [FakeHost(1), FakeHost(2)])
-
-
-class FakeFilter(object):
- def __init__(self, id=0):
- self.ObjectNameOrId = id
-
-
-class FakeTimestamp(object):
- def __init__(self, monitor_name='file_system', last_stamp=1):
- self.MonitorName = monitor_name
- self.LastMonitoringTimestamp = last_stamp
-
-
-class NetAppShareDriverTestCase(test.TestCase):
- """Tests Netapp-specific share driver.
- """
-
- def setUp(self):
- super(NetAppShareDriverTestCase, self).setUp()
- self._context = context.get_admin_context()
- self._db = self.mox.CreateMockAnything()
- self._driver = netapp.NetAppShareDriver(
- self._db,
- configuration=Configuration(None))
- self._driver._client = self.mox.CreateMock(netapp.NetAppApiClient)
- cifs_helper = self.mox.CreateMock(netapp.NetAppCIFSHelper)
- nfs_helper = self.mox.CreateMock(netapp.NetAppNFSHelper)
- self._driver._helpers = {'CIFS': cifs_helper, 'NFS': nfs_helper}
-
- def test_setup_check(self):
- self._driver._client.do_setup()
- self.mox.ReplayAll()
- self._driver.do_setup(self._context)
-
- def test_load_balancer(self):
- drv = self._driver
- max_aggr_id = 123
-
- drv._client.get_available_aggregates().AndReturn(
- FakeAggregates(max_aggr_id))
-
- self.mox.ReplayAll()
-
- aggr = drv._find_best_aggregate()
-
- self.assertEquals(max_aggr_id, aggr.FilerId)
-
- def test_allocate_container(self):
- drv = self._driver
- client = drv._client
- share = {'id': 'fakeshareid', 'size': 1}
- max_aggr_id = 123
-
- client.get_available_aggregates().AndReturn(
- FakeAggregates(max_aggr_id))
- client.send_request_to(max_aggr_id, 'volume-create', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.allocate_container(self._context, share)
-
- self.assertEqual(max_aggr_id, drv._share_table[share['id']])
-
- def test_allocate_container_from_snapshot(self):
- drv = self._driver
- client = drv._client
- share_id = 'fakeshareid'
- share = {'id': share_id, 'size': 1}
- snapshot = {'id': 'fakesnapshotid', 'size': 1,
- 'share_id': share_id}
- max_aggr_id = 123
-
- drv._share_table[share_id] = max_aggr_id
-
- client.send_request_to(max_aggr_id, 'volume-clone-create', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.allocate_container_from_snapshot(self._context, share, snapshot)
-
- self.assertEqual(max_aggr_id, drv._share_table[share['id']])
-
- def test_deallocate_container_target_exists(self):
- drv = self._driver
- client = drv._client
- share_id = 'share-vol_id'
- share = {'id': share_id, 'size': 1}
- max_aggr_id = 123
-
- client.get_available_aggregates().AndReturn(
- FakeAggregates(max_aggr_id))
- client.send_request_to(max_aggr_id, 'volume-create', IgnoreArg())
- client.send_request_to(max_aggr_id, 'volume-offline', IgnoreArg())
- client.send_request_to(max_aggr_id, 'volume-destroy', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.allocate_container(self._context, share)
- drv.deallocate_container(self._context, share)
-
- self.assertEquals(len(drv._share_table.keys()), 0)
-
- def test_share_create(self):
- drv = self._driver
- ctx = self._context
- share_proto = 'CIFS'
- share = {'id': '1234-abcd-5678',
- 'share_proto': share_proto,
- 'size': 1}
-
- drv._helpers[share_proto].create_share(IgnoreArg(), share)
-
- self.mox.ReplayAll()
-
- drv.create_share(ctx, share)
-
- def test_share_delete(self):
- drv = self._driver
- ctx = self._context
- share_proto = 'NFS'
- helper = drv._helpers[share_proto]
- ip = '172.10.0.1'
- export = '/export_path'
- share = {'id': 'abcd-1234',
- 'share_proto': share_proto,
- 'export_location': ':'.join([ip, export])}
- fake_access_rules = [1, 2, 3]
-
- helper.get_target(share).AndReturn(ip)
- helper.delete_share(share)
-
- self.mox.ReplayAll()
-
- drv.delete_share(ctx, share)
-
- def test_create_snapshot(self):
- drv = self._driver
- client = drv._client
- share_id = 'fakeshareid'
- share = {'id': share_id, 'size': 1}
- snapshot = {'id': 'fakesnapshotid', 'size': 1,
- 'share_id': share_id}
- max_aggr_id = 123
-
- drv._share_table[share_id] = max_aggr_id
-
- client.send_request_to(max_aggr_id, 'snapshot-create', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.create_snapshot(self._context, snapshot)
-
- def test_delete_snapshot(self):
- drv = self._driver
- client = drv._client
- share_id = 'fakeshareid'
- share = {'id': share_id, 'size': 1}
- snapshot = {'id': 'fakesnapshotid', 'size': 1,
- 'share_id': share_id}
- max_aggr_id = 123
-
- drv._share_table[share_id] = max_aggr_id
-
- client.send_request_to(max_aggr_id, 'snapshot-list-info', IgnoreArg(),
- do_response_check=False).\
- AndReturn(FakeSnapshots(netapp._get_valid_snapshot_name(
- snapshot['id'])))
- client.send_request_to(max_aggr_id, 'snapshot-delete', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.delete_snapshot(self._context, snapshot)
-
- def test_delete_snapshot_if_busy(self):
- drv = self._driver
- client = drv._client
- share_id = 'fakeshareid'
- share = {'id': share_id, 'size': 1}
- snapshot = {'id': 'fakesnapshotid', 'size': 1,
- 'share_id': share_id}
- max_aggr_id = 123
-
- drv._share_table[share_id] = max_aggr_id
-
- client.send_request_to(max_aggr_id, 'snapshot-list-info', IgnoreArg(),
- do_response_check=False).\
- AndReturn(FakeSnapshots(netapp._get_valid_snapshot_name(
- snapshot['id']), is_busy='true'))
-
- self.mox.ReplayAll()
-
- self.assertRaises(exception.ShareSnapshotIsBusy, drv.delete_snapshot,
- self._context, snapshot)
-
- def test_allow_access(self):
- drv = self._driver
- share_proto = 'CIFS'
- ctx = self._context
- share = {'share_proto': share_proto}
- access = {}
-
- drv._helpers[share_proto].allow_access(ctx, share, access)
-
- self.mox.ReplayAll()
-
- drv.allow_access(ctx, share, access)
-
- def test_deny_access(self):
- drv = self._driver
- share_proto = 'CIFS'
- ctx = self._context
- share = {'share_proto': share_proto}
- access = {}
-
- drv._helpers[share_proto].deny_access(ctx, share, access)
-
- self.mox.ReplayAll()
-
- drv.deny_access(ctx, share, access)
-
- def test_no_aggregates_available(self):
- drv = self._driver
- ctx = self._context
- share = None
-
- drv._client.get_available_aggregates().AndReturn(None)
-
- self.mox.ReplayAll()
-
- self.assertRaises(exception.Error, drv.allocate_container, ctx, share)
-
-
-class NetAppNfsHelperTestCase(test.TestCase):
- """
- Tests Netapp-specific NFS driver.
- """
- def setUp(self):
- super(NetAppNfsHelperTestCase, self).setUp()
-
- fake_client = self.mox.CreateMock(netapp.NetAppApiClient)
- fake_conf = self.mox.CreateMock(Configuration)
- self._driver = netapp.NetAppNFSHelper(fake_client, fake_conf)
-
- def test_create_share(self):
- drv = self._driver
- client = drv._client
- target = 123
- share = {'id': 'abc-1234-567'}
-
- client.send_request_to(target, 'nfs-exportfs-append-rules-2',
- IgnoreArg())
- client.get_host_ip_by(target).AndReturn('host:export')
-
- self.mox.ReplayAll()
-
- export = drv.create_share(target, share)
-
- self.assertEquals(export.find('-'), -1)
-
- def test_delete_share(self):
- drv = self._driver
- client = drv._client
- share = {'export_location': 'host:export'}
-
- client.send_request_to(IgnoreArg(), 'nfs-exportfs-delete-rules',
- IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.delete_share(share)
-
- def test_invalid_allow_access(self):
- drv = self._driver
- share = None
- access = {'access_type': 'passwd'} # passwd type is not supported
-
- self.assertRaises(exception.Error, drv.allow_access, context, share,
- access)
-
- def test_allow_access(self):
- drv = self._driver
- client = drv._client
- share = {'export_location': 'host:export'}
- access = {'access_to': ['127.0.0.1', '127.0.0.2'],
- 'access_type': 'ip'}
-
- client.send_request_to(IgnoreArg(), 'nfs-exportfs-list-rules-2',
- IgnoreArg()).AndReturn(FakeNfsRules())
- client.send_request_to(IgnoreArg(), 'nfs-exportfs-append-rules-2',
- IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.allow_access(context, share, access)
-
- def test_deny_access(self):
- drv = self._driver
- client = drv._client
- share = {'export_location': 'host:export'}
- access = {'access_to': ['127.0.0.1', '127.0.0.2']}
-
- client.send_request_to(IgnoreArg(), 'nfs-exportfs-list-rules-2',
- IgnoreArg()).AndReturn(FakeNfsRules())
- client.send_request_to(IgnoreArg(), 'nfs-exportfs-append-rules-2',
- IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.deny_access(context, share, access)
-
- def test_get_target(self):
- drv = self._driver
- ip = '172.18.0.1'
- export_path = '/home'
- share = {'export_location': ':'.join([ip, export_path])}
-
- self.assertEquals(drv.get_target(share), ip)
-
-
-class NetAppCifsHelperTestCase(test.TestCase):
- """
- Tests Netapp-specific CIFS driver.
- """
- def setUp(self):
- super(NetAppCifsHelperTestCase, self).setUp()
-
- fake_client = self.mox.CreateMock(netapp.NetAppApiClient)
- fake_conf = self.mox.CreateMock(Configuration)
- self._driver = netapp.NetAppCIFSHelper(fake_client, fake_conf)
-
- def tearDown(self):
- super(NetAppCifsHelperTestCase, self).tearDown()
-
- def test_create_share(self):
- drv = self._driver
- client = drv._client
- target = 123
- share = {'id': 'abc-1234-567'}
- ip = '172.0.0.1'
-
- client.send_request_to(target, 'cifs-status').AndReturn(
- FakeStatus('stopped'))
- client.send_request_to(target, 'cifs-start',
- do_response_check=False)
- client.send_request_to(target, 'system-cli', IgnoreArg())
- client.send_request_to(target, 'cifs-share-add', IgnoreArg())
- client.send_request_to(target, 'cifs-share-ace-delete', IgnoreArg())
- client.get_host_ip_by(target).AndReturn(ip)
-
- self.mox.ReplayAll()
-
- export = drv.create_share(target, share)
-
- self.assertEquals(export.find('-'), -1)
- self.assertTrue(export.startswith('//' + ip))
-
- def test_delete_share(self):
- drv = self._driver
- client = drv._client
- ip = '172.10.0.1'
- export = 'home'
- share = {'export_location': '//%s/%s' % (ip, export)}
-
- client.send_request_to(IgnoreArg(), 'cifs-share-delete', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.delete_share(share)
-
- def test_allow_access_by_ip(self):
- drv = self._driver
- access = {'access_type': 'ip', 'access_to': '123.123.123.123'}
- share = None
-
- self.assertRaises(exception.Error, drv.allow_access, context, share,
- access)
-
- def test_allow_access_by_passwd_invalid_user(self):
- drv = self._driver
- client = drv._client
- access = {'access_type': 'passwd', 'access_to': 'user:pass'}
- ip = '172.0.0.1'
- export = 'export_path'
- share = {'export_location': '//%s/%s' % (ip, export)}
- status = FakeStatus('failed')
-
- client.send_request_to(ip, 'useradmin-user-list', IgnoreArg(),
- do_response_check=False).AndReturn(status)
-
- self.mox.ReplayAll()
-
- self.assertRaises(exception.Error, drv.allow_access, context, share,
- access)
-
- def test_allow_access_by_passwd_existing_user(self):
- drv = self._driver
- client = drv._client
- access = {'access_type': 'passwd', 'access_to': 'user:pass'}
- ip = '172.0.0.1'
- export = 'export_path'
- share = {'export_location': '//%s/%s' % (ip, export)}
- status = FakeStatus('passed')
-
- client.send_request_to(ip, 'useradmin-user-list', IgnoreArg(),
- do_response_check=False).AndReturn(status)
- client.send_request_to(ip, 'cifs-share-ace-set', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.allow_access(context, share, access)
-
- def test_deny_access(self):
- drv = self._driver
- client = drv._client
- access = {'access_type': 'passwd', 'access_to': 'user:pass'}
- ip = '172.0.0.1'
- export = 'export_path'
- share = {'export_location': '//%s/%s' % (ip, export)}
-
- client.send_request_to(ip, 'cifs-share-ace-delete', IgnoreArg())
-
- self.mox.ReplayAll()
-
- drv.deny_access(context, share, access)
-
- def test_get_target(self):
- drv = self._driver
- ip = '172.10.0.1'
- export = 'export_path'
- share = {'export_location': '//%s/%s' % (ip, export)}
-
- self.assertEquals(drv.get_target(share), ip)
-
-
-class NetAppNASHelperTestCase(test.TestCase):
- def setUp(self):
- super(NetAppNASHelperTestCase, self).setUp()
-
- fake_client = self.mox.CreateMock(suds.client.Client)
- fake_conf = self.mox.CreateMock(Configuration)
- self._driver = netapp.NetAppNASHelperBase(fake_client, fake_conf)
-
- def tearDown(self):
- super(NetAppNASHelperTestCase, self).tearDown()
-
- def test_create_share(self):
- drv = self._driver
- target_id = None
- share = None
- self.assertRaises(NotImplementedError, drv.create_share, target_id,
- share)
-
- def test_delete_share(self):
- drv = self._driver
- share = None
- self.assertRaises(NotImplementedError, drv.delete_share, share)
-
- def test_allow_access(self):
- drv = self._driver
- share = None
- ctx = None
- access = None
- self.assertRaises(NotImplementedError, drv.allow_access, ctx, share,
- access)
-
- def test_deny_access(self):
- drv = self._driver
- share = None
- ctx = None
- access = None
- self.assertRaises(NotImplementedError, drv.deny_access, ctx, share,
- access)
-
- def test_get_target(self):
- drv = self._driver
- share = None
- self.assertRaises(NotImplementedError, drv.get_target, share)
-
-
-class NetAppApiClientTestCase(test.TestCase):
- """Tests for NetApp DFM API client.
- """
-
- def setUp(self):
- super(NetAppApiClientTestCase, self).setUp()
- self.fake_conf = self.mox.CreateMock(Configuration)
- self._context = context.get_admin_context()
- self._driver = netapp.NetAppApiClient(self.fake_conf)
-
- self._driver._client = self.mox.CreateMock(suds.client.Client)
- self._driver._client.factory = self.mox.CreateMock(suds.client.Factory)
- # service object is generated dynamically from XML
- self._driver._client.service = self.mox.CreateMockAnything(
- suds.client.ServiceSelector)
-
- def test_get_host_by_ip(self):
- drv = self._driver
- client = drv._client
- service = client.service
- host_id = 123
-
- # can't use 'filter' because it's predefined in Python
- fltr = client.factory.create('HostListInfoIterStart').AndReturn(
- FakeFilter())
-
- resp = service.HostListInfoIterStart(HostListInfoIterStart=fltr)
- resp = resp.AndReturn(FakeStartResp())
- service_list = service.HostListInfoIterNext(Tag=resp.Tag,
- Maximum=resp.Records)
- service_list.AndReturn(FakeHostInfo())
- service.HostListInfoIterEnd(Tag=resp.Tag)
-
- self.mox.ReplayAll()
-
- drv.get_host_ip_by(host_id)
-
- def test_get_available_aggregates(self):
- drv = self._driver
- client = drv._client
- service = client.service
-
- resp = service.AggregateListInfoIterStart().AndReturn(FakeStartResp())
- service.AggregateListInfoIterNext(Tag=resp.Tag, Maximum=resp.Records)
- service.AggregateListInfoIterEnd(resp.Tag)
-
- self.mox.ReplayAll()
-
- drv.get_available_aggregates()
-
- def test_send_successfull_request(self):
- drv = self._driver
- client = drv._client
- service = client.service
- factory = client.factory
-
- target = 1
- args = ''
- responce_check = False
- request = factory.create('Request').AndReturn(FakeRequest())
-
- service.ApiProxy(Target=target, Request=request)
-
- self.mox.ReplayAll()
-
- drv.send_request_to(target, request, args, responce_check)
-
- def test_send_failing_request(self):
- drv = self._driver
- client = drv._client
- service = client.service
- factory = client.factory
-
- target = 1
- args = ''
- responce_check = True
- request = factory.create('Request').AndReturn(FakeRequest())
-
- service.ApiProxy(Target=target, Request=request).AndRaise(
- exception.Error())
-
- self.mox.ReplayAll()
-
- self.assertRaises(exception.Error, drv.send_request_to,
- target, request, args, responce_check)
-
- def test_successfull_setup(self):
- drv = self._driver
- for flag in drv.REQUIRED_FLAGS:
- setattr(netapp.CONF, flag, 'val')
- conf_obj = Configuration(netapp.CONF)
- drv.check_configuration(conf_obj)
-
- def test_failing_setup(self):
- drv = self._driver
- self.assertRaises(exception.Error, drv.check_configuration,
- Configuration(netapp.CONF))