# Copyright (c) 2022 DataCore Software Corp. 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.

"""Base Driver for DataCore SANsymphony storage array."""

import math
import time

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

from cinder import context as cinder_context
from cinder import exception as cinder_exception
from cinder.i18n import _
from cinder import interface
from cinder import utils as cinder_utils
from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume.drivers.datacore import api
from cinder.volume.drivers.datacore import exception as datacore_exception
from cinder.volume.drivers.datacore import utils as datacore_utils
from cinder.volume.drivers.san import san
from cinder.volume import volume_types

LOG = logging.getLogger(__name__)

datacore_opts = [
    cfg.StrOpt('datacore_disk_type',
               default='single',
               choices=['single', 'mirrored'],
               help='DataCore virtual disk type (single/mirrored). '
                    'Mirrored virtual disks require two storage servers in '
                    'the server group.'),
    cfg.StrOpt('datacore_storage_profile',
               default=None,
               help='DataCore virtual disk storage profile.'),
    cfg.ListOpt('datacore_disk_pools',
                default=[],
                help='List of DataCore disk pools that can be used '
                     'by volume driver.'),
    cfg.IntOpt('datacore_api_timeout',
               default=300,
               min=1,
               help='Seconds to wait for a response from a '
                    'DataCore API call.'),
    cfg.IntOpt('datacore_disk_failed_delay',
               default=300,
               min=0,
               help='Seconds to wait for DataCore virtual '
                    'disk to come out of the "Failed" state.'),
]

CONF = cfg.CONF
CONF.register_opts(datacore_opts, group=configuration.SHARED_CONF_GROUP)


