text: # 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 C-mode block storage systems.
  """
  
  import copy
  
  from oslo_log import log as logging
  from oslo_service import loopingcall
  from oslo_utils import units
  import six
  
  from cinder import exception
  from cinder.i18n import _
  from cinder import utils
  from cinder.volume.drivers.netapp.dataontap import block_base
  from cinder.volume.drivers.netapp.dataontap.client import client_cmode
  from cinder.volume.drivers.netapp.dataontap.performance import perf_cmode
  from cinder.volume.drivers.netapp.dataontap import ssc_cmode
  from cinder.volume.drivers.netapp.dataontap.utils import capabilities
  from cinder.volume.drivers.netapp import options as na_opts
  from cinder.volume.drivers.netapp import utils as na_utils
  
  
  LOG = logging.getLogger(__name__)
  QOS_CLEANUP_INTERVAL_SECONDS = 60
  
  
  @six.add_metaclass(utils.TraceWrapperMetaclass)
  class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary):
      """NetApp block storage library for Data ONTAP (Cluster-mode)."""
  
      REQUIRED_CMODE_FLAGS = ['netapp_vserver']
  
      def __init__(self, driver_name, driver_protocol, **kwargs):
          super(NetAppBlockStorageCmodeLibrary, self).__init__(driver_name,
                                                               driver_protocol,
                                                               **kwargs)
          self.configuration.append_config_values(na_opts.netapp_cluster_opts)
          self.driver_mode = 'cluster'
  
      def do_setup(self, context):
          super(NetAppBlockStorageCmodeLibrary, self).do_setup(context)
          na_utils.check_flags(self.REQUIRED_CMODE_FLAGS, self.configuration)
  
          self.vserver = self.configuration.netapp_vserver
  
          self.zapi_client = client_cmode.Client(
              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,
              vserver=self.vserver)
  
          self.ssc_vols = {}
          self.stale_vols = set()
          self.perf_library = perf_cmode.PerformanceCmodeLibrary(
              self.zapi_client)
          self.ssc_library = capabilities.CapabilitiesLibrary(self.zapi_client)
  
      def check_for_setup_error(self):
          """Check that the driver is working and can communicate."""
          self.ssc_library.check_api_permissions()
          ssc_cmode.refresh_cluster_ssc(self, self.zapi_client.get_connection(),
                                        self.vserver, synchronous=True)
          if not self._get_filtered_pools():
              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)
          super(NetAppBlockStorageCmodeLibrary, self).check_for_setup_error()
          self._start_periodic_tasks()
  
      def _start_periodic_tasks(self):
          # Start the task that harvests soft-deleted QoS policy groups.
          harvest_qos_periodic_task = loopingcall.FixedIntervalLoopingCall(
              self.zapi_client.remove_unused_qos_policy_groups)
          harvest_qos_periodic_task.start(
              interval=QOS_CLEANUP_INTERVAL_SECONDS,
              initial_delay=QOS_CLEANUP_INTERVAL_SECONDS)
  
      def _create_lun(self, volume_name, lun_name, size,
                      metadata, qos_policy_group_name=None):
          """Creates a LUN, handling Data ONTAP differences as needed."""
  
          self.zapi_client.create_lun(
              volume_name, lun_name, size, metadata, qos_policy_group_name)
  
          self._update_stale_vols(
              volume=ssc_cmode.NetAppVolume(volume_name, self.vserver))
  
      def _create_lun_handle(self, metadata):
          """Returns LUN handle based on filer type."""
          return '%s:%s' % (self.vserver, metadata['Path'])
  
      def _find_mapped_lun_igroup(self, path, initiator_list):
          """Find an igroup for a LUN mapped to the given initiator(s)."""
          initiator_igroups = self.zapi_client.get_igroup_by_initiators(
              initiator_list)
          lun_maps = self.zapi_client.get_lun_map(path)
          if initiator_igroups and lun_maps:
              for igroup in initiator_igroups:
                  igroup_name = igroup['initiator-group-name']
                  if igroup_name.startswith(na_utils.OPENSTACK_PREFIX):
                      for lun_map in lun_maps:
                          if lun_map['initiator-group'] == igroup_name:
                              return igroup_name, lun_map['lun-id']
          return None, None
  
      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):
          """Clone LUN with the given handle to the new name."""
          if not space_reserved:
              space_reserved = self.lun_space_reservation
          metadata = self._get_lun_attr(name, 'metadata')
          volume = metadata['Volume']
  
          self.zapi_client.clone_lun(volume, name, new_name, space_reserved,
                                     qos_policy_group_name=qos_policy_group_name,
                                     src_block=src_block, dest_block=dest_block,
                                     block_count=block_count,
                                     source_snapshot=source_snapshot)
  
          LOG.debug("Cloned LUN with new name %s", new_name)
          lun = self.zapi_client.get_lun_by_args(vserver=self.vserver,
                                                 path='/vol/%s/%s'
                                                 % (volume, new_name))
          if len(lun) == 0:
              msg = _("No cloned LUN named %s found on the filer")
              raise exception.VolumeBackendAPIException(data=msg % new_name)
          clone_meta = self._create_lun_meta(lun[0])
          self._add_lun_to_table(
              block_base.NetAppLun('%s:%s' % (clone_meta['Vserver'],
                                              clone_meta['Path']),
                                   new_name,
                                   lun[0].get_child_content('size'),
                                   clone_meta))
          self._update_stale_vols(
              volume=ssc_cmode.NetAppVolume(volume, self.vserver))
  
      def _create_lun_meta(self, lun):
          """Creates LUN metadata dictionary."""
          self.zapi_client.check_is_naelement(lun)
          meta_dict = {}
          meta_dict['Vserver'] = lun.get_child_content('vserver')
          meta_dict['Volume'] = lun.get_child_content('volume')
          meta_dict['Qtree'] = lun.get_child_content('qtree')
          meta_dict['Path'] = lun.get_child_content('path')
          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):
          return self.zapi_client.get_fc_target_wwpns()
  
      def _configure_tunneling(self, do_tunneling=False):
          """Configures tunneling for Data ONTAP cluster."""
          if do_tunneling:
              self.zapi_client.set_vserver(self.vserver)
          else:
              self.zapi_client.set_vserver(None)
  
      def _update_volume_stats(self, filter_function=None,
                               goodness_function=None):
          """Retrieve stats info from vserver."""
  
          sync = True if self.ssc_vols is None else False
          ssc_cmode.refresh_cluster_ssc(self, self.zapi_client.get_connection(),
                                        self.vserver, synchronous=sync)
  
          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)
          self._stats = data
  
      def _get_pool_stats(self, filter_function=None, goodness_function=None):
          """Retrieve pool (Data ONTAP volume) stats info from SSC volumes."""
  
          pools = []
  
          if not self.ssc_vols:
              return pools
  
          filtered_pools = self._get_filtered_pools()
          self.perf_library.update_performance_cache(filtered_pools)
  
          for vol in filtered_pools:
              pool_name = vol.id['name']
  
              pool = dict()
              pool['pool_name'] = pool_name
              pool['QoS_support'] = True
              pool['reserved_percentage'] = (
                  self.reserved_percentage)
              pool['max_over_subscription_ratio'] = (
                  self.max_over_subscription_ratio)
  
              # Get capacity info and convert to GB
              capacity = self.zapi_client.get_flexvol_capacity(
                  flexvol_name=pool_name)
  
              size_total_gb = capacity['size-total'] / units.Gi
              pool['total_capacity_gb'] = na_utils.round_down(size_total_gb)
  
              size_available_gb = capacity['size-available'] / units.Gi
              pool['free_capacity_gb'] = na_utils.round_down(size_available_gb)
  
              pool['provisioned_capacity_gb'] = (round(
                  pool['total_capacity_gb'] - pool['free_capacity_gb'], 2))
  
              pool['netapp_raid_type'] = vol.aggr['raid_type']
              pool['netapp_disk_type'] = vol.aggr['disk_type']
  
              mirrored = vol in self.ssc_vols['mirrored']
              pool['netapp_mirrored'] = six.text_type(mirrored).lower()
              pool['netapp_unmirrored'] = six.text_type(not mirrored).lower()
  
              dedup = vol in self.ssc_vols['dedup']
              pool['netapp_dedup'] = six.text_type(dedup).lower()
              pool['netapp_nodedup'] = six.text_type(not dedup).lower()
  
              compression = vol in self.ssc_vols['compression']
              pool['netapp_compression'] = six.text_type(compression).lower()
              pool['netapp_nocompression'] = six.text_type(
                  not compression).lower()
  
              thin = vol in self.ssc_vols['thin']
              pool['netapp_thin_provisioned'] = six.text_type(thin).lower()
              pool['netapp_thick_provisioned'] = six.text_type(not thin).lower()
              thick = (not thin and
                       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_for_pool(
                  pool_name)
              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 filtered pools given a pool name search pattern."""
          pool_regex = na_utils.get_pool_name_filter_regex(self.configuration)
  
          filtered_pools = []
          for vol in self.ssc_vols.get('all', []):
              vol_name = vol.id['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)
              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
  
      @utils.synchronized('update_stale')
      def _update_stale_vols(self, volume=None, reset=False):
          """Populates stale vols with vol and returns set copy if reset."""
          if volume:
              self.stale_vols.add(volume)
          if reset:
              set_copy = copy.deepcopy(self.stale_vols)
              self.stale_vols.clear()
              return set_copy
  
      @utils.synchronized("refresh_ssc_vols")
      def refresh_ssc_vols(self, vols):
          """Refreshes ssc_vols with latest entries."""
          self.ssc_vols = vols
  
      def delete_volume(self, volume):
          """Driver entry point for destroying existing volumes."""
          lun = self.lun_table.get(volume['name'])
          netapp_vol = None
          if lun:
              netapp_vol = lun.get_metadata_property('Volume')
          super(NetAppBlockStorageCmodeLibrary, self).delete_volume(volume)
          try:
              qos_policy_group_info = na_utils.get_valid_qos_policy_group_info(
                  volume)
          except exception.Invalid:
              # Delete even if there was invalid qos policy specified for the
              # volume.
              qos_policy_group_info = None
          self._mark_qos_policy_group_for_deletion(qos_policy_group_info)
          if netapp_vol:
              self._update_stale_vols(
                  volume=ssc_cmode.NetAppVolume(netapp_vol, self.vserver))
          msg = 'Deleted LUN with name %(name)s and QoS info %(qos)s'
          LOG.debug(msg, {'name': volume['name'], 'qos': qos_policy_group_info})
  
      def delete_snapshot(self, snapshot):
          """Driver entry point for deleting a snapshot."""
          lun = self.lun_table.get(snapshot['name'])
          netapp_vol = lun.get_metadata_property('Volume') if lun else None
  
          super(NetAppBlockStorageCmodeLibrary, self).delete_snapshot(snapshot)
  
          if netapp_vol:
              self._update_stale_vols(
                  volume=ssc_cmode.NetAppVolume(netapp_vol, self.vserver))
  
      def _get_preferred_target_from_list(self, target_details_list,
                                          filter=None):
          # cDOT iSCSI LIFs do not migrate from controller to controller
          # in failover.  Rather, an iSCSI LIF must be configured on each
          # controller and the initiator has to take responsibility for
          # using a LIF that is UP.  In failover, the iSCSI LIF on the
          # downed controller goes DOWN until the controller comes back up.
          #
          # Currently Nova only accepts a single target when obtaining
          # target details from Cinder, so we pass back the first portal
          # with an UP iSCSI LIF.  There are plans to have Nova accept
          # and try multiple targets.  When that happens, we can and should
          # remove this filter and return all targets since their operational
          # state could change between the time we test here and the time
          # Nova uses the target.
  
          operational_addresses = (
              self.zapi_client.get_operational_network_interface_addresses())
  
          return (super(NetAppBlockStorageCmodeLibrary, self)
                  ._get_preferred_target_from_list(target_details_list,
                                                   filter=operational_addresses))
  
      def _setup_qos_for_volume(self, volume, extra_specs):
          try:
              qos_policy_group_info = na_utils.get_valid_qos_policy_group_info(
                  volume, extra_specs)
          except exception.Invalid:
              msg = _('Invalid QoS specification detected while getting QoS '
                      'policy for volume %s') % volume['id']
              raise exception.VolumeBackendAPIException(data=msg)
          self.zapi_client.provision_qos_policy_group(qos_policy_group_info)
          return qos_policy_group_info
  
      def _mark_qos_policy_group_for_deletion(self, qos_policy_group_info):
          self.zapi_client.mark_qos_policy_group_for_deletion(
              qos_policy_group_info)
  
      def unmanage(self, volume):
          """Removes the specified volume from Cinder management.
  
             Does not delete the underlying backend storage object.
          """
          try:
              qos_policy_group_info = na_utils.get_valid_qos_policy_group_info(
                  volume)
          except exception.Invalid:
              # Unmanage even if there was invalid qos policy specified for the
              # volume.
              qos_policy_group_info = None
          self._mark_qos_policy_group_for_deletion(qos_policy_group_info)
          super(NetAppBlockStorageCmodeLibrary, self).unmanage(volume)
  