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