# Copyright 2015 Hewlett Packard Enterprise Development LP
#
#    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.

"""HPE 3PAR Driver for OpenStack Manila."""

import datetime
import hashlib
import inspect
import os
import re

from oslo_config import cfg
from oslo_config import types
from oslo_log import log
import six

from manila.common import config
from manila import exception
from manila.i18n import _
from manila.share import driver
from manila.share.drivers.hpe import hpe_3par_mediator
from manila.share import share_types
from manila.share import utils as share_utils
from manila import utils

LOG = log.getLogger(__name__)


class FPG(types.String, types.IPAddress):
    """FPG type.

    Used to represent multiple pools per backend values.
    Converts configuration value to an FPGs value.
    FPGs value format::

        FPG name, IP address 1, IP address 2, ..., IP address 4

    where FPG name is a string value,
    IP address is of type types.IPAddress

    Optionally doing range checking.
    If value is whitespace or empty string will raise error

    :param min_ip: Optional check that number of min IP address of VFS.
    :param max_ip: Optional check that number of max IP address of VFS.
    :param type_name: Type name to be used in the sample config file.

    """

    MAX_SUPPORTED_IP_PER_VFS = 4

    def __init__(self, min_ip=0, max_ip=MAX_SUPPORTED_IP_PER_VFS,
                 type_name='FPG'):
        types.String.__init__(self, type_name=type_name)
        types.IPAddress.__init__(self, type_name=type_name)

        if max_ip < min_ip:
            msg = _("Pool's max acceptable IP cannot be less than min.")
            raise exception.HPE3ParInvalid(err=msg)

        if min_ip < 0:
            msg = _("Pools must be configured with zero or more IPs.")
            raise exception.HPE3ParInvalid(err=msg)

        if max_ip > FPG.MAX_SUPPORTED_IP_PER_VFS:
            msg = (_("Pool's max acceptable IP cannot be greater than "
                     "supported value=%s.") % FPG.MAX_SUPPORTED_IP_PER_VFS)
            raise exception.HPE3ParInvalid(err=msg)

        self.min_ip = min_ip
        self.max_ip = max_ip

    def __call__(self, value):
        if value is None or value.strip(' ') == '':
            message = _("Invalid configuration. hpe3par_fpg must be set.")
            LOG.error(message)
            raise exception.HPE3ParInvalid(err=message)

        ips = []
        values = value.split(",")
        # Extract pool name
        pool_name = values.pop(0).strip()

        # values will now be ['ip1', ...]
        if len(values) < self.min_ip:
            msg = (_("Require at least %s IPs configured per "
                     "pool") % self.min_ip)
            raise exception.HPE3ParInvalid(err=msg)
        if len(values) > self.max_ip:
            msg = (_("Cannot configure IPs more than max supported "
                     "%s IPs per pool") % self.max_ip)
            raise exception.HPE3ParInvalid(err=msg)

        for ip_addr in values:
            ip_addr = types.String.__call__(self, ip_addr.strip())
            try:
                ips.append(types.IPAddress.__call__(self, ip_addr))
            except ValueError as verror:
                raise exception.HPE3ParInvalid(err=verror)
        fpg = {pool_name: ips}
        return fpg

    def __repr__(self):
        return 'FPG'

    def _formatter(self, value):
        return six.text_type(value)


HPE3PAR_OPTS = [
    cfg.StrOpt('hpe3par_api_url',
               default='',
               help="3PAR WSAPI Server Url like "
                    "https://<3par ip>:8080/api/v1"),
    cfg.StrOpt('hpe3par_username',
               default='',
               help="3PAR username with the 'edit' role"),
    cfg.StrOpt('hpe3par_password',
               default='',
               help="3PAR password for the user specified in hpe3par_username",
               secret=True),
    cfg.HostAddressOpt('hpe3par_san_ip',
                       help="IP address of SAN controller"),
    cfg.StrOpt('hpe3par_san_login',
               default='',
               help="Username for SAN controller"),
    cfg.StrOpt('hpe3par_san_password',
               default='',
               help="Password for SAN controller",
               secret=True),
    cfg.PortOpt('hpe3par_san_ssh_port',
                default=22,
                help='SSH port to use with SAN'),
    cfg.MultiOpt('hpe3par_fpg',
                 item_type=FPG(min_ip=0, max_ip=FPG.MAX_SUPPORTED_IP_PER_VFS),
                 help="The File Provisioning Group (FPG) to use"),
    cfg.BoolOpt('hpe3par_fstore_per_share',
                default=False,
                help="Use one filestore per share"),
    cfg.BoolOpt('hpe3par_require_cifs_ip',
                default=False,
                help="Require IP access rules for CIFS (in addition to user)"),
    cfg.BoolOpt('hpe3par_debug',
                default=False,
                help="Enable HTTP debugging to 3PAR"),
    cfg.StrOpt('hpe3par_cifs_admin_access_username',
               default='',
               help="File system admin user name for CIFS."),
    cfg.StrOpt('hpe3par_cifs_admin_access_password',
               default='',
               help="File system admin password for CIFS.",
               secret=True),
    cfg.StrOpt('hpe3par_cifs_admin_access_domain',
               default='LOCAL_CLUSTER',
               help="File system domain for the CIFS admin user."),
    cfg.StrOpt('hpe3par_share_mount_path',
               default='/mnt/',
               help="The path where shares will be mounted when deleting "
                    "nested file trees."),
]

