manila/manila/share/drivers/netapp.py

745 lines
27 KiB
Python

# 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.
"""
NetApp specific NAS storage driver. Supports NFS and CIFS protocols.
This driver requires NetApp OnCommand 5.0 and one or more Data
ONTAP 7-mode storage systems with installed CIFS and NFS licenses.
"""
import suds
from suds.sax import text
from manila import exception
from manila.openstack.common import log
from manila.share import driver
from oslo.config import cfg
LOG = log.getLogger(__name__)
NETAPP_NAS_OPTS = [
cfg.StrOpt('netapp_nas_wsdl_url',
default=None,
help='URL of the WSDL file for the DFM server'),
cfg.StrOpt('netapp_nas_login',
default=None,
help='User name for the DFM server'),
cfg.StrOpt('netapp_nas_password',
default=None,
help='Password for the DFM server'),
cfg.StrOpt('netapp_nas_server_hostname',
default=None,
help='Hostname for the DFM server'),
cfg.IntOpt('netapp_nas_server_port',
default=8088,
help='Port number for the DFM server'),
cfg.BoolOpt('netapp_nas_server_secure',
default=True,
help='Use secure connection to server.'),
]
CONF = cfg.CONF
CONF.register_opts(NETAPP_NAS_OPTS)
class NetAppShareDriver(driver.ShareDriver):
"""
NetApp specific NAS driver. Allows for NFS and CIFS NAS storage usage.
"""
def __init__(self, db, *args, **kwargs):
super(NetAppShareDriver, self).__init__(*args, **kwargs)
self.db = db
self._helpers = None
self._share_table = {}
self.configuration.append_config_values(NETAPP_NAS_OPTS)
self._client = NetAppApiClient(self.configuration)
def allocate_container(self, context, share):
"""Allocate space for the share on aggregates."""
aggregate = self._find_best_aggregate()
filer = aggregate.FilerId
self._allocate_share_space(aggregate, share)
self._remember_share(share['id'], filer)
def allocate_container_from_snapshot(self, context, share, snapshot):
"""Creates a share from a snapshot."""
share_name = _get_valid_share_name(share['id'])
parent_share_name = _get_valid_share_name(snapshot['share_id'])
parent_snapshot_name = _get_valid_snapshot_name(snapshot['id'])
filer = self._get_filer(snapshot['share_id'])
xml_args = ('<volume>%s</volume>'
'<parent-volume>%s</parent-volume>'
'<parent-snapshot>%s</parent-snapshot>') % \
(share_name, parent_share_name, parent_snapshot_name)
self._client.send_request_to(filer, 'volume-clone-create', xml_args)
self._remember_share(share['id'], filer)
def deallocate_container(self, context, share):
"""Free share space."""
target = self._get_filer(share['id'])
if target:
self._share_offline(target, share)
self._delete_share(target, share)
self._forget_share(share['id'])
def create_share(self, context, share):
"""Creates NAS storage."""
helper = self._get_helper(share)
filer = self._get_filer(share['id'])
export_location = helper.create_share(filer, share)
return export_location
def create_snapshot(self, context, snapshot):
"""Creates a snapshot of a share."""
share_name = _get_valid_share_name(snapshot['share_id'])
snapshot_name = _get_valid_snapshot_name(snapshot['id'])
filer = self._get_filer(snapshot['share_id'])
xml_args = ('<volume>%s</volume>'
'<snapshot>%s</snapshot>') % (share_name, snapshot_name)
self._client.send_request_to(filer, 'snapshot-create', xml_args)
def delete_share(self, context, share):
"""Deletes NAS storage."""
helper = self._get_helper(share)
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."""
share_name = _get_valid_share_name(snapshot['share_id'])
snapshot_name = _get_valid_snapshot_name(snapshot['id'])
filer = self._get_filer(snapshot['share_id'])
self._is_snapshot_busy(filer, share_name, snapshot_name)
xml_args = ('<snapshot>%s</snapshot>'
'<volume>%s</volume>') % (snapshot_name, share_name)
self._client.send_request_to(filer, 'snapshot-delete', xml_args)
def create_export(self, context, share):
"""Share already exported."""
pass
def remove_export(self, context, share):
"""Share already removed."""
pass
def ensure_share(self, context, share):
"""Remember previously created shares."""
helper = self._get_helper(share)
filer = helper.get_target(share)
self._remember_share(share['id'], filer)
def allow_access(self, context, share, access):
"""Allows access to a given NAS storage for IPs in :access:"""
helper = self._get_helper(share)
return helper.allow_access(context, share, access)
def deny_access(self, context, share, access):
"""Denies access to a given NAS storage for IPs in :access:"""
helper = self._get_helper(share)
return helper.deny_access(context, share, access)
def do_setup(self, context):
"""Prepare once the driver.
Called once by the manager after the driver is loaded.
Validate the flags we care about and setup the suds (web
services) client.
"""
self._client.do_setup()
self._setup_helpers()
def check_for_setup_error(self):
"""Raises error if prerequisites are not met."""
self._client.check_configuration(self.configuration)
def _get_filer(self, share_id):
"""Returns filer name for the share_id."""
try:
return self._share_table[share_id]
except KeyError:
return
def _remember_share(self, share_id, filer):
"""Stores required share info in local state."""
self._share_table[share_id] = filer
def _forget_share(self, share_id):
"""Remove share info about share."""
try:
self._share_table.pop(share_id)
except KeyError:
pass
def _share_offline(self, target, share):
"""Sends share offline. Required before deleting a share."""
share_name = _get_valid_share_name(share['id'])
xml_args = ('<name>%s</name>') % share_name
self._client.send_request_to(target, 'volume-offline', xml_args)
def _delete_share(self, target, share):
"""Destroys share on a target OnTap device."""
share_name = _get_valid_share_name(share['id'])
xml_args = ('<force>true</force>'
'<name>%s</name>') % share_name
self._client.send_request_to(target, 'volume-destroy', xml_args)
def _setup_helpers(self):
"""Initializes protocol-specific NAS drivers."""
#TODO(rushiagr): better way to handle configuration instead of just
# passing to the helper
self._helpers = {
'CIFS': NetAppCIFSHelper(self._client,
self.configuration),
'NFS': NetAppNFSHelper(self._client,
self.configuration),
}
def _get_helper(self, share):
"""Returns driver which implements share protocol."""
share_proto = share['share_proto']
for proto in self._helpers.keys():
if share_proto.upper().startswith(proto):
return self._helpers[proto]
err_msg = _("Invalid NAS protocol supplied: %s. ") % (share_proto)
raise exception.Error(err_msg)
def _find_best_aggregate(self):
"""Returns aggregate with the most free space left."""
aggrs = self._client.get_available_aggregates()
if aggrs is None:
raise exception.Error(_("No aggregates available"))
best_aggregate = max(aggrs.Aggregates.AggregateInfo,
key=lambda ai: ai.AggregateSize.SizeAvailable)
return best_aggregate
def _allocate_share_space(self, aggregate, share):
"""Create new share on aggregate."""
filer_id = aggregate.FilerId
aggr_name = aggregate.AggregateName.split(':')[1]
share_name = _get_valid_share_name(share['id'])
args_xml = ('<containing-aggr-name>%s</containing-aggr-name>'
'<size>%dg</size>'
'<volume>%s</volume>') % (aggr_name, share['size'],
share_name)
self._client.send_request_to(filer_id, 'volume-create', args_xml)
def _is_snapshot_busy(self, filer, share_name, snapshot_name):
"""Raises ShareSnapshotIsBusy if snapshot is busy."""
xml_args = ('<volume>%s</volume>') % share_name
snapshots = self._client.send_request_to(filer,
'snapshot-list-info',
xml_args,
do_response_check=False)
for snap in snapshots.Results.snapshots[0]['snapshot-info']:
if snap['name'][0] == snapshot_name and snap['busy'][0] == 'true':
raise exception.ShareSnapshotIsBusy(
snapshot_name=snapshot_name)
def get_share_stats(self, refresh=False):
"""Get share status.
If 'refresh' is True, run update the stats first."""
if refresh:
self._update_share_status()
return self._stats
def _update_share_status(self):
"""Retrieve status info from share volume group."""
LOG.debug(_("Updating share status"))
data = {}
# Note(zhiteng): These information are driver/backend specific,
# each driver may define these values in its own config options
# or fetch from driver specific configuration file.
data["share_backend_name"] = 'NetApp_7_mode'
data["vendor_name"] = 'NetApp'
data["driver_version"] = '1.0'
#TODO(rushiagr): Pick storage_protocol from the helper used.
data["storage_protocol"] = 'NFS_CIFS'
data['total_capacity_gb'] = 'infinite'
data['free_capacity_gb'] = 'infinite'
data['reserved_percentage'] = 0
data['QoS_support'] = False
self._stats = data
def _check_response(request, response):
"""Checks RPC responses from NetApp devices."""
if response.Status == 'failed':
name = request.Name
reason = response.Reason
msg = _('API %(name)s failed: %(reason)s')
raise exception.Error(msg % locals())
def _get_valid_share_name(share_id):
"""The name can contain letters, numbers, and the underscore
character (_). The first character must be a letter or an
underscore."""
return 'share_' + share_id.replace('-', '_')
def _get_valid_snapshot_name(snapshot_id):
"""The name can contain letters, numbers, and the underscore
character (_). The first character must be a letter or an
underscore."""
return 'share_snapshot_' + snapshot_id.replace('-', '_')
class NetAppApiClient(object):
"""Wrapper around DFM commands."""
REQUIRED_FLAGS = ['netapp_nas_wsdl_url',
'netapp_nas_login',
'netapp_nas_password',
'netapp_nas_server_hostname',
'netapp_nas_server_port']
def __init__(self, configuration):
self.configuration = configuration
self._client = None
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))
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)
LOG.info('NetApp RPC client started')
def send_request_to(self, target, request, xml_args=None,
do_response_check=True):
"""
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.
"""
client = self._client
srv = client.service
rpc = client.factory.create('Request')
rpc.Name = request
rpc.Args = text.Raw(xml_args)
response = srv.ApiProxy(Request=rpc, Target=target)
if do_response_check:
_check_response(rpc, response)
return response
def get_available_aggregates(self):
"""Returns list of aggregates known by DFM."""
srv = self._client.service
resp = srv.AggregateListInfoIterStart()
tag = resp.Tag
try:
avail_aggrs = srv.AggregateListInfoIterNext(Tag=tag,
Maximum=resp.Records)
finally:
srv.AggregateListInfoIterEnd(tag)
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
try:
filers = srv.HostListInfoIterNext(Tag=tag, Maximum=resp.Records)
finally:
srv.HostListInfoIterEnd(Tag=tag)
ip = None
for host in filers.Hosts.HostInfo:
if int(host.HostId) == int(host_id):
ip = host.HostAddress
return ip
@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)
class NetAppNASHelperBase(object):
"""Interface for protocol-specific NAS drivers."""
def __init__(self, suds_client, config_object):
self.configuration = config_object
self._client = suds_client
def create_share(self, target_id, share):
"""Creates NAS share."""
raise NotImplementedError()
def delete_share(self, share):
"""Deletes NAS share."""
raise NotImplementedError()
def allow_access(self, context, share, new_rules):
"""Allows new_rules to a given NAS storage for IPs in :new_rules."""
raise NotImplementedError()
def deny_access(self, context, share, new_rules):
"""Denies new_rules to a given NAS storage for IPs in :new_rules:."""
raise NotImplementedError()
def get_target(self, share):
"""Returns host where the share located.."""
raise NotImplementedError()
class NetAppNFSHelper(NetAppNASHelperBase):
"""Netapp specific NFS sharing driver."""
def __init__(self, suds_client, config_object):
self.configuration = config_object
super(NetAppNFSHelper, self).__init__(suds_client, config_object)
def create_share(self, target_id, share):
"""Creates NFS share"""
args_xml = ('<rules>'
'<exports-rule-info-2>'
'<pathname>%s</pathname>'
'<security-rules>'
'<security-rule-info>'
'<read-write>'
'<exports-hostname-info>'
'<name>localhost</name>'
'</exports-hostname-info>'
'</read-write>'
'<root>'
'<exports-hostname-info>'
'<all-hosts>false</all-hosts>'
'<name>localhost</name>'
'</exports-hostname-info>'
'</root>'
'</security-rule-info>'
'</security-rules>'
'</exports-rule-info-2>'
'</rules>')
client = self._client
valid_share_name = _get_valid_share_name(share['id'])
export_pathname = '/vol/' + valid_share_name
client.send_request_to(target_id, 'nfs-exportfs-append-rules-2',
args_xml % export_pathname)
export_ip = client.get_host_ip_by(target_id)
export_location = ':'.join([export_ip, export_pathname])
return export_location
def delete_share(self, share):
"""Deletes NFS share."""
target, export_path = self._get_export_path(share)
xml_args = ('<pathnames>'
'<pathname-info>'
'<name>%s</name>'
'</pathname-info>'
'</pathnames>') % export_path
self._client.send_request_to(target, 'nfs-exportfs-delete-rules',
xml_args)
def allow_access(self, context, share, access):
"""Allows access to a given NFS storage for IPs in :access:."""
if access['access_type'] != 'ip':
raise exception.Error(('Invalid access type supplied. '
'Only \'ip\' type is supported'))
ips = access['access_to']
existing_rules = self._get_exisiting_rules(share)
new_rules_xml = self._append_new_rules_to(existing_rules, ips)
self._modify_rule(share, new_rules_xml)
def deny_access(self, context, share, access):
"""Denies access to a given NFS storage for IPs in :access:."""
denied_ips = access['access_to']
existing_rules = self._get_exisiting_rules(share)
if type(denied_ips) is not list:
denied_ips = [denied_ips]
for deny_rule in denied_ips:
try:
existing_rules.remove(deny_rule)
except ValueError:
pass
new_rules_xml = self._append_new_rules_to([], existing_rules)
self._modify_rule(share, new_rules_xml)
def get_target(self, share):
"""Returns ID of target OnTap device based on export location."""
return self._get_export_path(share)[0]
def _modify_rule(self, share, rw_rules):
"""Modifies access rule for a share."""
target, export_path = self._get_export_path(share)
xml_args = ('<persistent>true</persistent>'
'<rules>'
'<exports-rule-info-2>'
'<pathname>%s</pathname>'
'<security-rules>%s'
'</security-rules>'
'</exports-rule-info-2>'
'</rules>') % (export_path, ''.join(rw_rules))
self._client.send_request_to(target, 'nfs-exportfs-append-rules-2',
xml_args)
def _get_exisiting_rules(self, share):
"""Returns available access rules for the share."""
target, export_path = self._get_export_path(share)
xml_args = '<pathname>%s</pathname>' % export_path
response = self._client.send_request_to(target,
'nfs-exportfs-list-rules-2',
xml_args)
rules = response.Results.rules[0]
security_rule = rules['exports-rule-info-2'][0]['security-rules'][0]
security_info = security_rule['security-rule-info'][0]
root_rules = security_info['root'][0]
allowed_hosts = root_rules['exports-hostname-info']
existing_rules = []
for allowed_host in allowed_hosts:
if 'name' in allowed_host:
existing_rules.append(allowed_host['name'][0])
return existing_rules
@staticmethod
def _append_new_rules_to(existing_rules, new_rules):
"""Adds new rules to existing."""
security_rule_xml = ('<security-rule-info>'
'<read-write>%s'
'</read-write>'
'<root>%s'
'</root>'
'</security-rule-info>')
hostname_info_xml = ('<exports-hostname-info>'
'<name>%s</name>'
'</exports-hostname-info>')
allowed_hosts_xml = []
if type(new_rules) is not list:
new_rules = [new_rules]
all_rules = existing_rules + new_rules
for ip in all_rules:
allowed_hosts_xml.append(hostname_info_xml % ip)
return security_rule_xml % (allowed_hosts_xml, allowed_hosts_xml)
@staticmethod
def _get_export_path(share):
"""Returns IP address and export location of a share."""
export_location = share['export_location']
if export_location is None:
export_location = ':'
return export_location.split(':')
class NetAppCIFSHelper(NetAppNASHelperBase):
"""Netapp specific NFS sharing driver."""
CIFS_USER_GROUP = 'Administrators'
def __init__(self, suds_client, config_object):
self.configuration = config_object
super(NetAppCIFSHelper, self).__init__(suds_client, config_object)
def create_share(self, target_id, share):
"""Creates CIFS storage."""
cifs_status = self._get_cifs_status(target_id)
if cifs_status == 'stopped':
self._start_cifs_service(target_id)
share_name = _get_valid_share_name(share['id'])
self._set_qtree_security(target_id, share)
self._add_share(target_id, share_name)
self._restrict_access(target_id, 'everyone', share_name)
ip_address = self._client.get_host_ip_by(target_id)
cifs_location = self._set_export_location(ip_address, share_name)
return cifs_location
def delete_share(self, share):
"""Deletes CIFS storage."""
host_ip, share_name = self._get_export_location(share)
xml_args = '<share-name>%s</share-name>' % share_name
self._client.send_request_to(host_ip, 'cifs-share-delete', xml_args)
def allow_access(self, context, share, access):
"""Allows access to a given CIFS storage for IPs in :access:."""
if access['access_type'] != 'passwd':
ex_text = ('NetApp only supports "passwd" access type for CIFS.')
raise exception.Error(ex_text)
user = access['access_to']
target, share_name = self._get_export_location(share)
if self._user_exists(target, user):
self._allow_access_for(target, user, share_name)
else:
exc_text = ('User "%s" does not exist on %s OnTap.') % (user,
target)
raise exception.Error(exc_text)
def deny_access(self, context, share, access):
"""Denies access to a given CIFS storage for IPs in access."""
host_ip, share_name = self._get_export_location(share)
user = access['access_to']
self._restrict_access(host_ip, user, share_name)
def get_target(self, share):
"""Returns OnTap target IP based on share export location."""
return self._get_export_location(share)[0]
def _set_qtree_security(self, target, share):
client = self._client
share_name = '/vol/' + _get_valid_share_name(share['id'])
xml_args = ('<args>'
'<arg>qtree</arg>'
'<arg>security</arg>'
'<arg>%s</arg>'
'<arg>mixed</arg>'
'</args>') % share_name
client.send_request_to(target, 'system-cli', xml_args)
def _restrict_access(self, target, user_name, share_name):
xml_args = ('<user-name>%s</user-name>'
'<share-name>%s</share-name>') % (user_name, share_name)
self._client.send_request_to(target, 'cifs-share-ace-delete',
xml_args)
def _start_cifs_service(self, target_id):
"""Starts CIFS service on OnTap target."""
client = self._client
return client.send_request_to(target_id, 'cifs-start',
do_response_check=False)
@staticmethod
def _get_export_location(share):
"""Returns export location for a given CIFS share."""
export_location = share['export_location']
if export_location is None:
export_location = '///'
_, _, host_ip, share_name = export_location.split('/')
return host_ip, share_name
@staticmethod
def _set_export_location(ip, share_name):
"""Returns export location of a share."""
return "//%s/%s" % (ip, share_name)
def _get_cifs_status(self, target_id):
"""Returns status of a CIFS service on target OnTap."""
client = self._client
response = client.send_request_to(target_id, 'cifs-status')
return response.Status
def _allow_access_for(self, target, username, share_name):
"""Allows access to the CIFS share for a given user."""
xml_args = ('<access-rights>rwx</access-rights>'
'<share-name>%s</share-name>'
'<user-name>%s</user-name>') % (share_name, username)
self._client.send_request_to(target, 'cifs-share-ace-set', xml_args)
def _user_exists(self, target, user):
"""Returns True if user already exists on a target OnTap."""
xml_args = ('<user-name>%s</user-name>') % user
resp = self._client.send_request_to(target,
'useradmin-user-list',
xml_args,
do_response_check=False)
return (resp.Status == 'passed')
def _add_share(self, target_id, share_name):
"""Creates CIFS share on target OnTap host."""
client = self._client
xml_args = ('<path>/vol/%s</path>'
'<share-name>%s</share-name>') % (share_name, share_name)
client.send_request_to(target_id, 'cifs-share-add', xml_args)