manila/manila/share/drivers/netapp/dataontap/client/client_cmode.py

1444 lines
52 KiB
Python

# Copyright (c) 2014 Alex Meade. All rights reserved.
# Copyright (c) 2015 Clinton Knight. All rights reserved.
# Copyright (c) 2015 Tom Barron. 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 copy
import hashlib
import time
from oslo_log import log
from oslo_utils import strutils
import six
from manila import exception
from manila.i18n import _, _LE, _LW
from manila.share.drivers.netapp.dataontap.client import api as netapp_api
from manila.share.drivers.netapp.dataontap.client import client_base
from manila.share.drivers.netapp import utils as na_utils
LOG = log.getLogger(__name__)
DELETED_PREFIX = 'deleted_manila_'
class NetAppCmodeClient(client_base.NetAppBaseClient):
def __init__(self, **kwargs):
super(NetAppCmodeClient, self).__init__(**kwargs)
self.vserver = kwargs.get('vserver')
self.connection.set_vserver(self.vserver)
# Default values to run first api.
self.connection.set_api_version(1, 15)
(major, minor) = self.get_ontapi_version(cached=False)
self.connection.set_api_version(major, minor)
def _invoke_vserver_api(self, na_element, vserver):
server = copy.copy(self.connection)
server.set_vserver(vserver)
result = server.invoke_successfully(na_element, True)
return result
def _has_records(self, api_result_element):
if (not api_result_element.get_child_content('num-records') or
api_result_element.get_child_content('num-records') == '0'):
return False
else:
return True
def set_vserver(self, vserver):
self.vserver = vserver
self.connection.set_vserver(vserver)
@na_utils.trace
def create_vserver(self, vserver_name, root_volume_aggregate_name,
root_volume_name, aggregate_names):
"""Creates new vserver and assigns aggregates."""
create_args = {
'vserver-name': vserver_name,
'root-volume-security-style': 'unix',
'root-volume-aggregate': root_volume_aggregate_name,
'root-volume': root_volume_name,
'name-server-switch': {
'nsswitch': 'file',
},
}
self.send_request('vserver-create', create_args)
aggr_list = [{'aggr-name': aggr_name} for aggr_name in aggregate_names]
modify_args = {
'aggr-list': aggr_list,
'vserver-name': vserver_name,
}
self.send_request('vserver-modify', modify_args)
@na_utils.trace
def vserver_exists(self, vserver_name):
"""Checks if Vserver exists."""
LOG.debug('Checking if Vserver %s exists', vserver_name)
api_args = {
'query': {
'vserver-info': {
'vserver-name': vserver_name,
},
},
'desired-attributes': {
'vserver-info': {
'vserver-name': None,
},
},
}
result = self.send_request('vserver-get-iter', api_args)
return self._has_records(result)
@na_utils.trace
def get_vserver_root_volume_name(self, vserver_name):
"""Get the root volume name of the vserver."""
api_args = {
'query': {
'vserver-info': {
'vserver-name': vserver_name,
},
},
'desired-attributes': {
'vserver-info': {
'root-volume': None,
},
},
}
vserver_info = self.send_request('vserver-get-iter', api_args)
try:
root_volume_name = vserver_info.get_child_by_name(
'attributes-list').get_child_by_name(
'vserver-info').get_child_content('root-volume')
except AttributeError:
msg = _('Could not determine root volume name '
'for Vserver %s.') % vserver_name
raise exception.NetAppException(msg)
return root_volume_name
@na_utils.trace
def list_vservers(self, vserver_type='data'):
"""Get the names of vservers present, optionally filtered by type."""
query = {
'vserver-info': {
'vserver-type': vserver_type,
}
} if vserver_type else None
api_args = {
'desired-attributes': {
'vserver-info': {
'vserver-name': None,
},
},
}
if query:
api_args['query'] = query
result = self.send_request('vserver-get-iter', api_args)
vserver_info_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
return [vserver_info.get_child_content('vserver-name')
for vserver_info in vserver_info_list.get_children()]
@na_utils.trace
def get_vserver_volume_count(self, max_records=20):
"""Get the number of volumes present on a cluster or vserver.
Call this on a vserver client to see how many volumes exist
on that vserver.
"""
api_args = {
'max-records': max_records,
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'name': None,
},
},
},
}
volumes_data = self.send_request('volume-get-iter', api_args)
return int(volumes_data.get_child_content('num-records'))
@na_utils.trace
def delete_vserver(self, vserver_name, vserver_client,
security_services=None):
"""Delete Vserver.
Checks if Vserver exists and does not have active shares.
Offlines and destroys root volumes. Deletes Vserver.
"""
if not self.vserver_exists(vserver_name):
LOG.error(_LE("Vserver %s does not exist."), vserver_name)
return
root_volume_name = self.get_vserver_root_volume_name(vserver_name)
volumes_count = vserver_client.get_vserver_volume_count(max_records=2)
if volumes_count == 1:
try:
vserver_client.offline_volume(root_volume_name)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EVOLUMEOFFLINE:
LOG.error(_LE("Volume %s is already offline."),
root_volume_name)
else:
raise e
vserver_client.delete_volume(root_volume_name)
elif volumes_count > 1:
msg = _("Cannot delete Vserver. Vserver %s has shares.")
raise exception.NetAppException(msg % vserver_name)
if security_services:
self._terminate_vserver_services(vserver_name, vserver_client,
security_services)
self.send_request('vserver-destroy', {'vserver-name': vserver_name})
@na_utils.trace
def _terminate_vserver_services(self, vserver_name, vserver_client,
security_services):
for service in security_services:
if service['type'] == 'active_directory':
api_args = {
'admin-password': service['password'],
'admin-username': service['user'],
}
try:
vserver_client.send_request('cifs-server-delete', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EOBJECTNOTFOUND:
LOG.error(_LE('CIFS server does not exist for '
'Vserver %s.'), vserver_name)
else:
vserver_client.send_request('cifs-server-delete')
@na_utils.trace
def list_cluster_nodes(self):
"""Get all available cluster nodes."""
api_args = {
'desired-attributes': {
'node-details-info': {
'node': None,
},
},
}
result = self.send_request('system-node-get-iter', api_args)
nodes_info_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
return [node_info.get_child_content('node') for node_info
in nodes_info_list.get_children()]
@na_utils.trace
def list_node_data_ports(self, node):
ports = self.get_node_data_ports(node)
return [port.get('port') for port in ports]
@na_utils.trace
def get_node_data_ports(self, node):
"""Get applicable data ports on the node."""
api_args = {
'query': {
'net-port-info': {
'node': node,
'link-status': 'up',
'port-type': 'physical|if_group',
'role': 'data',
},
},
'desired-attributes': {
'node-details-info': {
'port': None,
'node': None,
'operational-speed': None,
'ifgrp-port': None,
},
},
}
result = self.send_request('net-port-get-iter', api_args)
net_port_info_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
ports = []
for port_info in net_port_info_list.get_children():
# Skip physical ports that are part of interface groups.
if port_info.get_child_content('ifgrp-port'):
continue
port = {
'node': port_info.get_child_content('node'),
'port': port_info.get_child_content('port'),
'speed': port_info.get_child_content('operational-speed'),
}
ports.append(port)
return self._sort_data_ports_by_speed(ports)
@na_utils.trace
def _sort_data_ports_by_speed(self, ports):
def sort_key(port):
value = port.get('speed')
if not (value and isinstance(value, six.string_types)):
return 0
elif value.isdigit():
return int(value)
elif value == 'auto':
return 3
elif value == 'undef':
return 2
else:
return 1
return sorted(ports, key=sort_key, reverse=True)
@na_utils.trace
def list_aggregates(self):
"""Get names of all aggregates."""
try:
api_args = {
'desired-attributes': {
'aggr-attributes': {
'aggregate-name': None,
},
},
}
result = self.send_request('aggr-get-iter', api_args)
aggr_list = result.get_child_by_name(
'attributes-list').get_children()
except AttributeError:
msg = _("Could not list aggregates.")
raise exception.NetAppException(msg)
return [aggr.get_child_content('aggregate-name') for aggr
in aggr_list]
@na_utils.trace
def list_vserver_aggregates(self):
"""Returns a list of aggregates available to a vserver.
This must be called against a Vserver LIF.
"""
return self.get_vserver_aggregate_capacities().keys()
@na_utils.trace
def create_network_interface(self, ip, netmask, vlan, node, port,
vserver_name, allocation_id,
lif_name_template):
"""Creates LIF on VLAN port."""
home_port_name = port
if vlan:
self._create_vlan(node, port, vlan)
home_port_name = '%(port)s-%(tag)s' % {'port': port, 'tag': vlan}
interface_name = (lif_name_template %
{'node': node, 'net_allocation_id': allocation_id})
LOG.debug('Creating LIF %(lif)s for Vserver %(vserver)s ',
{'lif': interface_name, 'vserver': vserver_name})
api_args = {
'address': ip,
'administrative-status': 'up',
'data-protocols': [
{'data-protocol': 'nfs'},
{'data-protocol': 'cifs'},
],
'home-node': node,
'home-port': home_port_name,
'netmask': netmask,
'interface-name': interface_name,
'role': 'data',
'vserver': vserver_name,
}
self.send_request('net-interface-create', api_args)
@na_utils.trace
def _create_vlan(self, node, port, vlan):
try:
api_args = {
'vlan-info': {
'parent-interface': port,
'node': node,
'vlanid': vlan,
},
}
self.send_request('net-vlan-create', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EDUPLICATEENTRY:
LOG.debug('VLAN %(vlan)s already exists on port %(port)s',
{'vlan': vlan, 'port': port})
else:
msg = _('Failed to create VLAN %(vlan)s on '
'port %(port)s. %(err_msg)s')
msg_args = {'vlan': vlan, 'port': port, 'err_msg': e.message}
raise exception.NetAppException(msg % msg_args)
@na_utils.trace
def network_interface_exists(self, vserver_name, node, port, ip, netmask,
vlan):
"""Checks if LIF exists."""
home_port_name = (port if not vlan else
'%(port)s-%(tag)s' % {'port': port, 'tag': vlan})
api_args = {
'query': {
'net-interface-info': {
'address': ip,
'home-node': node,
'home-port': home_port_name,
'netmask': netmask,
'vserver': vserver_name,
},
},
'desired-attributes': {
'net-interface-info': {
'interface-name': None,
},
},
}
result = self.send_request('net-interface-get-iter', api_args)
return self._has_records(result)
@na_utils.trace
def list_network_interfaces(self):
"""Get the names of available LIFs."""
api_args = {
'desired-attributes': {
'net-interface-info': {
'interface-name': None,
},
},
}
result = self.send_request('net-interface-get-iter', api_args)
lif_info_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
return [lif_info.get_child_content('interface-name') for lif_info
in lif_info_list.get_children()]
@na_utils.trace
def get_network_interfaces(self, protocols=None):
"""Get available LIFs."""
protocols = na_utils.convert_to_list(protocols)
protocols = [protocol.lower() for protocol in protocols]
api_args = {
'query': {
'net-interface-info': {
'data-protocols': {
'data-protocol': '|'.join(protocols),
}
}
}
} if protocols else None
result = self.send_request('net-interface-get-iter', api_args)
lif_info_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
interfaces = []
for lif_info in lif_info_list.get_children():
lif = {
'address': lif_info.get_child_content('address'),
'home-node': lif_info.get_child_content('home-node'),
'home-port': lif_info.get_child_content('home-port'),
'interface-name': lif_info.get_child_content('interface-name'),
'netmask': lif_info.get_child_content('netmask'),
'role': lif_info.get_child_content('role'),
'vserver': lif_info.get_child_content('vserver'),
}
interfaces.append(lif)
return interfaces
@na_utils.trace
def delete_network_interface(self, interface_name):
"""Deletes LIF."""
api_args = {'vserver': None, 'interface-name': interface_name}
self.send_request('net-interface-delete', api_args)
@na_utils.trace
def get_cluster_aggregate_capacities(self, aggregate_names):
"""Calculates capacity of one or more aggregates.
Returns dictionary of aggregate capacity metrics.
'size-used' is the actual space consumed on the aggregate.
'size-available' is the actual space remaining.
'size-total' is the defined total aggregate size, such that
used + available = total.
"""
if aggregate_names is not None and len(aggregate_names) == 0:
return {}
desired_attributes = {
'aggr-attributes': {
'aggregate-name': None,
'aggr-space-attributes': {
'size-available': None,
'size-total': None,
'size-used': None,
},
},
}
aggrs = self._get_aggregates(aggregate_names=aggregate_names,
desired_attributes=desired_attributes)
aggr_space_dict = dict()
for aggr in aggrs:
aggr_name = aggr.get_child_content('aggregate-name')
aggr_space_attrs = aggr.get_child_by_name('aggr-space-attributes')
aggr_space_dict[aggr_name] = {
'available':
int(aggr_space_attrs.get_child_content('size-available')),
'total':
int(aggr_space_attrs.get_child_content('size-total')),
'used':
int(aggr_space_attrs.get_child_content('size-used')),
}
return aggr_space_dict
@na_utils.trace
def get_vserver_aggregate_capacities(self, aggregate_names=None):
"""Calculates capacity of one or more aggregates for a vserver.
Returns dictionary of aggregate capacity metrics. This must
be called against a Vserver LIF.
"""
if aggregate_names is not None and len(aggregate_names) == 0:
return {}
api_args = {
'desired-attributes': {
'vserver-info': {
'vserver-name': None,
'vserver-aggr-info-list': {
'vserver-aggr-info': {
'aggr-name': None,
'aggr-availsize': None,
},
},
},
},
}
result = self.send_request('vserver-get', api_args)
attributes = result.get_child_by_name('attributes')
if not attributes:
raise exception.NetAppException('Failed to read Vserver info')
vserver_info = attributes.get_child_by_name('vserver-info')
vserver_name = vserver_info.get_child_content('vserver-name')
vserver_aggr_info_element = vserver_info.get_child_by_name(
'vserver-aggr-info-list') or netapp_api.NaElement('none')
vserver_aggr_info_list = vserver_aggr_info_element.get_children()
if not vserver_aggr_info_list:
LOG.warning(_LW('No aggregates assigned to Vserver %s.'),
vserver_name)
# Return dict of key-value pair of aggr_name:aggr_size_available.
aggr_space_dict = {}
for aggr_info in vserver_aggr_info_list:
aggr_name = aggr_info.get_child_content('aggr-name')
if aggregate_names is None or aggr_name in aggregate_names:
aggr_size = int(aggr_info.get_child_content('aggr-availsize'))
aggr_space_dict[aggr_name] = {'available': aggr_size}
LOG.debug('Found available Vserver aggregates: %s', aggr_space_dict)
return aggr_space_dict
@na_utils.trace
def _get_aggregates(self, aggregate_names=None, desired_attributes=None):
query = {
'aggr-attributes': {
'aggregate-name': '|'.join(aggregate_names),
}
} if aggregate_names else None
api_args = {}
if query:
api_args['query'] = query
if desired_attributes:
api_args['desired-attributes'] = desired_attributes
result = self.send_request('aggr-get-iter', api_args)
if not self._has_records(result):
return []
else:
return result.get_child_by_name('attributes-list').get_children()
@na_utils.trace
def setup_security_services(self, security_services, vserver_client,
vserver_name):
api_args = {
'name-mapping-switch': {
'nmswitch': 'ldap,file',
},
'name-server-switch': {
'nsswitch': 'ldap,file',
},
'vserver-name': vserver_name,
}
self.send_request('vserver-modify', api_args)
for security_service in security_services:
if security_service['type'].lower() == 'ldap':
vserver_client.configure_ldap(security_service)
elif security_service['type'].lower() == 'active_directory':
vserver_client.configure_active_directory(security_service,
vserver_name)
elif security_service['type'].lower() == 'kerberos':
self.create_kerberos_realm(security_service)
vserver_client.configure_kerberos(security_service,
vserver_name)
else:
msg = _('Unsupported security service type %s for '
'Data ONTAP driver')
raise exception.NetAppException(msg % security_service['type'])
@na_utils.trace
def enable_nfs(self):
"""Enables NFS on Vserver."""
self.send_request('nfs-enable')
self.send_request('nfs-service-modify', {'is-nfsv40-enabled': 'true'})
api_args = {
'client-match': '0.0.0.0/0',
'policy-name': 'default',
'ro-rule': {
'security-flavor': 'any',
},
'rw-rule': {
'security-flavor': 'never',
},
}
self.send_request('export-rule-create', api_args)
@na_utils.trace
def configure_ldap(self, security_service):
"""Configures LDAP on Vserver."""
config_name = hashlib.md5(security_service['id']).hexdigest()
api_args = {
'ldap-client-config': config_name,
'servers': {
'ip-address': security_service['server'],
},
'tcp-port': '389',
'schema': 'RFC-2307',
'bind-password': security_service['password'],
}
self.send_request('ldap-client-create', api_args)
api_args = {'client-config': config_name, 'client-enabled': 'true'}
self.send_request('ldap-config-create', api_args)
@na_utils.trace
def configure_active_directory(self, security_service, vserver_name):
"""Configures AD on Vserver."""
self.configure_dns(security_service)
# 'cifs-server' is CIFS Server NetBIOS Name, max length is 15.
# Should be unique within each domain (data['domain']).
cifs_server = (vserver_name[0:7] + '..' + vserver_name[-6:]).upper()
api_args = {
'admin-username': security_service['user'],
'admin-password': security_service['password'],
'force-account-overwrite': 'true',
'cifs-server': cifs_server,
'domain': security_service['domain'],
}
try:
LOG.debug("Trying to setup CIFS server with data: %s", api_args)
self.send_request('cifs-server-create', api_args)
except netapp_api.NaApiError as e:
msg = _("Failed to create CIFS server entry. %s")
raise exception.NetAppException(msg % e.message)
@na_utils.trace
def create_kerberos_realm(self, security_service):
"""Creates Kerberos realm on cluster."""
api_args = {
'admin-server-ip': security_service['server'],
'admin-server-port': '749',
'clock-skew': '5',
'comment': '',
'config-name': security_service['id'],
'kdc-ip': security_service['server'],
'kdc-port': '88',
'kdc-vendor': 'other',
'password-server-ip': security_service['server'],
'password-server-port': '464',
'realm': security_service['domain'].upper(),
}
try:
self.send_request('kerberos-realm-create', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EDUPLICATEENTRY:
LOG.debug('Kerberos realm config already exists.')
else:
msg = _('Failed to create Kerberos realm. %s')
raise exception.NetAppException(msg % e.message)
@na_utils.trace
def configure_kerberos(self, security_service, vserver_name):
"""Configures Kerberos for NFS on Vserver."""
self.configure_dns(security_service)
spn = self._get_kerberos_service_principal_name(
security_service, vserver_name)
lifs = self.list_network_interfaces()
if not lifs:
msg = _("Cannot set up Kerberos. There are no LIFs configured.")
raise exception.NetAppException(msg)
for lif_name in lifs:
api_args = {
'admin-password': security_service['password'],
'admin-user-name': security_service['user'],
'interface-name': lif_name,
'is-kerberos-enabled': 'true',
'service-principal-name': spn,
}
self.send_request('kerberos-config-modify', api_args)
@na_utils.trace
def _get_kerberos_service_principal_name(self, security_service,
vserver_name):
return 'nfs/' + vserver_name.replace('_', '-') + '.' + \
security_service['domain'] + '@' + \
security_service['domain'].upper()
@na_utils.trace
def configure_dns(self, security_service):
api_args = {
'domains': {
'string': security_service['domain'],
},
'name-servers': {
'ip-address': security_service['dns_ip'],
},
'dns-state': 'enabled',
}
try:
self.send_request('net-dns-create', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EDUPLICATEENTRY:
LOG.error(_LE("DNS exists for Vserver."))
else:
msg = _("Failed to configure DNS. %s")
raise exception.NetAppException(msg % e.message)
@na_utils.trace
def create_volume(self, aggregate_name, volume_name, size_gb,
thin_provisioned=False, snapshot_policy=None,
language=None, dedup_enabled=False,
compression_enabled=False, max_files=None):
"""Creates a volume."""
api_args = {
'containing-aggr-name': aggregate_name,
'size': six.text_type(size_gb) + 'g',
'volume': volume_name,
'junction-path': '/%s' % volume_name,
}
if thin_provisioned:
api_args['space-reserve'] = 'none'
if snapshot_policy is not None:
api_args['snapshot-policy'] = snapshot_policy
if language is not None:
api_args['language-code'] = language
self.send_request('volume-create', api_args)
# cDOT compression requires that deduplication be enabled.
if dedup_enabled or compression_enabled:
self.enable_dedup(volume_name)
if compression_enabled:
self.enable_compression(volume_name)
if max_files is not None:
self.set_volume_max_files(volume_name, max_files)
@na_utils.trace
def enable_dedup(self, volume_name):
"""Enable deduplication on volume."""
api_args = {'path': '/vol/%s' % volume_name}
self.send_request('sis-enable', api_args)
@na_utils.trace
def enable_compression(self, volume_name):
"""Enable compression on volume."""
api_args = {
'path': '/vol/%s' % volume_name,
'enable-compression': 'true'
}
self.send_request('sis-set-config', api_args)
@na_utils.trace
def set_volume_max_files(self, volume_name, max_files):
"""Set flexvol file limit."""
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': volume_name,
},
},
},
'attributes': {
'volume-attributes': {
'volume-inode-attributes': {
'files-total': max_files,
},
},
},
}
self.send_request('volume-modify-iter', api_args)
@na_utils.trace
def volume_exists(self, volume_name):
"""Checks if volume exists."""
LOG.debug('Checking if volume %s exists', volume_name)
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': volume_name,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'name': None,
},
},
},
}
result = self.send_request('volume-get-iter', api_args)
return self._has_records(result)
@na_utils.trace
def get_aggregate_for_volume(self, volume_name):
"""Get the name of the aggregate containing a volume."""
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': volume_name,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': None,
'name': None,
},
},
},
}
result = self.send_request('volume-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
volume_attributes = attributes_list.get_child_by_name(
'volume-attributes') or netapp_api.NaElement('none')
volume_id_attributes = volume_attributes.get_child_by_name(
'volume-id-attributes') or netapp_api.NaElement('none')
aggregate = volume_id_attributes.get_child_content(
'containing-aggregate-name')
if not aggregate:
msg = _('Could not find aggregate for volume %s.')
raise exception.NetAppException(msg % volume_name)
return aggregate
@na_utils.trace
def create_volume_clone(self, volume_name, parent_volume_name,
parent_snapshot_name=None):
"""Clones a volume."""
api_args = {
'volume': volume_name,
'parent-volume': parent_volume_name,
'parent-snapshot': parent_snapshot_name,
'junction-path': '/%s' % volume_name,
}
self.send_request('volume-clone-create', api_args)
@na_utils.trace
def split_volume_clone(self, volume_name):
"""Begins splitting a clone from its parent."""
api_args = {'volume': volume_name}
self.send_request('volume-clone-split-start', api_args)
@na_utils.trace
def get_volume_junction_path(self, volume_name, is_style_cifs=False):
"""Gets a volume junction path."""
api_args = {
'volume': volume_name,
'is-style-cifs': six.text_type(is_style_cifs).lower(),
}
result = self.send_request('volume-get-volume-path', api_args)
return result.get_child_content('junction')
@na_utils.trace
def offline_volume(self, volume_name):
"""Offlines a volume."""
try:
self.send_request('volume-offline', {'name': volume_name})
except netapp_api.NaApiError as e:
if e.code == netapp_api.EVOLUMEOFFLINE:
return
raise
@na_utils.trace
def _unmount_volume(self, volume_name, force=False):
"""Unmounts a volume."""
api_args = {
'volume-name': volume_name,
'force': six.text_type(force).lower(),
}
try:
self.send_request('volume-unmount', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EVOL_NOT_MOUNTED:
return
raise
@na_utils.trace
def unmount_volume(self, volume_name, force=False, wait_seconds=30):
"""Unmounts a volume, retrying if a clone split is ongoing.
NOTE(cknight): While unlikely to happen in normal operation, any client
that tries to delete volumes immediately after creating volume clones
is likely to experience failures if cDOT isn't quite ready for the
delete. The volume unmount is the first operation in the delete
path that fails in this case, and there is no proactive check we can
use to reliably predict the failure. And there isn't a specific error
code from volume-unmount, so we have to check for a generic error code
plus certain language in the error code. It's ugly, but it works, and
it's better than hard-coding a fixed delay.
"""
# Do the unmount, handling split-related errors with retries.
retry_interval = 3 # seconds
for retry in range(wait_seconds / retry_interval):
try:
self._unmount_volume(volume_name, force=force)
LOG.debug('Volume %s unmounted.', volume_name)
return
except netapp_api.NaApiError as e:
if e.code == netapp_api.EAPIERROR and 'job ID' in e.message:
msg = _LW('Could not unmount volume %(volume)s due to '
'ongoing volume operation: %(exception)s')
msg_args = {'volume': volume_name, 'exception': e}
LOG.warning(msg, msg_args)
time.sleep(retry_interval)
continue
raise
msg = _('Failed to unmount volume %(volume)s after '
'waiting for %(wait_seconds)s seconds.')
msg_args = {'volume': volume_name, 'wait_seconds': wait_seconds}
LOG.error(msg, msg_args)
raise exception.NetAppException(msg % msg_args)
@na_utils.trace
def delete_volume(self, volume_name):
"""Deletes a volume."""
self.send_request('volume-destroy', {'name': volume_name})
@na_utils.trace
def create_snapshot(self, volume_name, snapshot_name):
"""Creates a volume snapshot."""
api_args = {'volume': volume_name, 'snapshot': snapshot_name}
self.send_request('snapshot-create', api_args)
@na_utils.trace
def get_snapshot(self, volume_name, snapshot_name):
"""Gets a single snapshot."""
api_args = {
'query': {
'snapshot-info': {
'name': snapshot_name,
'volume': volume_name,
},
},
'desired-attributes': {
'snapshot-info': {
'name': None,
'volume': None,
'busy': None,
'snapshot-owners-list': {
'snapshot-owner': None,
}
},
},
}
result = self.send_request('snapshot-get-iter', api_args)
error_record_list = result.get_child_by_name(
'volume-errors') or netapp_api.NaElement('none')
errors = error_record_list.get_children()
if errors:
error = errors[0]
error_code = error.get_child_content('errno')
error_reason = error.get_child_content('reason')
msg = _('Could not read information for snapshot %(name)s. '
'Code: %(code)s. Reason: %(reason)s')
msg_args = {
'name': snapshot_name,
'code': error_code,
'reason': error_reason
}
if error_code == netapp_api.ESNAPSHOTNOTALLOWED:
raise exception.SnapshotUnavailable(msg % msg_args)
else:
raise exception.NetAppException(msg % msg_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
snapshot_info_list = attributes_list.get_children()
if not self._has_records(result):
raise exception.SnapshotNotFound(name=snapshot_name)
elif len(snapshot_info_list) > 1:
msg = _('Could not find unique snapshot %(snap)s on '
'volume %(vol)s.')
msg_args = {'snap': snapshot_name, 'vol': volume_name}
raise exception.NetAppException(msg % msg_args)
snapshot_info = snapshot_info_list[0]
snapshot = {
'name': snapshot_info.get_child_content('name'),
'volume': snapshot_info.get_child_content('volume'),
'busy': strutils.bool_from_string(
snapshot_info.get_child_content('busy')),
}
snapshot_owners_list = snapshot_info.get_child_by_name(
'snapshot-owners-list') or netapp_api.NaElement('none')
snapshot_owners = set([
snapshot_owner.get_child_content('owner')
for snapshot_owner in snapshot_owners_list.get_children()])
snapshot['owners'] = snapshot_owners
return snapshot
@na_utils.trace
def delete_snapshot(self, volume_name, snapshot_name):
"""Deletes a volume snapshot."""
api_args = {'volume': volume_name, 'snapshot': snapshot_name}
self.send_request('snapshot-delete', api_args)
@na_utils.trace
def create_cifs_share(self, share_name):
share_path = '/%s' % share_name
api_args = {'path': share_path, 'share-name': share_name}
self.send_request('cifs-share-create', api_args)
@na_utils.trace
def add_cifs_share_access(self, share_name, user_name):
api_args = {
'permission': 'full_control',
'share': share_name,
'user-or-group': user_name,
}
self.send_request('cifs-share-access-control-create', api_args)
@na_utils.trace
def remove_cifs_share_access(self, share_name, user_name):
api_args = {'user-or-group': user_name, 'share': share_name}
self.send_request('cifs-share-access-control-delete', api_args)
@na_utils.trace
def remove_cifs_share(self, share_name):
self.send_request('cifs-share-delete', {'share-name': share_name})
@na_utils.trace
def add_nfs_export_rule(self, policy_name, rule, readonly):
rule_indices = self._get_nfs_export_rule_indices(policy_name, rule)
if not rule_indices:
self._add_nfs_export_rule(policy_name, rule, readonly)
else:
# Update first rule and delete the rest
self._update_nfs_export_rule(
policy_name, rule, readonly, rule_indices.pop(0))
self._remove_nfs_export_rules(policy_name, rule_indices)
@na_utils.trace
def _add_nfs_export_rule(self, policy_name, rule, readonly):
api_args = {
'policy-name': policy_name,
'client-match': rule,
'ro-rule': {
'security-flavor': 'sys',
},
'rw-rule': {
'security-flavor': 'sys' if not readonly else 'never',
},
'super-user-security': {
'security-flavor': 'sys',
},
}
self.send_request('export-rule-create', api_args)
@na_utils.trace
def _update_nfs_export_rule(self, policy_name, rule, readonly, rule_index):
api_args = {
'policy-name': policy_name,
'rule-index': rule_index,
'client-match': rule,
'ro-rule': {
'security-flavor': 'sys'
},
'rw-rule': {
'security-flavor': 'sys' if not readonly else 'never'
},
'super-user-security': {
'security-flavor': 'sys'
},
}
self.send_request('export-rule-modify', api_args)
@na_utils.trace
def _get_nfs_export_rule_indices(self, policy_name, rule):
api_args = {
'query': {
'export-rule-info': {
'policy-name': policy_name,
'client-match': rule,
},
},
'desired-attributes': {
'export-rule-info': {
'vserver-name': None,
'policy-name': None,
'client-match': None,
'rule-index': None,
},
},
}
result = self.send_request('export-rule-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
export_rule_info_list = attributes_list.get_children()
rule_indices = [int(export_rule_info.get_child_content('rule-index'))
for export_rule_info in export_rule_info_list]
rule_indices.sort()
return [six.text_type(rule_index) for rule_index in rule_indices]
@na_utils.trace
def remove_nfs_export_rule(self, policy_name, rule):
rule_indices = self._get_nfs_export_rule_indices(policy_name, rule)
self._remove_nfs_export_rules(policy_name, rule_indices)
@na_utils.trace
def _remove_nfs_export_rules(self, policy_name, rule_indices):
for rule_index in rule_indices:
api_args = {
'policy-name': policy_name,
'rule-index': rule_index
}
try:
self.send_request('export-rule-destroy', api_args)
except netapp_api.NaApiError as e:
if e.code != netapp_api.EOBJECTNOTFOUND:
raise
@na_utils.trace
def clear_nfs_export_policy_for_volume(self, volume_name):
self.set_nfs_export_policy_for_volume(volume_name, 'default')
@na_utils.trace
def set_nfs_export_policy_for_volume(self, volume_name, policy_name):
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': volume_name,
},
},
},
'attributes': {
'volume-attributes': {
'volume-export-attributes': {
'policy': policy_name,
},
},
},
}
self.send_request('volume-modify-iter', api_args)
@na_utils.trace
def get_nfs_export_policy_for_volume(self, volume_name):
"""Get the name of the export policy for a volume."""
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'name': volume_name,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-export-attributes': {
'policy': None,
},
},
},
}
result = self.send_request('volume-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
volume_attributes = attributes_list.get_child_by_name(
'volume-attributes') or netapp_api.NaElement('none')
volume_export_attributes = volume_attributes.get_child_by_name(
'volume-export-attributes') or netapp_api.NaElement('none')
export_policy = volume_export_attributes.get_child_content('policy')
if not export_policy:
msg = _('Could not find export policy for volume %s.')
raise exception.NetAppException(msg % volume_name)
return export_policy
@na_utils.trace
def create_nfs_export_policy(self, policy_name):
api_args = {'policy-name': policy_name}
try:
self.send_request('export-policy-create', api_args)
except netapp_api.NaApiError as e:
if e.code != netapp_api.EDUPLICATEENTRY:
raise
@na_utils.trace
def soft_delete_nfs_export_policy(self, policy_name):
try:
self.delete_nfs_export_policy(policy_name)
except netapp_api.NaApiError:
# NOTE(cknight): Policy deletion can fail if called too soon after
# removing from a flexvol. So rename for later harvesting.
self.rename_nfs_export_policy(policy_name,
DELETED_PREFIX + policy_name)
@na_utils.trace
def delete_nfs_export_policy(self, policy_name):
api_args = {'policy-name': policy_name}
try:
self.send_request('export-policy-destroy', api_args)
except netapp_api.NaApiError as e:
if e.code == netapp_api.EOBJECTNOTFOUND:
return
raise
@na_utils.trace
def rename_nfs_export_policy(self, policy_name, new_policy_name):
api_args = {
'policy-name': policy_name,
'new-policy-name': new_policy_name
}
self.send_request('export-policy-rename', api_args)
@na_utils.trace
def prune_deleted_nfs_export_policies(self):
deleted_policy_map = self._get_deleted_nfs_export_policies()
for vserver in deleted_policy_map:
client = copy.deepcopy(self)
client.set_vserver(vserver)
for policy in deleted_policy_map[vserver]:
try:
client.delete_nfs_export_policy(policy)
except netapp_api.NaApiError:
LOG.debug('Could not delete export policy %s.' % policy)
@na_utils.trace
def _get_deleted_nfs_export_policies(self):
api_args = {
'query': {
'export-policy-info': {
'policy-name': DELETED_PREFIX + '*',
},
},
'desired-attributes': {
'export-policy-info': {
'policy-name': None,
'vserver': None,
},
},
}
result = self.send_request('export-policy-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
policy_map = {}
for export_info in attributes_list.get_children():
vserver = export_info.get_child_content('vserver')
policies = policy_map.get(vserver, [])
policies.append(export_info.get_child_content('policy-name'))
policy_map[vserver] = policies
return policy_map
@na_utils.trace
def _get_ems_log_destination_vserver(self):
"""Returns the best vserver destination for EMS messages."""
major, minor = self.get_ontapi_version(cached=True)
if (major > 1) or (major == 1 and minor > 15):
# Prefer admin Vserver (requires cluster credentials).
admin_vservers = self.list_vservers(vserver_type='admin')
if admin_vservers:
return admin_vservers[0]
# Fall back to data Vserver.
data_vservers = self.list_vservers(vserver_type='data')
if data_vservers:
return data_vservers[0]
# If older API version, or no other Vservers found, use node Vserver.
node_vservers = self.list_vservers(vserver_type='node')
if node_vservers:
return node_vservers[0]
raise exception.NotFound("No Vserver found to receive EMS messages.")
@na_utils.trace
def send_ems_log_message(self, message_dict):
"""Sends a message to the Data ONTAP EMS log."""
node_client = copy.deepcopy(self)
node_client.connection.set_timeout(25)
try:
node_client.set_vserver(self._get_ems_log_destination_vserver())
node_client.send_request('ems-autosupport-log', message_dict)
LOG.debug('EMS executed successfully.')
except netapp_api.NaApiError as e:
LOG.warning(_LW('Failed to invoke EMS. %s') % e)
@na_utils.trace
def get_aggregate_raid_types(self, aggregate_names):
"""Get the RAID type of one or more aggregates."""
desired_attributes = {
'aggr-attributes': {
'aggregate-name': None,
'aggr-raid-attributes': {
'raid-type': None,
},
},
}
aggr_list = self._get_aggregates(aggregate_names=aggregate_names,
desired_attributes=desired_attributes)
aggr_raid_dict = {}
for aggr in aggr_list:
aggr_name = aggr.get_child_content('aggregate-name')
aggr_raid_attrs = aggr.get_child_by_name('aggr-raid-attributes')
aggr_raid_dict[aggr_name] = aggr_raid_attrs.get_child_content(
'raid-type')
return aggr_raid_dict
@na_utils.trace
def get_aggregate_disk_types(self, aggregate_names):
"""Get the disk type of one or more aggregates."""
aggr_disk_type_dict = {}
for aggregate_name in aggregate_names:
# Only get 1 disk, since apart from hybrid aggregates all disks
# must be the same type.
api_args = {
'max-records': 1,
'query': {
'storage-disk-info': {
'disk-raid-info': {
'disk-aggregate-info': {
'aggregate-name': aggregate_name,
},
},
},
},
'desired-attributes': {
'storage-disk-info': {
'disk-raid-info': {
'effective-disk-type': None,
},
},
},
}
result = self.send_request('storage-disk-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
storage_disk_info_list = attributes_list.get_children()
if len(storage_disk_info_list) >= 1:
storage_disk_info = storage_disk_info_list[0]
disk_raid_info = storage_disk_info.get_child_by_name(
'disk-raid-info')
if disk_raid_info:
disk_type = disk_raid_info.get_child_content(
'effective-disk-type')
if disk_type:
aggr_disk_type_dict[aggregate_name] = disk_type
return aggr_disk_type_dict
@na_utils.trace
def check_for_cluster_credentials(self):
try:
self.list_cluster_nodes()
# API succeeded, so definitely a cluster management LIF
return True
except netapp_api.NaApiError as e:
if e.code == netapp_api.EAPINOTFOUND:
LOG.debug('Not connected to cluster management LIF.')
return False
else:
raise e