# Copyright (c) 2012 NetApp, Inc. All rights reserved. # Copyright (c) 2014 Ben Swartzlander. All rights reserved. # Copyright (c) 2014 Navneet Singh. All rights reserved. # Copyright (c) 2014 Clinton Knight. All rights reserved. # Copyright (c) 2014 Alex Meade. All rights reserved. # Copyright (c) 2014 Andrew Kerr. All rights reserved. # Copyright (c) 2014 Jeff Applewhite. All rights reserved. # Copyright (c) 2015 Tom Barron. All rights reserved. # Copyright (c) 2015 Goutham Pacha Ravi. All rights reserved. # Copyright (c) 2016 Mike Rooney. 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. """ Volume driver library for NetApp 7-mode block storage systems. """ from oslo_log import log as logging from oslo_log import versionutils from oslo_utils import timeutils from oslo_utils import units import six from cinder import exception from cinder.i18n import _, _LW from cinder import utils from cinder.volume import configuration from cinder.volume.drivers.netapp.dataontap import block_base from cinder.volume.drivers.netapp.dataontap.client import client_7mode from cinder.volume.drivers.netapp.dataontap.performance import perf_7mode from cinder.volume.drivers.netapp import options as na_opts from cinder.volume.drivers.netapp import utils as na_utils LOG = logging.getLogger(__name__) @six.add_metaclass(utils.TraceWrapperMetaclass) class NetAppBlockStorage7modeLibrary(block_base.NetAppBlockStorageLibrary): """NetApp block storage library for Data ONTAP (7-mode).""" def __init__(self, driver_name, driver_protocol, **kwargs): super(NetAppBlockStorage7modeLibrary, self).__init__(driver_name, driver_protocol, **kwargs) self.configuration.append_config_values(na_opts.netapp_7mode_opts) self.driver_mode = '7mode' def do_setup(self, context): super(NetAppBlockStorage7modeLibrary, self).do_setup(context) self.volume_list = [] self.vfiler = self.configuration.netapp_vfiler self.zapi_client = client_7mode.Client( self.volume_list, transport_type=self.configuration.netapp_transport_type, username=self.configuration.netapp_login, password=self.configuration.netapp_password, hostname=self.configuration.netapp_server_hostname, port=self.configuration.netapp_server_port, vfiler=self.vfiler) self._do_partner_setup() self.vol_refresh_time = None self.vol_refresh_interval = 1800 self.vol_refresh_running = False self.vol_refresh_voluntary = False self.root_volume_name = self._get_root_volume_name() self.perf_library = perf_7mode.Performance7modeLibrary( self.zapi_client) # This driver has been marked 'deprecated' in the Ocata release and # can be removed in Queens. msg = _("The 7-mode Data ONTAP driver is deprecated and will be " "removed in a future release.") versionutils.report_deprecated_feature(LOG, msg) def _do_partner_setup(self): partner_backend = self.configuration.netapp_partner_backend_name if partner_backend: config = configuration.Configuration(na_opts.netapp_7mode_opts, partner_backend) config.append_config_values(na_opts.netapp_connection_opts) config.append_config_values(na_opts.netapp_basicauth_opts) config.append_config_values(na_opts.netapp_transport_opts) self.partner_zapi_client = client_7mode.Client( None, transport_type=config.netapp_transport_type, username=config.netapp_login, password=config.netapp_password, hostname=config.netapp_server_hostname, port=config.netapp_server_port, vfiler=None) def check_for_setup_error(self): """Check that the driver is working and can communicate.""" api_version = self.zapi_client.get_ontapi_version() if api_version: major, minor = api_version if major == 1 and minor < 9: msg = _("Unsupported Data ONTAP version." " Data ONTAP version 7.3.1 and above is supported.") raise exception.VolumeBackendAPIException(data=msg) else: msg = _("API version could not be determined.") raise exception.VolumeBackendAPIException(data=msg) self._refresh_volume_info() if not self.volume_list: msg = _('No pools are available for provisioning volumes. ' 'Ensure that the configuration option ' 'netapp_pool_name_search_pattern is set correctly.') raise exception.NetAppDriverException(msg) self._add_looping_tasks() super(NetAppBlockStorage7modeLibrary, self).check_for_setup_error() def _add_looping_tasks(self): """Add tasks that need to be executed at a fixed interval.""" super(NetAppBlockStorage7modeLibrary, self)._add_looping_tasks() def _get_volume_model_update(self, volume): """Provide any updates necessary for a volume being created/managed.""" def _create_lun(self, volume_name, lun_name, size, metadata, qos_policy_group_name=None): """Creates a LUN, handling Data ONTAP differences as needed.""" if qos_policy_group_name is not None: msg = _('Data ONTAP operating in 7-Mode does not support QoS ' 'policy groups.') raise exception.VolumeDriverException(msg) self.zapi_client.create_lun( volume_name, lun_name, size, metadata, qos_policy_group_name) self.vol_refresh_voluntary = True def _get_root_volume_name(self): # switch to volume-get-root-name API when possible vols = self.zapi_client.get_filer_volumes() for vol in vols: volume_name = vol.get_child_content('name') if self._get_vol_option(volume_name, 'root') == 'true': return volume_name LOG.warning(_LW('Could not determine root volume name ' 'on %s.'), self._get_owner()) return None def _get_owner(self): if self.vfiler: owner = '%s:%s' % (self.configuration.netapp_server_hostname, self.vfiler) else: owner = self.configuration.netapp_server_hostname return owner def _create_lun_handle(self, metadata): """Returns LUN handle based on filer type.""" owner = self._get_owner() return '%s:%s' % (owner, metadata['Path']) def _find_mapped_lun_igroup(self, path, initiator_list): """Find an igroup for a LUN mapped to the given initiator(s).""" initiator_set = set(initiator_list) result = self.zapi_client.get_lun_map(path) initiator_groups = result.get_child_by_name('initiator-groups') if initiator_groups: for initiator_group_info in initiator_groups.get_children(): initiator_set_for_igroup = set() for initiator_info in initiator_group_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_info.get_child_content( 'initiator-group-name') lun_id = initiator_group_info.get_child_content( 'lun-id') return igroup, lun_id return None, None def _has_luns_mapped_to_initiators(self, initiator_list, include_partner=True): """Checks whether any LUNs are mapped to the given initiator(s).""" if self.zapi_client.has_luns_mapped_to_initiators(initiator_list): return True if include_partner and self.partner_zapi_client and \ self.partner_zapi_client.has_luns_mapped_to_initiators( initiator_list): return True return False def _clone_lun(self, name, new_name, space_reserved=None, qos_policy_group_name=None, src_block=0, dest_block=0, block_count=0, source_snapshot=None, is_snapshot=False): """Clone LUN with the given handle to the new name. :param: is_snapshot Not used, present for method signature consistency """ if not space_reserved: space_reserved = self.lun_space_reservation if qos_policy_group_name is not None: msg = _('Data ONTAP operating in 7-Mode does not support QoS ' 'policy groups.') raise exception.VolumeDriverException(msg) metadata = self._get_lun_attr(name, 'metadata') path = metadata['Path'] (parent, _splitter, name) = path.rpartition('/') clone_path = '%s/%s' % (parent, new_name) self.zapi_client.clone_lun(path, clone_path, name, new_name, space_reserved, src_block=src_block, dest_block=dest_block, block_count=block_count, source_snapshot=source_snapshot) self.vol_refresh_voluntary = True luns = self.zapi_client.get_lun_by_args(path=clone_path) cloned_lun = luns[0] self.zapi_client.set_space_reserve(clone_path, space_reserved) clone_meta = self._create_lun_meta(cloned_lun) handle = self._create_lun_handle(clone_meta) self._add_lun_to_table( block_base.NetAppLun(handle, new_name, cloned_lun.get_child_content('size'), clone_meta)) def _create_lun_meta(self, lun): """Creates LUN metadata dictionary.""" self.zapi_client.check_is_naelement(lun) meta_dict = {} meta_dict['Path'] = lun.get_child_content('path') meta_dict['Volume'] = lun.get_child_content('path').split('/')[2] meta_dict['OsType'] = lun.get_child_content('multiprotocol-type') meta_dict['SpaceReserved'] = lun.get_child_content( 'is-space-reservation-enabled') meta_dict['UUID'] = lun.get_child_content('uuid') return meta_dict def _get_fc_target_wwpns(self, include_partner=True): wwpns = self.zapi_client.get_fc_target_wwpns() if include_partner and self.partner_zapi_client: wwpns.extend(self.partner_zapi_client.get_fc_target_wwpns()) return wwpns def _update_volume_stats(self, filter_function=None, goodness_function=None): """Retrieve stats info from filer.""" # ensure we get current data self.vol_refresh_voluntary = True self._refresh_volume_info() LOG.debug('Updating volume stats') data = {} backend_name = self.configuration.safe_get('volume_backend_name') data['volume_backend_name'] = backend_name or self.driver_name data['vendor_name'] = 'NetApp' data['driver_version'] = self.VERSION data['storage_protocol'] = self.driver_protocol data['pools'] = self._get_pool_stats( filter_function=filter_function, goodness_function=goodness_function) data['sparse_copy_volume'] = True self.zapi_client.provide_ems(self, self.driver_name, self.app_version, server_type=self.driver_mode) self._stats = data def _get_pool_stats(self, filter_function=None, goodness_function=None): """Retrieve pool (i.e. Data ONTAP volume) stats info from volumes.""" pools = [] self.perf_library.update_performance_cache() for vol in self.vols: volume_name = vol.get_child_content('name') # omit volumes not specified in the config if self.volume_list and volume_name not in self.volume_list: continue # omit root volume if volume_name == self.root_volume_name: continue # ensure good volume state state = vol.get_child_content('state') inconsistent = vol.get_child_content('is-inconsistent') invalid = vol.get_child_content('is-invalid') if (state != 'online' or inconsistent != 'false' or invalid != 'false'): continue pool = dict() pool['pool_name'] = volume_name pool['QoS_support'] = False pool['multiattach'] = True pool['reserved_percentage'] = ( self.reserved_percentage) pool['max_over_subscription_ratio'] = ( self.max_over_subscription_ratio) # convert sizes to GB total = float(vol.get_child_content('size-total') or 0) total /= units.Gi pool['total_capacity_gb'] = na_utils.round_down(total, '0.01') free = float(vol.get_child_content('size-available') or 0) free /= units.Gi pool['free_capacity_gb'] = na_utils.round_down(free, '0.01') pool['provisioned_capacity_gb'] = (round( pool['total_capacity_gb'] - pool['free_capacity_gb'], 2)) thick = ( self.configuration.netapp_lun_space_reservation == 'enabled') pool['thick_provisioning_support'] = thick pool['thin_provisioning_support'] = not thick utilization = self.perf_library.get_node_utilization() pool['utilization'] = na_utils.round_down(utilization, '0.01') pool['filter_function'] = filter_function pool['goodness_function'] = goodness_function pool['consistencygroup_support'] = True pools.append(pool) return pools def _get_filtered_pools(self): """Return available pools filtered by a pool name search pattern.""" # Inform deprecation of legacy option. if self.configuration.safe_get('netapp_volume_list'): msg = _LW("The option 'netapp_volume_list' is deprecated and " "will be removed in the future releases. Please use " "the option 'netapp_pool_name_search_pattern' instead.") versionutils.report_deprecated_feature(LOG, msg) pool_regex = na_utils.get_pool_name_filter_regex(self.configuration) filtered_pools = [] for vol in self.vols: vol_name = vol.get_child_content('name') if pool_regex.match(vol_name): msg = ("Volume '%(vol_name)s' matches against regular " "expression: %(vol_pattern)s") LOG.debug(msg, {'vol_name': vol_name, 'vol_pattern': pool_regex.pattern}) filtered_pools.append(vol_name) else: msg = ("Volume '%(vol_name)s' does not match against regular " "expression: %(vol_pattern)s") LOG.debug(msg, {'vol_name': vol_name, 'vol_pattern': pool_regex.pattern}) return filtered_pools def _get_lun_block_count(self, path): """Gets block counts for the LUN.""" bs = super(NetAppBlockStorage7modeLibrary, self)._get_lun_block_count(path) api_version = self.zapi_client.get_ontapi_version() if api_version: major = api_version[0] minor = api_version[1] if major == 1 and minor < 15: bs -= 1 return bs def _refresh_volume_info(self): """Saves the volume information for the filer.""" if (self.vol_refresh_time is None or self.vol_refresh_voluntary or timeutils.is_newer_than(self.vol_refresh_time, self.vol_refresh_interval)): try: job_set = na_utils.set_safe_attr(self, 'vol_refresh_running', True) if not job_set: LOG.warning(_LW("Volume refresh job already running. " "Returning...")) return self.vol_refresh_voluntary = False self.vols = self.zapi_client.get_filer_volumes() self.volume_list = self._get_filtered_pools() self.vol_refresh_time = timeutils.utcnow() except Exception as e: LOG.warning(_LW("Error refreshing volume info. Message: %s"), e) finally: na_utils.set_safe_attr(self, 'vol_refresh_running', False) def delete_volume(self, volume): """Driver entry point for destroying existing volumes.""" super(NetAppBlockStorage7modeLibrary, self).delete_volume(volume) self.vol_refresh_voluntary = True LOG.debug('Deleted LUN with name %s', volume['name']) def delete_snapshot(self, snapshot): """Driver entry point for deleting a snapshot.""" super(NetAppBlockStorage7modeLibrary, self).delete_snapshot(snapshot) self.vol_refresh_voluntary = True def _is_lun_valid_on_storage(self, lun): """Validate LUN specific to storage system.""" if self.volume_list: lun_vol = lun.get_metadata_property('Volume') if lun_vol not in self.volume_list: return False return True def _check_volume_type_for_lun(self, volume, lun, existing_ref, extra_specs): """Check if LUN satisfies volume type.""" if extra_specs: legacy_policy = extra_specs.get('netapp:qos_policy_group') if legacy_policy is not None: raise exception.ManageExistingVolumeTypeMismatch( reason=_("Setting LUN QoS policy group is not supported " "on this storage family and ONTAP version.")) volume_type = na_utils.get_volume_type_from_volume(volume) if volume_type is None: return spec = na_utils.get_backend_qos_spec_from_volume_type(volume_type) if spec is not None: raise exception.ManageExistingVolumeTypeMismatch( reason=_("Back-end QoS specs are not supported on this " "storage family and ONTAP version.")) def _get_preferred_target_from_list(self, target_details_list, filter=None): # 7-mode iSCSI LIFs migrate from controller to controller # in failover and flap operational state in transit, so # we don't filter these on operational state. return (super(NetAppBlockStorage7modeLibrary, self) ._get_preferred_target_from_list(target_details_list)) def _get_backing_flexvol_names(self): """Returns a list of backing flexvol names.""" return self.volume_list or []