# Copyright (c) 2015 Alex Meade
# Copyright (c) 2015 Rushil Chugh
# Copyright (c) 2015 Navneet Singh
# Copyright (c) 2015 Yogesh Kshirsagar
# Copyright (c) 2015 Jose Porrua
# Copyright (c) 2015 Michael Price
# Copyright (c) 2015 Tom Barron
#  All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import copy
import math
import socket
import time
import uuid

from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
from oslo_service import loopingcall
from oslo_utils import excutils
from oslo_utils import units
import six

from cinder import exception
from cinder.i18n import _
from cinder import utils as cinder_utils
from cinder.volume.drivers.netapp.eseries import client
from cinder.volume.drivers.netapp.eseries import exception as eseries_exc
from cinder.volume.drivers.netapp.eseries import host_mapper
from cinder.volume.drivers.netapp.eseries import utils
from cinder.volume.drivers.netapp import options as na_opts
from cinder.volume.drivers.netapp import utils as na_utils
from cinder.volume import utils as volume_utils
from cinder.zonemanager import utils as fczm_utils


LOG = logging.getLogger(__name__)

CONF = cfg.CONF


@six.add_metaclass(cinder_utils.TraceWrapperMetaclass)
class NetAppESeriesLibrary(object):
    """Executes commands relating to Volumes."""

    DRIVER_NAME = 'NetApp_iSCSI_ESeries'
    AUTOSUPPORT_INTERVAL_SECONDS = 3600  # hourly
    VERSION = "1.0.0"
    REQUIRED_FLAGS = ['netapp_server_hostname', 'netapp_controller_ips',
                      'netapp_login', 'netapp_password']
    SLEEP_SECS = 5
    HOST_TYPES = {'factoryDefault': 'FactoryDefault',
                  'linux_atto': 'LnxTPGSALUA',
                  'linux_dm_mp': 'LnxALUA',
                  'linux_mpp_rdac': 'LNX',
                  'linux_pathmanager': 'LnxTPGSALUA_PM',
                  'linux_sf': 'LnxTPGSALUA_SF',
                  'ontap': 'ONTAP_ALUA',
                  'ontap_rdac': 'ONTAP_RDAC',
                  'vmware': 'VmwTPGSALUA',
                  'windows': 'W2KNETNCL',
                  'windows_atto': 'WinTPGSALUA',
                  'windows_clustered': 'W2KNETCL',
                  }
    # NOTE(ameade): This maps what is reported by the e-series api to a
    # consistent set of values that are reported by all NetApp drivers
    # to the cinder scheduler.
    SSC_DISK_TYPE_MAPPING = {
        'scsi': 'SCSI',
        'fibre': 'FCAL',
        'sas': 'SAS',
        'sata': 'SATA',
        'ssd': 'SSD',
    }
    SSC_RAID_TYPE_MAPPING = {
        'raidDiskPool': 'DDP',
        'raid0': 'raid0',
        'raid1': 'raid1',
        # RAID3 is being deprecated and is actually implemented as RAID5
        'raid3': 'raid5',
        'raid5': 'raid5',
        'raid6': 'raid6',
    }
    READ_CACHE_Q_SPEC = 'netapp:read_cache'
    WRITE_CACHE_Q_SPEC = 'netapp:write_cache'
    DA_UQ_SPEC = 'netapp_eseries_data_assurance'
    FLASH_CACHE_UQ_SPEC = 'netapp_eseries_flash_read_cache'
    DISK_TYPE_UQ_SPEC = 'netapp_disk_type'
    ENCRYPTION_UQ_SPEC = 'netapp_disk_encryption'
    SPINDLE_SPD_UQ_SPEC = 'netapp_eseries_disk_spindle_speed'
    RAID_UQ_SPEC = 'netapp_raid_type'
    THIN_UQ_SPEC = 'netapp_thin_provisioned'
    SSC_UPDATE_INTERVAL = 60  # seconds
    SA_COMM_TIMEOUT = 30
    WORLDWIDENAME = 'worldWideName'

    DEFAULT_HOST_TYPE = 'linux_dm_mp'
    DEFAULT_CHAP_USER_NAME = 'eserieschapuser'

    # Define name marker string to use in snapshot groups that are for copying
    # volumes.  This is to differentiate them from ordinary snapshot groups.
    SNAPSHOT_VOL_COPY_SUFFIX = 'SGCV'
    # Define a name marker string used to identify snapshot volumes that have
    # an underlying snapshot that is awaiting deletion.
    SNAPSHOT_VOL_DEL_SUFFIX = '_DEL'
    # Maximum number of snapshots per snapshot group
    MAX_SNAPSHOT_COUNT = 32
    # Maximum number of snapshot groups
    MAX_SNAPSHOT_GROUP_COUNT = 4
    RESERVED_SNAPSHOT_GROUP_COUNT = 1
    SNAPSHOT_PERSISTENT_STORE_KEY = 'cinder-snapshots'
    SNAPSHOT_PERSISTENT_STORE_LOCK = str(uuid.uuid4())

    def __init__(self, driver_name, driver_protocol="iSCSI",
                 configuration=None, **kwargs):
        self.configuration = configuration
        self._app_version = kwargs.pop("app_version", "unknown")
        self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
        self.configuration.append_config_values(
            na_opts.netapp_connection_opts)
        self.configuration.append_config_values(na_opts.netapp_transport_opts)
        self.configuration.append_config_values(na_opts.netapp_eseries_opts)
        self.configuration.append_config_values(na_opts.netapp_san_opts)
        self.lookup_service = fczm_utils.create_lookup_service()
        self._backend_name = self.configuration.safe_get(
            "volume_backend_name") or "NetApp_ESeries"
        self.driver_name = driver_name
        self.driver_protocol = driver_protocol
        self._stats = {}
        self._ssc_stats = {}

    def do_setup(self, context):
        """Any initialization the volume driver does while starting."""
        self.context = context
        na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration)

        self._client = self._create_rest_client(self.configuration)
        self._check_mode_get_or_register_storage_system()
        self._version_check()
        if self.configuration.netapp_enable_multiattach:
            self._ensure_multi_attach_host_group_exists()

    def _create_rest_client(self, configuration):
        port = configuration.netapp_server_port
        scheme = configuration.netapp_transport_type.lower()
        if port is None:
            if scheme == 'http':
                port = 8080
            elif scheme == 'https':
                port = 8443

        return client.RestClient(
            scheme=scheme,
            host=configuration.netapp_server_hostname,
            port=port,
            service_path=configuration.netapp_webservice_path,
            username=configuration.netapp_login,
            password=configuration.netapp_password)

    def _version_check(self):
        """Ensure that the minimum version of the REST API is available"""
        if not self._client.features.REST_1_4_RELEASE:
            min_version = (
                self._client.features.REST_1_4_RELEASE.minimum_version)
            raise exception.NetAppDriverException(
                'This version (%(cur)s of the NetApp SANtricity Webservices '
                'Proxy is not supported. Install version %(supp)s or '
                'later.' % {'cur': self._client.api_version,
                            'supp': min_version})

    def _start_periodic_tasks(self):
        ssc_periodic_task = loopingcall.FixedIntervalLoopingCall(
            self._update_ssc_info)
        ssc_periodic_task.start(interval=self.SSC_UPDATE_INTERVAL)

        # Start the task that logs autosupport (ASUP) data to the controller
        asup_periodic_task = loopingcall.FixedIntervalLoopingCall(
            self._create_asup, CONF.host)
        asup_periodic_task.start(interval=self.AUTOSUPPORT_INTERVAL_SECONDS,
                                 initial_delay=0)

    def check_for_setup_error(self):
        self._check_host_type()
        self._check_multipath()
        # It is important that this be called before any other methods that
        # interact with the storage-system. It blocks until the
        # storage-system comes online.
        self._check_storage_system()
        self._check_pools()
        self._start_periodic_tasks()

    def _check_host_type(self):
        """Validate that the configured host-type is available for the array.

        Not all host-types are available on every firmware version.
        """
        requested_host_type = (self.configuration.netapp_host_type
                               or self.DEFAULT_HOST_TYPE)
        actual_host_type = (
            self.HOST_TYPES.get(requested_host_type, requested_host_type))

        for host_type in self._client.list_host_types():
            if(host_type.get('code') == actual_host_type or
               host_type.get('name') == actual_host_type):
                self.host_type = host_type.get('code')
                return
        exc_msg = _("The host-type '%s' is not supported on this storage "
                    "system.")
        raise exception.NetAppDriverException(exc_msg % requested_host_type)

    def _check_multipath(self):
        if not self.configuration.use_multipath_for_image_xfer:
            LOG.warning('Production use of "%(backend)s" backend requires '
                        'the Cinder controller to have multipathing '
                        'properly set up and the configuration option '
                        '"%(mpflag)s" to be set to "True".',
                        {'backend': self._backend_name,
                         'mpflag': 'use_multipath_for_image_xfer'})

    def _check_pools(self):
        """Ensure that the pool listing contains at least one pool"""
        if not self._get_storage_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)

    def _ensure_multi_attach_host_group_exists(self):
        try:
            host_group = self._client.get_host_group_by_name(
                utils.MULTI_ATTACH_HOST_GROUP_NAME)
            LOG.info("The multi-attach E-Series host group '%(label)s' "
                     "already exists with clusterRef %(clusterRef)s",
                     host_group)
        except exception.NotFound:
            host_group = self._client.create_host_group(
                utils.MULTI_ATTACH_HOST_GROUP_NAME)
            LOG.info("Created multi-attach E-Series host group %(label)s "
                     "with clusterRef %(clusterRef)s", host_group)

    def _check_mode_get_or_register_storage_system(self):
        """Does validity checks for storage system registry and health."""
        def _resolve_host(host):
            try:
                ip = na_utils.resolve_hostname(host)
                return ip
            except socket.gaierror as e:
                LOG.error('Error resolving host %(host)s. Error - %(e)s.',
                          {'host': host, 'e': e})
                raise exception.NoValidBackend(
                    _("Controller IP '%(host)s' could not be resolved: %(e)s.")
                    % {'host': host, 'e': e})

        ips = self.configuration.netapp_controller_ips
        ips = [i.strip() for i in ips.split(",")]
        ips = [x for x in ips if _resolve_host(x)]
        host = na_utils.resolve_hostname(
            self.configuration.netapp_server_hostname)
        if host in ips:
            LOG.info('Embedded mode detected.')
            system = self._client.list_storage_systems()[0]
        else:
            LOG.info('Proxy mode detected.')
            system = self._client.register_storage_system(
                ips, password=self.configuration.netapp_sa_password)
        self._client.set_system_id(system.get('id'))
        self._client._init_features()

    def _check_password_status(self, system):
        """Determine if the storage system's password status is valid.

        The password status has the following possible states: unknown, valid,
        invalid.

        If the password state cannot be retrieved from the storage system,
        an empty string will be returned as the status, and the password
        status will be assumed to be valid. This is done to ensure that
        access to a storage system will not be blocked in the event of a
        problem with the API.

        This method returns a tuple consisting of the storage system's
        password status and whether or not the status is valid.

        Example: (invalid, True)

        :returns: (str, bool)
        """

        status = system.get('passwordStatus')
        status = status.lower() if status else ''
        return status, status not in ['invalid', 'unknown']

    def _check_storage_system_status(self, system):
        """Determine if the storage system's status is valid.

        The storage system status has the following possible states:
        neverContacted, offline, optimal, needsAttn.

        If the storage system state cannot be retrieved, an empty string will
        be returned as the status, and the storage system's status will be
        assumed to be valid. This is done to ensure that access to a storage
        system will not be blocked in the event of a problem with the API.

        This method returns a tuple consisting of the storage system's
        password status and whether or not the status is valid.

        Example: (needsAttn, True)

        :returns: (str, bool)
        """
        status = system.get('status')
        status = status.lower() if status else ''
        return status, status not in ['nevercontacted', 'offline']

    def _check_storage_system(self):
        """Checks whether system is registered and has good status."""
        try:
            self._client.list_storage_system()
        except exception.NetAppDriverException:
            with excutils.save_and_reraise_exception():
                LOG.info("System with controller addresses [%s] is not "
                         "registered with web service.",
                         self.configuration.netapp_controller_ips)

        # Update the stored password
        # We do this to trigger the webservices password validation routine
        new_pwd = self.configuration.netapp_sa_password
        self._client.update_stored_system_password(new_pwd)

        start_time = int(time.time())

        def check_system_status():
            system = self._client.list_storage_system()
            pass_status, pass_status_valid = (
                self._check_password_status(system))
            status, status_valid = self._check_storage_system_status(system)
            msg_dict = {'id': system.get('id'), 'status': status,
                        'pass_status': pass_status}
            # wait if array not contacted or
            # password was not in sync previously.
            if not (pass_status_valid and status_valid):
                if not pass_status_valid:
                    LOG.info('Waiting for web service to validate the '
                             'configured password.')
                else:
                    LOG.info('Waiting for web service array communication.')
                if int(time.time() - start_time) >= self.SA_COMM_TIMEOUT:
                    if not status_valid:
                        raise exception.NetAppDriverException(
                            _("System %(id)s found with bad status - "
                              "%(status)s.") % msg_dict)
                    else:
                        raise exception.NetAppDriverException(
                            _("System %(id)s found with bad password status - "
                              "%(pass_status)s.") % msg_dict)

            # The system was found to have a good status
            else:
                LOG.info("System %(id)s has %(status)s status.", msg_dict)
                raise loopingcall.LoopingCallDone()

        checker = loopingcall.FixedIntervalLoopingCall(f=check_system_status)
        checker.start(interval = self.SLEEP_SECS,
                      initial_delay=self.SLEEP_SECS).wait()

        return True

    def _get_volume(self, uid):
        """Retrieve a volume by its label"""
        if uid is None:
            raise exception.InvalidInput(_('The volume label is required'
                                           ' as input.'))

        uid = utils.convert_uuid_to_es_fmt(uid)

        return self._client.list_volume(uid)

    def _get_snapshot_group_for_snapshot(self, snapshot):
        snapshot = self._get_snapshot(snapshot)
        try:
            return self._client.list_snapshot_group(snapshot['pitGroupRef'])
        except (exception.NetAppDriverException,
                eseries_exc.WebServiceException):
            msg = _("Specified snapshot group with id %s could not be found.")
            raise exception.NotFound(msg % snapshot['pitGroupRef'])

    def _get_snapshot_legacy(self, snapshot):
        """Find an E-Series snapshot by the name of the snapshot group.

        Snapshots were previously identified by the unique name of the
        snapshot group. A snapshot volume is now utilized to uniquely
        identify the snapshot, so any snapshots previously defined in this
        way must be updated.

        :param snapshot_id: Cinder snapshot identifer
        :return: An E-Series snapshot image
        """
        label = utils.convert_uuid_to_es_fmt(snapshot['id'])
        for group in self._client.list_snapshot_groups():
            if group['label'] == label:
                image = self._get_oldest_image_in_snapshot_group(group['id'])
                group_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
                # Modify the group label so we don't have a name collision
                self._client.update_snapshot_group(group['id'],
                                                   group_label)

                snapshot.update({'provider_id': image['id']})
                snapshot.save()

                return image

        raise exception.NotFound(_('Snapshot with id of %s could not be '
                                   'found.') % snapshot['id'])

    def _get_snapshot(self, snapshot):
        """Find an E-Series snapshot by its Cinder identifier

        An E-Series snapshot image does not have a configuration name/label,
        so we define a snapshot volume underneath of it that will help us to
        identify it. We retrieve the snapshot volume with the matching name,
        and then we find its underlying snapshot.

        :param snapshot_id: Cinder snapshot identifer
        :return: An E-Series snapshot image
        """
        try:
            return self._client.list_snapshot_image(
                snapshot.get('provider_id'))
        except (eseries_exc.WebServiceException,
                exception.NetAppDriverException):
            try:
                LOG.debug('Unable to locate snapshot by its id, falling '
                          'back to legacy behavior.')
                return self._get_snapshot_legacy(snapshot)
            except exception.NetAppDriverException:
                raise exception.NotFound(_('Snapshot with id of %s could not'
                                           ' be found.') % snapshot['id'])

    def _get_snapshot_group(self, snapshot_group_id):
        try:
            return self._client.list_snapshot_group(snapshot_group_id)
        except exception.NetAppDriverException:
            raise exception.NotFound(_('Unable to retrieve snapshot group '
                                       'with id of %s.') % snapshot_group_id)

    def _get_ordered_images_in_snapshot_group(self, snapshot_group_id):
        images = self._client.list_snapshot_images()
        if images:
            filtered_images = [img for img in images if img['pitGroupRef'] ==
                               snapshot_group_id]
            sorted_imgs = sorted(filtered_images, key=lambda x: x[
                'pitTimestamp'])
            return sorted_imgs
        return list()

    def _get_oldest_image_in_snapshot_group(self, snapshot_group_id):
        group = self._get_snapshot_group(snapshot_group_id)
        images = self._get_ordered_images_in_snapshot_group(snapshot_group_id)
        if images:
            return images[0]

        msg = _("No snapshot image found in snapshot group %s.")
        raise exception.NotFound(msg % group['label'])

    def _get_latest_image_in_snapshot_group(self, snapshot_group_id):
        group = self._get_snapshot_group(snapshot_group_id)
        images = self._get_ordered_images_in_snapshot_group(snapshot_group_id)
        if images:
            return images[-1]

        msg = _("No snapshot image found in snapshot group %s.")
        raise exception.NotFound(msg % group['label'])

    def _is_volume_containing_snaps(self, label):
        """Checks if volume contains snapshot groups."""
        vol_id = utils.convert_es_fmt_to_uuid(label)
        for snap in self._client.list_snapshot_groups():
            if snap['baseVolume'] == vol_id:
                return True
        return False

    def get_pool(self, volume):
        """Return pool name where volume resides.

        :param volume: The volume hosted by the driver.
        :returns: Name of the pool where given volume is hosted.
        """
        eseries_volume = self._get_volume(volume['name_id'])
        storage_pool = self._client.get_storage_pool(
            eseries_volume['volumeGroupRef'])
        if storage_pool:
            return storage_pool.get('label')

    def _add_volume_to_consistencygroup(self, volume):
        if volume.get('consistencygroup_id'):
            es_cg = self._get_consistencygroup(volume['consistencygroup'])
            self._update_consistency_group_members(es_cg, [volume], [])

    def create_volume(self, volume):
        """Creates a volume."""

        LOG.debug('create_volume on %s', volume['host'])

        # get E-series pool label as pool name
        eseries_pool_label = volume_utils.extract_host(volume['host'],
                                                       level='pool')

        if eseries_pool_label is None:
            msg = _("Pool is not available in the volume host field.")
            raise exception.InvalidHost(reason=msg)

        eseries_volume_label = utils.convert_uuid_to_es_fmt(volume['name_id'])

        extra_specs = na_utils.get_volume_extra_specs(volume)

        # get size of the requested volume creation
        size_gb = int(volume['size'])
        self._create_volume(eseries_pool_label, eseries_volume_label, size_gb,
                            extra_specs)

        self._add_volume_to_consistencygroup(volume)

    def _create_volume(self, eseries_pool_label, eseries_volume_label,
                       size_gb, extra_specs=None):
        """Creates volume with given label and size."""
        if extra_specs is None:
            extra_specs = {}

        if self.configuration.netapp_enable_multiattach:
            volumes = self._client.list_volumes()
            # NOTE(ameade): Ensure we do not create more volumes than we could
            # map to the multi attach ESeries host group.
            if len(volumes) > utils.MAX_LUNS_PER_HOST_GROUP:
                msg = (_("Cannot create more than %(req)s volumes on the "
                         "ESeries array when 'netapp_enable_multiattach' is "
                         "set to true.") %
                       {'req': utils.MAX_LUNS_PER_HOST_GROUP})
                raise exception.NetAppDriverException(msg)

        # These must be either boolean values, or None
        read_cache = extra_specs.get(self.READ_CACHE_Q_SPEC)
        if read_cache is not None:
            read_cache = na_utils.to_bool(read_cache)

        write_cache = extra_specs.get(self.WRITE_CACHE_Q_SPEC)
        if write_cache is not None:
            write_cache = na_utils.to_bool(write_cache)

        flash_cache = extra_specs.get(self.FLASH_CACHE_UQ_SPEC)
        if flash_cache is not None:
            flash_cache = na_utils.to_bool(flash_cache)

        data_assurance = extra_specs.get(self.DA_UQ_SPEC)
        if data_assurance is not None:
            data_assurance = na_utils.to_bool(data_assurance)

        thin_provision = extra_specs.get(self.THIN_UQ_SPEC)
        if(thin_provision is not None):
            thin_provision = na_utils.to_bool(thin_provision)

        target_pool = None

        pools = self._get_storage_pools()
        for pool in pools:
            if pool["label"] == eseries_pool_label:
                target_pool = pool
                break

        if not target_pool:
            msg = _("Pools %s does not exist")
            raise exception.NetAppDriverException(msg % eseries_pool_label)

        try:
            vol = self._client.create_volume(target_pool['volumeGroupRef'],
                                             eseries_volume_label, size_gb,
                                             read_cache=read_cache,
                                             write_cache=write_cache,
                                             flash_cache=flash_cache,
                                             data_assurance=data_assurance,
                                             thin_provision=thin_provision)
            LOG.info("Created volume with label %s.", eseries_volume_label)
        except exception.NetAppDriverException as e:
            with excutils.save_and_reraise_exception():
                LOG.error("Error creating volume. Msg - %s.", e)
                # There was some kind failure creating the volume, make sure no
                # partial flawed work exists
                try:
                    bad_vol = self._get_volume(eseries_volume_label)
                except Exception:
                    # Swallowing the exception intentionally because this is
                    # emergency cleanup to make sure no intermediate volumes
                    # were left. In this whole error situation, the more
                    # common route would be for no volume to have been created.
                    pass
                else:
                    # Some sort of partial volume was created despite the
                    # error.  Lets clean it out so no partial state volumes or
                    # orphans are left.
                    try:
                        self._client.delete_volume(bad_vol["id"])
                    except exception.NetAppDriverException as e2:
                        LOG.error(
                            "Error cleaning up failed volume creation.  "
                            "Msg - %s.", e2)

        return vol

    def _is_data_assurance_supported(self):
        """Determine if the storage backend is PI (DataAssurance) compatible"""
        return self.driver_protocol != "iSCSI"

    def _schedule_and_create_volume(self, label, size_gb):
        """Creates volume with given label and size."""
        avl_pools = self._get_sorted_available_storage_pools(size_gb)
        for pool in avl_pools:
            try:
                vol = self._client.create_volume(pool['volumeGroupRef'],
                                                 label, size_gb)
                LOG.info("Created volume with label %s.", label)
                return vol
            except exception.NetAppDriverException as e:
                LOG.error("Error creating volume. Msg - %s.", e)
        msg = _("Failure creating volume %s.")
        raise exception.NetAppDriverException(msg % label)

    def _create_volume_from_snapshot(self, volume, image):
        """Define a new volume based on an E-Series snapshot image.

        This method should be synchronized on the snapshot id.

        :param volume: a Cinder volume
        :param image: an E-Series snapshot image
        :return: the clone volume
        """
        label = utils.convert_uuid_to_es_fmt(volume['id'])
        size = volume['size']

        dst_vol = self._schedule_and_create_volume(label, size)
        src_vol = None
        try:
            src_vol = self._create_snapshot_volume(image)
            self._copy_volume_high_priority_readonly(src_vol, dst_vol)
            LOG.info("Created volume with label %s.", label)
        except exception.NetAppDriverException:
            with excutils.save_and_reraise_exception():
                self._client.delete_volume(dst_vol['volumeRef'])
        finally:
            if src_vol:
                try:
                    self._client.delete_snapshot_volume(src_vol['id'])
                except exception.NetAppDriverException as e:
                    LOG.error("Failure restarting snap vol. Error: %s.", e)
            else:
                LOG.warning("Snapshot volume creation failed for "
                            "snapshot %s.", image['id'])

        return dst_vol

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from a snapshot."""
        es_snapshot = self._get_snapshot(snapshot)
        cinder_utils.synchronized(snapshot['id'])(
            self._create_volume_from_snapshot)(volume, es_snapshot)

        self._add_volume_to_consistencygroup(volume)

    def _copy_volume_high_priority_readonly(self, src_vol, dst_vol):
        """Copies src volume to dest volume."""
        LOG.info("Copying src vol %(src)s to dest vol %(dst)s.",
                 {'src': src_vol['label'], 'dst': dst_vol['label']})
        job = None
        try:
            job = self._client.create_volume_copy_job(
                src_vol['id'], dst_vol['volumeRef'])

            def wait_for_copy():
                j_st = self._client.list_vol_copy_job(job['volcopyRef'])
                if (j_st['status'] in ['inProgress', 'pending', 'unknown']):
                    return
                if j_st['status'] == 'failed' or j_st['status'] == 'halted':
                    LOG.error("Vol copy job status %s.", j_st['status'])
                    raise exception.NetAppDriverException(
                        _("Vol copy job for dest %s failed.") %
                        dst_vol['label'])
                LOG.info("Vol copy job completed for dest %s.",
                         dst_vol['label'])
                raise loopingcall.LoopingCallDone()

            checker = loopingcall.FixedIntervalLoopingCall(wait_for_copy)
            checker.start(interval=self.SLEEP_SECS,
                          initial_delay=self.SLEEP_SECS,
                          stop_on_exception=True).wait()
        finally:
            if job:
                try:
                    self._client.delete_vol_copy_job(job['volcopyRef'])
                except exception.NetAppDriverException:
                    LOG.warning("Failure deleting job %s.", job['volcopyRef'])
            else:
                LOG.warning('Volume copy job for src vol %s not found.',
                            src_vol['id'])
        LOG.info('Copy job to dest vol %s completed.', dst_vol['label'])

    def create_cloned_volume(self, volume, src_vref):
        """Creates a clone of the specified volume."""
        es_vol = self._get_volume(src_vref['id'])

        es_snapshot = self._create_es_snapshot_for_clone(es_vol)

        try:
            self._create_volume_from_snapshot(volume, es_snapshot)
            self._add_volume_to_consistencygroup(volume)
        finally:
            try:
                self._client.delete_snapshot_group(es_snapshot['pitGroupRef'])
            except exception.NetAppDriverException:
                LOG.warning("Failure deleting temp snapshot %s.",
                            es_snapshot['id'])

    def delete_volume(self, volume):
        """Deletes a volume."""
        try:
            vol = self._get_volume(volume['name_id'])
            self._client.delete_volume(vol['volumeRef'])
        except (exception.NetAppDriverException, exception.VolumeNotFound):
            LOG.warning("Volume %s already deleted.", volume['id'])
            return

    def _is_cgsnapshot(self, snapshot_image):
        """Determine if an E-Series snapshot image is part of a cgsnapshot"""
        cg_id = snapshot_image.get('consistencyGroupId')
        # A snapshot that is not part of a consistency group may have a
        # cg_id of either none or a string of all 0's, so we check for both
        return not (cg_id is None or utils.NULL_REF == cg_id)

    def _create_snapshot_volume(self, image):
        """Creates snapshot volume for given group with snapshot_id."""
        group = self._get_snapshot_group(image['pitGroupRef'])

        LOG.debug("Creating snap vol for group %s", group['label'])

        label = utils.convert_uuid_to_es_fmt(uuid.uuid4())

        if self._is_cgsnapshot(image):
            return self._client.create_cg_snapshot_view(
                image['consistencyGroupId'], label, image['id'])
        else:
            return self._client.create_snapshot_volume(
                image['pitRef'], label, image['baseVol'])

    def _create_snapshot_group(self, label, volume, percentage_capacity=20.0):
        """Define a new snapshot group for a volume

        :param label: the label for the snapshot group
        :param volume: an E-Series volume
        :param percentage_capacity: an optional repository percentage
        :return: a new snapshot group
        """

        # Newer versions of the REST API are capable of automatically finding
        # the best pool candidate
        if not self._client.features.REST_1_3_RELEASE:
            vol_size_gb = int(volume['totalSizeInBytes']) / units.Gi
            pools = self._get_sorted_available_storage_pools(vol_size_gb)
            volume_pool = next(pool for pool in pools if volume[
                'volumeGroupRef'] == pool['id'])

            # A disk pool can only utilize a candidate from its own pool
            if volume_pool.get('raidLevel') == 'raidDiskPool':
                pool_id_to_use = volume_pool['volumeGroupRef']

            # Otherwise, choose the best available pool
            else:
                pool_id_to_use = pools[0]['volumeGroupRef']
            group = self._client.create_snapshot_group(
                label, volume['volumeRef'], pool_id_to_use,
                repo_percent=percentage_capacity)

        else:
            group = self._client.create_snapshot_group(
                label, volume['volumeRef'], repo_percent=percentage_capacity)

        return group

    def _get_snapshot_groups_for_volume(self, vol):
        """Find all snapshot groups associated with an E-Series volume

        :param vol: An E-Series volume object
        :return: A list of snapshot groups
        :raise NetAppDriverException: if the list of snapshot groups cannot be
        retrieved
        """
        return [grp for grp in self._client.list_snapshot_groups()
                if grp['baseVolume'] == vol['id']]

    def _get_available_snapshot_group(self, vol):
        """Find a snapshot group that has remaining capacity for snapshots.

        In order to minimize repository usage, we prioritize the snapshot
        group with remaining snapshot capacity that has most recently had a
        snapshot defined on it.

        :param vol: An E-Series volume object
        :return: A valid snapshot group that has available snapshot capacity,
         or None
        :raise NetAppDriverException: if the list of snapshot groups cannot be
        retrieved
        """
        groups_for_v = self._get_snapshot_groups_for_volume(vol)

        # Filter out reserved snapshot groups
        groups = [g for g in groups_for_v
                  if self.SNAPSHOT_VOL_COPY_SUFFIX not in g['label']]

        # Filter out groups that are part of a consistency group
        groups = [g for g in groups if not g['consistencyGroup']]
        # Find all groups with free snapshot capacity
        groups = [group for group in groups if group.get('snapshotCount') <
                  self.MAX_SNAPSHOT_COUNT]

        # Order by the last defined snapshot on the group
        if len(groups) > 1:
            group_by_id = {g['id']: g for g in groups}

            snap_imgs = list()
            for group in groups:
                try:
                    snap_imgs.append(
                        self._get_latest_image_in_snapshot_group(group['id']))
                except exception.NotFound:
                    pass

            snap_imgs = sorted(snap_imgs, key=lambda x: x['pitSequenceNumber'])

            if snap_imgs:
                # The newest image
                img = snap_imgs[-1]
                return group_by_id[img['pitGroupRef']]
            else:
                return groups[0] if groups else None

        # Skip the snapshot image checks if there is only one snapshot group
        elif groups:
            return groups[0]
        else:
            return None

    def _create_es_snapshot_for_clone(self, vol):
        group_name = (utils.convert_uuid_to_es_fmt(uuid.uuid4()) +
                      self.SNAPSHOT_VOL_COPY_SUFFIX)
        return self._create_es_snapshot(vol, group_name)

    def _create_es_snapshot(self, vol, group_name=None):
        snap_grp, snap_image = None, None
        try:
            snap_grp = self._get_available_snapshot_group(vol)
            # If a snapshot group is not available, create one if possible
            if snap_grp is None:
                snap_groups_for_vol = self._get_snapshot_groups_for_volume(
                    vol)

                # We need a reserved snapshot group
                if (group_name is not None and
                        (self.SNAPSHOT_VOL_COPY_SUFFIX in group_name)):

                    # First we search for an existing reserved group
                    for grp in snap_groups_for_vol:
                        if grp['label'].endswith(
                                self.SNAPSHOT_VOL_COPY_SUFFIX):
                            snap_grp = grp
                            break

                    # No reserved group exists, so we create it
                    if (snap_grp is None and
                            (len(snap_groups_for_vol) <
                             self.MAX_SNAPSHOT_GROUP_COUNT)):
                        snap_grp = self._create_snapshot_group(group_name,
                                                               vol)

                # Ensure we don't exceed the snapshot group limit
                elif (len(snap_groups_for_vol) <
                      (self.MAX_SNAPSHOT_GROUP_COUNT -
                       self.RESERVED_SNAPSHOT_GROUP_COUNT)):

                    label = group_name if group_name is not None else (
                        utils.convert_uuid_to_es_fmt(uuid.uuid4()))

                    snap_grp = self._create_snapshot_group(label, vol)
                    LOG.info("Created snap grp with label %s.", label)

                # We couldn't retrieve or create a snapshot group
                if snap_grp is None:
                    raise exception.SnapshotLimitExceeded(
                        allowed=(self.MAX_SNAPSHOT_COUNT *
                                 (self.MAX_SNAPSHOT_GROUP_COUNT -
                                  self.RESERVED_SNAPSHOT_GROUP_COUNT)))

            return self._client.create_snapshot_image(
                snap_grp['id'])

        except exception.NetAppDriverException:
            with excutils.save_and_reraise_exception():
                if snap_image is None and snap_grp:
                    self._delete_snapshot_group(snap_grp['id'])

    def create_snapshot(self, snapshot):
        """Creates a snapshot.

        :param snapshot: The Cinder snapshot
        :param group_name: An optional label for the snapshot group
        :returns: An E-Series snapshot image
        """

        os_vol = snapshot['volume']
        vol = self._get_volume(os_vol['name_id'])

        snap_image = cinder_utils.synchronized(vol['id'])(
            self._create_es_snapshot)(vol)
        model_update = {
            'provider_id': snap_image['id']
        }

        return model_update

    def _delete_es_snapshot(self, es_snapshot):
        """Perform a soft-delete on an E-Series snapshot.

        Mark the snapshot image as no longer needed, so that it can be
        purged from the backend when no other snapshots are dependent upon it.

        :param es_snapshot: an E-Series snapshot image
        :return: None
        """
        index = self._get_soft_delete_map()
        snapgroup_ref = es_snapshot['pitGroupRef']
        if snapgroup_ref in index:
            bitset = na_utils.BitSet(int((index[snapgroup_ref])))
        else:
            bitset = na_utils.BitSet(0)

        images = [img for img in self._client.list_snapshot_images() if
                  img['pitGroupRef'] == snapgroup_ref]
        for i, image in enumerate(sorted(images, key=lambda x: x[
                'pitSequenceNumber'])):
            if(image['pitSequenceNumber'] == es_snapshot[
                    'pitSequenceNumber']):
                bitset.set(i)
                break

        index_update, keys_to_del = (
            self._cleanup_snapshot_images(images, bitset))

        self._merge_soft_delete_changes(index_update, keys_to_del)

    def delete_snapshot(self, snapshot):
        """Delete a snapshot."""
        try:
            es_snapshot = self._get_snapshot(snapshot)
        except exception.NotFound:
            LOG.warning("Snapshot %s already deleted.", snapshot['id'])
        else:
            os_vol = snapshot['volume']
            vol = self._get_volume(os_vol['name_id'])

            cinder_utils.synchronized(vol['id'])(self._delete_es_snapshot)(
                es_snapshot)

    def _get_soft_delete_map(self):
        """Retrieve the snapshot index from the storage backend"""
        return self._client.list_backend_store(
            self.SNAPSHOT_PERSISTENT_STORE_KEY)

    @cinder_utils.synchronized(SNAPSHOT_PERSISTENT_STORE_LOCK)
    def _merge_soft_delete_changes(self, index_update, keys_to_del):
        """Merge changes to the snapshot index and save it on the backend

        This method merges provided changes into the index, locking, to ensure
        that concurrent changes that don't overlap are not overwritten. No
        update will occur if neither an update or keys to delete are provided.

        :param index_update: a dict of keys/value pairs to update in the index
        :param keys_to_del: a list of keys to purge from the index
        """
        if index_update or keys_to_del:
            index = self._get_soft_delete_map()
            if index_update:
                index.update(index_update)
            if keys_to_del:
                for key in keys_to_del:
                    if key in index:
                        del index[key]

            self._client.save_backend_store(
                self.SNAPSHOT_PERSISTENT_STORE_KEY, index)

    def _cleanup_snapshot_images(self, images, bitset):
        """Delete snapshot images that are marked for removal from the backend.

        This method will iterate over all snapshots (beginning with the
        oldest), that are defined on the same snapshot group as the provided
        snapshot image. If the snapshot is marked for deletion, it will be
        purged from the backend. Otherwise, the method will return because
        no further snapshots can be purged.

        The bitset will be updated based on the return from this method.
        Any updates to the index will be provided as a dict, and any keys
        to be removed from the index should be returned as (dict, list).

        :param images: a list of E-Series snapshot images
        :param bitset: a bitset representing the snapshot images that are
        no longer needed on the backend (and may be deleted when possible)
        :return (dict, list): a tuple containing a dict of updates for the
        index and a list of keys to remove from the index
        """
        snap_grp_ref = images[0]['pitGroupRef']
        # All images are marked as deleted, we can delete the snapshot group
        if bitset == 2 ** len(images) - 1:
            try:
                self._delete_snapshot_group(snap_grp_ref)
            except exception.NetAppDriverException as e:
                LOG.warning("Unable to remove snapshot group - %s.", e.msg)
            return None, [snap_grp_ref]
        else:
            # Order by their sequence number, from oldest to newest
            snapshots = sorted(images,
                               key=lambda x: x['pitSequenceNumber'])
            deleted = 0

            for i, snapshot in enumerate(snapshots):
                if bitset.is_set(i):
                    self._delete_snapshot_image(snapshot)
                    deleted += 1
                else:
                    # Snapshots must be deleted in order, so if the current
                    # snapshot is not pending deletion, we don't want to
                    # process any more
                    break

            if deleted:
                # Update the bitset based on the deleted snapshots
                bitset >>= deleted
                LOG.debug('Deleted %(count)s snapshot images from snapshot '
                          'group: %(grp)s.', {'count': deleted,
                                              'grp': snap_grp_ref})
                if deleted >= len(images):
                    try:
                        self._delete_snapshot_group(snap_grp_ref)
                    except exception.NetAppDriverException as e:
                        LOG.warning("Unable to remove snapshot group - %s.",
                                    e.msg)
                    return None, [snap_grp_ref]

            return {snap_grp_ref: repr(bitset)}, None

    def _delete_snapshot_group(self, group_id):
        try:
            self._client.delete_snapshot_group(group_id)
        except eseries_exc.WebServiceException as e:
            raise exception.NetAppDriverException(e.msg)

    def _delete_snapshot_image(self, es_snapshot):
        """Remove a snapshot image from the storage backend

        If a snapshot group has no remaining snapshot images associated with
        it, it will be deleted as well. When the snapshot is deleted,
        any snapshot volumes that are associated with it will be orphaned,
        so they are also deleted.

        :param es_snapshot: An E-Series snapshot image
        :param snapshot_volumes: Snapshot volumes associated with the snapshot
        """
        self._client.delete_snapshot_image(es_snapshot['id'])

    def ensure_export(self, context, volume):
        """Synchronously recreates an export for a volume."""
        pass

    def create_export(self, context, volume):
        """Exports the volume."""
        pass

    def remove_export(self, context, volume):
        """Removes an export for a volume."""
        pass

    def map_volume_to_host(self, volume, eseries_volume, initiators):
        """Ensures the specified initiator has access to the volume."""
        existing_maps = self._client.get_volume_mappings_for_volume(
            eseries_volume)
        host = self._get_or_create_host(initiators, self.host_type)
        # There can only be one or zero mappings on a volume in E-Series
        current_map = existing_maps[0] if existing_maps else None

        if self.configuration.netapp_enable_multiattach and current_map:
            self._ensure_multi_attach_host_group_exists()
            mapping = host_mapper.map_volume_to_multiple_hosts(self._client,
                                                               volume,
                                                               eseries_volume,
                                                               host,
                                                               current_map)
        else:
            mapping = host_mapper.map_volume_to_single_host(
                self._client, volume, eseries_volume, host, current_map,
                self.configuration.netapp_enable_multiattach)
        return mapping

    def initialize_connection_fc(self, volume, connector):
        """Initializes the connection and returns connection info.

        Assigns the specified volume to a compute node/host so that it can be
        used from that host.

        The driver returns a driver_volume_type of 'fibre_channel'.
        The target_wwn can be a single entry or a list of wwns that
        correspond to the list of remote wwn(s) that will export the volume.
        Example return values:

        .. code-block:: python

            {
                'driver_volume_type': 'fibre_channel'
                'data': {
                    'target_discovered': True,
                    'target_lun': 1,
                    'target_wwn': '500a098280feeba5',
                    'initiator_target_map': {
                        '21000024ff406cc3': ['500a098280feeba5'],
                        '21000024ff406cc2': ['500a098280feeba5']
                    }
                }
            }

        or

        .. code-block:: python

             {
                'driver_volume_type': 'fibre_channel'
                'data': {
                    'target_discovered': True,
                    'target_lun': 1,
                    'target_wwn': ['500a098280feeba5', '500a098290feeba5',
                                   '500a098190feeba5', '500a098180feeba5'],
                    'initiator_target_map': {
                        '21000024ff406cc3': ['500a098280feeba5',
                                             '500a098290feeba5'],
                        '21000024ff406cc2': ['500a098190feeba5',
                                             '500a098180feeba5']
                    }
                }
            }
        """

        initiators = [fczm_utils.get_formatted_wwn(wwpn)
                      for wwpn in connector['wwpns']]

        eseries_vol = self._get_volume(volume['name_id'])
        mapping = self.map_volume_to_host(volume, eseries_vol,
                                          initiators)
        lun_id = mapping['lun']

        initiator_info = self._build_initiator_target_map_fc(connector)
        target_wwpns, initiator_target_map, num_paths = initiator_info

        if target_wwpns:
            msg = ("Successfully fetched target details for LUN %(id)s "
                   "and initiator(s) %(initiators)s.")
            msg_fmt = {'id': volume['id'], 'initiators': initiators}
            LOG.debug(msg, msg_fmt)
        else:
            msg = _('Failed to get LUN target details for the LUN %s.')
            raise exception.VolumeBackendAPIException(data=msg % volume['id'])

        target_info = {'driver_volume_type': 'fibre_channel',
                       'data': {'target_discovered': True,
                                'target_lun': int(lun_id),
                                'target_wwn': target_wwpns,
                                'initiator_target_map': initiator_target_map}}

        return target_info

    def terminate_connection_fc(self, volume, connector, **kwargs):
        """Disallow connection from connector.

        Return empty data if other volumes are in the same zone.
        The FibreChannel ZoneManager doesn't remove zones
        if there isn't an initiator_target_map in the
        return of terminate_connection.

        :returns: data - the target_wwns and initiator_target_map if the
                         zone is to be removed, otherwise the same map with
                         an empty dict for the 'data' key
        """

        eseries_vol = self._get_volume(volume['name_id'])
        initiators = [fczm_utils.get_formatted_wwn(wwpn)
                      for wwpn in connector['wwpns']]
        host = self._get_host_with_matching_port(initiators)
        mappings = eseries_vol.get('listOfMappings', [])

        # There can only be one or zero mappings on a volume in E-Series
        mapping = mappings[0] if mappings else None

        if not mapping:
            raise eseries_exc.VolumeNotMapped(volume_id=volume['id'],
                                              host=host['label'])
        host_mapper.unmap_volume_from_host(self._client, volume, host, mapping)

        info = {'driver_volume_type': 'fibre_channel',
                'data': {}}

        if len(self._client.get_volume_mappings_for_host(
                host['hostRef'])) == 0:
            # No more exports for this host, so tear down zone.
            LOG.info("Need to remove FC Zone, building initiator target map.")

            initiator_info = self._build_initiator_target_map_fc(connector)
            target_wwpns, initiator_target_map, num_paths = initiator_info

            info['data'] = {'target_wwn': target_wwpns,
                            'initiator_target_map': initiator_target_map}

        return info

    def _build_initiator_target_map_fc(self, connector):
        """Build the target_wwns and the initiator target map."""

        # get WWPNs from controller and strip colons
        all_target_wwpns = self._client.list_target_wwpns()
        all_target_wwpns = [six.text_type(wwpn).replace(':', '')
                            for wwpn in all_target_wwpns]

        target_wwpns = []
        init_targ_map = {}
        num_paths = 0

        if self.lookup_service:
            # Use FC SAN lookup to determine which ports are visible.
            dev_map = self.lookup_service.get_device_mapping_from_network(
                connector['wwpns'],
                all_target_wwpns)

            for fabric_name in dev_map:
                fabric = dev_map[fabric_name]
                target_wwpns += fabric['target_port_wwn_list']
                for initiator in fabric['initiator_port_wwn_list']:
                    if initiator not in init_targ_map:
                        init_targ_map[initiator] = []
                    init_targ_map[initiator] += fabric['target_port_wwn_list']
                    init_targ_map[initiator] = list(set(
                        init_targ_map[initiator]))
                    for target in init_targ_map[initiator]:
                        num_paths += 1
            target_wwpns = list(set(target_wwpns))
        else:
            initiator_wwns = connector['wwpns']
            target_wwpns = all_target_wwpns

            for initiator in initiator_wwns:
                init_targ_map[initiator] = target_wwpns

        return target_wwpns, init_targ_map, num_paths

    def initialize_connection_iscsi(self, volume, connector):
        """Allow connection to connector and return connection info."""
        initiator_name = connector['initiator']
        eseries_vol = self._get_volume(volume['name_id'])
        mapping = self.map_volume_to_host(volume, eseries_vol,
                                          [initiator_name])

        lun_id = mapping['lun']
        msg_fmt = {'id': volume['id'], 'initiator_name': initiator_name}
        LOG.debug("Mapped volume %(id)s to the initiator %(initiator_name)s.",
                  msg_fmt)

        iscsi_details = self._get_iscsi_service_details()
        iscsi_portal = self._get_iscsi_portal_for_vol(eseries_vol,
                                                      iscsi_details)
        LOG.debug("Successfully fetched target details for volume %(id)s and "
                  "initiator %(initiator_name)s.", msg_fmt)
        iqn = iscsi_portal['iqn']
        address = iscsi_portal['ip']
        port = iscsi_portal['tcp_port']
        properties = na_utils.get_iscsi_connection_properties(lun_id, volume,
                                                              iqn, address,
                                                              port)
        if self.configuration.use_chap_auth:
            if self._client.features.CHAP_AUTHENTICATION:
                chap_username, chap_password = self._configure_chap(iqn)
                properties['data']['auth_username'] = chap_username
                properties['data']['auth_password'] = chap_password
                properties['data']['auth_method'] = 'CHAP'
                properties['data']['discovery_auth_username'] = chap_username
                properties['data']['discovery_auth_password'] = chap_password
                properties['data']['discovery_auth_method'] = 'CHAP'
            else:
                msg = _("E-series proxy API version %(current_version)s does "
                        "not support CHAP authentication. The proxy version "
                        "must be at least %(min_version)s.")
                min_version = (self._client.features.
                               CHAP_AUTHENTICATION.minimum_version)
                msg = msg % {'current_version': self._client.api_version,
                             'min_version': min_version}

                LOG.info(msg)
                raise exception.NetAppDriverException(msg)
        return properties

    def _configure_chap(self, target_iqn):
        chap_username = self.DEFAULT_CHAP_USER_NAME
        chap_password = volume_utils.generate_password()
        self._client.set_chap_authentication(target_iqn,
                                             chap_username,
                                             chap_password)
        return chap_username, chap_password

    def _get_iscsi_service_details(self):
        """Gets iscsi iqn, ip and port information."""
        ports = []
        hw_inventory = self._client.list_hardware_inventory()
        iscsi_ports = hw_inventory.get('iscsiPorts')
        if iscsi_ports:
            for port in iscsi_ports:
                if (port.get('ipv4Enabled') and port.get('iqn') and
                        port.get('ipv4Data') and
                        port['ipv4Data'].get('ipv4AddressData') and
                        port['ipv4Data']['ipv4AddressData']
                        .get('ipv4Address') and port['ipv4Data']
                        ['ipv4AddressData'].get('configState')
                        == 'configured'):
                    iscsi_det = {}
                    iscsi_det['ip'] =\
                        port['ipv4Data']['ipv4AddressData']['ipv4Address']
                    iscsi_det['iqn'] = port['iqn']
                    iscsi_det['tcp_port'] = port.get('tcpListenPort')
                    iscsi_det['controller'] = port.get('controllerId')
                    ports.append(iscsi_det)
        if not ports:
            msg = _('No good iscsi portals found for %s.')
            raise exception.NetAppDriverException(
                msg % self._client.get_system_id())
        return ports

    def _get_iscsi_portal_for_vol(self, volume, portals, anyController=True):
        """Get the iscsi portal info relevant to volume."""
        for portal in portals:
            if portal.get('controller') == volume.get('currentManager'):
                return portal
        if anyController and portals:
            return portals[0]
        msg = _('No good iscsi portal found in supplied list for %s.')
        raise exception.NetAppDriverException(
            msg % self._client.get_system_id())

    def _get_or_create_host(self, port_ids, host_type):
        """Fetch or create a host by given port."""
        try:
            host = self._get_host_with_matching_port(port_ids)
            ht_def = self._get_host_type_definition(host_type)
            if host.get('hostTypeIndex') != ht_def.get('index'):
                try:
                    host = self._client.update_host_type(
                        host['hostRef'], ht_def)
                except exception.NetAppDriverException as e:
                    LOG.warning("Unable to update host type for host with "
                                "label %(l)s. %(e)s",
                                {'l': host['label'], 'e': e.msg})
            return host
        except exception.NotFound as e:
            LOG.warning("Message - %s.", e.msg)
            return self._create_host(port_ids, host_type)

    def _get_host_with_matching_port(self, port_ids):
        """Gets or creates a host with given port id."""
        # Remove any extra colons
        port_ids = [six.text_type(wwpn).replace(':', '')
                    for wwpn in port_ids]
        hosts = self._client.list_hosts()
        for port_id in port_ids:
            for host in hosts:
                if host.get('hostSidePorts'):
                    ports = host.get('hostSidePorts')
                    for port in ports:
                        address = port.get('address').upper().replace(':', '')
                        if address == port_id.upper():
                            return host
        msg = _("Host with ports %(ports)s not found.")
        raise exception.NotFound(msg % {'ports': port_ids})

    def _create_host(self, port_ids, host_type, host_group=None):
        """Creates host on system with given initiator as port_id."""
        LOG.info("Creating host with ports %s.", port_ids)
        host_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
        host_type = self._get_host_type_definition(host_type)
        port_type = self.driver_protocol.lower()
        return self._client.create_host_with_ports(host_label,
                                                   host_type,
                                                   port_ids,
                                                   group_id=host_group,
                                                   port_type=port_type)

    def _get_host_type_definition(self, host_type):
        """Gets supported host type if available on storage system."""
        host_types = self._client.list_host_types()
        for ht in host_types:
            if ht.get('name', 'unknown').lower() == host_type.lower():
                return ht
        raise exception.NotFound(_("Host type %s not supported.") % host_type)

    def terminate_connection_iscsi(self, volume, connector, **kwargs):
        """Disallow connection from connector."""
        eseries_vol = self._get_volume(volume['name_id'])
        initiator = connector['initiator']
        host = self._get_host_with_matching_port([initiator])
        mappings = eseries_vol.get('listOfMappings', [])

        # There can only be one or zero mappings on a volume in E-Series
        mapping = mappings[0] if mappings else None

        if not mapping:
            raise eseries_exc.VolumeNotMapped(volume_id=volume['id'],
                                              host=host['label'])
        host_mapper.unmap_volume_from_host(self._client, volume, host, mapping)

    def get_volume_stats(self, refresh=False):
        """Return the current state of the volume service."""
        if refresh:
            if not self._ssc_stats:
                self._update_ssc_info()
            self._update_volume_stats()

        return self._stats

    def _update_volume_stats(self):
        """Update volume statistics."""
        LOG.debug("Updating volume stats.")
        data = dict()
        data["volume_backend_name"] = self._backend_name
        data["vendor_name"] = "NetApp"
        data["driver_version"] = self.VERSION
        data["storage_protocol"] = self.driver_protocol
        data["pools"] = []
        storage_volumes = self._client.list_volumes()

        for storage_pool in self._get_storage_pools():
            cinder_pool = {}
            cinder_pool["pool_name"] = storage_pool.get("label")
            cinder_pool["QoS_support"] = False
            cinder_pool["reserved_percentage"] = (
                self.configuration.reserved_percentage)
            cinder_pool["max_over_subscription_ratio"] = (
                self.configuration.max_over_subscription_ratio)
            tot_bytes = int(storage_pool.get("totalRaidedSpace", 0))
            used_bytes = int(storage_pool.get("usedSpace", 0))

            provisioned_capacity = 0
            for volume in storage_volumes:
                if (volume["volumeGroupRef"] == storage_pool.get('id') and
                        not volume['label'].startswith('repos_')):
                    provisioned_capacity += float(volume["capacity"])

            cinder_pool["provisioned_capacity_gb"] = (provisioned_capacity /
                                                      units.Gi)
            cinder_pool["free_capacity_gb"] = ((tot_bytes - used_bytes) /
                                               units.Gi)
            cinder_pool["total_capacity_gb"] = tot_bytes / units.Gi

            pool_ssc_stats = self._ssc_stats.get(
                storage_pool["volumeGroupRef"])

            if pool_ssc_stats:
                thin = pool_ssc_stats.get(self.THIN_UQ_SPEC) or False
                cinder_pool.update(pool_ssc_stats)
            else:
                thin = False
            cinder_pool["thin_provisioning_support"] = thin
            # All E-Series pools support thick provisioning
            cinder_pool["thick_provisioning_support"] = True

            data["pools"].append(cinder_pool)

        self._stats = data
        self._garbage_collect_tmp_vols()

    def _create_asup(self, cinder_host):
        if not self._client.features.AUTOSUPPORT:
            LOG.info("E-series proxy API version %s does not support "
                     "autosupport logging.", self._client.api_version)
            return

        event_source = ("Cinder driver %s" % self.DRIVER_NAME)
        category = "provisioning"
        event_description = "OpenStack Cinder connected to E-Series proxy"
        asup_info = self._client.get_asup_info()
        model = asup_info.get('model')
        firmware_version = asup_info.get('firmware_version')
        serial_numbers = asup_info.get('serial_numbers')
        chassis_sn = asup_info.get('chassis_sn')

        key = ("openstack-%s-%s-%s"
               % (cinder_host, serial_numbers[0], serial_numbers[1]))

        # The counter is being set here to a key-value combination
        # comprised of serial numbers and cinder host with a default
        # heartbeat of 1. The counter is set to inform the user that the
        # key does not have a stale value.
        self._client.set_counter("%s-heartbeat" % key, value=1)
        data = {
            'computer-name': cinder_host,
            'event-source': event_source,
            'app-version': self._app_version,
            'category': category,
            'event-description': event_description,
            'controller1-serial': serial_numbers[0],
            'controller2-serial': serial_numbers[1],
            'chassis-serial-number': chassis_sn,
            'model': model,
            'system-version': firmware_version,
            'operating-mode': self._client.api_operating_mode
        }
        self._client.add_autosupport_data(key, data)

    @cinder_utils.synchronized("netapp_update_ssc_info", external=False)
    def _update_ssc_info(self):
        """Periodically runs to update ssc information from the backend.

        The self._ssc_stats attribute is updated with the following format.
        {<volume_group_ref> : {<ssc_key>: <ssc_value>}}
        """
        LOG.info("Updating storage service catalog information for "
                 "backend '%s'", self._backend_name)

        relevant_pools = self._get_storage_pools()

        if self._client.features.SSC_API_V2:
            self._update_ssc_info_v2(relevant_pools)
        else:
            self._update_ssc_info_v1(relevant_pools)

    def _update_ssc_info_v1(self, relevant_pools):
        """Update ssc data using the legacy API

        :param relevant_pools: The pools that this driver cares about
        """
        LOG.info("E-series proxy API version %(version)s does not "
                 "support full set of SSC extra specs. The proxy version"
                 " must be at at least %(min_version)s.",
                 {'version': self._client.api_version,
                  'min_version':
                      self._client.features.SSC_API_V2.minimum_version})

        self._ssc_stats = (
            self._update_ssc_disk_encryption(relevant_pools))
        self._ssc_stats = (
            self._update_ssc_disk_types(relevant_pools))
        self._ssc_stats = (
            self._update_ssc_raid_type(relevant_pools))

    def _update_ssc_info_v2(self, relevant_pools):
        """Update the ssc dictionary with ssc info for relevant pools

        :param relevant_pools: The pools that this driver cares about
        """
        ssc_stats = copy.deepcopy(self._ssc_stats)

        storage_pool_labels = [pool['label'] for pool in relevant_pools]

        ssc_data = self._client.list_ssc_storage_pools()
        ssc_data = [pool for pool in ssc_data
                    if pool['name'] in storage_pool_labels]

        for pool in ssc_data:
            poolId = pool['poolId']
            if poolId not in ssc_stats:
                ssc_stats[poolId] = {}

            pool_ssc_info = ssc_stats[poolId]

            pool_ssc_info['consistencygroup_support'] = True

            pool_ssc_info[self.ENCRYPTION_UQ_SPEC] = (
                six.text_type(pool['encrypted']).lower())

            pool_ssc_info[self.SPINDLE_SPD_UQ_SPEC] = (pool['spindleSpeed'])

            flash_cache_capable = pool['flashCacheCapable']
            pool_ssc_info[self.FLASH_CACHE_UQ_SPEC] = (
                six.text_type(flash_cache_capable).lower())

            # Data Assurance is not compatible with some backend types
            da_capable = pool['dataAssuranceCapable'] and (
                self._is_data_assurance_supported())
            pool_ssc_info[self.DA_UQ_SPEC] = (
                six.text_type(da_capable).lower())

            pool_ssc_info[self.RAID_UQ_SPEC] = (
                self.SSC_RAID_TYPE_MAPPING.get(pool['raidLevel'], 'unknown'))

            pool_ssc_info[self.THIN_UQ_SPEC] = (
                six.text_type(pool['thinProvisioningCapable']).lower())

            if pool['pool'].get("driveMediaType") == 'ssd':
                pool_ssc_info[self.DISK_TYPE_UQ_SPEC] = 'SSD'
            else:
                pool_ssc_info[self.DISK_TYPE_UQ_SPEC] = (
                    self.SSC_DISK_TYPE_MAPPING.get(
                        pool['pool'].get('drivePhysicalType'), 'unknown'))

        self._ssc_stats = ssc_stats

    def _update_ssc_disk_types(self, storage_pools):
        """Updates the given ssc dictionary with new disk type information.

        :param storage_pools: The storage pools this driver cares about
        """
        ssc_stats = copy.deepcopy(self._ssc_stats)
        all_disks = self._client.list_drives()

        pool_ids = set(pool.get("volumeGroupRef") for pool in storage_pools)

        relevant_disks = [x for x in all_disks
                          if x.get('currentVolumeGroupRef') in pool_ids]
        for drive in relevant_disks:
            current_vol_group = drive.get('currentVolumeGroupRef')
            if current_vol_group not in ssc_stats:
                ssc_stats[current_vol_group] = {}

            if drive.get("driveMediaType") == 'ssd':
                ssc_stats[current_vol_group][self.DISK_TYPE_UQ_SPEC] = 'SSD'
            else:
                disk_type = drive.get('interfaceType').get('driveType')
                ssc_stats[current_vol_group][self.DISK_TYPE_UQ_SPEC] = (
                    self.SSC_DISK_TYPE_MAPPING.get(disk_type, 'unknown'))

        return ssc_stats

    def _update_ssc_disk_encryption(self, storage_pools):
        """Updates the given ssc dictionary with new disk encryption information.

        :param storage_pools: The storage pools this driver cares about
        """
        ssc_stats = copy.deepcopy(self._ssc_stats)
        for pool in storage_pools:
            current_vol_group = pool.get('volumeGroupRef')
            if current_vol_group not in ssc_stats:
                ssc_stats[current_vol_group] = {}

            ssc_stats[current_vol_group][self.ENCRYPTION_UQ_SPEC] = (
                six.text_type(pool['securityType'] == 'enabled').lower()
            )

        return ssc_stats

    def _update_ssc_raid_type(self, storage_pools):
        """Updates the given ssc dictionary with new RAID type information.

        :param storage_pools: The storage pools this driver cares about
        """
        ssc_stats = copy.deepcopy(self._ssc_stats)
        for pool in storage_pools:
            current_vol_group = pool.get('volumeGroupRef')
            if current_vol_group not in ssc_stats:
                ssc_stats[current_vol_group] = {}

            raid_type = pool.get('raidLevel')
            ssc_stats[current_vol_group]['netapp_raid_type'] = (
                self.SSC_RAID_TYPE_MAPPING.get(raid_type, 'unknown'))

        return ssc_stats

    def _get_storage_pools(self):
        """Retrieve storage pools that match user-configured search pattern."""

        # Inform deprecation of legacy option.
        if self.configuration.safe_get('netapp_storage_pools'):
            msg = ("The option 'netapp_storage_pools' 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)

        storage_pools = self._client.list_storage_pools()

        filtered_pools = []
        for pool in storage_pools:
            pool_name = pool['label']

            if pool_regex.match(pool_name):
                msg = ("Pool '%(pool_name)s' matches against regular "
                       "expression: %(pool_pattern)s")
                LOG.debug(msg, {'pool_name': pool_name,
                                'pool_pattern': pool_regex.pattern})
                filtered_pools.append(pool)
            else:
                msg = ("Pool '%(pool_name)s' does not match against regular "
                       "expression: %(pool_pattern)s")
                LOG.debug(msg, {'pool_name': pool_name,
                                'pool_pattern': pool_regex.pattern})

        return filtered_pools

    def _get_sorted_available_storage_pools(self, size_gb):
        """Returns storage pools sorted on available capacity."""
        size = size_gb * units.Gi
        sorted_pools = sorted(self._get_storage_pools(), key=lambda x:
                              (int(x.get('totalRaidedSpace', 0))
                               - int(x.get('usedSpace', 0))), reverse=True)
        avl_pools = filter(lambda x: ((int(x.get('totalRaidedSpace', 0)) -
                                       int(x.get('usedSpace', 0)) >= size)),
                           sorted_pools)

        if not avl_pools:
            LOG.warning("No storage pool found with available capacity %s.",
                        size_gb)
        return avl_pools

    def _is_thin_provisioned(self, volume):
        """Determine if a volume is thin provisioned"""
        return volume.get('objectType') == 'thinVolume' or volume.get(
            'thinProvisioned', False)

    def _get_pool_operation_progress(self, pool_id, action=None):
        """Retrieve the progress of a long running operation on a pool

        The return will be a tuple containing: a bool representing whether
        or not the operation is complete, a set of actions that are
        currently running on the storage pool, and the estimated time
        remaining in minutes.

        An action type may be passed in such that once no actions of that type
        remain active on the pool, the operation will be considered
        completed. If no action str is passed in, it is assumed that
        multiple actions compose the operation, and none are terminal,
        so the operation will not be considered completed until there are no
        actions remaining to be completed on any volume on the pool.

        :param pool_id: The id of a storage pool
        :param action: The anticipated action
        :returns: A tuple (bool, set(str), int)
        """
        actions = set()
        eta = 0
        for progress in self._client.get_pool_operation_progress(pool_id):
            actions.add(progress.get('currentAction'))
            eta += progress.get('estimatedTimeToCompletion', 0)
        if action is not None:
            complete = action not in actions
        else:
            complete = not actions
        return complete, actions, eta

    def extend_volume(self, volume, new_size):
        """Extend an existing volume to the new size."""
        src_vol = self._get_volume(volume['name_id'])
        thin_provisioned = self._is_thin_provisioned(src_vol)
        self._client.expand_volume(src_vol['id'], new_size, thin_provisioned)

        # If the volume is thin or defined on a disk pool, there is no need
        # to block.
        if not (thin_provisioned or src_vol.get('diskPool')):
            # Wait for the expansion to start

            def check_progress():
                complete, actions, eta = (
                    self._get_pool_operation_progress(src_vol[
                                                      'volumeGroupRef'],
                                                      'remappingDve'))
                if complete:
                    raise loopingcall.LoopingCallDone()
                else:
                    LOG.info("Waiting for volume expansion of %(vol)s to "
                             "complete, current remaining actions are "
                             "%(action)s. ETA: %(eta)s mins.",
                             {'vol': volume['name_id'],
                              'action': ', '.join(actions), 'eta': eta})

            checker = loopingcall.FixedIntervalLoopingCall(
                check_progress)

            checker.start(interval=self.SLEEP_SECS,
                          initial_delay=self.SLEEP_SECS,
                          stop_on_exception=True).wait()

    def create_cgsnapshot(self, cgsnapshot, snapshots):
        """Creates a cgsnapshot."""
        cg_id = cgsnapshot['consistencygroup_id']
        cg_name = utils.convert_uuid_to_es_fmt(cg_id)

        # Retrieve the E-Series consistency group
        es_cg = self._get_consistencygroup_by_name(cg_name)

        # Define an E-Series CG Snapshot
        es_snaphots = self._client.create_consistency_group_snapshot(
            es_cg['id'])

        # Build the snapshot updates
        snapshot_updates = list()
        for snap in snapshots:
            es_vol = self._get_volume(snap['volume']['id'])
            for es_snap in es_snaphots:
                if es_snap['baseVol'] == es_vol['id']:
                    snapshot_updates.append({
                        'id': snap['id'],
                        # Directly track the backend snapshot ID
                        'provider_id': es_snap['id'],
                        'status': 'available'
                    })

        return None, snapshot_updates

    def delete_cgsnapshot(self, cgsnapshot, snapshots):
        """Deletes a cgsnapshot."""

        cg_id = cgsnapshot['consistencygroup_id']
        cg_name = utils.convert_uuid_to_es_fmt(cg_id)

        # Retrieve the E-Series consistency group
        es_cg = self._get_consistencygroup_by_name(cg_name)

        # Find the smallest sequence number defined on the group
        min_seq_num = min(es_cg['uniqueSequenceNumber'])

        es_snapshots = self._client.get_consistency_group_snapshots(
            es_cg['id'])
        es_snap_ids = set(snap.get('provider_id') for snap in snapshots)

        # We need to find a single snapshot that is a part of the CG snap
        seq_num = None
        for snap in es_snapshots:
            if snap['id'] in es_snap_ids:
                seq_num = snap['pitSequenceNumber']
                break

        if seq_num is None:
            raise exception.CgSnapshotNotFound(cgsnapshot_id=cg_id)

        # Perform a full backend deletion of the cgsnapshot
        if int(seq_num) <= int(min_seq_num):
            self._client.delete_consistency_group_snapshot(
                es_cg['id'], seq_num)
            return None, None
        else:
            # Perform a soft-delete, removing this snapshot from cinder
            # management, and marking it as available for deletion.
            return cinder_utils.synchronized(cg_id)(
                self._soft_delete_cgsnapshot)(
                es_cg, seq_num)

    def _soft_delete_cgsnapshot(self, es_cg, snap_seq_num):
        """Mark a cgsnapshot as available for deletion from the backend.

        E-Series snapshots cannot be deleted out of order, as older
        snapshots in the snapshot group are dependent on the newer
        snapshots. A "soft delete" results in the cgsnapshot being removed
        from Cinder management, with the snapshot marked as available for
        deletion once all snapshots dependent on it are also deleted.

        :param es_cg: E-Series consistency group
        :param snap_seq_num: unique sequence number of the cgsnapshot
        :return: an update to the snapshot index
        """

        index = self._get_soft_delete_map()
        cg_ref = es_cg['id']
        if cg_ref in index:
            bitset = na_utils.BitSet(int((index[cg_ref])))
        else:
            bitset = na_utils.BitSet(0)

        seq_nums = (
            set([snap['pitSequenceNumber'] for snap in
                 self._client.get_consistency_group_snapshots(cg_ref)]))

        # Determine the relative index of the snapshot's sequence number
        for i, seq_num in enumerate(sorted(seq_nums)):
            if snap_seq_num == seq_num:
                bitset.set(i)
                break

        index_update = (
            self._cleanup_cg_snapshots(cg_ref, seq_nums, bitset))

        self._merge_soft_delete_changes(index_update, None)

        return None, None

    def _cleanup_cg_snapshots(self, cg_ref, seq_nums, bitset):
        """Delete cg snapshot images that are marked for removal

        The snapshot index tracks all snapshots that have been removed from
        Cinder, and are therefore available for deletion when this operation
        is possible.

        CG snapshots are tracked by unique sequence numbers that are
        associated with 1 or more snapshot images. The sequence numbers are
        tracked (relative to the 32 images allowed per group), within the
        snapshot index.

        This method will purge CG snapshots that have been marked as
        available for deletion within the backend persistent store.

        :param cg_ref: reference to an E-Series consistent group
        :param seq_nums: set of unique sequence numbers associated with the
        consistency group
        :param bitset: the bitset representing which sequence numbers are
        marked for deletion
        :return: update for the snapshot index
        """
        deleted = 0
        # Order by their sequence number, from oldest to newest
        for i, seq_num in enumerate(sorted(seq_nums)):
            if bitset.is_set(i):
                self._client.delete_consistency_group_snapshot(cg_ref,
                                                               seq_num)
                deleted += 1
            else:
                # Snapshots must be deleted in order, so if the current
                # snapshot is not pending deletion, we don't want to
                # process any more
                break

        if deleted:
            # We need to update the bitset to reflect the fact that older
            # snapshots have been deleted, so snapshot relative indexes
            # have now been updated.
            bitset >>= deleted

            LOG.debug('Deleted %(count)s snapshot images from '
                      'consistency group: %(grp)s.', {'count': deleted,
                                                      'grp': cg_ref})
        # Update the index
        return {cg_ref: repr(bitset)}

    def create_consistencygroup(self, cinder_cg):
        """Define a consistency group."""
        self._create_consistency_group(cinder_cg)

        return {'status': 'available'}

    def _create_consistency_group(self, cinder_cg):
        """Define a new consistency group on the E-Series backend"""
        name = utils.convert_uuid_to_es_fmt(cinder_cg['id'])
        return self._client.create_consistency_group(name)

    def _get_consistencygroup(self, cinder_cg):
        """Retrieve an E-Series consistency group"""
        name = utils.convert_uuid_to_es_fmt(cinder_cg['id'])
        return self._get_consistencygroup_by_name(name)

    def _get_consistencygroup_by_name(self, name):
        """Retrieve an E-Series consistency group by name"""

        for cg in self._client.list_consistency_groups():
            if name == cg['name']:
                return cg

        raise exception.ConsistencyGroupNotFound(consistencygroup_id=name)

    def delete_consistencygroup(self, group, volumes):
        """Deletes a consistency group."""

        volume_update = list()

        for volume in volumes:
            LOG.info('Deleting volume %s.', volume['id'])
            volume_update.append({
                'status': 'deleted', 'id': volume['id'],
            })
            self.delete_volume(volume)

        try:
            cg = self._get_consistencygroup(group)
        except exception.ConsistencyGroupNotFound:
            LOG.warning('Consistency group already deleted.')
        else:
            self._client.delete_consistency_group(cg['id'])
            try:
                self._merge_soft_delete_changes(None, [cg['id']])
            except (exception.NetAppDriverException,
                    eseries_exc.WebServiceException):
                LOG.warning('Unable to remove CG from the deletion map.')

        model_update = {'status': 'deleted'}

        return model_update, volume_update

    def _update_consistency_group_members(self, es_cg,
                                          add_volumes, remove_volumes):
        """Add or remove consistency group members

        :param es_cg: The E-Series consistency group
        :param add_volumes: A list of Cinder volumes to add to the
        consistency group
        :param remove_volumes: A list of Cinder volumes to remove from the
        consistency group
        :return: None
        """
        for volume in remove_volumes:
            es_vol = self._get_volume(volume['id'])
            LOG.info(
                'Removing volume %(v)s from consistency group %(''cg)s.',
                {'v': es_vol['label'], 'cg': es_cg['label']})
            self._client.remove_consistency_group_member(es_vol['id'],
                                                         es_cg['id'])

        for volume in add_volumes:
            es_vol = self._get_volume(volume['id'])
            LOG.info('Adding volume %(v)s to consistency group %(cg)s.',
                     {'v': es_vol['label'], 'cg': es_cg['label']})
            self._client.add_consistency_group_member(
                es_vol['id'], es_cg['id'])

    def update_consistencygroup(self, group,
                                add_volumes, remove_volumes):
        """Add or remove volumes from an existing consistency group"""
        cg = self._get_consistencygroup(group)

        self._update_consistency_group_members(
            cg, add_volumes, remove_volumes)

        return None, None, None

    def create_consistencygroup_from_src(self, group, volumes,
                                         cgsnapshot, snapshots,
                                         source_cg, source_vols):
        """Define a consistency group based on an existing group

        Define a new consistency group from a source consistency group. If
        only a source_cg is provided, then clone each base volume and add
        it to a new consistency group. If a cgsnapshot is provided,
        clone each snapshot image to a new volume and add it to the cg.

        :param group: The new consistency group to define
        :param volumes: The volumes to add to the consistency group
        :param cgsnapshot: The cgsnapshot to base the group on
        :param snapshots: The list of snapshots on the source cg
        :param source_cg: The source consistency group
        :param source_vols: The volumes added to the source cg
        """
        cg = self._create_consistency_group(group)
        if cgsnapshot:
            for vol, snap in zip(volumes, snapshots):
                image = self._get_snapshot(snap)
                self._create_volume_from_snapshot(vol, image)
        else:
            for vol, src in zip(volumes, source_vols):
                es_vol = self._get_volume(src['id'])
                es_snapshot = self._create_es_snapshot_for_clone(es_vol)
                try:
                    self._create_volume_from_snapshot(vol, es_snapshot)
                finally:
                    self._delete_es_snapshot(es_snapshot)

        self._update_consistency_group_members(cg, volumes, [])

        return None, None

    def _garbage_collect_tmp_vols(self):
        """Removes tmp vols with no snapshots."""
        try:
            if not na_utils.set_safe_attr(self, 'clean_job_running', True):
                LOG.warning('Returning as clean tmp vol job already running.')
                return

            for vol in self._client.list_volumes():
                label = vol['label']
                if (label.startswith('tmp-') and
                        not self._is_volume_containing_snaps(label)):
                    try:
                        self._client.delete_volume(vol['volumeRef'])
                    except exception.NetAppDriverException as e:
                        LOG.debug("Error deleting vol with label %(label)s:"
                                  " %(error)s.", {'label': label, 'error': e})
        finally:
            na_utils.set_safe_attr(self, 'clean_job_running', False)

    @cinder_utils.synchronized('manage_existing')
    def manage_existing(self, volume, existing_ref):
        """Brings an existing storage object under Cinder management."""
        vol = self._get_existing_vol_with_manage_ref(existing_ref)
        label = utils.convert_uuid_to_es_fmt(volume['id'])
        if label == vol['label']:
            LOG.info("Volume with given ref %s need not be renamed during"
                     " manage operation.", existing_ref)
            managed_vol = vol
        else:
            managed_vol = self._client.update_volume(vol['id'], label)
        LOG.info("Manage operation completed for volume with new label"
                 " %(label)s and wwn %(wwn)s.",
                 {'label': label, 'wwn': managed_vol[self.WORLDWIDENAME]})

    def manage_existing_get_size(self, volume, existing_ref):
        """Return size of volume to be managed by manage_existing.

        When calculating the size, round up to the next GB.
        """
        vol = self._get_existing_vol_with_manage_ref(existing_ref)
        return int(math.ceil(float(vol['capacity']) / units.Gi))

    def _get_existing_vol_with_manage_ref(self, existing_ref):
        try:
            vol_id = existing_ref.get('source-name') or existing_ref.get(
                'source-id')
            if vol_id is None:
                raise exception.InvalidInput(message='No valid identifier '
                                                     'was available for the '
                                                     'volume.')
            return self._client.list_volume(vol_id)
        except exception.InvalidInput:
            reason = _('Reference must contain either source-name'
                       ' or source-id element.')
            raise exception.ManageExistingInvalidReference(
                existing_ref=existing_ref, reason=reason)
        except exception.VolumeNotFound:
            raise exception.ManageExistingInvalidReference(
                existing_ref=existing_ref,
                reason=_('Volume not found on configured storage pools.'))

    def unmanage(self, volume):
        """Removes the specified volume from Cinder management.

           Does not delete the underlying backend storage object. Logs a
           message to indicate the volume is no longer under Cinder's control.
        """
        managed_vol = self._get_volume(volume['id'])
        LOG.info("Unmanaged volume with current label %(label)s and wwn "
                 "%(wwn)s.", {'label': managed_vol['label'],
                              'wwn': managed_vol[self.WORLDWIDENAME]})
