Added Cmode driver
Added new module NetAppClusteredShareDriver. Implements bp add-netapp-clustered-share-driver Change-Id: Ia35445d77f69e9a17560543fdbe1faa1db85b326
This commit is contained in:
parent
6849850945
commit
d41c35947c
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
<root>
|
||||
<elem1>vl1</elem1>
|
||||
<elem2>vl2</elem2>
|
||||
<elem3>vl3</elem3>
|
||||
</root>
|
||||
The above can be achieved by doing
|
||||
root = NaElement('root')
|
||||
root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2',
|
||||
'elem3': 'vl3'})
|
||||
2.
|
||||
<root>
|
||||
<elem1>vl1</elem1>
|
||||
<elem2>vl2</elem2>
|
||||
<elem1>vl3</elem1>
|
||||
</root>
|
||||
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)
|
||||
|
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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'})
|
|
@ -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 = '<xml></xml>'
|
||||
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 = '<xml></xml>'
|
||||
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))
|
Loading…
Reference in New Issue