# Copyright (c) 2015 Red Hat, 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.

"""GlusterFS directory mapped share layout."""

import math
import os

from oslo_config import cfg
from oslo_log import log
import six
import xml.etree.cElementTree as etree

from manila import exception
from manila.i18n import _
from manila.share.drivers.glusterfs import common
from manila.share.drivers.glusterfs import layout
from manila import utils

LOG = log.getLogger(__name__)

glusterfs_directory_mapped_opts = [
    cfg.StrOpt('glusterfs_target',
               help='Specifies the GlusterFS volume to be mounted on the '
                    'Manila host. It is of the form '
                    '[remoteuser@]<volserver>:<volid>.'),
    cfg.StrOpt('glusterfs_mount_point_base',
               default='$state_path/mnt',
               help='Base directory containing mount points for Gluster '
                    'volumes.'),
]

CONF = cfg.CONF
CONF.register_opts(glusterfs_directory_mapped_opts)


class GlusterfsDirectoryMappedLayout(layout.GlusterfsShareLayoutBase):

    def __init__(self, driver, *args, **kwargs):
        super(GlusterfsDirectoryMappedLayout, self).__init__(
            driver, *args, **kwargs)
        self.configuration.append_config_values(
            common.glusterfs_common_opts)
        self.configuration.append_config_values(
            glusterfs_directory_mapped_opts)

    def _glustermanager(self, gluster_address):
        """Create GlusterManager object for gluster_address."""

        return common.GlusterManager(
            gluster_address, self.driver._execute,
            self.configuration.glusterfs_path_to_private_key,
            self.configuration.glusterfs_server_password,
            requires={'volume': True})

    def do_setup(self, context):
        """Prepares the backend and appropriate NAS helpers."""
        if not self.configuration.glusterfs_target:
            raise exception.GlusterfsException(
                _('glusterfs_target configuration that specifies the GlusterFS'
                  ' volume to be mounted on the Manila host is not set.'))
        self.gluster_manager = self._glustermanager(
            self.configuration.glusterfs_target)
        self.gluster_manager.check_gluster_version(
            self.driver.GLUSTERFS_VERSION_MIN)
        self._check_mount_glusterfs()

        # enable quota options of a GlusteFS volume to allow
        # creation of shares of specific size
        args = ('volume', 'quota', self.gluster_manager.volume, 'enable')
        try:
            self.gluster_manager.gluster_call(*args)
        except exception.GlusterfsException:
            if (self.gluster_manager.
                    get_vol_option('features.quota')) != 'on':
                LOG.exception("Error in tuning GlusterFS volume to enable "
                              "creation of shares of specific size.")
                raise

        self._ensure_gluster_vol_mounted()

    def _share_manager(self, share):
        comp_path = self.gluster_manager.components.copy()
        comp_path.update({'path': '/' + share['name']})
        return self._glustermanager(comp_path)

    def _get_mount_point_for_gluster_vol(self):
        """Return mount point for the GlusterFS volume."""
        return os.path.join(self.configuration.glusterfs_mount_point_base,
                            self.gluster_manager.volume)

    def _ensure_gluster_vol_mounted(self):
        """Ensure GlusterFS volume is native-mounted on Manila host."""
        mount_path = self._get_mount_point_for_gluster_vol()
        try:
            common._mount_gluster_vol(self.driver._execute,
                                      self.gluster_manager.export, mount_path,
                                      ensure=True)
        except exception.GlusterfsException:
            LOG.exception('Could not mount the Gluster volume %s',
                          self.gluster_manager.volume)
            raise

    def _get_local_share_path(self, share):
        """Determine mount path of the GlusterFS volume in the Manila host."""
        local_vol_path = self._get_mount_point_for_gluster_vol()
        if not os.access(local_vol_path, os.R_OK):
            raise exception.GlusterfsException('share path %s does not exist' %
                                               local_vol_path)
        return os.path.join(local_vol_path, share['name'])

    def _update_share_stats(self):
        """Retrieve stats info from the GlusterFS volume."""

        # sanity check for gluster ctl mount
        smpb = os.stat(self.configuration.glusterfs_mount_point_base)
        smp = os.stat(self._get_mount_point_for_gluster_vol())
        if smpb.st_dev == smp.st_dev:
            raise exception.GlusterfsException(
                _("GlusterFS control mount is not available")
            )
        smpv = os.statvfs(self._get_mount_point_for_gluster_vol())

        return {'total_capacity_gb': (smpv.f_blocks * smpv.f_frsize) >> 30,
                'free_capacity_gb': (smpv.f_bavail * smpv.f_frsize) >> 30}

    def create_share(self, ctx, share, share_server=None):
        """Create a sub-directory/share in the GlusterFS volume."""
        # probe into getting a NAS protocol helper for the share in order
        # to facilitate early detection of unsupported protocol type
        local_share_path = self._get_local_share_path(share)
        cmd = ['mkdir', local_share_path]

        try:
            self.driver._execute(*cmd, run_as_root=True)
            self._set_directory_quota(share, share['size'])
        except Exception as exc:
            if isinstance(exc, exception.ProcessExecutionError):
                exc = exception.GlusterfsException(exc)
            if isinstance(exc, exception.GlusterfsException):
                self._cleanup_create_share(local_share_path, share['name'])
                LOG.error('Unable to create share %s', share['name'])
            raise exc

        comp_share = self.gluster_manager.components.copy()
        comp_share['path'] = '/' + share['name']
        export_location = self.driver._setup_via_manager(
            {'share': share,
             'manager': self._glustermanager(comp_share)})

        return export_location

    def _cleanup_create_share(self, share_path, share_name):
        """Cleanup share that errored out during its creation."""
        if os.path.exists(share_path):
            cmd = ['rm', '-rf', share_path]
            try:
                self.driver._execute(*cmd, run_as_root=True)
            except exception.ProcessExecutionError as exc:
                LOG.error('Cannot cleanup share, %s, that errored out '
                          'during its creation, but exists in GlusterFS '
                          'volume.', share_name)
                raise exception.GlusterfsException(exc)

    def delete_share(self, context, share, share_server=None):
        """Remove a sub-directory/share from the GlusterFS volume."""
        local_share_path = self._get_local_share_path(share)
        cmd = ['rm', '-rf', local_share_path]
        try:
            self.driver._execute(*cmd, run_as_root=True)
        except exception.ProcessExecutionError:
            LOG.exception('Unable to delete share %s', share['name'])
            raise

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

    def create_share_from_snapshot(self, context, share, snapshot,
                                   share_server=None, parent_share=None):
        raise NotImplementedError

    def create_snapshot(self, context, snapshot, share_server=None):
        raise NotImplementedError

    def delete_snapshot(self, context, snapshot, share_server=None):
        raise NotImplementedError

    def manage_existing(self, share, driver_options):
        raise NotImplementedError

    def unmanage(self, share):
        raise NotImplementedError

    def extend_share(self, share, new_size, share_server=None):
        """Extend a sub-directory/share in the GlusterFS volume."""
        self._set_directory_quota(share, new_size)

    def shrink_share(self, share, new_size, share_server=None):
        """Shrink a sub-directory/share in the GlusterFS volume."""
        usage = self._get_directory_usage(share)
        consumed_limit = int(math.ceil(usage))
        if consumed_limit > new_size:
            raise exception.ShareShrinkingPossibleDataLoss(
                share_id=share['id'])

        self._set_directory_quota(share, new_size)

    def _set_directory_quota(self, share, new_size):
        sizestr = six.text_type(new_size) + 'GB'
        share_dir = '/' + share['name']

        args = ('volume', 'quota', self.gluster_manager.volume,
                'limit-usage', share_dir, sizestr)

        try:
            self.gluster_manager.gluster_call(*args)
        except exception.GlusterfsException:
            LOG.error('Unable to set quota share %s', share['name'])
            raise

    def _get_directory_usage(self, share):
        share_dir = '/' + share['name']

        args = ('--xml', 'volume', 'quota', self.gluster_manager.volume,
                'list', share_dir)

        try:
            out, err = self.gluster_manager.gluster_call(*args)
        except exception.GlusterfsException:
            LOG.error('Unable to get quota share %s', share['name'])
            raise

        volxml = etree.fromstring(out)
        usage_byte = volxml.find('./volQuota/limit/used_space').text
        usage = utils.translate_string_size_to_float(usage_byte)

        return usage
