Browse Source

Added Cmode driver

Added new module NetAppClusteredShareDriver.
Implements bp add-netapp-clustered-share-driver

Change-Id: Ia35445d77f69e9a17560543fdbe1faa1db85b326
changes/00/59100/53
Yulia Portnova 7 years ago
parent
commit
d41c35947c
  1. 4
      manila/db/sqlalchemy/api.py
  2. 4
      manila/exception.py
  3. 15
      manila/share/drivers/netapp/__init__.py
  4. 539
      manila/share/drivers/netapp/api.py
  5. 717
      manila/share/drivers/netapp/cluster_mode.py
  6. 809
      manila/share/drivers/netapp/driver.py
  7. 1
      manila/test.py
  8. 0
      manila/tests/netapp/__init__.py
  9. 275
      manila/tests/netapp/test_7mode_drv.py
  10. 397
      manila/tests/netapp/test_cmode_drv.py
  11. 690
      manila/tests/test_share_netapp.py

4
manila/db/sqlalchemy/api.py

@ -1097,7 +1097,7 @@ def share_data_get_for_project(context, project_id, user_id, session=None):
else:
result = query.first()
return (result[0] or 0, result[1] or 0)
return (result[1] or 0, result[2] or 0)
@require_context
@ -1259,7 +1259,7 @@ def snapshot_data_get_for_project(context, project_id, user_id, session=None):
else:
result = query.first()
return (result[0] or 0, result[1] or 0)
return (result[1] or 0, result[2] or 0)
@require_context

4
manila/exception.py

@ -541,3 +541,7 @@ class BridgeDoesNotExist(ManilaException):
class ServiceInstanceException(ManilaException):
message = _("Exception in service instance manager occurred.")
class NetAppException(ManilaException):
message = _("Exception due to NetApp failure.")

15
manila/share/drivers/netapp/__init__.py

@ -1,15 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Openstack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

539
manila/share/drivers/netapp/api.py

@ -1,4 +1,4 @@
# Copyright 2012 NetApp
# Copyright (c) 2014 NetApp, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -12,116 +12,489 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import suds
from suds.sax import text
"""
NetApp api for ONTAP and OnCommand DFM.
from manila import exception
from manila.openstack.common import log
Contains classes required to issue api calls to ONTAP and OnCommand DFM.
"""
from lxml import etree
import urllib2
from oslo.config import cfg
from manila.openstack.common import log
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')
Supports http and https transport types.
"""
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 send_request_to(self, target, request, xml_args=None,
do_response_check=True):
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.
"""
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 style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD,
NaServer.STYLE_CERTIFICATE):
raise ValueError('Unsupported authentication style')
self._auth_style = style.lower()
def get_server_type(self):
"""Get the target server type."""
return self._server_type
def set_server_type(self, server_type):
"""Set the target server type.
Supports filer and dfm server types.
"""
client = self._client
srv = client.service
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
rpc = client.factory.create('Request')
rpc.Name = request
rpc.Args = text.Raw(xml_args)
response = srv.ApiProxy(Request=rpc, Target=target)
def set_api_version(self, major, minor):
"""Set the api version."""
try:
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
if do_response_check:
_check_response(rpc, response)
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
return response
def set_port(self, port):
"""Set the server communication port."""
try:
int(port)
except ValueError:
raise ValueError('Port must be integer')
self._port = str(port)
self._refresh_conn = True
def get_available_aggregates(self):
"""Returns list of aggregates known by DFM."""
srv = self._client.service
resp = srv.AggregateListInfoIterStart()
tag = resp.Tag
def get_port(self):
"""Get the server communication port."""
return self._port
def set_timeout(self, seconds):
"""Sets the timeout in seconds."""
try:
avail_aggrs = srv.AggregateListInfoIterNext(Tag=tag,
Maximum=resp.Records)
finally:
srv.AggregateListInfoIterEnd(tag)
self._timeout = int(seconds)
except ValueError:
raise ValueError('timeout in seconds must be integer')
return avail_aggrs
def get_timeout(self):
"""Gets the timeout in seconds if set."""
if hasattr(self, '_timeout'):
return self._timeout
return None
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
def get_vfiler(self):
"""Get the vfiler to use in tunneling."""
return self._vfiler
client = self._client
srv = client.service
def set_vfiler(self, vfiler):
"""Set the vfiler to use if tunneling gets enabled."""
self._vfiler = vfiler
filer_filter = client.factory.create('HostListInfoIterStart')
filer_filter.ObjectNameOrId = host_id
resp = srv.HostListInfoIterStart(HostListInfoIterStart=filer_filter)
tag = resp.Tag
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:
filers = srv.HostListInfoIterNext(Tag=tag, Maximum=resp.Records)
finally:
srv.HostListInfoIterEnd(Tag=tag)
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
ip = None
for host in filers.Hosts.HostInfo:
if int(host.HostId) == int(host_id):
ip = host.HostAddress
def set_content(self, text):
"""Set the text string for the element."""
self._element.text = text
return ip
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("&lt;", "<")
text = text.replace("&gt;", ">")
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)

