# Copyright (c) 2016 EMC Corporation.
# 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.
"""
VNX Common Utils
"""

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils

from cinder import exception
from cinder.i18n import _
from cinder.volume import configuration
from cinder.volume.drivers.dell_emc.vnx import const
from cinder.volume import group_types
from cinder.volume import volume_types

storops = importutils.try_import('storops')
CONF = cfg.CONF

LOG = logging.getLogger(__name__)

DEFAULT_TIMEOUT = 60 * 60 * 24 * 365

INTERVAL_5_SEC = 5
INTERVAL_20_SEC = 20
INTERVAL_30_SEC = 30
INTERVAL_60_SEC = 60

SNAP_EXPIRATION_HOUR = '1h'


BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both'])
QOS_MAX_IOPS = 'maxIOPS'
QOS_MAX_BWS = 'maxBWS'


VNX_OPTS = [
    cfg.StrOpt('storage_vnx_authentication_type',
               default='global',
               help='VNX authentication scope type. '
               'By default, the value is global.'),
    cfg.StrOpt('storage_vnx_security_file_dir',
               help='Directory path that contains the VNX security file. '
               'Make sure the security file is generated first.'),
    cfg.StrOpt('naviseccli_path',
               help='Naviseccli Path.'),
    cfg.ListOpt('storage_vnx_pool_names',
                help='Comma-separated list of storage pool names to be used.'),
    cfg.IntOpt('default_timeout',
               default=DEFAULT_TIMEOUT,
               help='Default timeout for CLI operations in minutes. '
               'For example, LUN migration is a typical long '
               'running operation, which depends on the LUN size and '
               'the load of the array. '
               'An upper bound in the specific deployment can be set to '
               'avoid unnecessary long wait. '
               'By default, it is 365 days long.'),
    cfg.IntOpt('max_luns_per_storage_group',
               default=255,
               help='Default max number of LUNs in a storage group.'
               ' By default, the value is 255.'),
    cfg.BoolOpt('destroy_empty_storage_group',
                default=False,
                help='To destroy storage group '
                'when the last LUN is removed from it. '
                'By default, the value is False.'),
    # iscsi_initiators is a dict which key is string and value is a list.
    # This could be a DictOpt. Unfortunately DictOpt doesn't support the value
    # of list type.
    cfg.StrOpt('iscsi_initiators',
               help='Mapping between hostname and '
               'its iSCSI initiator IP addresses.'),
    cfg.ListOpt('io_port_list',
                help='Comma separated iSCSI or FC ports '
                'to be used in Nova or Cinder.'),
    cfg.BoolOpt('initiator_auto_registration',
                default=False,
                help='Automatically register initiators. '
                'By default, the value is False.'),
    cfg.BoolOpt('initiator_auto_deregistration',
                default=False,
                help='Automatically deregister initiators after the related '
                'storage group is destroyed. '
                'By default, the value is False.'),
    cfg.BoolOpt('check_max_pool_luns_threshold',
                default=False,
                deprecated_for_removal=True,
                help='DEPRECATED: Report free_capacity_gb as 0 when the limit '
                'to maximum number of pool LUNs is reached. '
                'By default, the value is False.'),
    cfg.BoolOpt('force_delete_lun_in_storagegroup',
                default=True,
                help='Delete a LUN even if it is in Storage Groups.'),
    cfg.BoolOpt('ignore_pool_full_threshold',
                default=False,
                help='Force LUN creation even if '
                'the full threshold of pool is reached. '
                'By default, the value is False.'),
    cfg.BoolOpt('vnx_async_migrate',
                default=True,
                help='Always use asynchronous migration during volume cloning '
                     'and creating from snapshot. As described in '
                     'configuration doc, async migration has some '
                     'constraints. Besides using metadata, customers could '
                     'use this option to disable async migration. Be aware '
                     'that `async_migrate` in metadata overrides this '
                     'option when both are set. By default, the value is True.'
                )
]

CONF.register_opts(VNX_OPTS, group=configuration.SHARED_CONF_GROUP)


PROTOCOL_FC = 'fc'
PROTOCOL_ISCSI = 'iscsi'


