# Copyright (c) 2015 Tintri.  All rights reserved.
# Copyright (c) 2012 NetApp, 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.
"""
Volume driver for Tintri storage.
"""

import datetime
import json
import math
import os
import re
import socket

from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import units
import requests
from six.moves import urllib

from cinder import exception
from cinder import utils
from cinder.i18n import _, _LE, _LI, _LW
from cinder.image import image_utils
from cinder import interface
from cinder.volume import driver
from cinder.volume.drivers import nfs

LOG = logging.getLogger(__name__)
default_api_version = 'v310'
img_prefix = 'image-'
tintri_path = '/tintri/'


tintri_opts = [
    cfg.StrOpt('tintri_server_hostname',
               help='The hostname (or IP address) for the storage system'),
    cfg.StrOpt('tintri_server_username',
               help='User name for the storage system'),
    cfg.StrOpt('tintri_server_password',
               help='Password for the storage system',
               secret=True),
    cfg.StrOpt('tintri_api_version',
               default=default_api_version,
               help='API version for the storage system'),
    cfg.IntOpt('tintri_image_cache_expiry_days',
               default=30,
               help='Delete unused image snapshots older than mentioned days'),
    cfg.StrOpt('tintri_image_shares_config',
               help='Path to image nfs shares file'),
]

CONF = cfg.CONF
CONF.register_opts(tintri_opts)


