# Copyright (c) 2016 by Tegile Systems, Inc.
# 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.
"""
Share driver for Tegile storage.
"""

import json
import requests
import six

from oslo_config import cfg
from oslo_log import log

from manila import exception
from manila.i18n import _
from manila.share import driver
from manila.share import utils as share_utils
from manila import utils

tegile_opts = [
    cfg.HostAddressOpt('tegile_nas_server',
                       help='Tegile NAS server hostname or IP address.'),
    cfg.StrOpt('tegile_nas_login',
               help='User name for the Tegile NAS server.'),
    cfg.StrOpt('tegile_nas_password',
               help='Password for the Tegile NAS server.'),
    cfg.StrOpt('tegile_default_project',
               help='Create shares in this project')]


CONF = cfg.CONF
CONF.register_opts(tegile_opts)

LOG = log.getLogger(__name__)
DEFAULT_API_SERVICE = 'openstack'
TEGILE_API_PATH = 'zebi/api'
TEGILE_LOCAL_CONTAINER_NAME = 'Local'
TEGILE_SNAPSHOT_PREFIX = 'Manual-S-'
VENDOR = 'Tegile Systems Inc.'
DEFAULT_BACKEND_NAME = 'Tegile'
VERSION = '1.0.0'
DEBUG_LOGGING = False  # For debugging purposes


def debugger(func):
    """Returns a wrapper that wraps func.

    The wrapper will log the entry and exit points of the function.
    """

    def wrapper(*args, **kwds):
        if DEBUG_LOGGING:
            LOG.debug('Entering %(classname)s.%(funcname)s',
                      {
                          'classname': args[0].__class__.__name__,
                          'funcname': func.__name__,
                      })
            LOG.debug('Arguments: %(args)s, %(kwds)s',
                      {
                          'args': args[1:],
                          'kwds': kwds,
                      })
        f_result = func(*args, **kwds)
        if DEBUG_LOGGING:
            LOG.debug('Exiting %(classname)s.%(funcname)s',
                      {
                          'classname': args[0].__class__.__name__,
                          'funcname': func.__name__,
                      })
            LOG.debug('Results: %(result)s',
                      {'result': f_result})
        return f_result

    return wrapper


class TegileAPIExecutor(object):
    def __init__(self, classname, hostname, username, password):
        self._classname = classname
        self._hostname = hostname
        self._username = username
        self._password = password

    def __call__(self, *args, **kwargs):
        return self._send_api_request(*args, **kwargs)

    @debugger
    @utils.retry(retry_param=(requests.ConnectionError, requests.Timeout),
                 interval=30,
                 retries=3,
                 backoff_rate=1)
    def _send_api_request(self, method, params=None,
                          request_type='post',
                          api_service=DEFAULT_API_SERVICE,
                          fine_logging=DEBUG_LOGGING):
        if params is not None:
            params = json.dumps(params)

        url = 'https://%s/%s/%s/%s' % (self._hostname,
                                       TEGILE_API_PATH,
                                       api_service,
                                       method)
        if fine_logging:
            LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, '
                      'url: %(url)s', {
                          'classname': self._classname,
                          'method': method,
                          'url': url,
                      })
        if request_type == 'post':
            if fine_logging:
                LOG.debug('TegileAPIExecutor(%(classname)s) '
                          'method: %(method)s, payload: %(payload)s',
                          {
                              'classname': self._classname,
                              'method': method,
                              'payload': params,
                          })
            req = requests.post(url,
                                data=params,
                                auth=(self._username, self._password),
                                verify=False)
        else:
            req = requests.get(url,
                               auth=(self._username, self._password),
                               verify=False)

        if fine_logging:
            LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, '
                      'return code: %(retcode)s',
                      {
                          'classname': self._classname,
                          'method': method,
                          'retcode': req,
                      })
        try:
            response = req.json()
            if fine_logging:
                LOG.debug('TegileAPIExecutor(%(classname)s) '
                          'method: %(method)s, response: %(response)s',
                          {
                              'classname': self._classname,
                              'method': method,
                              'response': response,
                          })
        except ValueError:
            # Some APIs don't return output and that's fine
            response = ''
        req.close()

        if req.status_code != 200:
            raise exception.TegileAPIException(response=req.text)

        return response