class ExtraSpecs(object):
    _provision_key = 'provisioning:type'
    _tier_key = 'storagetype:tiering'
    _replication_key = 'replication_enabled'

    PROVISION_DEFAULT = const.PROVISION_THICK
    TIER_DEFAULT = None

    def __init__(self, extra_specs, group_specs=None):
        self.specs = extra_specs
        self._provision = self._get_provision()
        self.provision = self._provision
        self._tier = self._get_tier()
        self.tier = self._tier
        self.apply_default_values()
        self.group_specs = group_specs if group_specs else {}

    def apply_default_values(self):
        self.provision = (ExtraSpecs.PROVISION_DEFAULT
                          if self.provision is None
                          else self.provision)
        # Can not set Tier when provision is set to deduped. So don't set the
        # tier default when provision is deduped.
        if self.provision != storops.VNXProvisionEnum.DEDUPED:
            self.tier = (ExtraSpecs.TIER_DEFAULT if self.tier is None
                         else self.tier)

    @classmethod
    def set_defaults(cls, provision_default, tier_default):
        cls.PROVISION_DEFAULT = provision_default
        cls.TIER_DEFAULT = tier_default

    def _get_provision(self):
        value = self._parse_to_enum(self._provision_key,
                                    storops.VNXProvisionEnum)
        return value

    def _get_tier(self):
        return self._parse_to_enum(self._tier_key, storops.VNXTieringEnum)

    @property
    def is_replication_enabled(self):
        return self.specs.get('replication_enabled', '').lower() == '<is> true'

    @property
    def is_group_replication_enabled(self):
        return self.group_specs.get(
            'consistent_group_replication_enabled', '').lower() == '<is> true'

    def _parse_to_enum(self, key, enum_class):
        value = (self.specs[key]
                 if key in self.specs else None)
        if value is not None:
            try:
                value = enum_class.parse(value)
            except ValueError:
                reason = (_("The value %(value)s for key %(key)s in extra "
                            "specs is invalid.") %
                          {'key': key, 'value': value})
                raise exception.InvalidVolumeType(reason=reason)
        return value

    @classmethod
    def from_volume(cls, volume):
        specs = {}
        type_id = volume['volume_type_id']
        if type_id is not None:
            specs = volume_types.get_volume_type_extra_specs(type_id)

        return cls(specs)

    @classmethod
    def from_group(cls, group):
        group_specs = {}

        if group and group.group_type_id:
            group_specs = group_types.get_group_type_specs(
                group.group_type_id)

        return cls(extra_specs={}, group_specs=group_specs)

    @classmethod
    def from_volume_type(cls, type):
        return cls(type['extra_specs'])

    @classmethod
    def from_lun(cls, lun):
        ex = cls({})
        ex.provision = lun.provision
        ex.tier = (lun.tier
                   if lun.provision != storops.VNXProvisionEnum.DEDUPED
                   else None)
        return ex

    def match_with_lun(self, lun):
        ex = ExtraSpecs.from_lun(lun)
        return (self.provision == ex.provision and
                self.tier == ex.tier)

    def validate(self, enabler_status):
        """Checks whether the extra specs are valid.

        :param enabler_status: Instance of VNXEnablerStatus
        """
        if "storagetype:pool" in self.specs:
            LOG.warning("Extra spec key 'storagetype:pool' is obsoleted "
                        "since driver version 5.1.0. This key will be "
                        "ignored.")

        if (self._provision == storops.VNXProvisionEnum.DEDUPED and
                self._tier is not None):
            msg = _("Can not set tiering policy for a deduplicated volume. "
                    "Set the tiering policy on the pool where the "
                    "deduplicated volume locates.")
            raise exception.InvalidVolumeType(reason=msg)

        if (self._provision == storops.VNXProvisionEnum.COMPRESSED
                and not enabler_status.compression_enabled):
            msg = _("Compression Enabler is not installed. "
                    "Can not create compressed volume.")
            raise exception.InvalidVolumeType(reason=msg)

        if (self._provision == storops.VNXProvisionEnum.DEDUPED
                and not enabler_status.dedup_enabled):
            msg = _("Deduplication Enabler is not installed. "
                    "Can not create deduplicated volume.")
            raise exception.InvalidVolumeType(reason=msg)

        if (self._provision in [storops.VNXProvisionEnum.THIN,
                                storops.VNXProvisionEnum.COMPRESSED,
                                storops.VNXProvisionEnum.DEDUPED]
                and not enabler_status.thin_enabled):
            msg = _("ThinProvisioning Enabler is not installed. "
                    "Can not create thin volume.")
            raise exception.InvalidVolumeType(reason=msg)

        if (self._tier is not None
                and not enabler_status.fast_enabled):
            msg = _("FAST VP Enabler is not installed. "
                    "Can not set tiering policy for the volume.")
            raise exception.InvalidVolumeType(reason=msg)
        return True

    def __len__(self):
        return len(self.specs)

    def __getitem__(self, key):
        return self.specs[key]

    def __iter__(self):
        return iter(self.specs)

    def __contains__(self, item):
        return item in self.specs

    def __eq__(self, other):
        if isinstance(other, ExtraSpecs):
            return self.specs == other.specs
        elif isinstance(other, dict):
            return self.specs == other
        else:
            return False

    def __hash__(self):
        return self.specs.__hash__()