717
manila/share/drivers/netapp/cluster_mode.py

@ -0,0 +1,717 @@
# Copyright (c) 2014 NetApp, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
NetApp specific NAS storage driver. Supports NFS and CIFS protocols.
This driver requires ONTAP Cluster mode storage system
with installed CIFS and NFS licenses.
"""
import hashlib
import os
import re
from oslo.config import cfg
from manila import exception
from manila.openstack.common import excutils
from manila.openstack.common import log
from manila.share.drivers.netapp import api as naapi
from manila.share.drivers.netapp import driver
from manila import utils
NETAPP_NAS_OPTS = [
cfg.StrOpt('netapp_vserver_name_template',
default='os_%(net_id)s',
help='Name template to use for new vserver.'),
cfg.StrOpt('netapp_lif_name_template',
default='os_%(net_allocation_id)s',
help='Lif name template'),
cfg.StrOpt('netapp_aggregate_name_search_pattern',
default='(.*)',
help='Pattern for searching available aggregates'
' for provisioning.'),
cfg.StrOpt('netapp_root_volume_aggregate',
help='Name of aggregate to create root volume on.'),
cfg.StrOpt('netapp_root_volume_name',
default='root',
help='Root volume name.')
]
CONF = cfg.CONF
CONF.register_opts(NETAPP_NAS_OPTS)
LOG = log.getLogger(__name__)
class NetAppClusteredShareDriver(driver.NetAppShareDriver):
"""
NetApp specific ONTAP C-mode driver.
Supports NFS and CIFS protocols.
Uses Ontap devices as backend to create shares
and snapshots.
Sets up vServer for each share_network.
Connectivity between storage and client VM is organized
by plugging vServer's network interfaces into neutron subnet
that VM is using.
"""
def __init__(self, db, *args, **kwargs):
super(NetAppClusteredShareDriver, self).__init__(db, *args, **kwargs)
if self.configuration:
self.configuration.append_config_values(NETAPP_NAS_OPTS)
self.api_version = (1, 15)
def do_setup(self, context):
"""Prepare once the driver.
Called once by the manager after the driver is loaded.
Sets up clients, check licenses, sets up protocol
specific helpers.
"""
self._client = driver.NetAppApiClient(self.api_version,
configuration=self.configuration)
self._setup_helpers()
def check_for_setup_error(self):
"""Raises error if prerequisites are not met."""
self._check_licenses()
def setup_network(self, network_info, metadata=None):
"""Creates and configures new vserver."""
LOG.debug(_('Configuring network %s') % network_info['id'])
self._vserver_create_if_not_exists(network_info)
def _get_cluster_nodes(self):
"""Get all available cluster nodes."""
response = self._client.send_request('system-node-get-iter')
nodes_info_list = response.get_child_by_name('attributes-list')\
.get_children() if response.get_child_by_name('attributes-list') \
else []
nodes = [node_info.get_child_content('node') for node_info
in nodes_info_list]
return nodes
def _get_node_data_port(self, node):
"""Get data port on the node."""
args = {'query': {'net-port-info': {'node': node,
'port-type': 'physical',
'role': 'data'}}}
port_info = self._client.send_request('net-port-get-iter', args)
try:
port = port_info.get_child_by_name('attributes-list')\
.get_child_by_name('net-port-info')\
.get_child_content('port')
except AttributeError:
msg = _("Data port does not exists for node %s") % node
LOG.error(msg)
raise exception.NetAppException(msg)
return port
def _create_vserver(self, vserver_name):
"""Creates new vserver and assigns aggregates."""
create_args = {'vserver-name': vserver_name,
'root-volume-security-style': 'unix',
'root-volume-aggregate':
self.configuration.netapp_root_volume_aggregate,
'root-volume':
self.configuration.netapp_root_volume_name,
'name-server-switch': {'nsswitch': 'file'}}
self._client.send_request('vserver-create', create_args)
aggr_list = self._find_match_aggregates()
modify_args = {'aggr-list': aggr_list,
'vserver-name': vserver_name}
self._client.send_request('vserver-modify', modify_args)
def _find_match_aggregates(self):
"""Find all aggregates match pattern."""
pattern = self.configuration.netapp_aggregate_name_search_pattern
try:
aggrs = self._client.send_request('aggr-get-iter')\
.get_child_by_name('attributes-list').get_children()
except AttributeError:
msg = _("Have not found aggregates match pattern %s")\
% pattern
LOG.error(msg)
raise exception.NetAppException(msg)
aggr_list = [
{'aggr-name': aggr} for aggr in
map(lambda x: x.get_child_content('aggregate-name'), aggrs)
if re.match(pattern, aggr)
]
return aggr_list
def get_network_allocations_number(self):
"""Get number of network interfaces to be created."""
return int(self._client.send_request(
'system-node-get-iter').get_child_content('num-records'))
def _delete_vserver(self, vserver_name, vserver_client):
"""Deletes vserver."""
vserver_client.send_request(
'volume-offline',
{'name': self.configuration.netapp_root_volume_name})
vserver_client.send_request(
'volume-destroy',
{'name': self.configuration.netapp_root_volume_name})
args = {'vserver-name': vserver_name}
self._client.send_request('vserver-destroy', args)
def _create_net_iface(self, ip, netmask, vlan, node, port, vserver_name,
allocation_id):
"""Creates lif on vlan port."""
vlan_iface_name = "%(port)s-%(tag)s" % {'port': port, 'tag': vlan}
try:
args = {
'vlan-info': {
'parent-interface': port,
'node': node,
'vlanid': vlan
}
}
self._client.send_request('net-vlan-create', args)
except naapi.NaApiError as e:
if e.code == '13130':
LOG.debug(_("Vlan %(vlan)s already exists on port %(port)s") %
{'vlan': vlan, 'port': port})
else:
raise exception.NetAppException(
_("Failed to create vlan %(vlan)s on "
"port %(port)s. %(err_msg)") %
{'vlan': vlan, 'port': port, 'err_msg': e.message})
iface_name = self.configuration.netapp_lif_name_template % \
{'node': node, 'net_allocation_id': allocation_id}
LOG.debug(_('Creating LIF %(lif)r for vserver %(vserver)s ')
% {'lif': iface_name, 'vserver': vserver_name})
args = {'address': ip,
'administrative-status': 'up',
'data-protocols': [
{'data-protocol': 'nfs'},
{'data-protocol': 'cifs'}
],
'home-node': node,
'home-port': vlan_iface_name,
'netmask': netmask,
'interface-name': iface_name,
'role': 'data',
'vserver': vserver_name,
}
self._client.send_request('net-interface-create', args)
def _delete_net_iface(self, iface_name):
"""Deletes lif."""
args = {'vserver': None,
'interface-name': iface_name}
self._client.send_request('net-interface-delete', args)
def _setup_helpers(self):
"""Initializes protocol-specific NAS drivers."""
self._helpers = {'CIFS': NetAppClusteredCIFSHelper(),
'NFS': NetAppClusteredNFSHelper()}
def _get_vserver_name(self, net_id):
return self.configuration.netapp_vserver_name_template \
% {'net_id': net_id}
def _vserver_create_if_not_exists(self, network_info):
"""Creates vserver if not exists with given parameters."""
vserver_name = self._get_vserver_name(network_info['id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver_name,
configuration=self.configuration)
args = {'query': {'vserver-info': {'vserver-name': vserver_name}}}
LOG.debug(_('Checking if vserver is configured'))
vserver_info = self._client.send_request('vserver-get-iter', args)
if not int(vserver_info.get_child_content('num-records')):
LOG.debug(_('Vserver %s does not exist, creating') % vserver_name)
self._create_vserver(vserver_name)
nodes = self._get_cluster_nodes()
node_network_info = zip(nodes, network_info['network_allocations'])
netmask = utils.cidr_to_netmask(network_info['cidr'])
try:
for node, net_info in node_network_info:
port = self._get_node_data_port(node)
ip = net_info['ip_address']
self._create_lif_if_not_exists(
vserver_name, net_info['id'],
network_info['segmentation_id'], node, port,
ip, netmask, vserver_client)
except naapi.NaApiError:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed to create network interface"))
self._delete_vserver(vserver_name, vserver_client)
self._enable_nfs(vserver_client)
if network_info.get('security_services'):
for security_service in network_info['security_services']:
if security_service['type'].lower() == "ldap":
self._configure_ldap(security_service, vserver_client)
elif security_service['type'].lower() == "active_directory":
self._configure_active_directory(security_service,
vserver_client)
elif security_service['type'].lower() == "kerberos":
self._configure_kerberos(vserver_name, security_service,
vserver_client)
else:
raise exception.NetAppException(
_('Unsupported protocol %s for NetApp driver')
% security_service['type'])
return vserver_name
def _enable_nfs(self, vserver_client):
"""Enables NFS on vserver."""
vserver_client.send_request('nfs-enable')
args = {'is-nfsv40-enabled': 'true'}
vserver_client.send_request('nfs-service-modify', args)
args = {
'client-match': '0.0.0.0/0',
'policy-name': 'default',
'ro-rule': {
'security-flavor': 'any'
},
'rw-rule': {
'security-flavor': 'any'
}
}
vserver_client.send_request('export-rule-create', args)
def _configure_ldap(self, data, vserver_client):
"""Configures LDAP on vserver."""
config_name = hashlib.md5(data['id']).hexdigest()
args = {'ldap-client-config': config_name,
'servers': {
'ip-address': data['server']
},
'tcp-port': '389',
'schema': 'RFC-2307',
'bind-password': data['password']}
vserver_client.send_request('ldap-client-create', args)
args = {'client-config': config_name,
'client-enabled': 'true'}
vserver_client.send_request('ldap-config-create', args)
def _configure_dns(self, data, vserver_client):
args = {
'domains': {
'string': data['domain']
},
'name-servers': {
'ip-address': data['dns_ip']
},
'dns-state': 'enabled'
}
try:
vserver_client.send_request('net-dns-create', args)
except naapi.NaApiError as e:
if e.code == '13130':
LOG.error(_("Dns exists for vserver"))
else:
raise exception.NetAppException(
_("Failed to configure DNS. %s") % e.message)
def _configure_kerberos(self, vserver, data, vserver_client):
"""Configures Kerberos for NFS on vServer."""
args = {'admin-server-ip': data['server'],
'admin-server-port': '749',
'clock-skew': '5',
'comment': '',
'config-name': data['id'],
'kdc-ip': data['server'],
'kdc-port': '88',
'kdc-vendor': 'other',
'password-server-ip': data['server'],
'password-server-port': '464',
'realm': data['domain'].upper()}
try:
self._client.send_request('kerberos-realm-create', args)
except naapi.NaApiError as e:
if e.code == '13130':
LOG.debug(_("Kerberos realm config already exists"))
else:
raise exception.NetAppException(
_("Failed to configure Kerberos. %s") % e.message)
self._configure_dns(data, vserver_client)
spn = 'nfs/' + vserver.replace('_', '-') + '.' + data['domain'] + '@'\
+ data['domain'].upper()
lifs = self._get_lifs(vserver_client)
if not lifs:
msg = _("Cannot set up kerberos. There are no lifs configured")
LOG.error(msg)
raise Exception(msg)
for lif_name in lifs:
args = {'admin-password': data['password'],
'admin-user-name': data['sid'],
'interface-name': lif_name,
'is-kerberos-enabled': 'true',
'service-principal-name': spn
}
vserver_client.send_request('kerberos-config-modify', args)
def _configure_active_directory(self, data, vserver_client):
"""Configures AD on vserver."""
self._configure_dns(data, vserver_client)
args = {'admin-username': data['sid'],
'admin-password': data['password'],
'force-account-overwrite': 'true',
'cifs-server': data['server'],
'domain': data['domain']}
try:
vserver_client.send_request('cifs-server-create', args)
except naapi.NaApiError as e:
if e.code == '13001':
LOG.debug(_("CIFS server entry already exists"))
else:
raise exception.NetAppException(
_("Failed to create CIFS server entry. %s") % e.message)
def _get_lifs(self, vserver_client):
lifs_info = vserver_client.send_request('net-interface-get-iter')
try:
lif_names = [lif.get_child_content('interface-name') for lif in
lifs_info.get_child_by_name('attributes-list')
.get_children()]
except AttributeError:
lif_names = []
return lif_names
def _create_lif_if_not_exists(self, vserver_name, allocation_id, vlan,
node, port, ip, netmask, vserver_client):
"""Creates lif for vserver."""
args = {
'query': {
'net-interface-info': {
'address': ip,
'home-node': node,
'home-port': port,
'netmask': netmask,
'vserver': vserver_name}
}
}
ifaces = vserver_client.send_request('net-interface-get-iter',
args)
if not ifaces.get_child_content('num_records') or \
ifaces.get_child_content('num_records') == '0':
self._create_net_iface(ip, netmask, vlan, node, port, vserver_name,
allocation_id)
def get_available_aggregates_for_vserver(self, vserver, vserver_client):
"""Returns aggregate list for the vserver."""
LOG.debug(_('Finding available aggreagates for vserver %s') % vserver)
response = vserver_client.send_request('vserver-get')
vserver_info = response.get_child_by_name('attributes')\
.get_child_by_name('vserver-info')
aggr_list_elements = vserver_info\
.get_child_by_name('vserver-aggr-info-list').get_children()
if not aggr_list_elements:
msg = _("No aggregate assigned to vserver %s")
raise exception.NetAppException(msg % vserver)
# return dict of key-value pair of aggr_name:si$
aggr_dict = {}
for aggr_elem in aggr_list_elements:
aggr_name = aggr_elem.get_child_content('aggr-name')
aggr_size = int(aggr_elem.get_child_content('aggr-availsize'))
aggr_dict[aggr_name] = aggr_size
LOG.debug(_("Found available aggregates: %r") % aggr_dict)
return aggr_dict
def create_share(self, context, share):
"""Creates new share."""
if not share.get('network_info'):
msg = _("Cannot create share %s. "
"No share network provided") % share['id']
LOG.error(msg)
raise exception.NetAppException(message=msg)
vserver = self._get_vserver_name(share['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
self._allocate_container(share, vserver, vserver_client)
return self._create_export(share, vserver, vserver_client)
def create_share_from_snapshot(self, context, share, snapshot,
net_details=None):
"""Creates new share form snapshot."""
vserver = self._get_vserver_name(share['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
self._allocate_container_from_snapshot(share, snapshot, vserver,
vserver_client)
return self._create_export(share, vserver, vserver_client)
def _allocate_container(self, share, vserver, vserver_client):
"""Create new share on aggregate."""
share_name = self._get_valid_share_name(share['id'])
aggregates = self.get_available_aggregates_for_vserver(vserver,
vserver_client)
aggregate = max(aggregates, key=lambda m: aggregates[m])
LOG.debug(_('Creating volume %(share_name)s on '
'aggregate %(aggregate)s')
% {'share_name': share_name, 'aggregate': aggregate})
args = {'containing-aggr-name': aggregate,
'size': str(share['size']) + 'g',
'volume': share_name,
'junction-path': '/%s' % share_name
}
vserver_client.send_request('volume-create', args)
def _allocate_container_from_snapshot(self, share, snapshot, vserver,
vserver_client):
"""Clones existing share."""
share_name = self._get_valid_share_name(share['id'])
parent_share_name = self._get_valid_share_name(snapshot['share_id'])
parent_snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
LOG.debug(_('Creating volume from snapshot %s') % snapshot['id'])
args = {'volume': share_name,
'parent-volume': parent_share_name,
'parent-snapshot': parent_snapshot_name,
'junction-path': '/%s' % share_name
}
vserver_client.send_request('volume-clone-create', args)
def _share_exists(self, share_name, vserver_client):
args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': share_name
}
}
}
}
response = vserver_client.send_request('volume-get-iter', args)
if int(response.get_child_content('num-records')):
return True
def _deallocate_container(self, share, vserver_client):
"""Free share space."""
self._share_unmount(share, vserver_client)
self._offline_share(share, vserver_client)
self._delete_share(share, vserver_client)
def _offline_share(self, share, vserver_client):
"""Sends share offline. Required before deleting a share."""
share_name = self._get_valid_share_name(share['id'])
args = {'name': share_name}
LOG.debug(_('Offline volume %s') % share_name)
vserver_client.send_request('volume-offline', args)
def _delete_share(self, share, vserver_client):
"""Destroys share on a target OnTap device."""
share_name = self._get_valid_share_name(share['id'])
args = {'name': share_name}
LOG.debug(_('Deleting share %s') % share_name)
vserver_client.send_request('volume-destroy', args)
def delete_share(self, context, share):
"""Deletes share."""
share_name = self._get_valid_share_name(share['id'])
vserver = self._get_vserver_name(share['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
if self._share_exists(share_name, vserver_client):
self._remove_export(share, vserver_client)
self._deallocate_container(share, vserver_client)
else:
LOG.info(_("Share %s does not exists") % share['id'])
def _create_export(self, share, vserver, vserver_client):
"""Creates NAS storage."""
helper = self._get_helper(share)
helper.set_client(vserver_client)
share_name = self._get_valid_share_name(share['id'])
network_allocations = share['network_info']['network_allocations']
ip_address = network_allocations[0]['ip_address']
export_location = helper.create_share(share_name, ip_address)
return export_location
def create_snapshot(self, context, snapshot):
"""Creates a snapshot of a share."""
vserver = self._get_vserver_name(snapshot['share']['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
share_name = self._get_valid_share_name(snapshot['share_id'])
snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
args = {'volume': share_name,
'snapshot': snapshot_name}
LOG.debug(_('Creating snapshot %s') % snapshot_name)
vserver_client.send_request('snapshot-create', args)
def _remove_export(self, share, vserver_client):
"""Deletes NAS storage."""
helper = self._get_helper(share)
helper.set_client(vserver_client)
target = helper.get_target(share)
# share may be in error state, so there's no share and target
if target:
helper.delete_share(share)
def delete_snapshot(self, context, snapshot):
"""Deletes a snapshot of a share."""
vserver = self._get_vserver_name(snapshot['share']['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
share_name = self._get_valid_share_name(snapshot['share_id'])
snapshot_name = self._get_valid_snapshot_name(snapshot['id'])
self._is_snapshot_busy(share_name, snapshot_name, vserver_client)
args = {'snapshot': snapshot_name,
'volume': share_name}
LOG.debug(_('Deleting snapshot %s') % snapshot_name)
vserver_client.send_request('snapshot-delete', args)
def _is_snapshot_busy(self, share_name, snapshot_name, vserver_client):
"""Raises ShareSnapshotIsBusy if snapshot is busy."""
args = {'volume': share_name}
snapshots = vserver_client.send_request('snapshot-list-info',
args)
for snap in snapshots.get_child_by_name('snapshots')\
.get_children():
if snap.get_child_by_name('name').get_content() == snapshot_name\
and snap.get_child_by_name('busy').get_content() == 'true':
return True
def _share_unmount(self, share, vserver_client):
"""Unmounts share (required before deleting)."""
share_name = self._get_valid_share_name(share['id'])
args = {'volume-name': share_name}
LOG.debug(_('Unmounting volume %s') % share_name)
vserver_client.send_request('volume-unmount', args)
def allow_access(self, context, share, access):
"""Allows access to a given NAS storage for IPs in access."""
vserver = self._get_vserver_name(share['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
helper = self._get_helper(share)
helper.set_client(vserver_client)
return helper.allow_access(context, share, access)
def deny_access(self, context, share, access):
"""Denies access to a given NAS storage for IPs in access."""
vserver = self._get_vserver_name(share['share_network_id'])
vserver_client = driver.NetAppApiClient(
self.api_version, vserver=vserver,
configuration=self.configuration)
helper = self._get_helper(share)
helper.set_client(vserver_client)
return helper.deny_access(context, share, access)
class NetAppClusteredNFSHelper(driver.NetAppNFSHelper):
"""Netapp specific cluster-mode NFS sharing driver."""
def create_share(self, share_name, export_ip):
"""Creates NFS share."""
export_pathname = os.path.join('/', share_name)
self.add_rules(export_pathname, ['localhost'])
export_location = ':'.join([export_ip, export_pathname])
return export_location
def allow_access_by_sid(self, share, sid):
user, _x, group = sid.partition(':')
args = {
'attributes': {
'volume-attributes': {
'volume-security-attributes': {
'volume-security-unix-attributes': {
'user-id': user,
'group-id': group or 'root'
}
}
}
},
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': self._get_export_path(share)
}
}
}
}
self._client.send_request('volume-modify-iter', args)
def deny_access_by_sid(self, share, sid):
args = {
'attributes': {
'volume-security-attributes': {
'volume-security-unix-attributes': {
'user': 'root'
}
}
},
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': self._get_export_path(share)
}
}
}
}
self._client.send_request('volume-modify-iter', args)
class NetAppClusteredCIFSHelper(driver.NetAppCIFSHelper):
"""Netapp specific cluster-mode CIFS sharing driver."""
def create_share(self, share_name, export_ip):
self._add_share(share_name)
cifs_location = self._set_export_location(export_ip, share_name)
self._restrict_access('Everyone', share_name)
return cifs_location
def _add_share(self, share_name):
"""Creates CIFS share on target OnTap host."""
share_path = '/%s' % share_name
args = {'path': share_path,
'share-name': share_name}
self._client.send_request('cifs-share-create', args)
def delete_share(self, share):
"""Deletes CIFS storage."""
host_ip, share_name = self._get_export_location(share)
args = {'share-name': share_name}
self._client.send_request('cifs-share-delete', args)
def _allow_access_for(self, username, share_name):
"""Allows access to the CIFS share for a given user."""
args = {'permission': 'full_control',
'share': share_name,
'user-or-group': username}
self._client.send_request('cifs-share-access-control-create', args)
def _restrict_access(self, user_name, share_name):
args = {'user-or-group': user_name,
'share': share_name}
self._client.send_request('cifs-share-access-control-delete', args)

809
manila/share/drivers/netapp/driver.py
File diff suppressed because it is too large
View File

1
manila/test.py

@ -32,7 +32,6 @@ import nose.plugins.skip
from oslo.config import cfg
import stubout
from manila.common import config
from manila.openstack.common import importutils
from manila.openstack.common import log as logging
from manila.openstack.common import timeutils

0
manila/tests/netapp/__init__.py

275
manila/tests/netapp/test_7mode_drv.py

@ -0,0 +1,275 @@
# Copyright (c) 2014 NetApp, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo.config import cfg
from manila import context
from manila import exception
from manila.share import configuration
from manila.share.drivers.netapp import api as naapi
from manila.share.drivers.netapp import driver
from manila import test
CONF = cfg.CONF
class NetApp7modeDrvTestCase(test.TestCase):
"""Tests for NetApp 7mode driver."""
def setUp(self):
super(NetApp7modeDrvTestCase, self).setUp()
self._context = context.get_admin_context()
self._db = mock.Mock()
self.driver = driver.NetAppShareDriver(
self._db, configuration=configuration.Configuration(None))
self.driver._client = mock.Mock()
self.driver._client.send_request = mock.Mock()
self.share = {'id': 'fake_uuid',
'tenant_id': 'fake_tenant_id',
'name': 'fake_name',
'size': 1,
'share_proto': 'fake'}
self.snapshot = {'id': 'fake_snapshot_uuid',
'tenant_id': 'fake_tenant_id',
'share_id': 'fake_share_id'}
self.helper = mock.Mock()
self.driver._helpers = {'FAKE': self.helper}
self.driver._licenses = ['fake']
def test_check_vfiler_exists(self):
elem = naapi.NaElement('fake')
elem['status'] = 'running'
self.driver._client.send_request = mock.Mock(return_value=elem)
self.driver._check_vfiler_exists()
def test_check_vfiler_exists_error(self):
elem = naapi.NaElement('fake')
elem['status'] = 'error'
self.driver._client.send_request = mock.Mock(return_value=elem)
self.assertRaises(exception.NetAppException,
self.driver._check_vfiler_exists)
def test_check_licenses(self):
root = naapi.NaElement('fake')
elem = naapi.NaElement('licenses')
licenses = ['l1', 'l2']
for license in licenses:
el = naapi.NaElement('license')
el['package'] = license
elem.add_child_elem(el)
root.add_child_elem(elem)
self.driver._client.send_request = mock.Mock(return_value=root)
self.driver._check_licenses()
self.assertEqual(self.driver._licenses, licenses)
def test_create_share(self):
self.driver.configuration.netapp_nas_server_hostname\
= 'fake-netapp-location'
root = naapi.NaElement('root')
aggregates = naapi.NaElement('aggregates')
for i in range(1, 4):
aggregates.add_node_with_children('aggr-attributes',
**{'name': 'fake%s' % i,
'size-available': '%s' % i})
root.add_child_elem(aggregates)
self.driver._client.send_request = mock.Mock(return_value=root)
self.helper.create_share = mock.Mock(return_value="fake-location")
export_location = self.driver.create_share(self._context, self.share)
args = {'containing-aggr-name': 'fake3',
'size': '1g',
'volume': 'share_fake_uuid'}
self.driver._client.send_request.assert_called_with('volume-create',
args)
self.helper.create_share.assert_called_once_with(
"share_%s" % self.share['id'], 'fake-netapp-location')
self.assertEqual(export_location, "fake-location")
def test_create_share_from_snapshot(self):
self.helper.create_share = mock.Mock(return_value="fake-location")
export_location = self.driver.create_share_from_snapshot(self._context,
self.share,
self.snapshot)
args = {'volume': 'share_fake_uuid',
'parent-volume': 'share_fake_share_id',
'parent-snapshot': 'share_snapshot_fake_snapshot_uuid'}
self.driver._client.send_request.assert_called_once_with(
'volume-clone-create', args)
self.assertEqual(export_location, "fake-location")
def test_delete_share(self):
self.driver.delete_share(self._context, self.share)
self.driver._client.send_request.assert_has_calls([
mock.call('volume-list-info', {'volume': 'share_fake_uuid'}),
mock.call('volume-offline', {'name': 'share_fake_uuid'}),
mock.call('volume-destroy', {'name': 'share_fake_uuid'})
])
self.helper.get_target.assert_called_once_with(self.share)
self.helper.delete_share.assert_called_once_with(self.share)
def test_delete_share_not_exists(self):
self.driver._client.send_request = mock.Mock(