# Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 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 math from oslo_log import log as logging import six from cinder import exception from cinder.i18n import _, _LW from cinder import utils from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp import utils as na_utils LOG = logging.getLogger(__name__) DELETED_PREFIX = 'deleted_cinder_' @six.add_metaclass(utils.TraceWrapperMetaclass) class Client(client_base.Client): def __init__(self, **kwargs): super(Client, self).__init__(**kwargs) self.vserver = kwargs.get('vserver', None) 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) self._init_features() def _init_features(self): super(Client, self)._init_features() ontapi_version = self.get_ontapi_version() # major, minor ontapi_1_30 = ontapi_version >= (1, 30) self.features.add_feature('FAST_CLONE_DELETE', supported=ontapi_1_30) 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): num_records = api_result_element.get_child_content('num-records') return bool(num_records and '0' != num_records) def set_vserver(self, vserver): self.connection.set_vserver(vserver) def get_iscsi_target_details(self): """Gets the iSCSI target portal details.""" iscsi_if_iter = netapp_api.NaElement('iscsi-interface-get-iter') result = self.connection.invoke_successfully(iscsi_if_iter, True) tgt_list = [] num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: attr_list = result.get_child_by_name('attributes-list') iscsi_if_list = attr_list.get_children() for iscsi_if in iscsi_if_list: d = dict() d['address'] = iscsi_if.get_child_content('ip-address') d['port'] = iscsi_if.get_child_content('ip-port') d['tpgroup-tag'] = iscsi_if.get_child_content('tpgroup-tag') d['interface-enabled'] = iscsi_if.get_child_content( 'is-interface-enabled') tgt_list.append(d) return tgt_list def get_fc_target_wwpns(self): """Gets the FC target details.""" wwpns = [] port_name_list_api = netapp_api.NaElement('fcp-port-name-get-iter') port_name_list_api.add_new_child('max-records', '100') result = self.connection.invoke_successfully(port_name_list_api, True) num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: for port_name_info in result.get_child_by_name( 'attributes-list').get_children(): if port_name_info.get_child_content('is-used') != 'true': continue wwpn = port_name_info.get_child_content('port-name').lower() wwpns.append(wwpn) return wwpns def get_iscsi_service_details(self): """Returns iscsi iqn.""" iscsi_service_iter = netapp_api.NaElement('iscsi-service-get-iter') result = self.connection.invoke_successfully(iscsi_service_iter, True) if result.get_child_content('num-records') and\ int(result.get_child_content('num-records')) >= 1: attr_list = result.get_child_by_name('attributes-list') iscsi_service = attr_list.get_child_by_name('iscsi-service-info') return iscsi_service.get_child_content('node-name') LOG.debug('No iSCSI service found for vserver %s', self.vserver) return None def get_lun_list(self): """Gets the list of LUNs on filer. Gets the LUNs from cluster with vserver. """ luns = [] tag = None while True: api = netapp_api.NaElement('lun-get-iter') api.add_new_child('max-records', '100') if tag: api.add_new_child('tag', tag, True) lun_info = netapp_api.NaElement('lun-info') lun_info.add_new_child('vserver', self.vserver) query = netapp_api.NaElement('query') query.add_child_elem(lun_info) api.add_child_elem(query) result = self.connection.invoke_successfully(api, True) if result.get_child_by_name('num-records') and\ int(result.get_child_content('num-records')) >= 1: attr_list = result.get_child_by_name('attributes-list') luns.extend(attr_list.get_children()) tag = result.get_child_content('next-tag') if tag is None: break return luns def get_lun_map(self, path): """Gets the LUN map by LUN path.""" tag = None map_list = [] while True: lun_map_iter = netapp_api.NaElement('lun-map-get-iter') lun_map_iter.add_new_child('max-records', '100') if tag: lun_map_iter.add_new_child('tag', tag, True) query = netapp_api.NaElement('query') lun_map_iter.add_child_elem(query) query.add_node_with_children('lun-map-info', **{'path': path}) result = self.connection.invoke_successfully(lun_map_iter, True) tag = result.get_child_content('next-tag') if result.get_child_content('num-records') and \ int(result.get_child_content('num-records')) >= 1: attr_list = result.get_child_by_name('attributes-list') lun_maps = attr_list.get_children() for lun_map in lun_maps: lun_m = dict() lun_m['initiator-group'] = lun_map.get_child_content( 'initiator-group') lun_m['lun-id'] = lun_map.get_child_content('lun-id') lun_m['vserver'] = lun_map.get_child_content('vserver') map_list.append(lun_m) if tag is None: break return map_list def _get_igroup_by_initiator_query(self, initiator, tag): igroup_get_iter = netapp_api.NaElement('igroup-get-iter') igroup_get_iter.add_new_child('max-records', '100') if tag: igroup_get_iter.add_new_child('tag', tag, True) query = netapp_api.NaElement('query') igroup_info = netapp_api.NaElement('initiator-group-info') query.add_child_elem(igroup_info) igroup_info.add_new_child('vserver', self.vserver) initiators = netapp_api.NaElement('initiators') igroup_info.add_child_elem(initiators) igroup_get_iter.add_child_elem(query) initiators.add_node_with_children( 'initiator-info', **{'initiator-name': initiator}) # limit results to just the attributes of interest desired_attrs = netapp_api.NaElement('desired-attributes') desired_igroup_info = netapp_api.NaElement('initiator-group-info') desired_igroup_info.add_node_with_children( 'initiators', **{'initiator-info': None}) desired_igroup_info.add_new_child('vserver', None) desired_igroup_info.add_new_child('initiator-group-name', None) desired_igroup_info.add_new_child('initiator-group-type', None) desired_igroup_info.add_new_child('initiator-group-os-type', None) desired_attrs.add_child_elem(desired_igroup_info) igroup_get_iter.add_child_elem(desired_attrs) return igroup_get_iter def get_igroup_by_initiators(self, initiator_list): """Get igroups exactly matching a set of initiators.""" tag = None igroup_list = [] if not initiator_list: return igroup_list initiator_set = set(initiator_list) while True: # C-mode getter APIs can't do an 'and' query, so match the first # initiator (which will greatly narrow the search results) and # filter the rest in this method. query = self._get_igroup_by_initiator_query(initiator_list[0], tag) result = self.connection.invoke_successfully(query, True) tag = result.get_child_content('next-tag') num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: for igroup_info in result.get_child_by_name( 'attributes-list').get_children(): initiator_set_for_igroup = set() for initiator_info in igroup_info.get_child_by_name( 'initiators').get_children(): initiator_set_for_igroup.add( initiator_info.get_child_content('initiator-name')) if initiator_set == initiator_set_for_igroup: igroup = {'initiator-group-os-type': igroup_info.get_child_content( 'initiator-group-os-type'), 'initiator-group-type': igroup_info.get_child_content( 'initiator-group-type'), 'initiator-group-name': igroup_info.get_child_content( 'initiator-group-name')} igroup_list.append(igroup) if tag is None: break return igroup_list def clone_lun(self, volume, name, new_name, space_reserved='true', qos_policy_group_name=None, src_block=0, dest_block=0, block_count=0): # zAPI can only handle 2^24 blocks per range bc_limit = 2 ** 24 # 8GB # zAPI can only handle 32 block ranges per call br_limit = 32 z_limit = br_limit * bc_limit # 256 GB z_calls = int(math.ceil(block_count / float(z_limit))) zbc = block_count if z_calls == 0: z_calls = 1 for _call in range(0, z_calls): if zbc > z_limit: block_count = z_limit zbc -= z_limit else: block_count = zbc clone_create = netapp_api.NaElement.create_node_with_children( 'clone-create', **{'volume': volume, 'source-path': name, 'destination-path': new_name, 'space-reserve': space_reserved}) if qos_policy_group_name is not None: clone_create.add_new_child('qos-policy-group-name', qos_policy_group_name) if block_count > 0: block_ranges = netapp_api.NaElement("block-ranges") segments = int(math.ceil(block_count / float(bc_limit))) bc = block_count for _segment in range(0, segments): if bc > bc_limit: block_count = bc_limit bc -= bc_limit else: block_count = bc block_range =\ netapp_api.NaElement.create_node_with_children( 'block-range', **{'source-block-number': six.text_type(src_block), 'destination-block-number': six.text_type(dest_block), 'block-count': six.text_type(block_count)}) block_ranges.add_child_elem(block_range) src_block += int(block_count) dest_block += int(block_count) clone_create.add_child_elem(block_ranges) self.connection.invoke_successfully(clone_create, True) def get_lun_by_args(self, **args): """Retrieves LUN with specified args.""" lun_iter = netapp_api.NaElement('lun-get-iter') lun_iter.add_new_child('max-records', '100') query = netapp_api.NaElement('query') lun_iter.add_child_elem(query) query.add_node_with_children('lun-info', **args) luns = self.connection.invoke_successfully(lun_iter, True) attr_list = luns.get_child_by_name('attributes-list') if not attr_list: return [] return attr_list.get_children() def file_assign_qos(self, flex_vol, qos_policy_group_name, file_path): """Assigns the named QoS policy-group to a file.""" api_args = { 'volume': flex_vol, 'qos-policy-group-name': qos_policy_group_name, 'file': file_path, 'vserver': self.vserver, } return self.send_request('file-assign-qos', api_args, False) def provision_qos_policy_group(self, qos_policy_group_info): """Create QOS policy group on the backend if appropriate.""" if qos_policy_group_info is None: return # Legacy QOS uses externally provisioned QOS policy group, # so we don't need to create one on the backend. legacy = qos_policy_group_info.get('legacy') if legacy is not None: return spec = qos_policy_group_info.get('spec') if spec is not None: if not self.qos_policy_group_exists(spec['policy_name']): self.qos_policy_group_create(spec['policy_name'], spec['max_throughput']) else: self.qos_policy_group_modify(spec['policy_name'], spec['max_throughput']) def qos_policy_group_exists(self, qos_policy_group_name): """Checks if a QOS policy group exists.""" api_args = { 'query': { 'qos-policy-group-info': { 'policy-group': qos_policy_group_name, }, }, 'desired-attributes': { 'qos-policy-group-info': { 'policy-group': None, }, }, } result = self.send_request('qos-policy-group-get-iter', api_args, False) return self._has_records(result) def qos_policy_group_create(self, qos_policy_group_name, max_throughput): """Creates a QOS policy group.""" api_args = { 'policy-group': qos_policy_group_name, 'max-throughput': max_throughput, 'vserver': self.vserver, } return self.send_request('qos-policy-group-create', api_args, False) def qos_policy_group_modify(self, qos_policy_group_name, max_throughput): """Modifies a QOS policy group.""" api_args = { 'policy-group': qos_policy_group_name, 'max-throughput': max_throughput, } return self.send_request('qos-policy-group-modify', api_args, False) def qos_policy_group_delete(self, qos_policy_group_name): """Attempts to delete a QOS policy group.""" api_args = {'policy-group': qos_policy_group_name} return self.send_request('qos-policy-group-delete', api_args, False) def qos_policy_group_rename(self, qos_policy_group_name, new_name): """Renames a QOS policy group.""" api_args = { 'policy-group-name': qos_policy_group_name, 'new-name': new_name, } return self.send_request('qos-policy-group-rename', api_args, False) def mark_qos_policy_group_for_deletion(self, qos_policy_group_info): """Do (soft) delete of backing QOS policy group for a cinder volume.""" if qos_policy_group_info is None: return spec = qos_policy_group_info.get('spec') # For cDOT we want to delete the QoS policy group that we created for # this cinder volume. Because the QoS policy may still be "in use" # after the zapi call to delete the volume itself returns successfully, # we instead rename the QoS policy group using a specific pattern and # later attempt on a best effort basis to delete any QoS policy groups # matching that pattern. if spec is not None: current_name = spec['policy_name'] new_name = DELETED_PREFIX + current_name try: self.qos_policy_group_rename(current_name, new_name) except netapp_api.NaApiError as ex: msg = _LW('Rename failure in cleanup of cDOT QOS policy group ' '%(name)s: %(ex)s') LOG.warning(msg, {'name': current_name, 'ex': ex}) # Attempt to delete any QoS policies named "delete-openstack-*". self.remove_unused_qos_policy_groups() def remove_unused_qos_policy_groups(self): """Deletes all QOS policy groups that are marked for deletion.""" api_args = { 'query': { 'qos-policy-group-info': { 'policy-group': '%s*' % DELETED_PREFIX, 'vserver': self.vserver, } }, 'max-records': 3500, 'continue-on-failure': 'true', 'return-success-list': 'false', 'return-failure-list': 'false', } try: self.send_request('qos-policy-group-delete-iter', api_args, False) except netapp_api.NaApiError as ex: msg = 'Could not delete QOS policy groups. Details: %(ex)s' msg_args = {'ex': ex} LOG.debug(msg % msg_args) def set_lun_qos_policy_group(self, path, qos_policy_group): """Sets qos_policy_group on a LUN.""" api_args = { 'path': path, 'qos-policy-group': qos_policy_group, } return self.send_request('lun-set-qos-policy-group', api_args) def get_if_info_by_ip(self, ip): """Gets the network interface info by ip.""" net_if_iter = netapp_api.NaElement('net-interface-get-iter') net_if_iter.add_new_child('max-records', '10') query = netapp_api.NaElement('query') net_if_iter.add_child_elem(query) query.add_node_with_children( 'net-interface-info', **{'address': na_utils.resolve_hostname(ip)}) result = self.connection.invoke_successfully(net_if_iter, True) num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: attr_list = result.get_child_by_name('attributes-list') return attr_list.get_children() raise exception.NotFound( _('No interface found on cluster for ip %s') % ip) def get_vol_by_junc_vserver(self, vserver, junction): """Gets the volume by junction path and vserver.""" vol_iter = netapp_api.NaElement('volume-get-iter') vol_iter.add_new_child('max-records', '10') query = netapp_api.NaElement('query') vol_iter.add_child_elem(query) vol_attrs = netapp_api.NaElement('volume-attributes') query.add_child_elem(vol_attrs) vol_attrs.add_node_with_children( 'volume-id-attributes', **{'junction-path': junction, 'owning-vserver-name': vserver}) des_attrs = netapp_api.NaElement('desired-attributes') des_attrs.add_node_with_children('volume-attributes', **{'volume-id-attributes': None}) vol_iter.add_child_elem(des_attrs) result = self._invoke_vserver_api(vol_iter, vserver) num_records = result.get_child_content('num-records') if num_records and int(num_records) >= 1: attr_list = result.get_child_by_name('attributes-list') vols = attr_list.get_children() vol_id = vols[0].get_child_by_name('volume-id-attributes') return vol_id.get_child_content('name') msg_fmt = {'vserver': vserver, 'junction': junction} raise exception.NotFound(_("No volume on cluster with vserver " "%(vserver)s and junction path " "%(junction)s ") % msg_fmt) def clone_file(self, flex_vol, src_path, dest_path, vserver, dest_exists=False): """Clones file on vserver.""" LOG.debug("Cloning with params volume %(volume)s, src %(src_path)s, " "dest %(dest_path)s, vserver %(vserver)s", {'volume': flex_vol, 'src_path': src_path, 'dest_path': dest_path, 'vserver': vserver}) clone_create = netapp_api.NaElement.create_node_with_children( 'clone-create', **{'volume': flex_vol, 'source-path': src_path, 'destination-path': dest_path}) major, minor = self.connection.get_api_version() if major == 1 and minor >= 20 and dest_exists: clone_create.add_new_child('destination-exists', 'true') self._invoke_vserver_api(clone_create, vserver) def get_file_usage(self, path, vserver): """Gets the file unique bytes.""" LOG.debug('Getting file usage for %s', path) file_use = netapp_api.NaElement.create_node_with_children( 'file-usage-get', **{'path': path}) res = self._invoke_vserver_api(file_use, vserver) unique_bytes = res.get_child_content('unique-bytes') LOG.debug('file-usage for path %(path)s is %(bytes)s', {'path': path, 'bytes': unique_bytes}) return unique_bytes def get_vserver_ips(self, vserver): """Get ips for the vserver.""" result = netapp_api.invoke_api( self.connection, api_name='net-interface-get-iter', is_iter=True, tunnel=vserver) if_list = [] for res in result: records = res.get_child_content('num-records') if records > 0: attr_list = res['attributes-list'] ifs = attr_list.get_children() if_list.extend(ifs) return if_list def check_apis_on_cluster(self, api_list=None): """Checks API availability and permissions on cluster. Checks API availability and permissions for executing user. Returns a list of failed apis. """ api_list = api_list or [] failed_apis = [] if api_list: api_version = self.connection.get_api_version() if api_version: major, minor = api_version if major == 1 and minor < 20: for api_name in api_list: na_el = netapp_api.NaElement(api_name) try: self.connection.invoke_successfully(na_el) except Exception as e: if isinstance(e, netapp_api.NaApiError): if (e.code == netapp_api.NaErrors ['API_NOT_FOUND'].code or e.code == netapp_api.NaErrors ['INSUFFICIENT_PRIVS'].code): failed_apis.append(api_name) elif major == 1 and minor >= 20: failed_apis = copy.copy(api_list) result = netapp_api.invoke_api( self.connection, api_name='system-user-capability-get-iter', api_family='cm', additional_elems=None, is_iter=True) for res in result: attr_list = res.get_child_by_name('attributes-list') if attr_list: capabilities = attr_list.get_children() for capability in capabilities: op_list = capability.get_child_by_name( 'operation-list') if op_list: ops = op_list.get_children() for op in ops: apis = op.get_child_content( 'api-name') if apis: api_list = apis.split(',') for api_name in api_list: if (api_name and api_name.strip() in failed_apis): failed_apis.remove( api_name) else: continue else: msg = _("Unsupported Clustered Data ONTAP version.") raise exception.VolumeBackendAPIException(data=msg) else: msg = _("Data ONTAP API version could not be determined.") raise exception.VolumeBackendAPIException(data=msg) return failed_apis def get_operational_network_interface_addresses(self): """Gets the IP addresses of operational LIFs on the vserver.""" api_args = { 'query': { 'net-interface-info': { 'operational-status': 'up' } }, 'desired-attributes': { 'net-interface-info': { 'address': 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('address') for lif_info in lif_info_list.get_children()] def get_flexvol_capacity(self, flexvol_path): """Gets total capacity and free capacity, in bytes, of the flexvol.""" api_args = { 'query': { 'volume-attributes': { 'volume-id-attributes': { 'junction-path': flexvol_path } } }, 'desired-attributes': { 'volume-attributes': { 'volume-space-attributes': { 'size-available': None, 'size-total': None, } } }, } result = self.send_request('volume-get-iter', api_args) attributes_list = result.get_child_by_name('attributes-list') volume_attributes = attributes_list.get_child_by_name( 'volume-attributes') volume_space_attributes = volume_attributes.get_child_by_name( 'volume-space-attributes') size_available = float( volume_space_attributes.get_child_content('size-available')) size_total = float( volume_space_attributes.get_child_content('size-total')) return size_total, size_available @utils.trace_method def delete_file(self, path_to_file): """Delete file at path.""" api_args = { 'path': path_to_file, } # Use fast clone deletion engine if it is supported. if self.features.FAST_CLONE_DELETE: api_args['is-clone-file'] = 'true' self.send_request('file-delete-file', api_args, True)