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

718 lines
30 KiB
Python

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