# Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2016 Mike Rooney. All rights reserved. # Copyright (c) 2017 Jose Porrua. 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 import re from oslo_log import log as logging from oslo_utils import strutils from oslo_utils import units import six from cinder import exception from cinder.i18n import _ 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 from cinder.volume import volume_utils LOG = logging.getLogger(__name__) DEFAULT_MAX_PAGE_LENGTH = 50 ONTAP_SELECT_MODEL = 'FDvM300' ONTAP_C190 = 'C190' @six.add_metaclass(volume_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) ontap_version = self.get_ontap_version(cached=False) self.connection.set_ontap_version(ontap_version) self._init_features() def _init_features(self): super(Client, self)._init_features() ontapi_version = self.get_ontapi_version() # major, minor ontapi_1_20 = ontapi_version >= (1, 20) ontapi_1_2x = (1, 20) <= ontapi_version < (1, 30) ontapi_1_30 = ontapi_version >= (1, 30) ontapi_1_100 = ontapi_version >= (1, 100) ontapi_1_1xx = (1, 100) <= ontapi_version < (1, 200) ontapi_1_60 = ontapi_version >= (1, 160) ontapi_1_40 = ontapi_version >= (1, 140) ontapi_1_50 = ontapi_version >= (1, 150) nodes_info = self._get_cluster_nodes_info() for node in nodes_info: qos_min_block = False qos_min_nfs = False if node['model'] == ONTAP_SELECT_MODEL: qos_min_block = node['is_all_flash_select'] and ontapi_1_60 qos_min_nfs = qos_min_block elif ONTAP_C190 in node['model']: qos_min_block = node['is_all_flash'] and ontapi_1_60 qos_min_nfs = qos_min_block else: qos_min_block = node['is_all_flash'] and ontapi_1_20 qos_min_nfs = node['is_all_flash'] and ontapi_1_30 qos_name = na_utils.qos_min_feature_name(True, node['name']) self.features.add_feature(qos_name, supported=qos_min_nfs) qos_name = na_utils.qos_min_feature_name(False, node['name']) self.features.add_feature(qos_name, supported=qos_min_block) self.features.add_feature('SNAPMIRROR_V2', supported=ontapi_1_20) self.features.add_feature('USER_CAPABILITY_LIST', supported=ontapi_1_20) self.features.add_feature('SYSTEM_METRICS', supported=ontapi_1_2x) self.features.add_feature('CLONE_SPLIT_STATUS', supported=ontapi_1_30) self.features.add_feature('FAST_CLONE_DELETE', supported=ontapi_1_30) self.features.add_feature('SYSTEM_CONSTITUENT_METRICS', supported=ontapi_1_30) self.features.add_feature('ADVANCED_DISK_PARTITIONING', supported=ontapi_1_30) self.features.add_feature('BACKUP_CLONE_PARAM', supported=ontapi_1_100) self.features.add_feature('CLUSTER_PEER_POLICY', supported=ontapi_1_30) self.features.add_feature('FLEXVOL_ENCRYPTION', supported=ontapi_1_1xx) self.features.add_feature('ADAPTIVE_QOS', supported=ontapi_1_40) self.features.add_feature('ADAPTIVE_QOS_BLOCK_SIZE', supported=ontapi_1_50) self.features.add_feature('ADAPTIVE_QOS_EXPECTED_IOPS_ALLOCATION', supported=ontapi_1_50) LOG.info('Reported ONTAPI Version: %(major)s.%(minor)s', {'major': ontapi_version[0], 'minor': ontapi_version[1]}) 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 _get_record_count(self, api_result_element): try: return int(api_result_element.get_child_content('num-records')) except TypeError: msg = _('Missing record count for NetApp iterator API invocation.') raise na_utils.NetAppDriverException(msg) def set_vserver(self, vserver): self.vserver = vserver self.connection.set_vserver(vserver) def send_iter_request(self, api_name, api_args=None, enable_tunneling=True, max_page_length=DEFAULT_MAX_PAGE_LENGTH): """Invoke an iterator-style getter API.""" if not api_args: api_args = {} api_args['max-records'] = max_page_length # Get first page result = self.connection.send_request( api_name, api_args, enable_tunneling=enable_tunneling) # Most commonly, we can just return here if there is no more data next_tag = result.get_child_content('next-tag') if not next_tag: return result # Ensure pagination data is valid and prepare to store remaining pages num_records = self._get_record_count(result) attributes_list = result.get_child_by_name('attributes-list') if not attributes_list: msg = _('Missing attributes list for API %s.') % api_name raise na_utils.NetAppDriverException(msg) # Get remaining pages, saving data into first page while next_tag is not None: next_api_args = copy.deepcopy(api_args) next_api_args['tag'] = next_tag next_result = self.connection.send_request( api_name, next_api_args, enable_tunneling=enable_tunneling) next_attributes_list = next_result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') for record in next_attributes_list.get_children(): attributes_list.add_child_elem(record) num_records += self._get_record_count(next_result) next_tag = next_result.get_child_content('next-tag') result.get_child_by_name('num-records').set_content( six.text_type(num_records)) result.get_child_by_name('next-tag').set_content('') return result def _get_cluster_nodes_info(self): """Return a list of models of the nodes in the cluster""" api_args = { 'desired-attributes': { 'node-details-info': { 'node': None, 'node-model': None, 'is-all-flash-select-optimized': None, 'is-all-flash-optimized': None, } } } nodes = [] try: result = self.send_iter_request('system-node-get-iter', api_args, enable_tunneling=False) system_node_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') for system_node in system_node_list.get_children(): node = { 'model': system_node.get_child_content('node-model'), 'name': system_node.get_child_content('node'), 'is_all_flash': system_node.get_child_content( 'is-all-flash-optimized') == 'true', 'is_all_flash_select': system_node.get_child_content( 'is-all-flash-select-optimized') == 'true', } nodes.append(node) except netapp_api.NaApiError as e: if e.code == netapp_api.EAPINOTFOUND: LOG.debug('Cluster nodes can only be collected with ' 'cluster scoped credentials.') else: LOG.exception('Failed to get the cluster nodes.') return nodes 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_iter_request('vserver-get-iter', api_args, enable_tunneling=False) 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()] 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.") def send_ems_log_message(self, message_dict): """Sends a message to the Data ONTAP EMS log.""" # NOTE(cknight): Cannot use deepcopy on the connection context node_client = copy.copy(self) node_client.connection = copy.copy(self.connection) node_client.connection.set_timeout(25) try: node_client.set_vserver(self._get_ems_log_destination_vserver()) node_client.connection.send_request('ems-autosupport-log', message_dict) LOG.debug('EMS executed successfully.') except netapp_api.NaApiError as e: LOG.warning('Failed to invoke EMS. %s', e) 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 set_iscsi_chap_authentication(self, iqn, username, password): """Provides NetApp host's CHAP credentials to the backend.""" initiator_exists = self.check_iscsi_initiator_exists(iqn) command_template = ('iscsi security %(mode)s -vserver %(vserver)s ' '-initiator-name %(iqn)s -auth-type CHAP ' '-user-name %(username)s') if initiator_exists: LOG.debug('Updating CHAP authentication for %(iqn)s.', {'iqn': iqn}) command = command_template % { 'mode': 'modify', 'vserver': self.vserver, 'iqn': iqn, 'username': username, } else: LOG.debug('Adding initiator %(iqn)s with CHAP authentication.', {'iqn': iqn}) command = command_template % { 'mode': 'create', 'vserver': self.vserver, 'iqn': iqn, 'username': username, } try: with self.ssh_client.ssh_connect_semaphore: ssh_pool = self.ssh_client.ssh_pool with ssh_pool.item() as ssh: self.ssh_client.execute_command_with_prompt(ssh, command, 'Password:', password) except Exception as e: msg = _('Failed to set CHAP authentication for target IQN %(iqn)s.' ' Details: %(ex)s') % { 'iqn': iqn, 'ex': e, } LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) def check_iscsi_initiator_exists(self, iqn): """Returns True if initiator exists.""" initiator_exists = True try: auth_list = netapp_api.NaElement('iscsi-initiator-get-auth') auth_list.add_new_child('initiator', iqn) self.connection.invoke_successfully(auth_list, True) except netapp_api.NaApiError: initiator_exists = False return initiator_exists 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 _validate_qos_policy_group(self, is_adaptive, spec=None, qos_min_support=False): if is_adaptive and not self.features.ADAPTIVE_QOS: msg = _("Adaptive QoS feature requires ONTAP 9.4 or later.") raise na_utils.NetAppDriverException(msg) if not spec: return qos_spec_support = [ {'key': 'min_throughput', 'support': qos_min_support, 'reason': _('is not supported by this back end.')}, {'key': 'block_size', 'support': self.features.ADAPTIVE_QOS_BLOCK_SIZE, 'reason': _('requires ONTAP >= 9.5.')}, {'key': 'expected_iops_allocation', 'support': self.features.ADAPTIVE_QOS_EXPECTED_IOPS_ALLOCATION, 'reason': _('requires ONTAP >= 9.5.')}, ] for feature in qos_spec_support: if feature['key'] in spec and not feature['support']: msg = '%(key)s %(reason)s' raise na_utils.NetAppDriverException(msg % { 'key': feature['key'], 'reason': feature['reason']}) 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, source_snapshot=None, is_snapshot=False, qos_policy_group_is_adaptive=False): self._validate_qos_policy_group(qos_policy_group_is_adaptive) # ONTAP handles only 128 MB per call as of v9.1 bc_limit = 2 ** 18 # 2^18 blocks * 512 bytes/block = 128 MB z_calls = int(math.ceil(block_count / float(bc_limit))) zbc = block_count if z_calls == 0: z_calls = 1 for _call in range(0, z_calls): if zbc > bc_limit: block_count = bc_limit zbc -= bc_limit else: block_count = zbc zapi_args = { 'volume': volume, 'source-path': name, 'destination-path': new_name, 'space-reserve': space_reserved, } if source_snapshot: zapi_args['snapshot-name'] = source_snapshot if is_snapshot and self.features.BACKUP_CLONE_PARAM: zapi_args['is-backup'] = 'true' clone_create = netapp_api.NaElement.create_node_with_children( 'clone-create', **zapi_args) if qos_policy_group_name is not None: child_name = 'qos-%spolicy-group-name' % ( 'adaptive-' if qos_policy_group_is_adaptive else '') clone_create.add_new_child(child_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(int(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, qos_policy_group_is_adaptive, file_path): """Assigns the named QoS policy-group to a file.""" self._validate_qos_policy_group(qos_policy_group_is_adaptive) qos_arg_name = "qos-%spolicy-group-name" % ( "adaptive-" if qos_policy_group_is_adaptive else "") api_args = { 'volume': flex_vol, qos_arg_name: qos_policy_group_name, 'file': file_path, 'vserver': self.vserver, } return self.connection.send_request('file-assign-qos', api_args, False) def provision_qos_policy_group(self, qos_policy_group_info, qos_min_support): """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: return spec = qos_policy_group_info.get('spec') if not spec: return is_adaptive = na_utils.is_qos_policy_group_spec_adaptive( qos_policy_group_info) self._validate_qos_policy_group(is_adaptive, spec=spec, qos_min_support=qos_min_support) if is_adaptive: if not self.qos_policy_group_exists(spec['policy_name'], is_adaptive=True): self.qos_adaptive_policy_group_create(spec) else: self.qos_adaptive_policy_group_modify(spec) else: if not self.qos_policy_group_exists(spec['policy_name']): self.qos_policy_group_create(spec) else: self.qos_policy_group_modify(spec) def qos_policy_group_exists(self, qos_policy_group_name, is_adaptive=False): """Checks if a QOS policy group exists.""" query_name = 'qos-%spolicy-group-info' % ( 'adaptive-' if is_adaptive else '') request_name = 'qos-%spolicy-group-get-iter' % ( 'adaptive-' if is_adaptive else '') api_args = { 'query': { query_name: { 'policy-group': qos_policy_group_name, }, }, 'desired-attributes': { query_name: { 'policy-group': None, }, }, } result = self.connection.send_request(request_name, api_args, False) return self._has_records(result) def _qos_spec_to_api_args(self, spec, **kwargs): """Convert a QoS spec to ZAPI args.""" formatted_spec = {k.replace('_', '-'): v for k, v in spec.items() if v} formatted_spec['policy-group'] = formatted_spec.pop('policy-name') formatted_spec = {**formatted_spec, **kwargs} return formatted_spec def qos_policy_group_create(self, spec): """Creates a QOS policy group.""" api_args = self._qos_spec_to_api_args( spec, vserver=self.vserver) return self.connection.send_request( 'qos-policy-group-create', api_args, False) def qos_adaptive_policy_group_create(self, spec): """Creates a QOS adaptive policy group.""" api_args = self._qos_spec_to_api_args( spec, vserver=self.vserver) return self.connection.send_request( 'qos-adaptive-policy-group-create', api_args, False) def qos_policy_group_modify(self, spec): """Modifies a QOS policy group.""" api_args = self._qos_spec_to_api_args(spec) return self.connection.send_request( 'qos-policy-group-modify', api_args, False) def qos_adaptive_policy_group_modify(self, spec): """Modifies a QOS adaptive policy group.""" api_args = self._qos_spec_to_api_args(spec) return self.connection.send_request( 'qos-adaptive-policy-group-modify', api_args, False) def qos_policy_group_rename(self, qos_policy_group_name, new_name, is_adaptive=False): """Renames a QOS policy group.""" request_name = 'qos-%spolicy-group-rename' % ( 'adaptive-' if is_adaptive else '') api_args = { 'policy-group-name': qos_policy_group_name, 'new-name': new_name, } return self.connection.send_request(request_name, api_args, False) def mark_qos_policy_group_for_deletion(self, qos_policy_group_info, is_adaptive=False): """Soft delete a QOS policy group backing 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: current_name = spec['policy_name'] new_name = client_base.DELETED_PREFIX + current_name try: self.qos_policy_group_rename(current_name, new_name, is_adaptive) except netapp_api.NaApiError as ex: LOG.warning('Rename failure in cleanup of cDOT QOS policy ' 'group %(name)s: %(ex)s', {'name': current_name, 'ex': ex}) # Attempt to delete any QoS policies named "delete-openstack-*". self.remove_unused_qos_policy_groups() def _send_qos_policy_group_delete_iter_request(self, is_adaptive=False): request_name = 'qos-%spolicy-group-delete-iter' % ( 'adaptive-' if is_adaptive else '') query_name = 'qos-%spolicy-group-info' % ( 'adaptive-' if is_adaptive else '') api_args = { 'query': { query_name: { 'policy-group': '%s*' % client_base.DELETED_PREFIX, 'vserver': self.vserver, } }, 'max-records': 3500, 'continue-on-failure': 'true', 'return-success-list': 'false', 'return-failure-list': 'false', } try: self.connection.send_request(request_name, api_args, False) except netapp_api.NaApiError as ex: msg = ('Could not delete QOS %(prefix)spolicy groups. ' 'Details: %(ex)s') msg_args = { 'prefix': 'adaptive ' if is_adaptive else '', 'ex': ex, } LOG.debug(msg, msg_args) def remove_unused_qos_policy_groups(self): """Deletes all QOS policy groups that are marked for deletion.""" self._send_qos_policy_group_delete_iter_request() if self.features.ADAPTIVE_QOS: self._send_qos_policy_group_delete_iter_request(is_adaptive=True) def set_lun_qos_policy_group(self, path, qos_policy_group, is_adaptive=False): """Sets qos_policy_group on a LUN.""" self._validate_qos_policy_group(is_adaptive) policy_group_key = 'qos-%spolicy-group' % ( 'adaptive-' if is_adaptive else '') api_args = { 'path': path, policy_group_key: qos_policy_group, } return self.connection.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': volume_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, source_snapshot=None, is_snapshot=False): """Clones file on vserver.""" LOG.debug("Cloning with params volume %(volume)s, src %(src_path)s, " "dest %(dest_path)s, vserver %(vserver)s," "source_snapshot %(source_snapshot)s", {'volume': flex_vol, 'src_path': src_path, 'dest_path': dest_path, 'vserver': vserver, 'source_snapshot': source_snapshot}) zapi_args = { 'volume': flex_vol, 'source-path': src_path, 'destination-path': dest_path, } if is_snapshot and self.features.BACKUP_CLONE_PARAM: zapi_args['is-backup'] = 'true' if source_snapshot: zapi_args['snapshot-name'] = source_snapshot clone_create = netapp_api.NaElement.create_node_with_children( 'clone-create', **zapi_args) 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 check_cluster_api(self, object_name, operation_name, api): """Checks the availability of a cluster API. Returns True if the specified cluster API exists and may be called by the current user. The API is *called* on Data ONTAP versions prior to 8.2, while versions starting with 8.2 utilize an API designed for this purpose. """ if not self.features.USER_CAPABILITY_LIST: return self._check_cluster_api_legacy(api) else: return self._check_cluster_api(object_name, operation_name, api) def _check_cluster_api(self, object_name, operation_name, api): """Checks the availability of a cluster API. Returns True if the specified cluster API exists and may be called by the current user. This method assumes Data ONTAP 8.2 or higher. """ api_args = { 'query': { 'capability-info': { 'object-name': object_name, 'operation-list': { 'operation-info': { 'name': operation_name, }, }, }, }, 'desired-attributes': { 'capability-info': { 'operation-list': { 'operation-info': { 'api-name': None, }, }, }, }, } result = self.connection.send_request( 'system-user-capability-get-iter', api_args, False) if not self._has_records(result): return False capability_info_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') for capability_info in capability_info_list.get_children(): operation_list = capability_info.get_child_by_name( 'operation-list') or netapp_api.NaElement('none') for operation_info in operation_list.get_children(): api_name = operation_info.get_child_content('api-name') or '' api_names = api_name.split(',') if api in api_names: return True return False def _check_cluster_api_legacy(self, api): """Checks the availability of a cluster API. Returns True if the specified cluster API exists and may be called by the current user. This method should only be used for Data ONTAP 8.1, and only getter APIs may be tested because the API is actually called to perform the check. """ if not re.match(".*-get$|.*-get-iter$|.*-list-info$", api): raise ValueError(_('Non-getter API passed to API test method.')) try: self.connection.send_request(api, enable_tunneling=False) except netapp_api.NaApiError as ex: if ex.code in (netapp_api.EAPIPRIVILEGE, netapp_api.EAPINOTFOUND): return False return True def get_operational_lif_addresses(self): """Gets the IP addresses of operational LIFs on the vserver.""" net_interface_get_iter_args = { 'query': { 'net-interface-info': { 'operational-status': 'up' } }, 'desired-attributes': { 'net-interface-info': { 'address': None, } } } result = self.send_iter_request('net-interface-get-iter', net_interface_get_iter_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=None, flexvol_name=None): """Gets total capacity and free capacity, in bytes, of the flexvol.""" volume_id_attributes = {} if flexvol_path: volume_id_attributes['junction-path'] = flexvol_path if flexvol_name: volume_id_attributes['name'] = flexvol_name api_args = { 'query': { 'volume-attributes': { 'volume-id-attributes': volume_id_attributes, } }, 'desired-attributes': { 'volume-attributes': { 'volume-space-attributes': { 'size-available': None, 'size-total': None, } } }, } result = self.send_iter_request('volume-get-iter', api_args) if self._get_record_count(result) != 1: msg = _('Volume %s not found.') msg_args = flexvol_path or flexvol_name raise na_utils.NetAppDriverException(msg % msg_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_total, 'size-available': size_available, } def list_flexvols(self): """Returns the names of the flexvols on the controller.""" api_args = { 'query': { 'volume-attributes': { 'volume-id-attributes': { 'type': 'rw', 'style': 'flex', }, 'volume-state-attributes': { 'is-vserver-root': 'false', 'is-inconsistent': 'false', 'is-invalid': 'false', 'state': 'online', }, }, }, 'desired-attributes': { 'volume-attributes': { 'volume-id-attributes': { 'name': None, }, }, }, } result = self.send_iter_request('volume-get-iter', api_args) if not self._has_records(result): return [] volumes = [] attributes_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') for volume_attributes in attributes_list.get_children(): volume_id_attributes = volume_attributes.get_child_by_name( 'volume-id-attributes') or netapp_api.NaElement('none') volumes.append(volume_id_attributes.get_child_content('name')) return volumes def get_flexvol(self, flexvol_path=None, flexvol_name=None): """Get flexvol attributes needed for the storage service catalog.""" volume_id_attributes = {'type': 'rw', 'style': 'flex'} if flexvol_path: volume_id_attributes['junction-path'] = flexvol_path if flexvol_name: volume_id_attributes['name'] = flexvol_name api_args = { 'query': { 'volume-attributes': { 'volume-id-attributes': volume_id_attributes, 'volume-state-attributes': { 'is-vserver-root': 'false', 'is-inconsistent': 'false', 'is-invalid': 'false', 'state': 'online', }, }, }, 'desired-attributes': { 'volume-attributes': { 'volume-id-attributes': { 'name': None, 'owning-vserver-name': None, 'junction-path': None, 'containing-aggregate-name': None, 'type': None, }, 'volume-mirror-attributes': { 'is-data-protection-mirror': None, 'is-replica-volume': None, }, 'volume-space-attributes': { 'is-space-guarantee-enabled': None, 'space-guarantee': None, 'percentage-snapshot-reserve': None, 'size': None, }, 'volume-qos-attributes': { 'policy-group-name': None, }, 'volume-snapshot-attributes': { 'snapshot-policy': None, }, 'volume-language-attributes': { 'language-code': None, }, }, }, } result = self.send_iter_request('volume-get-iter', api_args) if self._get_record_count(result) != 1: msg = _('Could not find unique volume %(vol)s.') msg_args = {'vol': flexvol_name} raise exception.VolumeBackendAPIException(data=msg % msg_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') volume_space_attributes = volume_attributes.get_child_by_name( 'volume-space-attributes') or netapp_api.NaElement('none') volume_qos_attributes = volume_attributes.get_child_by_name( 'volume-qos-attributes') or netapp_api.NaElement('none') volume_snapshot_attributes = volume_attributes.get_child_by_name( 'volume-snapshot-attributes') or netapp_api.NaElement('none') volume_language_attributes = volume_attributes.get_child_by_name( 'volume-language-attributes') or netapp_api.NaElement('none') volume = { 'name': volume_id_attributes.get_child_content('name'), 'vserver': volume_id_attributes.get_child_content( 'owning-vserver-name'), 'junction-path': volume_id_attributes.get_child_content( 'junction-path'), 'aggregate': volume_id_attributes.get_child_content( 'containing-aggregate-name'), 'type': volume_id_attributes.get_child_content('type'), 'space-guarantee-enabled': strutils.bool_from_string( volume_space_attributes.get_child_content( 'is-space-guarantee-enabled')), 'space-guarantee': volume_space_attributes.get_child_content( 'space-guarantee'), 'percentage-snapshot-reserve': ( volume_space_attributes.get_child_content( 'percentage-snapshot-reserve')), 'size': volume_space_attributes.get_child_content('size'), 'qos-policy-group': volume_qos_attributes.get_child_content( 'policy-group-name'), 'snapshot-policy': volume_snapshot_attributes.get_child_content( 'snapshot-policy'), 'language': volume_language_attributes.get_child_content( 'language-code'), } return volume def get_flexvol_dedupe_info(self, flexvol_name): """Get dedupe attributes needed for the storage service catalog.""" api_args = { 'query': { 'sis-status-info': { 'path': '/vol/%s' % flexvol_name, }, }, 'desired-attributes': { 'sis-status-info': { 'state': None, 'is-compression-enabled': None, 'logical-data-size': None, 'logical-data-limit': None, }, }, } no_dedupe_response = { 'compression': False, 'dedupe': False, 'logical-data-size': 0, 'logical-data-limit': 1, } try: result = self.send_iter_request('sis-get-iter', api_args) except netapp_api.NaApiError: LOG.exception('Failed to get dedupe info for volume %s.', flexvol_name) return no_dedupe_response if self._get_record_count(result) != 1: return no_dedupe_response attributes_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') sis_status_info = attributes_list.get_child_by_name( 'sis-status-info') or netapp_api.NaElement('none') logical_data_size = sis_status_info.get_child_content( 'logical-data-size') or 0 logical_data_limit = sis_status_info.get_child_content( 'logical-data-limit') or 1 sis = { 'compression': strutils.bool_from_string( sis_status_info.get_child_content('is-compression-enabled')), 'dedupe': na_utils.to_bool( sis_status_info.get_child_content('state')), 'logical-data-size': int(logical_data_size), 'logical-data-limit': int(logical_data_limit), } return sis def get_flexvol_dedupe_used_percent(self, flexvol_name): """Determine how close a flexvol is to its shared block limit.""" # Note(cknight): The value returned by this method is computed from # values returned by two different APIs, one of which was new in # Data ONTAP 8.3. if not self.features.CLONE_SPLIT_STATUS: return 0.0 dedupe_info = self.get_flexvol_dedupe_info(flexvol_name) clone_split_info = self.get_clone_split_info(flexvol_name) total_dedupe_blocks = (dedupe_info.get('logical-data-size') + clone_split_info.get('unsplit-size')) dedupe_used_percent = (100.0 * float(total_dedupe_blocks) / dedupe_info.get('logical-data-limit')) return dedupe_used_percent def get_clone_split_info(self, flexvol_name): """Get the status of unsplit file/LUN clones in a flexvol.""" try: result = self.connection.send_request( 'clone-split-status', {'volume-name': flexvol_name}) except netapp_api.NaApiError: LOG.exception('Failed to get clone split info for volume %s.', flexvol_name) return {'unsplit-size': 0, 'unsplit-clone-count': 0} clone_split_info = result.get_child_by_name( 'clone-split-info') or netapp_api.NaElement('none') unsplit_size = clone_split_info.get_child_content('unsplit-size') or 0 unsplit_clone_count = clone_split_info.get_child_content( 'unsplit-clone-count') or 0 return { 'unsplit-size': int(unsplit_size), 'unsplit-clone-count': int(unsplit_clone_count), } def is_flexvol_mirrored(self, flexvol_name, vserver_name): """Check if flexvol is a SnapMirror source.""" api_args = { 'query': { 'snapmirror-info': { 'source-vserver': vserver_name, 'source-volume': flexvol_name, 'mirror-state': 'snapmirrored', 'relationship-type': 'data_protection', }, }, 'desired-attributes': { 'snapmirror-info': None, }, } try: result = self.send_iter_request('snapmirror-get-iter', api_args) except netapp_api.NaApiError: LOG.exception('Failed to get SnapMirror info for volume %s.', flexvol_name) return False if not self._has_records(result): return False return True def is_flexvol_encrypted(self, flexvol_name, vserver_name): """Check if a flexvol is encrypted.""" if not self.features.FLEXVOL_ENCRYPTION: return False api_args = { 'query': { 'volume-attributes': { 'encrypt': 'true', 'volume-id-attributes': { 'name': flexvol_name, 'owning-vserver-name': vserver_name, }, }, }, 'desired-attributes': { 'volume-attributes': { 'encrypt': None, }, }, } try: result = self.send_iter_request('volume-get-iter', api_args) except netapp_api.NaApiError: LOG.exception('Failed to get Encryption info for volume %s.', flexvol_name) return False if not self._has_records(result): return False return True def is_qos_min_supported(self, is_nfs, node_name): """Check if the node supports QoS minimum.""" qos_min_name = na_utils.qos_min_feature_name(is_nfs, node_name) return getattr(self.features, qos_min_name, False).__bool__() def create_flexvol(self, flexvol_name, aggregate_name, size_gb, space_guarantee_type=None, snapshot_policy=None, language=None, dedupe_enabled=False, compression_enabled=False, snapshot_reserve=None, volume_type='rw'): """Creates a volume.""" api_args = { 'containing-aggr-name': aggregate_name, 'size': six.text_type(size_gb) + 'g', 'volume': flexvol_name, 'volume-type': volume_type, } if volume_type == 'dp': snapshot_policy = None else: api_args['junction-path'] = '/%s' % flexvol_name if snapshot_policy is not None: api_args['snapshot-policy'] = snapshot_policy if space_guarantee_type: api_args['space-reserve'] = space_guarantee_type if language is not None: api_args['language-code'] = language if snapshot_reserve is not None: api_args['percentage-snapshot-reserve'] = six.text_type( snapshot_reserve) self.connection.send_request('volume-create', api_args) # cDOT compression requires that deduplication be enabled. if dedupe_enabled or compression_enabled: self.enable_flexvol_dedupe(flexvol_name) if compression_enabled: self.enable_flexvol_compression(flexvol_name) def flexvol_exists(self, volume_name): """Checks if a flexvol exists on the storage array.""" 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_iter_request('volume-get-iter', api_args) return self._has_records(result) def rename_flexvol(self, orig_flexvol_name, new_flexvol_name): """Set flexvol name.""" api_args = { 'volume': orig_flexvol_name, 'new-volume-name': new_flexvol_name, } self.connection.send_request('volume-rename', api_args) def mount_flexvol(self, flexvol_name, junction_path=None): """Mounts a volume on a junction path.""" api_args = { 'volume-name': flexvol_name, 'junction-path': (junction_path if junction_path else '/%s' % flexvol_name) } self.connection.send_request('volume-mount', api_args) def enable_flexvol_dedupe(self, flexvol_name): """Enable deduplication on volume.""" api_args = {'path': '/vol/%s' % flexvol_name} self.connection.send_request('sis-enable', api_args) def disable_flexvol_dedupe(self, flexvol_name): """Disable deduplication on volume.""" api_args = {'path': '/vol/%s' % flexvol_name} self.connection.send_request('sis-disable', api_args) def enable_flexvol_compression(self, flexvol_name): """Enable compression on volume.""" api_args = { 'path': '/vol/%s' % flexvol_name, 'enable-compression': 'true' } self.connection.send_request('sis-set-config', api_args) def disable_flexvol_compression(self, flexvol_name): """Disable compression on volume.""" api_args = { 'path': '/vol/%s' % flexvol_name, 'enable-compression': 'false' } self.connection.send_request('sis-set-config', api_args) @volume_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.connection.send_request('file-delete-file', api_args, True) 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.connection.send_request('aggr-get-iter', api_args, enable_tunneling=False) if not self._has_records(result): return [] else: return result.get_child_by_name('attributes-list').get_children() def get_node_for_aggregate(self, aggregate_name): """Get home node for the specified aggregate. This API could return None, most notably if it was sent to a Vserver LIF, so the caller must be able to handle that case. """ if not aggregate_name: return None desired_attributes = { 'aggr-attributes': { 'aggregate-name': None, 'aggr-ownership-attributes': { 'home-name': None, }, }, } try: aggrs = self._get_aggregates(aggregate_names=[aggregate_name], desired_attributes=desired_attributes) except netapp_api.NaApiError as e: if e.code == netapp_api.EAPINOTFOUND: return None else: raise if len(aggrs) < 1: return None aggr_ownership_attrs = aggrs[0].get_child_by_name( 'aggr-ownership-attributes') or netapp_api.NaElement('none') return aggr_ownership_attrs.get_child_content('home-name') def get_aggregate(self, aggregate_name): """Get aggregate attributes needed for the storage service catalog.""" if not aggregate_name: return {} desired_attributes = { 'aggr-attributes': { 'aggregate-name': None, 'aggr-raid-attributes': { 'raid-type': None, 'is-hybrid': None, }, 'aggr-ownership-attributes': { 'home-name': None, }, }, } try: aggrs = self._get_aggregates(aggregate_names=[aggregate_name], desired_attributes=desired_attributes) except netapp_api.NaApiError: LOG.exception('Failed to get info for aggregate %s.', aggregate_name) return {} if len(aggrs) < 1: return {} aggr_attributes = aggrs[0] aggr_raid_attrs = aggr_attributes.get_child_by_name( 'aggr-raid-attributes') or netapp_api.NaElement('none') aggr_ownership_attrs = aggrs[0].get_child_by_name( 'aggr-ownership-attributes') or netapp_api.NaElement('none') aggregate = { 'name': aggr_attributes.get_child_content('aggregate-name'), 'raid-type': aggr_raid_attrs.get_child_content('raid-type'), 'is-hybrid': strutils.bool_from_string( aggr_raid_attrs.get_child_content('is-hybrid')), 'node-name': aggr_ownership_attrs.get_child_content('home-name'), } return aggregate def get_aggregate_disk_types(self, aggregate_name): """Get the disk type(s) of an aggregate.""" disk_types = set() disk_types.update(self._get_aggregate_disk_types(aggregate_name)) if self.features.ADVANCED_DISK_PARTITIONING: disk_types.update(self._get_aggregate_disk_types(aggregate_name, shared=True)) return list(disk_types) if disk_types else None def _get_aggregate_disk_types(self, aggregate_name, shared=False): """Get the disk type(s) of an aggregate (may be a list).""" disk_types = set() if shared: disk_raid_info = { 'disk-shared-info': { 'aggregate-list': { 'shared-aggregate-info': { 'aggregate-name': aggregate_name, }, }, }, } else: disk_raid_info = { 'disk-aggregate-info': { 'aggregate-name': aggregate_name, }, } api_args = { 'query': { 'storage-disk-info': { 'disk-raid-info': disk_raid_info, }, }, 'desired-attributes': { 'storage-disk-info': { 'disk-raid-info': { 'effective-disk-type': None, }, }, }, } try: result = self.send_iter_request( 'storage-disk-get-iter', api_args, enable_tunneling=False) except netapp_api.NaApiError: LOG.exception('Failed to get disk info for aggregate %s.', aggregate_name) return disk_types attributes_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') for storage_disk_info in attributes_list.get_children(): disk_raid_info = storage_disk_info.get_child_by_name( 'disk-raid-info') or netapp_api.NaElement('none') disk_type = disk_raid_info.get_child_content( 'effective-disk-type') if disk_type: disk_types.add(disk_type) return disk_types def get_aggregate_capacities(self, aggregate_names): """Gets capacity info for multiple aggregates.""" if not isinstance(aggregate_names, list): return {} aggregates = {} for aggregate_name in aggregate_names: aggregates[aggregate_name] = self.get_aggregate_capacity( aggregate_name) return aggregates def get_aggregate_capacity(self, aggregate_name): """Gets capacity info for an aggregate.""" desired_attributes = { 'aggr-attributes': { 'aggr-space-attributes': { 'percent-used-capacity': None, 'size-available': None, 'size-total': None, }, }, } try: aggrs = self._get_aggregates(aggregate_names=[aggregate_name], desired_attributes=desired_attributes) except netapp_api.NaApiError as e: if e.code == netapp_api.EAPINOTFOUND: LOG.debug('Aggregate capacity can only be collected with ' 'cluster scoped credentials.') else: LOG.exception('Failed to get info for aggregate %s.', aggregate_name) return {} if len(aggrs) < 1: return {} aggr_attributes = aggrs[0] aggr_space_attributes = aggr_attributes.get_child_by_name( 'aggr-space-attributes') or netapp_api.NaElement('none') percent_used = int(aggr_space_attributes.get_child_content( 'percent-used-capacity')) size_available = float(aggr_space_attributes.get_child_content( 'size-available')) size_total = float( aggr_space_attributes.get_child_content('size-total')) return { 'percent-used': percent_used, 'size-available': size_available, 'size-total': size_total, } def get_performance_instance_uuids(self, object_name, node_name): """Get UUIDs of performance instances for a cluster node.""" api_args = { 'objectname': object_name, 'query': { 'instance-info': { 'uuid': node_name + ':*', } } } result = self.connection.send_request( 'perf-object-instance-list-info-iter', api_args, enable_tunneling=False) uuids = [] instances = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('None') for instance_info in instances.get_children(): uuids.append(instance_info.get_child_content('uuid')) return uuids def get_performance_counters(self, object_name, instance_uuids, counter_names): """Gets more cDOT performance counters.""" api_args = { 'objectname': object_name, 'instance-uuids': [ {'instance-uuid': instance_uuid} for instance_uuid in instance_uuids ], 'counters': [ {'counter': counter} for counter in counter_names ], } result = self.connection.send_request( 'perf-object-get-instances', api_args, enable_tunneling=False) counter_data = [] timestamp = result.get_child_content('timestamp') instances = result.get_child_by_name( 'instances') or netapp_api.NaElement('None') for instance in instances.get_children(): instance_name = instance.get_child_content('name') instance_uuid = instance.get_child_content('uuid') node_name = instance_uuid.split(':')[0] counters = instance.get_child_by_name( 'counters') or netapp_api.NaElement('None') for counter in counters.get_children(): counter_name = counter.get_child_content('name') counter_value = counter.get_child_content('value') counter_data.append({ 'instance-name': instance_name, 'instance-uuid': instance_uuid, 'node-name': node_name, 'timestamp': timestamp, counter_name: counter_value, }) return counter_data def get_snapshots_marked_for_deletion(self): """Get a list of snapshots marked for deletion.""" api_args = { 'query': { 'snapshot-info': { 'name': client_base.DELETED_PREFIX + '*', 'vserver': self.vserver, 'busy': 'false', }, }, 'desired-attributes': { 'snapshot-info': { 'name': None, 'volume': None, 'snapshot-instance-uuid': None, } }, } result = self.connection.send_request('snapshot-get-iter', api_args) snapshots = [] attributes = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') snapshot_info_list = attributes.get_children() for snapshot_info in snapshot_info_list: snapshot_name = snapshot_info.get_child_content('name') snapshot_id = snapshot_info.get_child_content( 'snapshot-instance-uuid') snapshot_volume = snapshot_info.get_child_content('volume') snapshots.append({ 'name': snapshot_name, 'instance_id': snapshot_id, 'volume_name': snapshot_volume, }) return snapshots 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.connection.send_request('snapshot-get-iter', api_args) self._handle_get_snapshot_return_failure(result, snapshot_name) attributes_list = result.get_child_by_name( 'attributes-list') or netapp_api.NaElement('none') snapshot_info_list = attributes_list.get_children() self._handle_snapshot_not_found(result, snapshot_info_list, snapshot_name, volume_name) 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 def _handle_get_snapshot_return_failure(self, result, snapshot_name): 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(data=msg % msg_args) else: raise exception.VolumeBackendAPIException(data=msg % msg_args) def _handle_snapshot_not_found(self, result, snapshot_info_list, snapshot_name, volume_name): if not self._has_records(result): raise exception.SnapshotNotFound(snapshot_id=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.VolumeBackendAPIException(data=msg % msg_args) def create_cluster_peer(self, addresses, username=None, password=None, passphrase=None): """Creates a cluster peer relationship.""" api_args = { 'peer-addresses': [ {'remote-inet-address': address} for address in addresses ], } if username: api_args['user-name'] = username if password: api_args['password'] = password if passphrase: api_args['passphrase'] = passphrase self.connection.send_request('cluster-peer-create', api_args) def get_cluster_peers(self, remote_cluster_name=None): """Gets one or more cluster peer relationships.""" api_args = {} if remote_cluster_name: api_args['query'] = { 'cluster-peer-info': { 'remote-cluster-name': remote_cluster_name, } } result = self.send_iter_request('cluster-peer-get-iter', api_args) if not self._has_records(result): return [] cluster_peers = [] for cluster_peer_info in result.get_child_by_name( 'attributes-list').get_children(): cluster_peer = { 'active-addresses': [], 'peer-addresses': [] } active_addresses = cluster_peer_info.get_child_by_name( 'active-addresses') or netapp_api.NaElement('none') for address in active_addresses.get_children(): cluster_peer['active-addresses'].append(address.get_content()) peer_addresses = cluster_peer_info.get_child_by_name( 'peer-addresses') or netapp_api.NaElement('none') for address in peer_addresses.get_children(): cluster_peer['peer-addresses'].append(address.get_content()) cluster_peer['availability'] = cluster_peer_info.get_child_content( 'availability') cluster_peer['cluster-name'] = cluster_peer_info.get_child_content( 'cluster-name') cluster_peer['cluster-uuid'] = cluster_peer_info.get_child_content( 'cluster-uuid') cluster_peer['remote-cluster-name'] = ( cluster_peer_info.get_child_content('remote-cluster-name')) cluster_peer['serial-number'] = ( cluster_peer_info.get_child_content('serial-number')) cluster_peer['timeout'] = cluster_peer_info.get_child_content( 'timeout') cluster_peers.append(cluster_peer) return cluster_peers def delete_cluster_peer(self, cluster_name): """Deletes a cluster peer relationship.""" api_args = {'cluster-name': cluster_name} self.connection.send_request('cluster-peer-delete', api_args) def get_cluster_peer_policy(self): """Gets the cluster peering policy configuration.""" if not self.features.CLUSTER_PEER_POLICY: return {} result = self.connection.send_request('cluster-peer-policy-get') attributes = result.get_child_by_name( 'attributes') or netapp_api.NaElement('none') cluster_peer_policy = attributes.get_child_by_name( 'cluster-peer-policy') or netapp_api.NaElement('none') policy = { 'is-unauthenticated-access-permitted': cluster_peer_policy.get_child_content( 'is-unauthenticated-access-permitted'), 'passphrase-minimum-length': cluster_peer_policy.get_child_content( 'passphrase-minimum-length'), } if policy['is-unauthenticated-access-permitted'] is not None: policy['is-unauthenticated-access-permitted'] = ( strutils.bool_from_string( policy['is-unauthenticated-access-permitted'])) if policy['passphrase-minimum-length'] is not None: policy['passphrase-minimum-length'] = int( policy['passphrase-minimum-length']) return policy def set_cluster_peer_policy(self, is_unauthenticated_access_permitted=None, passphrase_minimum_length=None): """Modifies the cluster peering policy configuration.""" if not self.features.CLUSTER_PEER_POLICY: return if (is_unauthenticated_access_permitted is None and passphrase_minimum_length is None): return api_args = {} if is_unauthenticated_access_permitted is not None: api_args['is-unauthenticated-access-permitted'] = ( 'true' if strutils.bool_from_string( is_unauthenticated_access_permitted) else 'false') if passphrase_minimum_length is not None: api_args['passphrase-minlength'] = six.text_type( passphrase_minimum_length) self.connection.send_request('cluster-peer-policy-modify', api_args) def create_vserver_peer(self, vserver_name, peer_vserver_name): """Creates a Vserver peer relationship for SnapMirrors.""" api_args = { 'vserver': vserver_name, 'peer-vserver': peer_vserver_name, 'applications': [ {'vserver-peer-application': 'snapmirror'}, ], } self.connection.send_request('vserver-peer-create', api_args) def delete_vserver_peer(self, vserver_name, peer_vserver_name): """Deletes a Vserver peer relationship.""" api_args = {'vserver': vserver_name, 'peer-vserver': peer_vserver_name} self.connection.send_request('vserver-peer-delete', api_args) def accept_vserver_peer(self, vserver_name, peer_vserver_name): """Accepts a pending Vserver peer relationship.""" api_args = {'vserver': vserver_name, 'peer-vserver': peer_vserver_name} self.connection.send_request('vserver-peer-accept', api_args) def get_vserver_peers(self, vserver_name=None, peer_vserver_name=None): """Gets one or more Vserver peer relationships.""" api_args = None if vserver_name or peer_vserver_name: api_args = {'query': {'vserver-peer-info': {}}} if vserver_name: api_args['query']['vserver-peer-info']['vserver'] = ( vserver_name) if peer_vserver_name: api_args['query']['vserver-peer-info']['peer-vserver'] = ( peer_vserver_name) result = self.send_iter_request('vserver-peer-get-iter', api_args) if not self._has_records(result): return [] vserver_peers = [] for vserver_peer_info in result.get_child_by_name( 'attributes-list').get_children(): vserver_peer = { 'vserver': vserver_peer_info.get_child_content('vserver'), 'peer-vserver': vserver_peer_info.get_child_content('peer-vserver'), 'peer-state': vserver_peer_info.get_child_content('peer-state'), 'peer-cluster': vserver_peer_info.get_child_content('peer-cluster'), } vserver_peers.append(vserver_peer) return vserver_peers def _ensure_snapmirror_v2(self): """Verify support for SnapMirror control plane v2.""" if not self.features.SNAPMIRROR_V2: msg = _('SnapMirror features require Data ONTAP 8.2 or later.') raise na_utils.NetAppDriverException(msg) def create_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume, schedule=None, policy=None, relationship_type='data_protection'): """Creates a SnapMirror relationship (cDOT 8.2 or later only).""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, 'relationship-type': relationship_type, } if schedule: api_args['schedule'] = schedule if policy: api_args['policy'] = policy try: self.connection.send_request('snapmirror-create', api_args) except netapp_api.NaApiError as e: if e.code != netapp_api.ERELATION_EXISTS: raise def initialize_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume, source_snapshot=None, transfer_priority=None): """Initializes a SnapMirror relationship (cDOT 8.2 or later only).""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } if source_snapshot: api_args['source-snapshot'] = source_snapshot if transfer_priority: api_args['transfer-priority'] = transfer_priority result = self.connection.send_request('snapmirror-initialize', api_args) result_info = {} result_info['operation-id'] = result.get_child_content( 'result-operation-id') result_info['status'] = result.get_child_content('result-status') result_info['jobid'] = result.get_child_content('result-jobid') result_info['error-code'] = result.get_child_content( 'result-error-code') result_info['error-message'] = result.get_child_content( 'result-error-message') return result_info def release_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume, relationship_info_only=False): """Removes a SnapMirror relationship on the source endpoint.""" self._ensure_snapmirror_v2() api_args = { 'query': { 'snapmirror-destination-info': { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, 'relationship-info-only': ('true' if relationship_info_only else 'false'), } } } self.connection.send_request('snapmirror-release-iter', api_args) def quiesce_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Disables future transfers to a SnapMirror destination.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } self.connection.send_request('snapmirror-quiesce', api_args) def abort_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume, clear_checkpoint=False): """Stops ongoing transfers for a SnapMirror relationship.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, 'clear-checkpoint': 'true' if clear_checkpoint else 'false', } try: self.connection.send_request('snapmirror-abort', api_args) except netapp_api.NaApiError as e: if e.code != netapp_api.ENOTRANSFER_IN_PROGRESS: raise def break_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Breaks a data protection SnapMirror relationship.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } self.connection.send_request('snapmirror-break', api_args) def modify_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume, schedule=None, policy=None, tries=None, max_transfer_rate=None): """Modifies a SnapMirror relationship.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } if schedule: api_args['schedule'] = schedule if policy: api_args['policy'] = policy if tries is not None: api_args['tries'] = tries if max_transfer_rate is not None: api_args['max-transfer-rate'] = max_transfer_rate self.connection.send_request('snapmirror-modify', api_args) def delete_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Destroys a SnapMirror relationship.""" self._ensure_snapmirror_v2() api_args = { 'query': { 'snapmirror-info': { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } } } self.connection.send_request('snapmirror-destroy-iter', api_args) def update_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Schedules a SnapMirror update.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } try: self.connection.send_request('snapmirror-update', api_args) except netapp_api.NaApiError as e: if (e.code != netapp_api.ETRANSFER_IN_PROGRESS and e.code != netapp_api.EANOTHER_OP_ACTIVE): raise def resume_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Resume a SnapMirror relationship if it is quiesced.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } try: self.connection.send_request('snapmirror-resume', api_args) except netapp_api.NaApiError as e: if e.code != netapp_api.ERELATION_NOT_QUIESCED: raise def resync_snapmirror(self, source_vserver, source_volume, destination_vserver, destination_volume): """Resync a SnapMirror relationship.""" self._ensure_snapmirror_v2() api_args = { 'source-volume': source_volume, 'source-vserver': source_vserver, 'destination-volume': destination_volume, 'destination-vserver': destination_vserver, } self.connection.send_request('snapmirror-resync', api_args) def _get_snapmirrors(self, source_vserver=None, source_volume=None, destination_vserver=None, destination_volume=None, desired_attributes=None): query = None if (source_vserver or source_volume or destination_vserver or destination_volume): query = {'snapmirror-info': {}} if source_volume: query['snapmirror-info']['source-volume'] = source_volume if destination_volume: query['snapmirror-info']['destination-volume'] = ( destination_volume) if source_vserver: query['snapmirror-info']['source-vserver'] = source_vserver if destination_vserver: query['snapmirror-info']['destination-vserver'] = ( destination_vserver) api_args = {} if query: api_args['query'] = query if desired_attributes: api_args['desired-attributes'] = desired_attributes result = self.send_iter_request('snapmirror-get-iter', api_args) if not self._has_records(result): return [] else: return result.get_child_by_name('attributes-list').get_children() def get_snapmirrors(self, source_vserver, source_volume, destination_vserver, destination_volume, desired_attributes=None): """Gets one or more SnapMirror relationships. Either the source or destination info may be omitted. Desired attributes should be a flat list of attribute names. """ self._ensure_snapmirror_v2() if desired_attributes is not None: desired_attributes = { 'snapmirror-info': {attr: None for attr in desired_attributes}, } result = self._get_snapmirrors( source_vserver=source_vserver, source_volume=source_volume, destination_vserver=destination_vserver, destination_volume=destination_volume, desired_attributes=desired_attributes) snapmirrors = [] for snapmirror_info in result: snapmirror = {} for child in snapmirror_info.get_children(): name = self._strip_xml_namespace(child.get_name()) snapmirror[name] = child.get_content() snapmirrors.append(snapmirror) return snapmirrors def get_provisioning_options_from_flexvol(self, flexvol_name): """Get a dict of provisioning options matching existing flexvol.""" flexvol_info = self.get_flexvol(flexvol_name=flexvol_name) dedupe_info = self.get_flexvol_dedupe_info(flexvol_name) provisioning_opts = { 'aggregate': flexvol_info['aggregate'], # space-guarantee can be 'none', 'file', 'volume' 'space_guarantee_type': flexvol_info.get('space-guarantee'), 'snapshot_policy': flexvol_info['snapshot-policy'], 'language': flexvol_info['language'], 'dedupe_enabled': dedupe_info['dedupe'], 'compression_enabled': dedupe_info['compression'], 'snapshot_reserve': flexvol_info['percentage-snapshot-reserve'], 'volume_type': flexvol_info['type'], 'size': int(math.ceil(float(flexvol_info['size']) / units.Gi)), } return provisioning_opts