class TegileShareDriver(driver.ShareDriver):
    """Tegile NAS driver. Allows for NFS and CIFS NAS storage usage."""
    def __init__(self, *args, **kwargs):
        super(TegileShareDriver, self).__init__(False, *args, **kwargs)

        self.configuration.append_config_values(tegile_opts)
        self._default_project = (self.configuration.safe_get(
            "tegile_default_project") or 'openstack')
        self._backend_name = (self.configuration.safe_get('share_backend_name')
                              or CONF.share_backend_name
                              or DEFAULT_BACKEND_NAME)
        self._hostname = self.configuration.safe_get('tegile_nas_server')
        username = self.configuration.safe_get('tegile_nas_login')
        password = self.configuration.safe_get('tegile_nas_password')
        self._api = TegileAPIExecutor(self.__class__.__name__,
                                      self._hostname,
                                      username,
                                      password)

    @debugger
    def create_share(self, context, share, share_server=None):
        """Is called to create share."""
        share_name = share['name']
        share_proto = share['share_proto']

        pool_name = share_utils.extract_host(share['host'], level='pool')

        params = (pool_name, self._default_project, share_name, share_proto)

        # Share name coming from the backend is the most reliable. Sometimes
        # a few options in Tegile array could cause sharename to be different
        # from the one passed to it. Eg. 'projectname-sharename' instead
        # of 'sharename' if inherited share properties are selected.
        ip, real_share_name = self._api('createShare', params).split()

        LOG.info("Created share %(sharename)s, share id %(shid)s.",
                 {'sharename': share_name, 'shid': share['id']})

        return self._get_location_path(real_share_name, share_proto, ip)

    @debugger
    def extend_share(self, share, new_size, share_server=None):
        """Is called to extend share.

        There is no resize for Tegile shares.
        We just adjust the quotas. The API is still called 'resizeShare'.
        """

        self._adjust_size(share, new_size, share_server)

    @debugger
    def shrink_share(self, shrink_share, shrink_size, share_server=None):
        """Uses resize_share to shrink a share.

        There is no shrink for Tegile shares.
        We just adjust the quotas. The API is still called 'resizeShare'.
        """
        self._adjust_size(shrink_share, shrink_size, share_server)

    @debugger
    def _adjust_size(self, share, new_size, share_server=None):
        pool, project, share_name = self._get_pool_project_share_name(share)
        params = ('%s/%s/%s/%s' % (pool,
                                   TEGILE_LOCAL_CONTAINER_NAME,
                                   project,
                                   share_name),
                  six.text_type(new_size),
                  'GB')
        self._api('resizeShare', params)

    @debugger
    def delete_share(self, context, share, share_server=None):
        """Is called to remove share."""
        pool, project, share_name = self._get_pool_project_share_name(share)
        params = ('%s/%s/%s/%s' % (pool,
                                   TEGILE_LOCAL_CONTAINER_NAME,
                                   project,
                                   share_name),
                  True,
                  False)

        self._api('deleteShare', params)

    @debugger
    def create_snapshot(self, context, snapshot, share_server=None):
        """Is called to create snapshot."""
        snap_name = snapshot['name']

        pool, project, share_name = self._get_pool_project_share_name(
            snapshot['share'])

        share = {
            'poolName': '%s' % pool,
            'projectName': '%s' % project,
            'name': share_name,
            'availableSize': 0,
            'totalSize': 0,
            'datasetPath': '%s/%s/%s' %
                           (pool,
                            TEGILE_LOCAL_CONTAINER_NAME,
                            project),
            'mountpoint': share_name,
            'local': 'true',
        }

        params = (share, snap_name, False)

        LOG.info('Creating snapshot for share_name=%(shr)s'
                 ' snap_name=%(name)s',
                 {'shr': share_name, 'name': snap_name})

        self._api('createShareSnapshot', params)

    @debugger
    def create_share_from_snapshot(self, context, share, snapshot,
                                   share_server=None, parent_share=None):
        """Create a share from a snapshot - clone a snapshot."""
        pool, project, share_name = self._get_pool_project_share_name(share)

        params = ('%s/%s/%s/%s@%s%s' % (pool,
                                        TEGILE_LOCAL_CONTAINER_NAME,
                                        project,
                                        snapshot['share_name'],
                                        TEGILE_SNAPSHOT_PREFIX,
                                        snapshot['name'],
                                        ),
                  share_name,
                  True,
                  )

        ip, real_share_name = self._api('cloneShareSnapshot',
                                        params).split()

        share_proto = share['share_proto']
        return self._get_location_path(real_share_name, share_proto, ip)

    @debugger
    def delete_snapshot(self, context, snapshot, share_server=None):
        """Is called to remove snapshot."""
        pool, project, share_name = self._get_pool_project_share_name(
            snapshot['share'])
        params = ('%s/%s/%s/%s@%s%s' % (pool,
                                        TEGILE_LOCAL_CONTAINER_NAME,
                                        project,
                                        share_name,
                                        TEGILE_SNAPSHOT_PREFIX,
                                        snapshot['name']),
                  False)

        self._api('deleteShareSnapshot', params)

    @debugger
    def ensure_share(self, context, share, share_server=None):
        """Invoked to sure that share is exported."""

        # Fetching share name from server, because some configuration
        # options can cause sharename different from the OpenStack share name
        pool, project, share_name = self._get_pool_project_share_name(share)
        params = [
            '%s/%s/%s/%s' % (pool,
                             TEGILE_LOCAL_CONTAINER_NAME,
                             project,
                             share_name),
        ]
        ip, real_share_name = self._api('getShareIPAndMountPoint',
                                        params).split()

        share_proto = share['share_proto']
        location = self._get_location_path(real_share_name, share_proto, ip)
        return [location]

    @debugger
    def _allow_access(self, context, share, access, share_server=None):
        """Allow access to the share."""
        share_proto = share['share_proto']
        access_type = access['access_type']
        access_level = access['access_level']
        access_to = access['access_to']

        self._check_share_access(share_proto, access_type)

        pool, project, share_name = self._get_pool_project_share_name(share)
        params = ('%s/%s/%s/%s' % (pool,
                                   TEGILE_LOCAL_CONTAINER_NAME,
                                   project,
                                   share_name),
                  share_proto,
                  access_type,
                  access_to,
                  access_level)

        self._api('shareAllowAccess', params)

    @debugger
    def _deny_access(self, context, share, access, share_server=None):
        """Deny access to the share."""
        share_proto = share['share_proto']
        access_type = access['access_type']
        access_level = access['access_level']
        access_to = access['access_to']

        self._check_share_access(share_proto, access_type)

        pool, project, share_name = self._get_pool_project_share_name(share)
        params = ('%s/%s/%s/%s' % (pool,
                                   TEGILE_LOCAL_CONTAINER_NAME,
                                   project,
                                   share_name),
                  share_proto,
                  access_type,
                  access_to,
                  access_level)

        self._api('shareDenyAccess', params)

    def _check_share_access(self, share_proto, access_type):
        if share_proto == 'CIFS' and access_type != 'user':
            reason = ('Only USER access type is allowed for '
                      'CIFS shares.')
            LOG.warning(reason)
            raise exception.InvalidShareAccess(reason=reason)
        elif share_proto == 'NFS' and access_type not in ('ip', 'user'):
            reason = ('Only IP or USER access types are allowed for '
                      'NFS shares.')
            LOG.warning(reason)
            raise exception.InvalidShareAccess(reason=reason)
        elif share_proto not in ('NFS', 'CIFS'):
            reason = ('Unsupported protocol \"%s\" specified for '
                      'access rule.') % share_proto
            raise exception.InvalidShareAccess(reason=reason)

    @debugger
    def update_access(self, context, share, access_rules, add_rules,
                      delete_rules, share_server=None):
        if not (add_rules or delete_rules):
            # Recovery mode
            pool, project, share_name = (
                self._get_pool_project_share_name(share))
            share_proto = share['share_proto']
            params = ('%s/%s/%s/%s' % (pool,
                                       TEGILE_LOCAL_CONTAINER_NAME,
                                       project,
                                       share_name),
                      share_proto)

            # Clears all current ACLs
            # Remove ip and user ACLs if share_proto is NFS
            # Remove user ACLs if share_proto is CIFS
            self._api('clearAccessRules', params)

            # Looping through all rules.
            # Will have one API call per rule.
            for access in access_rules:
                self._allow_access(context, share, access, share_server)
        else:
            # Adding/Deleting specific rules
            for access in delete_rules:
                self._deny_access(context, share, access, share_server)
            for access in add_rules:
                self._allow_access(context, share, access, share_server)

    @debugger
    def _update_share_stats(self, **kwargs):
        """Retrieve stats info."""

        try:
            data = self._api(method='getArrayStats',
                             request_type='get',
                             fine_logging=False)
            # fixing values coming back here as String to float
            for pool in data.get('pools', []):
                pool['total_capacity_gb'] = float(
                    pool.get('total_capacity_gb', 0))
                pool['free_capacity_gb'] = float(
                    pool.get('free_capacity_gb', 0))
                pool['allocated_capacity_gb'] = float(
                    pool.get('allocated_capacity_gb', 0))

                pool['qos'] = pool.pop('QoS_support', False)
                pool['reserved_percentage'] = (
                    self.configuration.reserved_share_percentage)
                pool['reserved_snapshot_percentage'] = (
                    self.configuration.reserved_share_from_snapshot_percentage
                    or self.configuration.reserved_share_percentage)
                pool['dedupe'] = True
                pool['compression'] = True
                pool['thin_provisioning'] = True
                pool['max_over_subscription_ratio'] = (
                    self.configuration.max_over_subscription_ratio)

            data['share_backend_name'] = self._backend_name
            data['vendor_name'] = VENDOR
            data['driver_version'] = VERSION
            data['storage_protocol'] = 'NFS_CIFS'
            data['snapshot_support'] = True
            data['create_share_from_snapshot_support'] = True
            data['qos'] = False

            super(TegileShareDriver, self)._update_share_stats(data)
        except Exception:
            msg = _('Unexpected error while trying to get the '
                    'usage stats from array.')
            LOG.exception(msg)
            raise

    @debugger
    def get_pool(self, share):
        """Returns pool name where share resides.

        :param share: The share hosted by the driver.
        :return: Name of the pool where given share is hosted.
        """
        pool = share_utils.extract_host(share['host'], level='pool')
        return pool

    @debugger
    def get_network_allocations_number(self):
        """Get number of network interfaces to be created."""
        return 0

    @debugger
    def _get_location_path(self, share_name, share_proto, ip=None):
        if ip is None:
            ip = self._hostname
        if share_proto == 'NFS':
            location = '%s:%s' % (ip, share_name)
        elif share_proto == 'CIFS':
            location = r'\\%s\%s' % (ip, share_name)
        else:
            message = _('Invalid NAS protocol supplied: %s.') % share_proto
            raise exception.InvalidInput(message)

        export_location = {
            'path': location,
            'is_admin_only': False,
            'metadata': {
                'preferred': True,
            },
        }
        return export_location

    @debugger
    def _get_pool_project_share_name(self, share):
        pool = share_utils.extract_host(share['host'], level='pool')
        project = self._default_project

        share_name = share['name']

        return pool, project, share_name
