#    (c) Copyright 2013-2015 Hewlett Packard Enterprise Development LP
#    All Rights Reserved.
#
#    Copyright 2012 OpenStack Foundation
#
#    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 class for HPE Storage Drivers.
This driver requires 3.1.3 or later firmware on the 3PAR array, using
the 4.x version of the hpe3parclient.

You will need to install the python hpe3parclient.
sudo pip install --upgrade "hpe3parclient>=4.0"

"""

try:
    from hpe3parclient import exceptions as hpeexceptions
except ImportError:
    hpeexceptions = None

from oslo_log import log as logging

from cinder import coordination
from cinder import exception
from cinder.i18n import _
from cinder.volume import driver
from cinder.volume.drivers.hpe import hpe_3par_common as hpecommon
from cinder.volume.drivers.san import san
from cinder.volume import volume_utils

LOG = logging.getLogger(__name__)


class HPE3PARDriverBase(driver.ManageableVD,
                        driver.ManageableSnapshotsVD,
                        driver.MigrateVD,
                        driver.BaseVD):
    """OpenStack base driver to enable 3PAR storage array.

    Version history:

    .. code-block:: none

        1.0.0 - Initial base driver
        1.0.1 - Adds consistency group capability in generic volume groups.
        1.0.2 - Adds capability.
        1.0.3 - Added Tiramisu feature on 3PAR.
        1.0.4 - Fixed Volume migration for "in-use" volume. bug #1744021
        1.0.5 - Set proper backend on subsequent operation, after group
                failover. bug #1773069

    """

    VERSION = "1.0.5"

    def __init__(self, *args, **kwargs):
        super(HPE3PARDriverBase, self).__init__(*args, **kwargs)
        self._active_backend_id = kwargs.get('active_backend_id', None)
        self.configuration.append_config_values(hpecommon.hpe3par_opts)
        self.configuration.append_config_values(san.san_opts)
        self.protocol = None
        self.common = None

    @staticmethod
    def get_driver_options():
        return hpecommon.HPE3PARCommon.get_driver_options()

    def _init_common(self):
        self.common = hpecommon.HPE3PARCommon(self.configuration,
                                              self._active_backend_id)
        return self.common

    def _login(self, timeout=None, array_id=None):
        self.common = self._init_common()
        # If replication is enabled and we cannot login, we do not want to
        # raise an exception so a failover can still be executed.
        try:
            self.common.do_setup(None, timeout=timeout, stats=self._stats,
                                 array_id=array_id)
            self.common.client_login()
        except Exception:
            if self.common._replication_enabled:
                LOG.warning("The primary array is not reachable at this "
                            "time. Since replication is enabled, "
                            "listing replication targets and failing over "
                            "a volume can still be performed.")
            else:
                raise
        return self.common

    def _logout(self, common):
        # If replication is enabled and we do not have a client ID, we did not
        # login, but can still failover. There is no need to logout.
        if common.client is None and common._replication_enabled:
            return
        common.client_logout()

    def _check_flags(self, common):
        """Sanity check to ensure we have required options set."""
        required_flags = ['hpe3par_api_url', 'hpe3par_username',
                          'hpe3par_password', 'san_ip', 'san_login',
                          'san_password']
        common.check_flags(self.configuration, required_flags)

    def get_volume_replication_driver_data(self, volume):
        if (volume.get("group_id") and volume.get("replication_status") and
           volume.get("replication_status") == "failed-over"):
            return int(volume.get("replication_driver_data"))
        return None

    @volume_utils.trace
    def get_volume_stats(self, refresh=False):
        # NOTE(geguileo): We don't need to login to the backed if we are not
        # going to refresh the stats, furthermore if we login, then we'll
        # return an empty dict, because the _login method calls calls
        # _init_common which returns a new HPE3PARCommon instance each time,
        # so it won't have any cached values.
        if not refresh:
            return self._stats

        self._stats = self.common.get_volume_stats(
            refresh,
            self.get_filter_function(),
            self.get_goodness_function())

        self._stats['storage_protocol'] = self.protocol
        self._stats['driver_version'] = self.VERSION
        backend_name = self.configuration.safe_get('volume_backend_name')
        self._stats['volume_backend_name'] = (backend_name or
                                              self.__class__.__name__)
        return self._stats

    def check_for_setup_error(self):
        """Setup errors are already checked for in do_setup so return pass."""
        pass

    @volume_utils.trace
    def create_volume(self, volume, perform_replica=True):
        return self.common.create_volume(volume)

    @volume_utils.trace
    def create_cloned_volume(self, volume, src_vref):
        """Clone an existing volume."""
        return self.common.create_cloned_volume(volume, src_vref)

    @volume_utils.trace
    def delete_volume(self, volume):
        return self.common.delete_volume(volume)

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

        TODO: support using the size from the user.
        """
        return self.common.create_volume_from_snapshot(volume, snapshot)

    @volume_utils.trace
    def create_snapshot(self, snapshot):
        return self.common.create_snapshot(snapshot)

    @volume_utils.trace
    def delete_snapshot(self, snapshot):
        return self.common.delete_snapshot(snapshot)

    @volume_utils.trace
    def extend_volume(self, volume, new_size):
        return self.common.extend_volume(volume, new_size)

    @volume_utils.trace
    def create_group(self, context, group):
        return self.common.create_group(context, group)

    @volume_utils.trace
    def create_group_from_src(self, context, group, volumes,
                              group_snapshot=None, snapshots=None,
                              source_group=None, source_vols=None):
        return self.common.create_group_from_src(
            context, group, volumes, group_snapshot, snapshots,
            source_group, source_vols)

    @volume_utils.trace
    def delete_group(self, context, group, volumes):
        return self.common.delete_group(context, group, volumes)

    @volume_utils.trace
    def update_group(self, context, group, add_volumes=None,
                     remove_volumes=None):
        return self.common.update_group(context, group, add_volumes,
                                        remove_volumes)

    @volume_utils.trace
    def create_group_snapshot(self, context, group_snapshot, snapshots):
        return self.common.create_group_snapshot(context, group_snapshot,
                                                 snapshots)

    @volume_utils.trace
    def delete_group_snapshot(self, context, group_snapshot, snapshots):
        return self.common.delete_group_snapshot(context, group_snapshot,
                                                 snapshots)

    @volume_utils.trace
    def manage_existing(self, volume, existing_ref):
        return self.common.manage_existing(volume, existing_ref)

    @volume_utils.trace
    def manage_existing_snapshot(self, snapshot, existing_ref):
        return self.common.manage_existing_snapshot(snapshot, existing_ref)

    @volume_utils.trace
    def manage_existing_get_size(self, volume, existing_ref):
        return self.common.manage_existing_get_size(volume, existing_ref)

    @volume_utils.trace
    def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
        return self.common.manage_existing_snapshot_get_size(snapshot,
                                                             existing_ref)

    @volume_utils.trace
    def unmanage(self, volume):
        return self.common.unmanage(volume)

    @volume_utils.trace
    def unmanage_snapshot(self, snapshot):
        return self.common.unmanage_snapshot(snapshot)

    @volume_utils.trace
    def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
                               sort_keys, sort_dirs):
        return self.common.get_manageable_volumes(cinder_volumes, marker,
                                                  limit, offset, sort_keys,
                                                  sort_dirs)

    @volume_utils.trace
    def get_manageable_snapshots(self, cinder_snapshots, marker, limit, offset,
                                 sort_keys, sort_dirs):
        return self.common.get_manageable_snapshots(cinder_snapshots, marker,
                                                    limit, offset, sort_keys,
                                                    sort_dirs)

    @volume_utils.trace
    def retype(self, context, volume, new_type, diff, host):
        """Convert the volume to be of the new type."""
        common = self._login()
        try:
            return common.retype(volume, new_type, diff, host)
        finally:
            self._logout(common)

    @volume_utils.trace
    def migrate_volume(self, context, volume, host):
        if volume['status'] == 'in-use':
            protocol = host['capabilities']['storage_protocol']
            if protocol != self.protocol:
                LOG.debug("3PAR %(protocol)s driver cannot migrate in-use "
                          "volume to a host with "
                          "storage_protocol=%(storage_protocol)s",
                          {'protocol': self.protocol,
                           'storage_protocol': protocol})
                return False, None

        return self.common.migrate_volume(volume, host)

    @volume_utils.trace
    def update_migrated_volume(self, context, volume, new_volume,
                               original_volume_status):
        """Update the name of the migrated volume to it's new ID."""
        return self.common.update_migrated_volume(context, volume, new_volume,
                                                  original_volume_status)

    @volume_utils.trace
    def get_pool(self, volume):
        try:
            return self.common.get_cpg(volume)
        except hpeexceptions.HTTPNotFound:
            reason = (_("Volume %s doesn't exist on array.") % volume)
            LOG.error(reason)
            raise exception.InvalidVolume(reason)

    @volume_utils.trace
    def revert_to_snapshot(self, context, volume, snapshot):
        """Revert volume to snapshot."""
        return self.common.revert_to_snapshot(volume, snapshot)

    @volume_utils.trace
    def failover_host(self, context, volumes, secondary_id=None, groups=None):
        """Force failover to a secondary replication target."""
        common = self._login(timeout=30)
        try:
            # Update the active_backend_id in the driver and return it.
            active_backend_id, volume_updates, group_update_list = (
                common.failover_host(
                    context, volumes, secondary_id, groups))
            self._active_backend_id = active_backend_id
            return active_backend_id, volume_updates, group_update_list
        finally:
            self._logout(common)

    def enable_replication(self, context, group, volumes):
        """Enable replication for a group.

        :param context: the context
        :param group: the group object
        :param volumes: the list of volumes
        :returns: model_update, None
        """
        return self.common.enable_replication(context, group, volumes)

    def disable_replication(self, context, group, volumes):
        """Disable replication for a group.

        :param context: the context
        :param group: the group object
        :param volumes: the list of volumes
        :returns: model_update, None
        """
        return self.common.disable_replication(context, group, volumes)

    def failover_replication(self, context, group, volumes,
                             secondary_backend_id=None):
        """Failover replication for a group.

        :param context: the context
        :param group: the group object
        :param volumes: the list of volumes
        :param secondary_backend_id: the secondary backend id - default None
        :returns: model_update, vol_model_updates
        """
        common = self._login()
        try:
            return common.failover_replication(
                context, group, volumes, secondary_backend_id)
        finally:
            self._logout(common)

    def do_setup(self, context):
        common = self._init_common()
        common.do_setup(context)
        self._check_flags(common)
        common.check_for_setup_error()
        self._do_setup(common)

    def _do_setup(self, common):
        pass

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

    def ensure_export(self, context, volume):
        pass

    def remove_export(self, context, volume):
        pass

    def terminate_connection(self, volume, connector, **kwargs):
        pass

    def initialize_connection(self, volume, connector):
        pass

    @volume_utils.trace
    def _init_vendor_properties(self):
        """Create a dictionary of vendor unique properties.

        This method creates a dictionary of vendor unique properties
        and returns both created dictionary and vendor name.
        Returned vendor name is used to check for name of vendor
        unique properties.

        - Vendor name shouldn't include colon(:) because of the separator
          and it is automatically replaced by underscore(_).
          ex. abc:d -> abc_d
        - Vendor prefix is equal to vendor name.
          ex. abcd
        - Vendor unique properties must start with vendor prefix + ':'.
          ex. abcd:maxIOPS

        Each backend driver needs to override this method to expose
        its own properties using _set_property() like this:

        self._set_property(
            properties,
            "vendorPrefix:specific_property",
            "Title of property",
            _("Description of property"),
            "type")

        : return dictionary of vendor unique properties
        : return vendor name

        prefix: HPE:3PAR --> HPE_3PAR
        """

        properties = {}
        valid_prov_values = ['thin', 'full', 'dedup']
        valid_persona_values = ['2 - Generic-ALUA',
                                '1 - Generic',
                                '3 - Generic-legacy',
                                '4 - HPEUX-legacy',
                                '5 - AIX-legacy',
                                '6 - EGENERA',
                                '7 - ONTAP-legacy',
                                '8 - VMware',
                                '9 - OpenVMS',
                                '10 - HPEUX',
                                '11 - WindowsServer']

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:snap_cpg",
            "Snap CPG Extra-specs.",
            _("Specifies the Snap CPG for a volume type. It overrides the "
              "hpe3par_cpg_snap setting. Defaults to the hpe3par_cpg_snap "
              "setting in the cinder.conf file. If hpe3par_cpg_snap is not "
              "set, it defaults to the hpe3par_cpg setting."),
            "string")

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:persona",
            "Host Persona Extra-specs.",
            _("Specifies the host persona property for a volume type. It "
              "overrides the hpe3par_cpg_snap setting. Defaults to the "
              "hpe3par_cpg_snap setting in the cinder.conf file. "
              "If hpe3par_cpg_snap is not set, "
              "it defaults to the hpe3par_cpg setting."),
            "string",
            enum=valid_persona_values,
            default="2 - Generic-ALUA")

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:vvs",
            "Virtual Volume Set Extra-specs.",
            _("The virtual volume set name that has been set up by the "
              "administrator that would have predefined QoS rules "
              "associated with it. If you specify extra_specs "
              "hpe3par:vvs, the qos_specs minIOPS, maxIOPS, minBWS, "
              "and maxBWS settings are ignored."),
            "string")

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:flash_cache",
            "Flash cache Extra-specs.",
            _("Enables Flash cache setting for a volume type."),
            "boolean",
            default=False)

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:provisioning",
            "Storage Provisioning Extra-specs.",
            _("Specifies the provisioning for a volume type."),
            "string",
            enum=valid_prov_values,
            default="thin")

        self._set_property(
            properties,
            "HPE:3PAR:hpe3par:compression",
            "Storage Provisioning Extra-specs.",
            _("Enables compression for a volume type. "
              "Minimum requirement of 3par OS version is 3.3.1 "
              "with SSD drives only. "
              "Volume size must have > 16 GB to enable "
              "compression on volume. "
              "A full provisioned volume cannot be compressed."),
            "boolean",
            default=False)

        self._set_property(
            properties,
            "HPE:3PAR:replication_enabled",
            "Volume Replication Extra-specs.",
            _("The valid value is: <is> True "
              "If True, the volume is to be replicated, if supported, "
              "by the backend driver. If the option is not specified or "
              "false, then replication is not enabled. This option is "
              "required to enable replication."),
            "string",
            enum=["<is> True"],
            default=False)

        self._set_property(
            properties,
            "HPE:3PAR:replication:mode",
            "Replication Mode Extra-specs.",
            _("Sets the replication mode for 3par."),
            "string",
            enum=["sync", "periodic"],
            default="periodic")

        self._set_property(
            properties,
            "HPE:3PAR:replication:sync_period",
            "Sync Period for Volume Replication Extra-specs.",
            _("Sets the time interval for synchronization. "
              "Only needed if replication:mode is periodic."),
            "integer",
            default=900)

        self._set_property(
            properties,
            "HPE:3PAR:replication:retention_count",
            "Retention Count for Replication Extra-specs.",
            _("Sets the number of snapshots that will be  "
              "saved on the primary array."),
            "integer",
            default=5)

        self._set_property(
            properties,
            "HPE:3PAR:replication:remote_retention_count",
            "Remote Retention Count for Replication Extra-specs.",
            _("Sets the number of snapshots that will be  "
              "saved on the secondary array."),
            "integer",
            default=5)

        # ###### QoS Settings ###### #

        self._set_property(
            properties,
            "HPE:3PAR:minIOPS",
            "Minimum IOPS QoS.",
            _("Sets the QoS, I/O issue count minimum goal. "
              "If not specified, there is no limit on I/O issue count."),
            "integer")

        self._set_property(
            properties,
            "HPE:3PAR:maxIOPS",
            "Maximum IOPS QoS.",
            _("Sets the QoS, I/O issue count rate limit. "
              "If not specified, there is no limit on I/O issue count."),
            "integer")

        self._set_property(
            properties,
            "HPE:3PAR:minBWS",
            "Minimum Bandwidth QoS.",
            _("Sets the QoS, I/O issue bandwidth minimum goal. "
              "If not specified, there is no limit on "
              "I/O issue bandwidth rate."),
            "integer")

        self._set_property(
            properties,
            "HPE:3PAR:maxBWS",
            "Maximum Bandwidth QoS.",
            _("Sets the QoS, I/O issue bandwidth rate limit. "
              "If not specified, there is no limit on I/O issue "
              "bandwidth rate."),
            "integer")

        self._set_property(
            properties,
            "HPE:3PAR:latency",
            "Latency QoS.",
            _("Sets the latency goal in milliseconds."),
            "integer")

        self._set_property(
            properties,
            "HPE:3PAR:priority",
            "Priority QoS.",
            _("Sets the priority of the QoS rule over other rules."),
            "string",
            enum=["low", "normal", "high"],
            default="normal")

        return properties, 'HPE:3PAR'

    @classmethod
    def clean_volume_file_locks(cls, volume_id):
        coordination.synchronized_remove('3par-' + volume_id)