class LUNState(object):
    INITIALIZING = 'Initializing'
    READY = 'Ready'
    FAULTED = 'Faulted'


class PoolState(object):
    INITIALIZING = 'Initializing'
    OFFLINE = 'Offline'
    DELETING = 'Deleting'
    VALID_CREATE_LUN_STATE = (INITIALIZING, OFFLINE, DELETING)


class VNXEnablerStatus(object):

    def __init__(self,
                 dedup=False,
                 compression=False,
                 fast=False,
                 thin=False,
                 snap=False):
        self.dedup_enabled = dedup
        self.compression_enabled = compression
        self.fast_enabled = fast
        self.thin_enabled = thin
        self.snap_enabled = snap


class WaitUtilTimeoutException(exception.VolumeDriverException):
    """Raised when timeout occurs in wait_until."""
    # TODO(Ryan) put this exception under Cinder shared module.
    pass


class Host(object):
    """The model of a host which acts as an initiator to access the storage."""

    def __init__(self, name, initiators, ip=None, wwpns=None):
        # ip and wwpns are optional.
        self.name = name
        if not self.name:
            raise ValueError(('Name of host cannot be empty.'))
        self.initiators = initiators
        if not self.initiators:
            raise ValueError(_('Initiators of host cannot be empty.'))
        self.ip = ip
        self.wwpns = wwpns


class Volume(object):
    """The internal volume which is used to pass in method call."""

    def __init__(self, name, id, vnx_lun_id=None):
        self.name = name
        self.id = id
        self.vnx_lun_id = vnx_lun_id


class ISCSITargetData(dict):
    def __init__(self, volume_id, is_discovered, iqn='unknown', iqns=None,
                 portal='unknown', portals=None, lun='unknown', luns=None):
        data = {'volume_id': volume_id, 'target_discovered': is_discovered,
                'target_iqn': iqn, 'target_iqns': iqns,
                'target_portal': portal, 'target_portals': portals,
                'target_lun': lun, 'target_luns': luns}
        self['driver_volume_type'] = 'iscsi'
        self['data'] = data

    def to_dict(self):
        """Converts to the dict.

        It helps serialize and deserialize the data before returning to nova.
        """
        return {key: value for (key, value) in self.items()}


class FCTargetData(dict):
    def __init__(self, volume_id, is_discovered, wwn=None, lun=None,
                 initiator_target_map=None):
        data = {'volume_id': volume_id, 'target_discovered': is_discovered,
                'target_lun': lun, 'target_wwn': wwn,
                'initiator_target_map': initiator_target_map}
        self['driver_volume_type'] = 'fibre_channel'
        self['data'] = {key: value for key, value in data.items()
                        if value is not None}

    def to_dict(self):
        """Converts to the dict.

        It helps serialize and deserialize the data before returning to nova.
        """
        return {key: value for (key, value) in self.items()}


class ReplicationDevice(object):
    def __init__(self, replication_device):
        self.replication_device = replication_device

    @property
    def backend_id(self):
        return self.replication_device.get('backend_id')

    @property
    def san_ip(self):
        return self.replication_device.get('san_ip')

    @property
    def san_login(self):
        return self.replication_device.get('san_login')

    @property
    def san_password(self):
        return self.replication_device.get('san_password')

    @property
    def storage_vnx_authentication_type(self):
        return self.replication_device.get(
            'storage_vnx_authentication_type',
            'global')

    @property
    def storage_vnx_security_file_dir(self):
        return self.replication_device.get('storage_vnx_security_file_dir')

    @property
    def pool_name(self):
        return self.replication_device.get('pool_name', None)