@interface.volumedriver
class TintriDriver(driver.ManageableVD,
                   driver.CloneableImageVD,
                   driver.SnapshotVD,
                   nfs.NfsDriver):
    """Base class for Tintri driver.

    Version History

    .. code-block:: none

        2.1.0.1 - Liberty driver
        2.2.0.1 - Mitaka driver
                -- Retype
                -- Image cache clean up
                -- Direct image clone fix
    """

    VENDOR = 'Tintri'
    VERSION = '2.2.0.1'
    # ThirdPartySystems wiki page
    CI_WIKI_NAME = "Tintri_CI"

    REQUIRED_OPTIONS = ['tintri_server_hostname', 'tintri_server_username',
                        'tintri_server_password']

    def __init__(self, *args, **kwargs):
        self._execute = None
        self._context = None
        super(TintriDriver, self).__init__(*args, **kwargs)
        self._execute_as_root = True
        self.configuration.append_config_values(tintri_opts)
        self.cache_cleanup = False
        self._mounted_image_shares = []

    def do_setup(self, context):
        self._image_shares_config = getattr(self.configuration,
                                            'tintri_image_shares_config')
        super(TintriDriver, self).do_setup(context)
        self._context = context
        self._check_ops(self.REQUIRED_OPTIONS, self.configuration)
        self._hostname = getattr(self.configuration, 'tintri_server_hostname')
        self._username = getattr(self.configuration, 'tintri_server_username')
        self._password = getattr(self.configuration, 'tintri_server_password')
        self._api_version = getattr(self.configuration, 'tintri_api_version',
                                    CONF.tintri_api_version)
        self._image_cache_expiry = getattr(self.configuration,
                                           'tintri_image_cache_expiry_days',
                                           CONF.tintri_image_cache_expiry_days)

    def get_pool(self, volume):
        """Returns pool name where volume resides.

        :param volume: The volume hosted by the driver.
        :return: Name of the pool where given volume is hosted.
        """
        return volume['provider_location']

    def _get_client(self):
        """Returns a Tintri REST client connection."""
        return TClient(self._hostname, self._username, self._password,
                       self._api_version)

    def create_snapshot(self, snapshot):
        """Creates a snapshot."""
        (__, path) = self._get_export_ip_path(snapshot.volume_id)
        volume_path = '%s/%s' % (path, snapshot.volume_name)
        volume_path = '%(path)s/%(volume_name)s' % {
            'path': path,
            'volume_name': snapshot.volume_name,
        }
        model_update = {}
        with self._get_client() as c:
            provider_id = c.create_snapshot(volume_path,
                                            snapshot.volume.display_name or
                                            snapshot.volume_name,
                                            snapshot.volume_id,
                                            snapshot.display_name or
                                            snapshot.name)
            snapshot.provider_id = provider_id
            # Store Tintri snapshot ID as snapshot provider_id
            model_update['provider_id'] = provider_id

        return model_update

    def delete_snapshot(self, snapshot):
        """Deletes a snapshot."""
        if snapshot.provider_id:
            with self._get_client() as c:
                c.delete_snapshot(snapshot.provider_id)
        else:
            LOG.info(_LI('Snapshot %s not found'), snapshot.name)

    def _check_ops(self, required_ops, configuration):
        """Ensures that the options we care about are set."""
        for op in required_ops:
            if not getattr(configuration, op):
                LOG.error(_LE('Configuration value %s is not set.'), op)
                raise exception.InvalidConfigurationValue(option=op,
                                                          value=None)

    def create_volume_from_snapshot(self, volume, snapshot):
        """Creates a volume from snapshot."""
        vol_size = volume.size
        snap_size = snapshot.volume_size

        self._clone_snapshot(snapshot.provider_id, volume.name,
                             snapshot.volume_id)
        share = self._get_provider_location(snapshot.volume_id)
        volume['provider_location'] = share
        path = self.local_path(volume)

        self._set_rw_permissions(path)
        if vol_size != snap_size:
            try:
                self.extend_volume(volume, vol_size)
            except Exception:
                LOG.error(_LE('Resizing %s failed. Cleaning volume.'),
                          volume.name)
                self._delete_file(path)
                raise

        return {'provider_location': volume['provider_location']}

    def _clone_snapshot(self, snapshot_id, clone_name, volume_id, share=None):
        """Clones volume from snapshot."""
        (host, path) = self._get_export_ip_path(volume_id, share)
        clone_path = '%s/%s-d' % (path, clone_name)
        with self._get_client() as c:
            c.clone_volume(snapshot_id, clone_path)

        self._move_cloned_volume(clone_name, volume_id, share)

    def _move_cloned_volume(self, clone_name, volume_id, share=None):
        local_path = self._get_local_path(volume_id, share)
        source_path = os.path.join(local_path, clone_name + '-d')
        if self._is_volume_present(source_path):
            source_file = os.listdir(source_path)[0]
            source = os.path.join(source_path, source_file)
            target = os.path.join(local_path, clone_name)
            moved = self._move_file(source, target)
            self._execute('rm', '-rf', source_path,
                          run_as_root=self._execute_as_root)
            if not moved:
                msg = (_('Failed to move volume %s.') % source)
                raise exception.VolumeDriverException(msg)
        else:
            raise exception.VolumeDriverException(
                _('Volume %s not found.') % source_path)

    def _clone_volume_to_volume(self, volume_name, clone_name,
                                volume_display_name, volume_id,
                                share=None, dst=None, image_id=None):
        """Creates volume snapshot then clones volume."""
        (__, path) = self._get_export_ip_path(volume_id, share)
        volume_path = '%s/%s' % (path, volume_name)
        if dst:
            (___, dst_path) = self._get_export_ip_path(None, dst)
            clone_path = '%s/%s-d' % (dst_path, clone_name)
        else:
            clone_path = '%s/%s-d' % (path, clone_name)
        with self._get_client() as c:
            if share and image_id:
                snapshot_id = self._create_image_snapshot(volume_name,
                                                          share,
                                                          image_id,
                                                          volume_display_name)
            else:
                snapshot_id = c.create_snapshot(
                    volume_path, volume_display_name, volume_id, volume_name,
                    deletion_policy='DELETE_ON_ZERO_CLONE_REFERENCES')
            c.clone_volume(snapshot_id, clone_path)

        self._move_cloned_volume(clone_name, volume_id, dst or share)

    @utils.synchronized('cache_cleanup')
    def _initiate_image_cache_cleanup(self):
        if self.cache_cleanup:
            LOG.debug('Image cache cleanup in progress.')
            return
        else:
            self.cache_cleanup = True
            timer = loopingcall.FixedIntervalLoopingCall(
                self._cleanup_cache)
            timer.start(interval=None)
            return timer

    def _cleanup_cache(self):
        LOG.debug('Cache cleanup: starting.')
        try:
            # Cleanup used cached image snapshots 30 days and older
            t = datetime.datetime.utcnow() - datetime.timedelta(
                days=self._image_cache_expiry)
            date = t.strftime("%Y-%m-%dT%H:%M:%S")
            with self._get_client() as c:
                # Get eligible snapshots to clean
                image_snaps = c.get_image_snapshots_to_date(date)
                if image_snaps:
                    for snap in image_snaps:
                        uuid = snap['uuid']['uuid']
                        LOG.debug(
                            'Cache cleanup: deleting image snapshot %s', uuid)
                        try:
                            c.delete_snapshot(uuid)
                        except Exception:
                            LOG.exception(_LE('Unexpected exception during '
                                              'cache cleanup of snapshot %s'),
                                          uuid)
                else:
                    LOG.debug('Cache cleanup: nothing to clean')
        finally:
            self.cache_cleanup = False
            LOG.debug('Cache cleanup: finished')
            raise loopingcall.LoopingCallDone()

    def _update_volume_stats(self):
        """Retrieves stats info from volume group."""

        data = {}
        backend_name = self.configuration.safe_get('volume_backend_name')
        data['volume_backend_name'] = backend_name or self.VENDOR
        data['vendor_name'] = self.VENDOR
        data['driver_version'] = self.get_version()
        data['storage_protocol'] = self.driver_volume_type

        self._ensure_shares_mounted()
        self._initiate_image_cache_cleanup()
        pools = []
        for share in self._mounted_shares:
            pool = dict()
            capacity, free, used = self._get_capacity_info(share)
            pool['pool_name'] = share
            pool['total_capacity_gb'] = capacity / float(units.Gi)
            pool['free_capacity_gb'] = free / float(units.Gi)
            pool['reserved_percentage'] = 0
            pool['QoS_support'] = True
            pools.append(pool)
        data['pools'] = pools

        self._stats = data

    def _get_provider_location(self, volume_id):
        """Returns provider location for given volume."""
        volume = self.db.volume_get(self._context, volume_id)
        return volume.provider_location

    def _get_host_ip(self, volume_id):
        """Returns IP address for the given volume."""
        return self._get_provider_location(volume_id).split(':')[0]

    def _get_export_path(self, volume_id):
        """Returns NFS export path for the given volume."""
        return self._get_provider_location(volume_id).split(':')[1]

    def _resolve_hostname(self, hostname):
        """Resolves host name to IP address."""
        res = socket.getaddrinfo(hostname, None)[0]
        family, socktype, proto, canonname, sockaddr = res
        return sockaddr[0]

    def _is_volume_present(self, volume_path):
        """Checks if volume exists."""
        try:
            self._execute('ls', volume_path,
                          run_as_root=self._execute_as_root)
        except Exception:
            return False
        return True

    def _get_volume_path(self, nfs_share, volume_name):
        """Gets local volume path for given volume name on given nfs share."""
        return os.path.join(self._get_mount_point_for_share(nfs_share),
                            volume_name)

    def create_cloned_volume(self, volume, src_vref):
        """Creates a clone of the specified volume."""
        vol_size = volume.size
        src_vol_size = src_vref.size
        self._clone_volume_to_volume(src_vref.name, volume.name,
                                     src_vref.display_name,
                                     src_vref.id)

        share = self._get_provider_location(src_vref.id)
        volume['provider_location'] = share
        path = self.local_path(volume)

        self._set_rw_permissions(path)
        if vol_size != src_vol_size:
            try:
                self.extend_volume(volume, vol_size)
            except Exception:
                LOG.error(_LE('Resizing %s failed. Cleaning volume.'),
                          volume.name)
                self._delete_file(path)
                raise

        return {'provider_location': volume['provider_location']}

    def copy_image_to_volume(self, context, volume, image_service, image_id):
        """Fetches the image from image_service and write it to the volume."""
        super(TintriDriver, self).copy_image_to_volume(
            context, volume, image_service, image_id)
        LOG.info(_LI('Copied image to volume %s using regular download.'),
                 volume['name'])
        self._create_image_snapshot(volume['name'],
                                    volume['provider_location'], image_id,
                                    img_prefix + image_id)

    def _create_image_snapshot(self, volume_name, share, image_id, image_name):
        """Creates an image snapshot."""
        snapshot_name = img_prefix + image_id
        LOG.info(_LI('Creating image snapshot %s'), snapshot_name)
        (host, path) = self._get_export_ip_path(None, share)
        volume_path = '%s/%s' % (path, volume_name)

        @utils.synchronized(snapshot_name, external=True)
        def _do_snapshot():
            with self._get_client() as c:
                snapshot_id = c.get_snapshot(image_id)
                if not snapshot_id:
                    snapshot_id = c.create_snapshot(volume_path, image_name,
                                                    image_id, snapshot_name)
                return snapshot_id

        try:
            return _do_snapshot()
        except Exception as e:
            LOG.warning(_LW('Exception while creating image %(image_id)s '
                            'snapshot. Exception: %(exc)s'),
                        {'image_id': image_id, 'exc': e})

    def _find_image_snapshot(self, image_id):
        """Finds image snapshot."""
        with self._get_client() as c:
            return c.get_snapshot(image_id)

    def _clone_image_snapshot(self, snapshot_id, dst, share):
        """Clones volume from image snapshot."""
        file_path = self._get_volume_path(share, dst)
        if not os.path.exists(file_path):
            LOG.info(_LI('Cloning from snapshot to destination %s'), dst)
            self._clone_snapshot(snapshot_id, dst, volume_id=None,
                                 share=share)

    def _delete_file(self, path):
        """Deletes file from disk and return result as boolean."""
        try:
            LOG.debug('Deleting file at path %s', path)
            cmd = ['rm', '-f', path]
            self._execute(*cmd, run_as_root=self._execute_as_root)
            return True
        except Exception as e:
            LOG.warning(_LW('Exception during deleting %s'), e)
            return False

    def _move_file(self, source_path, dest_path):
        """Moves source to destination."""

        @utils.synchronized(dest_path, external=True)
        def _do_move(src, dst):
            if os.path.exists(dst):
                LOG.warning(_LW('Destination %s already exists.'), dst)
                return False
            self._execute('mv', src, dst, run_as_root=self._execute_as_root)
            return True

        try:
            return _do_move(source_path, dest_path)
        except Exception as e:
            LOG.warning(_LW('Exception moving file %(src)s. Message: %(e)s'),
                        {'src': source_path, 'e': e})
        return False

    def clone_image(self, context, volume,
                    image_location, image_meta,
                    image_service):
        """Creates a volume efficiently from an existing image.

        image_location is a string whose format depends on the
        image service backend in use. The driver should use it
        to determine whether cloning is possible.

        Returns a dict of volume properties eg. provider_location,
        boolean indicating whether cloning occurred.
        """
        image_name = image_meta['name']
        image_id = image_meta['id']
        if 'properties' in image_meta:
            provider_location = image_meta['properties'].get(
                'provider_location')
            if provider_location:
                image_location = (provider_location, None)
        cloned = False
        post_clone = False
        try:
            snapshot_id = self._find_image_snapshot(image_id)
            if snapshot_id:
                cloned = self._clone_from_snapshot(volume, image_id,
                                                   snapshot_id)
            else:
                cloned = self._direct_clone(volume, image_location,
                                            image_id, image_name)
            if cloned:
                post_clone = self._post_clone_image(volume)
        except Exception as e:
            LOG.info(_LI('Image cloning unsuccessful for image '
                         '%(image_id)s. Message: %(msg)s'),
                     {'image_id': image_id, 'msg': e})
            vol_path = self.local_path(volume)
            volume['provider_location'] = None
            if os.path.exists(vol_path):
                self._delete_file(vol_path)
        finally:
            cloned = cloned and post_clone
            share = volume['provider_location'] if cloned else None
            bootable = True if cloned else False
            return {'provider_location': share, 'bootable': bootable}, cloned

    def _clone_from_snapshot(self, volume, image_id, snapshot_id):
        """Clones a copy from image snapshot."""
        cloned = False
        LOG.info(_LI('Cloning image %s from snapshot.'), image_id)
        for share in self._mounted_shares:
            # Repeat tries in other shares if failed in some
            LOG.debug('Image share: %s', share)
            if (share and
                    self._is_share_vol_compatible(volume, share)):
                try:
                    self._clone_image_snapshot(snapshot_id, volume['name'],
                                               share)
                    cloned = True
                    volume['provider_location'] = share
                    break
                except Exception:
                    LOG.warning(_LW('Unexpected exception during '
                                    'image cloning in share %s'), share)
        return cloned

    def _direct_clone(self, volume, image_location, image_id, image_name):
        """Clones directly in nfs share."""
        LOG.info(_LI('Checking image clone %s from glance share.'), image_id)
        cloned = False
        image_location = self._get_image_nfs_url(image_location)
        share = self._is_cloneable_share(image_location)
        run_as_root = self._execute_as_root

        dst_share = None
        for dst in self._mounted_shares:
            if dst and self._is_share_vol_compatible(volume, dst):
                dst_share = dst
                LOG.debug('Image dst share: %s', dst)
                break
        if not dst_share:
            return cloned

        LOG.debug('Share is cloneable %s', dst_share)
        volume['provider_location'] = dst_share
        (__, ___, img_file) = image_location.rpartition('/')
        dir_path = self._get_mount_point_for_share(share)
        dst_path = self._get_mount_point_for_share(dst_share)
        img_path = '%s/%s' % (dir_path, img_file)
        img_info = image_utils.qemu_img_info(img_path,
                                             run_as_root=run_as_root)
        if img_info.file_format == 'raw':
            LOG.debug('Image is raw %s', image_id)
            self._clone_volume_to_volume(
                img_file, volume['name'], image_name,
                volume_id=None, share=share, dst=dst_share, image_id=image_id)
            cloned = True
        else:
            LOG.info(_LI('Image will locally be converted to raw %s'),
                     image_id)
            dst = '%s/%s' % (dst_path, volume['name'])
            image_utils.convert_image(img_path, dst, 'raw',
                                      run_as_root=run_as_root)
            data = image_utils.qemu_img_info(dst, run_as_root=run_as_root)
            if data.file_format != "raw":
                raise exception.InvalidResults(
                    _('Converted to raw, but '
                      'format is now %s') % data.file_format)
            else:
                cloned = True
                self._create_image_snapshot(
                    volume['name'], volume['provider_location'],
                    image_id, image_name)
        return cloned

    def _post_clone_image(self, volume):
        """Performs operations post image cloning."""
        LOG.info(_LI('Performing post clone for %s'), volume['name'])
        vol_path = self.local_path(volume)
        self._set_rw_permissions(vol_path)
        self._resize_image_file(vol_path, volume['size'])
        return True

    def _resize_image_file(self, path, new_size):
        """Resizes the image file on share to new size."""
        LOG.debug('Checking file for resize.')
        if self._is_file_size_equal(path, new_size):
            return
        else:
            LOG.info(_LI('Resizing file to %sG'), new_size)
            image_utils.resize_image(path, new_size,
                                     run_as_root=self._execute_as_root)
            if self._is_file_size_equal(path, new_size):
                return
            else:
                raise exception.InvalidResults(
                    _('Resizing image file failed.'))

    def _is_cloneable_share(self, image_location):
        """Finds if the image at location is cloneable."""
        conn, dr = self._check_nfs_path(image_location)
        return self._is_share_in_use(conn, dr)

    def _check_nfs_path(self, image_location):
        """Checks if the nfs path format is matched.

        WebNFS url format with relative-path is supported.
        Accepting all characters in path-names and checking against
        the mounted shares which will contain only allowed path segments.
        Returns connection and dir details.
        """
        conn, dr = None, None
        if image_location:
            nfs_loc_pattern = \
                '^nfs://(([\w\-\.]+:[\d]+|[\w\-\.]+)(/[^/].*)*(/[^/\\\\]+))$'
            matched = re.match(nfs_loc_pattern, image_location)
            if not matched:
                LOG.debug('Image location not in the expected format %s',
                          image_location)
            else:
                conn = matched.group(2)
                dr = matched.group(3) or '/'
        return conn, dr

    def _is_share_in_use(self, conn, dr):
        """Checks if share is cinder mounted and returns it."""
        try:
            if conn:
                host = conn.split(':')[0]
                ip = self._resolve_hostname(host)