CONF = cfg.CONF
CONF.register_opts(HPE3PAR_OPTS)


def to_list(var):
    """Convert var to list type if not"""
    if isinstance(var, six.string_types):
        return [var]
    else:
        return var


class HPE3ParShareDriver(driver.ShareDriver):
    """HPE 3PAR driver for Manila.

    Supports NFS and CIFS protocols on arrays with File Persona.

    Version history::

        1.0.0 - Begin Liberty development (post-Kilo)
        1.0.1 - Report thin/dedup/hp_flash_cache capabilities
        1.0.2 - Add share server/share network support
        2.0.0 - Rebranded HP to HPE
        2.0.1 - Add access_level (e.g. read-only support)
        2.0.2 - Add extend/shrink
        2.0.3 - Remove file tree on delete when using nested shares #1538800
        2.0.4 - Reduce the fsquota by share size
                when a share is deleted #1582931
        2.0.5 - Add update_access support
        2.0.6 - Multi pool support per backend
        2.0.7 - Fix get_vfs() to correctly validate conf IP addresses at
                boot up #1621016
        2.0.8 - Replace ConsistencyGroup with ShareGroup

    """

    VERSION = "2.0.8"

    def __init__(self, *args, **kwargs):
        super(HPE3ParShareDriver, self).__init__((True, False),
                                                 *args,
                                                 **kwargs)

        self.configuration = kwargs.get('configuration', None)
        self.configuration.append_config_values(HPE3PAR_OPTS)
        self.configuration.append_config_values(driver.ssh_opts)
        self.configuration.append_config_values(config.global_opts)
        self.fpgs = {}
        self._hpe3par = None  # mediator between driver and client

    def do_setup(self, context):
        """Any initialization the share driver does while starting."""

        LOG.info("Starting share driver %(driver_name)s (%(version)s)",
                 {'driver_name': self.__class__.__name__,
                  'version': self.VERSION})

        mediator = hpe_3par_mediator.HPE3ParMediator(
            hpe3par_username=self.configuration.hpe3par_username,
            hpe3par_password=self.configuration.hpe3par_password,
            hpe3par_api_url=self.configuration.hpe3par_api_url,
            hpe3par_debug=self.configuration.hpe3par_debug,
            hpe3par_san_ip=self.configuration.hpe3par_san_ip,
            hpe3par_san_login=self.configuration.hpe3par_san_login,
            hpe3par_san_password=self.configuration.hpe3par_san_password,
            hpe3par_san_ssh_port=self.configuration.hpe3par_san_ssh_port,
            hpe3par_fstore_per_share=(self.configuration
                                      .hpe3par_fstore_per_share),
            hpe3par_require_cifs_ip=self.configuration.hpe3par_require_cifs_ip,
            hpe3par_cifs_admin_access_username=(
                self.configuration.hpe3par_cifs_admin_access_username),
            hpe3par_cifs_admin_access_password=(
                self.configuration.hpe3par_cifs_admin_access_password),
            hpe3par_cifs_admin_access_domain=(
                self.configuration.hpe3par_cifs_admin_access_domain),
            hpe3par_share_mount_path=(
                self.configuration.hpe3par_share_mount_path),
            my_ip=self.configuration.my_ip,
            ssh_conn_timeout=self.configuration.ssh_conn_timeout,
        )

        mediator.do_setup()

        def _validate_pool_ips(addresses, conf_pool_ips):
            # Pool configured IP addresses should be subset of IP addresses
            # retured from vfs
            if not set(conf_pool_ips) <= set(addresses):
                msg = _("Incorrect configuration. "
                        "Configuration pool IP address did not match with "
                        "IP addresses at 3par array")
                raise exception.HPE3ParInvalid(err=msg)

        def _construct_fpg():
            # FPG must be configured and must exist.
            # self.configuration.safe_get('hpe3par_fpg') will have value in
            # following format:
            # [ {'pool_name':['ip_addr', 'ip_addr', ...]}, ... ]
            for fpg in self.configuration.safe_get('hpe3par_fpg'):
                pool_name = list(fpg)[0]
                conf_pool_ips = fpg[pool_name]

                # Validate the FPG and discover the VFS
                # This also validates the client, connection, firmware, WSAPI,
                # FPG...
                vfs_info = mediator.get_vfs(pool_name)
                if self.driver_handles_share_servers:
                    # Use discovered IP(s) from array
                    self.fpgs[pool_name] = {
                        vfs_info['vfsname']: vfs_info['vfsip']['address']}
                elif conf_pool_ips == []:
                    # not DHSS and IPs not configured in manila.conf.
                    if not vfs_info['vfsip']['address']:
                        msg = _("Unsupported configuration. "
                                "hpe3par_fpg must have IP address "
                                "or be discoverable at 3PAR")
                        LOG.error(msg)
                        raise exception.HPE3ParInvalid(err=msg)
                    else:
                        # Use discovered pool ips
                        self.fpgs[pool_name] = {
                            vfs_info['vfsname']: vfs_info['vfsip']['address']}
                else:
                    # not DHSS and IPs configured in manila.conf
                    _validate_pool_ips(vfs_info['vfsip']['address'],
                                       conf_pool_ips)
                    self.fpgs[pool_name] = {
                        vfs_info['vfsname']: conf_pool_ips}

        _construct_fpg()

        # Don't set _hpe3par until it is ready. Otherwise _update_stats fails.
        self._hpe3par = mediator

    def _get_pool_location_from_share_host(self, share_instance_host):
        # Return pool name, vfs, IPs for a pool from share instance host
        pool_name = share_utils.extract_host(share_instance_host, level='pool')
        if not pool_name:
            message = (_("Pool is not available in the share host %s.") %
                       share_instance_host)
            raise exception.InvalidHost(reason=message)

        if pool_name not in self.fpgs:
            message = (_("Pool location lookup failed. "
                         "Could not find pool %s") %
                       pool_name)
            raise exception.InvalidHost(reason=message)

        vfs = list(self.fpgs[pool_name])[0]
        ips = self.fpgs[pool_name][vfs]

        return (pool_name, vfs, ips)

    def _get_pool_location(self, share, share_server=None):
        # Return pool name, vfs, IPs for a pool from share host field
        # Use share_server if provided, instead of self.fpgs
        if share_server is not None:
            # When DHSS
            ips = share_server['backend_details'].get('ip')
            ips = to_list(ips)
            vfs = share_server['backend_details'].get('vfs')
            pool_name = share_server['backend_details'].get('fpg')
            return (pool_name, vfs, ips)
        else:
            # When DHSS = false
            return self._get_pool_location_from_share_host(share['host'])

    def check_for_setup_error(self):

        try:
            # Log the source SHA for support.  Only do this with DEBUG.
            if LOG.isEnabledFor(log.DEBUG):
                LOG.debug('HPE3ParShareDriver SHA1: %s',
                          self.sha1_hash(HPE3ParShareDriver))
                LOG.debug('HPE3ParMediator SHA1: %s',
                          self.sha1_hash(hpe_3par_mediator.HPE3ParMediator))
        except Exception as e:
            # Don't let any exceptions during the SHA1 logging interfere
            # with startup.  This is just debug info to identify the source
            # code.  If it doesn't work, just log a debug message.
            LOG.debug('Source code SHA1 not logged due to: %s',
                      six.text_type(e))

    @staticmethod
    def sha1_hash(clazz):
        """Get the SHA1 hash for the source of a class."""
        source_file = inspect.getsourcefile(clazz)
        file_size = os.path.getsize(source_file)

        sha1 = hashlib.sha1()
        sha1.update(("blob %u\0" % file_size).encode('utf-8'))

        with open(source_file, 'rb') as f:
            sha1.update(f.read())

        return sha1.hexdigest()

    def get_network_allocations_number(self):
        return 1

    def choose_share_server_compatible_with_share(self, context, share_servers,
                                                  share, snapshot=None,
                                                  share_group=None):
        """Method that allows driver to choose share server for provided share.

        If compatible share-server is not found, method should return None.

        :param context: Current context
        :param share_servers: list with share-server models
        :param share:  share model
        :param snapshot: snapshot model
        :param share_group: ShareGroup model with shares
        :returns: share-server or None
        """
        # If creating in a share group, raise exception
        if share_group:
            msg = _("HPE 3PAR driver does not support share group")
            raise exception.InvalidRequest(message=msg)

        pool_name = share_utils.extract_host(share['host'], level='pool')
        for share_server in share_servers:
            if share_server['backend_details'].get('fpg') == pool_name:
                return share_server
        return None

    @staticmethod
    def _validate_network_type(network_type):
        if network_type not in ('flat', 'vlan', None):
            reason = _('Invalid network type. %s is not supported by the '
                       '3PAR driver.')
            raise exception.NetworkBadConfigurationException(
                reason=reason % network_type)

    def _create_share_server(self, network_info, request_host=None):
        """Is called to create/setup share server"""
        # Return pool name, vfs, IPs for a pool
        pool_name, vfs, ips = self._get_pool_location_from_share_host(
            request_host)

        ip = network_info['network_allocations'][0]['ip_address']
        if ip not in ips:
            # Besides DHSS, admin could have setup IP to VFS directly on array
            if len(ips) > (FPG.MAX_SUPPORTED_IP_PER_VFS - 1):
                message = (_("Pool %s has exceeded 3PAR's "
                             "max supported VFS IP address") % pool_name)
                LOG.error(message)
                raise exception.Invalid(message)

            subnet = utils.cidr_to_netmask(network_info['cidr'])
            vlantag = network_info['segmentation_id']

            self._hpe3par.create_fsip(ip, subnet, vlantag, pool_name, vfs)
            # Update in global saved config, self.fpgs[pool_name]
            ips.append(ip)

        return {'share_server_name': network_info['server_id'],
                'share_server_id': network_info['server_id'],
                'ip': ip,
                'subnet': subnet,
                'vlantag': vlantag if vlantag else 0,
                'fpg': pool_name,
                'vfs': vfs}

    def _setup_server(self, network_info, metadata=None):

        LOG.debug("begin _setup_server with %s", network_info)

        self._validate_network_type(network_info['network_type'])
        if metadata is not None and metadata['request_host'] is not None:
            return self._create_share_server(network_info,
                                             metadata['request_host'])

    def _teardown_server(self, server_details, security_services=None):
        LOG.debug("begin _teardown_server with %s", server_details)
        fpg = server_details.get('fpg')
        vfs = server_details.get('vfs')
        ip = server_details.get('ip')
        self._hpe3par.remove_fsip(ip, fpg, vfs)
        if ip in self.fpgs[fpg][vfs]:
            self.fpgs[fpg][vfs].remove(ip)

    @staticmethod
    def build_share_comment(share):
        """Create an informational only comment to help admins and testers."""

        info = {
            'name': share['display_name'],
            'host': share['host'],
            'now': datetime.datetime.now().strftime('%H%M%S'),
        }

        acceptable = re.compile(r'[^a-zA-Z0-9_=:@# \-]+', re.UNICODE)
        comment = ("OpenStack Manila - host=%(host)s  orig_name=%(name)s "
                   "created=%(now)s" % info)

        return acceptable.sub('_', comment)[:254]  # clean and truncate

    def create_share(self, context, share, share_server=None):
        """Is called to create share."""

        fpg, vfs, ips = self._get_pool_location(share, share_server)

        protocol = share['share_proto']
        extra_specs = share_types.get_extra_specs_from_share(share)

        path = self._hpe3par.create_share(
            share['project_id'],
            share['id'],
            protocol,
            extra_specs,
            fpg, vfs,
            size=share['size'],
            comment=self.build_share_comment(share)
        )

        return self._hpe3par.build_export_locations(protocol, ips, path)

    def create_share_from_snapshot(self, context, share, snapshot,
                                   share_server=None, parent_share=None):
        """Is called to create share from snapshot."""

        fpg, vfs, ips = self._get_pool_location(share, share_server)

        protocol = share['share_proto']
        extra_specs = share_types.get_extra_specs_from_share(share)

        path = self._hpe3par.create_share_from_snapshot(
            share['id'],
            protocol,
            extra_specs,
            share['project_id'],
            snapshot['share_id'],
            snapshot['id'],
            fpg,
            vfs,
            ips,
            size=share['size'],
            comment=self.build_share_comment(share)
        )

        return self._hpe3par.build_export_locations(protocol, ips, path)

    def delete_share(self, context, share, share_server=None):
        """Deletes share and its fstore."""

        fpg, vfs, ips = self._get_pool_location(share, share_server)
        self._hpe3par.delete_share(share['project_id'],
                                   share['id'],
                                   share['size'],
                                   share['share_proto'],
                                   fpg,
                                   vfs,
                                   ips[0])

    def create_snapshot(self, context, snapshot, share_server=None):
        """Creates a snapshot of a share."""

        fpg, vfs, ips = self._get_pool_location(snapshot['share'],
                                                share_server)
        self._hpe3par.create_snapshot(snapshot['share']['project_id'],
                                      snapshot['share']['id'],
                                      snapshot['share']['share_proto'],
                                      snapshot['id'],
                                      fpg,
                                      vfs)

    def delete_snapshot(self, context, snapshot, share_server=None):
        """Deletes a snapshot of a share."""

        fpg, vfs, ips = self._get_pool_location(snapshot['share'],
                                                share_server)
        self._hpe3par.delete_snapshot(snapshot['share']['project_id'],
                                      snapshot['share']['id'],
                                      snapshot['share']['share_proto'],
                                      snapshot['id'],
                                      fpg,
                                      vfs)

    def ensure_share(self, context, share, share_server=None):
        pass

    def update_access(self, context, share, access_rules, add_rules,
                      delete_rules, share_server=None):
        """Update access to the share."""
        extra_specs = None
        if 'NFS' == share['share_proto']:  # Avoiding DB call otherwise
            extra_specs = share_types.get_extra_specs_from_share(share)

        fpg, vfs, ips = self._get_pool_location(share, share_server)
        self._hpe3par.update_access(share['project_id'],
                                    share['id'],
                                    share['share_proto'],
                                    extra_specs,
                                    access_rules,
                                    add_rules,
                                    delete_rules,
                                    fpg,
                                    vfs)

    def extend_share(self, share, new_size, share_server=None):
        """Extends size of existing share."""

        fpg, vfs, ips = self._get_pool_location(share, share_server)
        self._hpe3par.resize_share(share['project_id'],
                                   share['id'],
                                   share['share_proto'],
                                   new_size,
                                   share['size'],
                                   fpg,
                                   vfs)

    def shrink_share(self, share, new_size, share_server=None):
        """Shrinks size of existing share."""

        fpg, vfs, ips = self._get_pool_location(share, share_server)
        self._hpe3par.resize_share(share['project_id'],
                                   share['id'],
                                   share['share_proto'],
                                   new_size,
                                   share['size'],
                                   fpg,
                                   vfs)

    def _update_share_stats(self):
        """Retrieve stats info from share group."""

        backend_name = self.configuration.safe_get(
            'share_backend_name') or "HPE_3PAR"

        max_over_subscription_ratio = self.configuration.safe_get(
            'max_over_subscription_ratio')

        reserved_share_percentage = self.configuration.safe_get(
            'reserved_share_percentage')
        if reserved_share_percentage is None:
            reserved_share_percentage = 0

        reserved_share_from_snapshot_percentage = self.configuration.safe_get(
            'reserved_share_from_snapshot_percentage')
        if reserved_share_from_snapshot_percentage is None:
            reserved_share_from_snapshot_percentage = reserved_share_percentage

        stats = {
            'share_backend_name': backend_name,
            'driver_handles_share_servers': self.driver_handles_share_servers,
            'vendor_name': 'HPE',
            'driver_version': self.VERSION,
            'storage_protocol': 'NFS_CIFS',
            'total_capacity_gb': 0,
            'free_capacity_gb': 0,
            'provisioned_capacity_gb': 0,
            'reserved_percentage': reserved_share_percentage,
            'reserved_snapshot_percentage':
                reserved_share_from_snapshot_percentage,
            'max_over_subscription_ratio': max_over_subscription_ratio,
            'qos': False,
            'thin_provisioning': True,  # 3PAR default is thin
        }

        if not self._hpe3par:
            LOG.info(
                "Skipping capacity and capabilities update. Setup has not "
                "completed.")
        else:
            for fpg in self.fpgs:
                fpg_status = self._hpe3par.get_fpg_status(fpg)
                fpg_status['reserved_percentage'] = reserved_share_percentage
                fpg_status['reserved_snapshot_percentage'] = (
                    reserved_share_from_snapshot_percentage)
                LOG.debug("FPG status = %s.", fpg_status)
                stats.setdefault('pools', []).append(fpg_status)

        super(HPE3ParShareDriver, self)._update_share_stats(stats)