class ReplicationDeviceList(list):
    """Replication devices configured in cinder.conf

    Cinder supports multiple replication_device,  while VNX driver
    only support one replication_device for now.
    """

    def __init__(self, configuration):
        self.list = []
        self.configuration = configuration
        self._device_map = dict()
        self.parse_configuration()

    def parse_configuration(self):
        if self.configuration.replication_device:
            for replication_device in self.configuration.replication_device:
                rd = ReplicationDevice(replication_device)
                if not rd.backend_id or not rd.san_ip:
                    msg = _('backend_id or san_ip cannot be empty for '
                            'replication_device.')
                    raise exception.InvalidInput(reason=msg)
                self._device_map[rd.backend_id] = rd
                self.list.append(rd)
        return self._device_map

    def get_device(self, backend_id):
        try:
            device = self._device_map[backend_id]
        except KeyError:
            device = None
            LOG.warning('Unable to find secondary device named: %s',
                        backend_id)
        return device

    @property
    def devices(self):
        return self._device_map.values()

    def __len__(self):
        return len(self.list)

    def __iter__(self):
        self._iter = self.list.__iter__()
        return self

    def next(self):
        return next(self._iter)

    def __next__(self):
        return self.next()

    def __getitem__(self, item):
        return self.list[item]

    @classmethod
    def get_backend_ids(cls, config):
        """Returns all configured device_id."""
        rep_list = cls(config)
        backend_ids = []
        for item in rep_list.devices:
            backend_ids.append(item.backend_id)
        return backend_ids


class VNXMirrorView(object):
    def __init__(self, primary_client, secondary_client):
        self.primary_client = primary_client
        self.secondary_client = secondary_client

    def create_mirror(self, name, primary_lun_id):
        self.primary_client.create_mirror(name, primary_lun_id)

    def create_secondary_lun(self, pool_name, lun_name, size, provision, tier):
        return self.secondary_client.create_lun(
            pool_name, lun_name, size, provision, tier)

    def delete_secondary_lun(self, lun_name):
        self.secondary_client.delete_lun(lun_name)

    def delete_mirror(self, mirror_name):
        self.primary_client.delete_mirror(mirror_name)

    def add_image(self, mirror_name, secondary_lun_id):
        sp_ip = self.secondary_client.get_available_ip()
        self.primary_client.add_image(mirror_name, sp_ip, secondary_lun_id)

    def remove_image(self, mirror_name):
        self.primary_client.remove_image(mirror_name)

    def fracture_image(self, mirror_name):
        self.primary_client.fracture_image(mirror_name)

    def promote_image(self, mirror_name):
        """Promote the image on the secondary array."""
        self.secondary_client.promote_image(mirror_name)

    def destroy_mirror(self, mirror_name, secondary_lun_name):
        """Destroy the mirror view's related VNX objects.

        NOTE: primary lun will not be deleted here.
        :param mirror_name: name of mirror to be destroyed
        :param secondary_lun_name: name of LUN name
        """
        mv = self.primary_client.get_mirror(mirror_name)
        if not mv.existed:
            # We will skip the mirror operations if not existed
            LOG.warning('Mirror view %s was deleted already.',
                        mirror_name)
            return
        self.fracture_image(mirror_name)
        self.remove_image(mirror_name)
        self.delete_mirror(mirror_name)
        self.delete_secondary_lun(lun_name=secondary_lun_name)

    def create_mirror_group(self, group_name):
        return self.primary_client.create_mirror_group(group_name)

    def delete_mirror_group(self, group_name):
        return self.primary_client.delete_mirror_group(group_name)

    def add_mirror(self, group_name, mirror_name):
        return self.primary_client.add_mirror(group_name, mirror_name)

    def remove_mirror(self, group_name, mirror_name):
        return self.primary_client.remove_mirror(group_name, mirror_name)

    def sync_mirror_group(self, group_name):
        return self.primary_client.sync_mirror_group(group_name)

    def promote_mirror_group(self, group_name):
        """Promote the mirror group on the secondary array."""
        return self.secondary_client.promote_mirror_group(group_name)

    def fracture_mirror_group(self, group_name):
        return self.primary_client.fracture_mirror_group(group_name)