@interface.volumedriver
class DataCoreVolumeDriver(driver.VolumeDriver):
    """DataCore SANsymphony base volume driver."""

    STORAGE_PROTOCOL = 'N/A'

    AWAIT_DISK_ONLINE_INTERVAL = 10
    AWAIT_SNAPSHOT_ONLINE_INTERVAL = 10
    AWAIT_SNAPSHOT_ONLINE_INITIAL_DELAY = 5

    DATACORE_SINGLE_DISK = 'single'
    DATACORE_MIRRORED_DISK = 'mirrored'
    VOLUME_TYPE_STRIPED = 'Striped'
    VOLUME_TYPE_SPANNED = 'Spanned'

    DATACORE_DISK_TYPE_KEY = 'datacore:disk_type'
    DATACORE_STORAGE_PROFILE_KEY = 'datacore:storage_profile'
    DATACORE_DISK_POOLS_KEY = 'datacore:disk_pools'

    VALID_VOLUME_TYPE_KEYS = (DATACORE_DISK_TYPE_KEY,
                              DATACORE_STORAGE_PROFILE_KEY,
                              DATACORE_DISK_POOLS_KEY,)

    def __init__(self, *args, **kwargs):
        super(DataCoreVolumeDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(san.san_opts)
        self.configuration.append_config_values(datacore_opts)
        self._api = None
        self._default_volume_options = None

    def do_setup(self, context):
        """Perform validations and establish connection to server.

        :param context: Context information
        """

        required_params = [
            'san_ip',
            'san_login',
            'san_password',
        ]
        for param in required_params:
            if not getattr(self.configuration, param, None):
                raise cinder_exception.InvalidInput(_("%s not set.") % param)

        self._api = api.DataCoreClient(
            self.configuration.san_ip,
            self.configuration.san_login,
            self.configuration.san_password,
            self.configuration.datacore_api_timeout)

        disk_type = self.configuration.datacore_disk_type
        if disk_type:
            disk_type = disk_type.lower()
        storage_profile = self.configuration.datacore_storage_profile
        if storage_profile:
            storage_profile = storage_profile.lower()
        disk_pools = self.configuration.datacore_disk_pools
        if disk_pools:
            disk_pools = [pool.lower() for pool in disk_pools]

        self._default_volume_options = {
            self.DATACORE_DISK_TYPE_KEY: disk_type,
            self.DATACORE_STORAGE_PROFILE_KEY: storage_profile,
            self.DATACORE_DISK_POOLS_KEY: disk_pools,
        }

    def check_for_setup_error(self):
        pass

    def get_volume_backend_name(self):
        """Get volume backend name of the volume service.

        :return: Volume backend name
        """

        backend_name = self.configuration.safe_get('volume_backend_name')
        return (backend_name or 'DataCore' + self.__class__.__name__)

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

        :param volume: Volume object
        :return: Dictionary of changes to the volume object to be persisted
        """

        volume_options = self._get_volume_options(volume)

        disk_type = volume_options[self.DATACORE_DISK_TYPE_KEY]
        if disk_type == self.DATACORE_MIRRORED_DISK:
            logical_disk_count = 2
            virtual_disk_type = 'MultiPathMirrored'
        elif disk_type == self.DATACORE_SINGLE_DISK:
            logical_disk_count = 1
            virtual_disk_type = 'NonMirrored'
        else:
            msg = _("Virtual disk type '%s' is not valid.") % disk_type
            LOG.error(msg)
            raise cinder_exception.VolumeDriverException(message=msg)

        profile_id = self._get_storage_profile_id(
            volume_options[self.DATACORE_STORAGE_PROFILE_KEY])

        pools = datacore_utils.get_distinct_by(
            lambda pool: pool.ServerId,
            self._get_available_disk_pools(
                volume_options[self.DATACORE_DISK_POOLS_KEY]))

        if len(pools) < logical_disk_count:
            msg = _("Suitable disk pools were not found for "
                    "creating virtual disk.")
            LOG.error(msg)
            raise cinder_exception.VolumeDriverException(message=msg)

        disk_size = self._get_size_in_bytes(volume.size)

        logical_disks = []
        virtual_disk = None
        try:
            for logical_disk_pool in pools[:logical_disk_count]:
                logical_disks.append(
                    self._api.create_pool_logical_disk(
                        logical_disk_pool.Id,
                        self.VOLUME_TYPE_STRIPED, disk_size))

            virtual_disk_data = self._api.build_virtual_disk_data(
                volume.id,
                virtual_disk_type,
                disk_size,
                volume.display_name,
                profile_id)

            virtual_disk = self._api.create_virtual_disk_ex2(
                virtual_disk_data,
                logical_disks[0].Id,
                logical_disks[1].Id if logical_disk_count == 2 else None,
                True)

            virtual_disk = self._await_virtual_disk_online(virtual_disk.Id)

        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.exception("Creation of volume %(volume)s failed.",
                              {'volume': volume.id})
                try:
                    if virtual_disk:
                        self._api.delete_virtual_disk(virtual_disk.Id, True)
                    else:
                        for logical_disk in logical_disks:
                            self._api.delete_logical_disk(logical_disk.Id)
                except datacore_exception.DataCoreException as e:
                    LOG.warning("An error occurred on a cleanup after failed "
                                "creation of volume %(volume)s: %(error)s.",
                                {'volume': volume.id, 'error': e})

        return {'provider_location': virtual_disk.Id}

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from a snapshot.

        :param volume: Volume object
        :param snapshot: Snapshot object
        :return: Dictionary of changes to the volume object to be persisted
        """
        return self._create_volume_from(volume, snapshot)

    def create_cloned_volume(self, volume, src_vref):
        """Creates volume clone.

        :param volume: New Volume object
        :param src_vref: Volume object that must be cloned
        :return: Dictionary of changes to the volume object to be persisted
        """

        return self._create_volume_from(volume, src_vref)

    def extend_volume(self, volume, new_size):
        """Extend an existing volume's size.

        :param volume: Volume object
        :param new_size: new size in GB to extend this volume to
        """

        virtual_disk = self._get_virtual_disk_for(volume, raise_not_found=True)
        self._set_virtual_disk_size(virtual_disk,
                                    self._get_size_in_bytes(new_size))
        virtual_disk = self._await_virtual_disk_online(virtual_disk.Id)

    def delete_volume(self, volume):
        """Deletes a volume.

        :param volume: Volume object
        """

        virtual_disk = self._get_virtual_disk_for(volume)
        if virtual_disk:
            if virtual_disk.IsServed:
                logical_disks = self._api.get_logical_disks()
                logical_units = self._api.get_logical_units()
                target_devices = self._api.get_target_devices()
                logical_disks = [disk.Id for disk in logical_disks
                                 if disk.VirtualDiskId == virtual_disk.Id]
                logical_unit_devices = [unit.VirtualTargetDeviceId
                                        for unit in logical_units
                                        if unit.LogicalDiskId in logical_disks]
                initiator_ports = set(device.InitiatorPortId
                                      for device in target_devices
                                      if device.Id in logical_unit_devices)
                for port in initiator_ports:
                    self._api.unserve_virtual_disks_from_port(
                        port, [virtual_disk.Id])
            self._api.delete_virtual_disk(virtual_disk.Id, True)

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

        :param snapshot: Snapshot object
        :return: Dictionary of changes to the snapshot object to be persisted
        """

        src_virtual_disk = self._get_virtual_disk_for(snapshot.volume,
                                                      raise_not_found=True)

        volume_options = self._get_volume_options(snapshot.volume)
        profile_name = volume_options[self.DATACORE_STORAGE_PROFILE_KEY]
        profile_id = self._get_storage_profile_id(profile_name)
        pool_names = volume_options[self.DATACORE_DISK_POOLS_KEY]

        if src_virtual_disk.DiskStatus != 'Online':
            LOG.warning("Attempting to make a snapshot from virtual disk "
                        "%(disk)s that is in %(state)s state.",
                        {'disk': src_virtual_disk.Id,
                         'state': src_virtual_disk.DiskStatus})

        snapshot_virtual_disk = self._create_virtual_disk_copy(
            src_virtual_disk,
            snapshot.id,
            snapshot.display_name,
            profile_id=profile_id,
            pool_names=pool_names)
        snapshot_virtual_disk = self._await_virtual_disk_online(
            snapshot_virtual_disk.Id)

        return {'provider_location': snapshot_virtual_disk.Id}

    def delete_snapshot(self, snapshot):
        """Deletes a snapshot.

        :param snapshot: Snapshot object
        """

        snapshot_virtual_disk = self._get_virtual_disk_for(snapshot)
        if snapshot_virtual_disk:
            self._api.delete_virtual_disk(snapshot_virtual_disk.Id, True)

    def ensure_export(self, context, volume):
        pass

    def create_export(self, context, volume, connector):
        pass

    def remove_export(self, context, volume):
        pass

    def unserve_virtual_disks_from_host(self, volume, connector):
        virtual_disk = self._get_virtual_disk_for(volume)
        if virtual_disk:
            if connector is None:
                clients = self._api.get_clients()
            else:
                clients = [self._get_client(connector['host'],
                                            create_new=False)]

            server_group = self._get_our_server_group()

            @cinder_utils.synchronized(
                'datacore-backend-%s' % server_group.Id, external=True)
            def unserve_virtual_disk(client_id):
                self._api.unserve_virtual_disks_from_host(
                    client_id, [virtual_disk.Id])

            for client in clients:
                unserve_virtual_disk(client.Id)

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

        :param volume: Volume object
        :param connector: Connector information
        """
        self.unserve_virtual_disks_from_host(volume, connector)

    def manage_existing(self, volume, existing_ref):
        return self.manage_existing_object(volume, existing_ref, "volume")

    def manage_existing_get_size(self, volume, existing_ref):
        return self.manage_existing_object_get_size(volume, existing_ref,
                                                    "volume")

    def manage_existing_snapshot(self, snapshot, existing_ref):
        return self.manage_existing_object(snapshot, existing_ref, "snapshot")

    def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
        return self.manage_existing_object_get_size(snapshot, existing_ref,
                                                    "snapshot")

    def manage_existing_object(self, existing_object, existing_ref,
                               object_type):
        if 'source-name' not in existing_ref:
            reason = _('Reference must contain source-name element.')
            raise cinder_exception.ManageExistingInvalidReference(
                existing_ref=existing_ref, reason=reason)

        vd_alias = existing_ref['source-name']
        virtual_disk = datacore_utils.get_first_or_default(
            lambda disk: disk.Alias == vd_alias,
            self._api.get_virtual_disks(),
            None)

        if not virtual_disk:
            kwargs = {'existing_ref': vd_alias,
                      'reason': 'Specified Virtual disk does not exist.'}
            raise cinder_exception.ManageExistingInvalidReference(**kwargs)

        return {'provider_location': virtual_disk.Id}

    def manage_existing_object_get_size(self, existing_object, existing_ref,
                                        object_type):
        if 'source-name' not in existing_ref:
            reason = _('Reference must contain source-name element.')
            raise cinder_exception.ManageExistingInvalidReference(
                existing_ref=existing_ref, reason=reason)

        vd_alias = existing_ref['source-name']
        virtual_disk = datacore_utils.get_first_or_default(
            lambda disk: disk.Alias == vd_alias,
            self._api.get_virtual_disks(),
            None)

        if not virtual_disk:
            kwargs = {'existing_ref': vd_alias,
                      'reason': 'Specified Virtual disk does not exist.'}
            raise cinder_exception.ManageExistingInvalidReference(**kwargs)
        return self._get_size_in_gigabytes(virtual_disk.Size.Value)

    def _update_volume_stats(self):
        performance_data = self._api.get_performance_by_type(
            ['DiskPoolPerformance'])
        total = 0
        available = 0
        reserved = 0
        for performance in performance_data:
            missing_perf_data = []

            if hasattr(performance.PerformanceData, 'BytesTotal'):
                total += performance.PerformanceData.BytesTotal
            else:
                missing_perf_data.append('BytesTotal')

            if hasattr(performance.PerformanceData, 'BytesAvailable'):
                available += performance.PerformanceData.BytesAvailable
            else:
                missing_perf_data.append('BytesAvailable')

            if hasattr(performance.PerformanceData, 'BytesReserved'):
                reserved += performance.PerformanceData.BytesReserved
            else:
                missing_perf_data.append('BytesReserved')

            if missing_perf_data:
                LOG.warning("Performance data %(data)s is missing for "
                            "disk pool %(pool)s",
                            {'data': missing_perf_data,
                             'pool': performance.ObjectId})
        provisioned = 0
        logical_disks = self._api.get_logical_disks()
        for disk in logical_disks:
            if getattr(disk, 'PoolId', None):
                provisioned += disk.Size.Value
        total_capacity_gb = self._get_size_in_gigabytes(total)
        free = available + reserved
        free_capacity_gb = self._get_size_in_gigabytes(free)
        provisioned_capacity_gb = self._get_size_in_gigabytes(provisioned)
        reserved_percentage = 100.0 * reserved / total if total else 0.0
        reserved_percentage = math.ceil(reserved_percentage)
        ratio = self.configuration.max_over_subscription_ratio
        stats_data = {
            'vendor_name': 'DataCore',
            'QoS_support': False,
            'volume_backend_name': self.get_volume_backend_name(),
            'driver_version': self.get_version(),
            'storage_protocol': self.STORAGE_PROTOCOL,
            'total_capacity_gb': total_capacity_gb,
            'free_capacity_gb': free_capacity_gb,
            'provisioned_capacity_gb': provisioned_capacity_gb,
            'reserved_percentage': reserved_percentage,
            'max_over_subscription_ratio': ratio,
            'thin_provisioning_support': True,
            'thick_provisioning_support': False,
            'online_extend_support': False,
        }
        self._stats = stats_data

    def _get_our_server_group(self):
        server_group = datacore_utils.get_first(lambda group: group.OurGroup,
                                                self._api.get_server_groups())

        return server_group

    def _get_volume_options_from_type(self, type_id, default_options):
        options = dict(default_options.items())
        if type_id:
            admin_context = cinder_context.get_admin_context()
            volume_type = volume_types.get_volume_type(admin_context, type_id)
            specs = dict(volume_type).get('extra_specs')

            for key, value in specs.items():
                if key in self.VALID_VOLUME_TYPE_KEYS:
                    if key == self.DATACORE_DISK_POOLS_KEY:
                        options[key] = [v.strip().lower()
                                        for v in value.split(',')]
                    else:
                        options[key] = value.lower()

        return options

    def _get_volume_options(self, volume):
        type_id = volume.volume_type_id

        volume_options = self._get_volume_options_from_type(
            type_id, self._default_volume_options)

        return volume_options

    def _get_online_servers(self):
        servers = self._api.get_servers()
        online_servers = [server for server in servers
                          if server.State == 'Online']
        return online_servers

    def _get_available_disk_pools(self, disk_pool_names=None):
        online_servers = [server.Id for server in self._get_online_servers()]

        pool_performance = {
            performance.ObjectId: performance.PerformanceData for performance
            in self._api.get_performance_by_type(['DiskPoolPerformance'])}

        disk_pools = self._api.get_disk_pools()

        lower_disk_pool_names = ([name.lower() for name in disk_pool_names]
                                 if disk_pool_names else [])

        available_disk_pools = [
            pool for pool in disk_pools
            if (self._is_pool_healthy(pool, pool_performance,
                                      online_servers) and
                (not lower_disk_pool_names or
                 pool.Caption.lower() in lower_disk_pool_names))]

        available_disk_pools.sort(
            key=lambda p: pool_performance[p.Id].BytesAvailable, reverse=True)

        return available_disk_pools

    def _get_virtual_disk_for(self, obj, raise_not_found=False):
        disk_id = obj.get('provider_location')

        virtual_disk = datacore_utils.get_first_or_default(
            lambda disk: disk.Id == disk_id,
            self._api.get_virtual_disks(),
            None)
        if not virtual_disk:
            msg = (_("Virtual disk not found for %(object)s %(object_id)s.")
                   % {'object': obj.__class__.__name__.lower(),
                      'object_id': obj['id']})
            if raise_not_found:
                LOG.error(msg)
                raise cinder_exception.VolumeDriverException(message=msg)
            else:
                LOG.warning(msg)

        return virtual_disk

    def _set_virtual_disk_size(self, virtual_disk, new_size):
        return self._api.set_virtual_disk_size(virtual_disk.Id, new_size)

    def _get_storage_profile(self, profile_name, raise_not_found=False):
        profiles = self._api.get_storage_profiles()
        profile = datacore_utils.get_first_or_default(
            lambda p: p.Caption.lower() == profile_name.lower(),
            profiles,
            None)
        if not profile and raise_not_found:
            msg = (_("Specified storage profile %s not found.")
                   % profile_name)
            LOG.error(msg)
            raise cinder_exception.VolumeDriverException(message=msg)

        return profile

    def _get_storage_profile_id(self, profile_name):
        profile_id = None
        if profile_name:
            profile = self._get_storage_profile(profile_name,
                                                raise_not_found=True)
            profile_id = profile.Id
        return profile_id

    def _await_virtual_disk_online(self, virtual_disk_id):
        def inner(start_time):
            disk_failed_delay = self.configuration.datacore_disk_failed_delay
            virtual_disk = datacore_utils.get_first(
                lambda disk: disk.Id == virtual_disk_id,
                self._api.get_virtual_disks())
            if virtual_disk.DiskStatus == 'Online':
                raise loopingcall.LoopingCallDone(virtual_disk)
            elif (
                    virtual_disk.DiskStatus != 'FailedRedundancy' and
                    time.time() - start_time >= disk_failed_delay):
                msg = (_("Virtual disk %(disk)s did not come out of the "
                         "%(state)s state after %(timeout)s seconds.")
                       % {'disk': virtual_disk.Id,
                          'state': virtual_disk.DiskStatus,
                          'timeout': disk_failed_delay})
                LOG.error(msg)
                raise cinder_exception.VolumeDriverException(message=msg)

        inner_loop = loopingcall.FixedIntervalLoopingCall(inner, time.time())
        return inner_loop.start(self.AWAIT_DISK_ONLINE_INTERVAL).wait()

    def _create_volume_from(self, volume, src_obj):
        src_virtual_disk = self._get_virtual_disk_for(src_obj,
                                                      raise_not_found=True)
        if src_virtual_disk.DiskStatus != 'Online':
            LOG.warning("Attempting to create a volume from virtual disk "
                        "%(disk)s that is in %(state)s state.",
                        {'disk': src_virtual_disk.Id,
                         'state': src_virtual_disk.DiskStatus})

        volume_options = self._get_volume_options(volume)
        profile_id = self._get_storage_profile_id(
            volume_options[self.DATACORE_STORAGE_PROFILE_KEY])
        pool_names = volume_options[self.DATACORE_DISK_POOLS_KEY]

        volume_virtual_disk = self._create_virtual_disk_copy(
            src_virtual_disk,
            volume.id,
            volume.display_name,
            profile_id=profile_id,
            pool_names=pool_names)

        volume_logical_disk = datacore_utils.get_first(
            lambda disk: disk.VirtualDiskId == volume_virtual_disk.Id,
            self._api.get_logical_disks())

        try:
            disk_type = volume_options[self.DATACORE_DISK_TYPE_KEY]
            if disk_type == self.DATACORE_MIRRORED_DISK:
                pools = self._get_available_disk_pools(pool_names)
                selected_pool = datacore_utils.get_first_or_default(
                    lambda pool: (pool.ServerId !=
                                  volume_logical_disk.ServerHostId and
                                  pool.Id != volume_logical_disk.PoolId),
                    pools, None)
                if selected_pool:
                    logical_disk = self._api.create_pool_logical_disk(
                        selected_pool.Id,
                        self.VOLUME_TYPE_STRIPED,
                        volume_virtual_disk.Size.Value)
                    self._api.bind_logical_disk(volume_virtual_disk.Id,
                                                logical_disk.Id,
                                                'Second',
                                                True,
                                                False,
                                                True)
                else:
                    msg = _("Can not create mirrored virtual disk. "
                            "Suitable disk pools not found.")
                    LOG.error(msg)
                    raise cinder_exception.VolumeDriverException(message=msg)
            volume_virtual_disk = self._await_virtual_disk_online(
                volume_virtual_disk.Id)
            try:
                source_size = src_obj.size
            except AttributeError:
                source_size = src_obj.volume_size
            if volume.size > source_size:
                self._set_virtual_disk_size(volume_virtual_disk,
                                            self._get_size_in_bytes(
                                                volume.size))
                volume_virtual_disk = datacore_utils.get_first(
                    lambda disk: disk.Id == volume_virtual_disk.Id,
                    self._api.get_virtual_disks())
                volume_virtual_disk = self._await_virtual_disk_size_change(
                    volume_virtual_disk.Id,
                    self._get_size_in_bytes(source_size))

        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.exception("Creation of volume %(volume)s failed.",
                              {'volume': volume.id})
                try:
                    self._api.delete_virtual_disk(volume_virtual_disk.Id, True)
                except datacore_exception.DataCoreException as e:
                    LOG.warning("An error occurred on a cleanup after failed "
                                "creation of volume %(volume)s: %(error)s.",
                                {'volume': volume.id, 'error': e})

        return {'provider_location': volume_virtual_disk.Id}

    def _create_full_snapshot(self, description, name, pool_names, profile_id,
                              src_virtual_disk):
        pools = self._get_available_disk_pools(pool_names)
        destination_pool = datacore_utils.get_first_or_default(
            lambda pool: (pool.ServerId == src_virtual_disk.FirstHostId or
                          pool.ServerId == src_virtual_disk.SecondHostId),
            pools, None)

        if not destination_pool:
            msg = _("Suitable snapshot destination disk pool not found for "
                    "virtual disk %s.") % src_virtual_disk.Id
            LOG.error(msg)
            raise cinder_exception.VolumeDriverException(message=msg)
        server = datacore_utils.get_first(
            lambda srv: srv.Id == destination_pool.ServerId,
            self._api.get_servers())
        if not server.SnapshotMapStorePoolId:
            self._api.designate_map_store(destination_pool.Id)
        snapshot = self._api.create_snapshot(src_virtual_disk.Id,
                                             name,
                                             description,
                                             destination_pool.Id,
                                             'Full',
                                             False,
                                             profile_id)
        return snapshot

    def _await_snapshot_migrated(self, snapshot_id):
        def inner():
            snapshot_data = datacore_utils.get_first(
                lambda snapshot: snapshot.Id == snapshot_id,
                self._api.get_snapshots())
            if snapshot_data.State == 'Migrated':
                raise loopingcall.LoopingCallDone(snapshot_data)
            elif (snapshot_data.State != 'Healthy' and
                  snapshot_data.Failure != 'NoFailure'):
                msg = (_("Full migration of snapshot %(snapshot)s failed. "
                         "Snapshot is in %(state)s state.")
                       % {'snapshot': snapshot_data.Id,
                          'state': snapshot_data.State})
                LOG.error(msg)
                raise cinder_exception.VolumeDriverException(message=msg)

        loop = loopingcall.FixedIntervalLoopingCall(inner)
        time.sleep(self.AWAIT_SNAPSHOT_ONLINE_INTERVAL)
        return loop.start(self.AWAIT_SNAPSHOT_ONLINE_INTERVAL,
                          self.AWAIT_SNAPSHOT_ONLINE_INITIAL_DELAY).wait()

    def _create_virtual_disk_copy(self, src_virtual_disk, name, description,
                                  profile_id=None, pool_names=None):
        snapshot = self._create_full_snapshot(
            description, name, pool_names, profile_id, src_virtual_disk)

        try:
            snapshot = self._await_snapshot_migrated(snapshot.Id)
            self._api.delete_snapshot(snapshot.Id)
            self._await_snapshot_split_state_change(snapshot)
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.exception("Split operation failed for snapshot "
                              "%(snapshot)s.", {'snapshot': snapshot.Id})
                try:
                    logical_disk_copy = datacore_utils.get_first(
                        lambda disk: (disk.Id ==
                                      snapshot.DestinationLogicalDiskId),
                        self._api.get_logical_disks())

                    virtual_disk_copy = datacore_utils.get_first(
                        lambda disk: (disk.Id ==
                                      logical_disk_copy.VirtualDiskId),
                        self._api.get_virtual_disks())

                    self._api.delete_virtual_disk(virtual_disk_copy.Id, True)
                except datacore_exception.DataCoreException as e:
                    LOG.warning("An error occurred on a cleanup after failed "
                                "split of snapshot %(snapshot)s: %(error)s.",
                                {'snapshot': snapshot.Id, 'error': e})

        logical_disk_copy = datacore_utils.get_first(
            lambda disk: disk.Id == snapshot.DestinationLogicalDiskId,
            self._api.get_logical_disks())

        virtual_disk_copy = datacore_utils.get_first(
            lambda disk: disk.Id == logical_disk_copy.VirtualDiskId,
            self._api.get_virtual_disks())

        return virtual_disk_copy

    def _get_client(self, name, create_new=False):
        client_hosts = self._api.get_clients()

        client = datacore_utils.get_first_or_default(
            lambda host: host.HostName.split('.')[0] == name.split('.')[0],
            client_hosts, None)

        if create_new:
            if not client:
                client = self._api.register_client(
                    name, None, 'Other', 'PreferredServer', None)
            self._api.set_client_capabilities(client.Id, True, True)

        return client

    @staticmethod
    def _is_pool_healthy(pool, pool_performance, online_servers):
        if (pool.PoolStatus == 'Running' and
                hasattr(pool_performance[pool.Id], 'BytesAvailable') and
                pool.ServerId in online_servers):
            return True
        return False

    @staticmethod
    def _get_size_in_bytes(size_in_gigabytes):
        return size_in_gigabytes * units.Gi

    @staticmethod
    def _get_size_in_gigabytes(size_in_bytes):
        return size_in_bytes / float(units.Gi)

    def _await_virtual_disk_size_change(self, virtual_disk_id, old_size):
        def inner(start_time):
            disk_failed_delay = self.configuration.datacore_disk_failed_delay
            virtual_disk = datacore_utils.get_first(
                lambda disk: disk.Id == virtual_disk_id,
                self._api.get_virtual_disks())
            if virtual_disk.DiskStatus == 'Online' \
                    and virtual_disk.Size.Value > old_size:
                raise loopingcall.LoopingCallDone(virtual_disk)
            elif (virtual_disk.DiskStatus != 'FailedRedundancy' and
                  time.time() - start_time >= disk_failed_delay):
                msg = (_("Virtual disk %(disk)s did not come out of the "
                         "%(state)s state after %(timeout)s seconds.")
                       % {'disk': virtual_disk.Id,
                          'state': virtual_disk.DiskStatus,
                          'timeout': disk_failed_delay})
                LOG.error(msg)
                raise cinder_exception.VolumeDriverException(message=msg)

        inner_loop = loopingcall.FixedIntervalLoopingCall(inner, time.time())
        time.sleep(self.AWAIT_DISK_ONLINE_INTERVAL)
        return inner_loop.start(self.AWAIT_DISK_ONLINE_INTERVAL).wait()

    def _await_snapshot_split_state_change(self, split_snapshot):
        def inner(start_time):
            disk_failed_delay = self.configuration.datacore_disk_failed_delay
            snapshot_found = False
            snapshot_list = self._api.get_snapshots()
            if not snapshot_list:
                raise loopingcall.LoopingCallDone()
            for entry in snapshot_list:
                if entry.Caption == split_snapshot.Caption:
                    snapshot_found = True
                    break
            if not snapshot_found:
                raise loopingcall.LoopingCallDone()
            if (time.time() - start_time >= disk_failed_delay):
                msg = (_("Split Snapshot disk %(disk)s did not happened "
                         "after %(timeout)s seconds.")
                       % {'disk': split_snapshot.Caption,
                          'timeout': disk_failed_delay})
                LOG.error(msg)
                raise loopingcall.LoopingCallDone()

        inner_loop = loopingcall.FixedIntervalLoopingCall(inner, time.time())
        return inner_loop.start(self.AWAIT_DISK_ONLINE_INTERVAL).wait()
