# Copyright (c) 2014 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 native protocol (glusterfs) driver for shares.

Manila share is a GlusterFS volume. Unlike the generic driver, this
does not use service VM approach. Instances directly talk with the
GlusterFS backend storage pool. Instance use the 'glusterfs' protocol
to mount the GlusterFS share. Access to the share is allowed via
SSL Certificates. Only the instance which has the SSL trust established
with the GlusterFS backend can mount and hence use the share.

Supports working with multiple glusterfs volumes.
"""

import re

from oslo_log import log

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

LOG = log.getLogger(__name__)


ACCESS_TYPE_CERT = 'cert'
AUTH_SSL_ALLOW = 'auth.ssl-allow'
CLIENT_SSL = 'client.ssl'
NFS_EXPORT_VOL = 'nfs.export-volumes'
SERVER_SSL = 'server.ssl'


class GlusterfsNativeShareDriver(driver.ExecuteMixin,
                                 layout.GlusterfsShareDriverBase):
    """GlusterFS native protocol (glusterfs) share driver.

    Executes commands relating to Shares.
    Supports working with multiple glusterfs volumes.

    API version history:

        1.0 - Initial version.
        1.1 - Support for working with multiple gluster volumes.
    """

    GLUSTERFS_VERSION_MIN = (3, 6)

    supported_layouts = ('layout_volume.GlusterfsVolumeMappedLayout',)
    supported_protocols = ('GLUSTERFS',)

    def __init__(self, *args, **kwargs):
        super(GlusterfsNativeShareDriver, self).__init__(
            False, *args, **kwargs)
        self._helpers = None
        self.backend_name = self.configuration.safe_get(
            'share_backend_name') or 'GlusterFS-Native'

    def _setup_via_manager(self, share_mgr, share_mgr_parent=None):
        # Enable gluster volumes for SSL access only.

        gluster_mgr = share_mgr['manager']
        gluster_mgr_parent = (share_mgr_parent or {}).get('manager', None)

        ssl_allow_opt = (gluster_mgr_parent if gluster_mgr_parent else
                         gluster_mgr).get_gluster_vol_option(
            AUTH_SSL_ALLOW)
        if not ssl_allow_opt:
            # Not having AUTH_SSL_ALLOW set is a problematic edge case.
            # - In GlusterFS 3.6, it implies that access is allowed to
            #   noone, including intra-service access, which causes
            #   problems internally in GlusterFS
            # - In GlusterFS 3.7, it implies that access control is
            #   disabled, which defeats the purpose of this driver --
            # so to avoid these possiblitiies, we throw an error in this case.
            msg = (_("Option %(option)s is not defined on gluster volume. "
                     "Volume: %(volname)s") %
                   {'volname': gluster_mgr.volume,
                    'option': AUTH_SSL_ALLOW})
            LOG.error(msg)
            raise exception.GlusterfsException(msg)

        gluster_actions = []
        if gluster_mgr_parent:
            # The clone of the snapshot, a new volume, retains the authorized
            # access list of the snapshotted volume/share, which includes TLS
            # identities of the backend servers, Manila hosts and clients.
            # Retain the identities of the GlusterFS servers and Manila host,
            # and exclude those of the clients in the authorized access list of
            # the new volume. The TLS identities of GlusterFS servers are
            # determined as those that are prefixed by 'glusterfs-server'.
            # And the TLS identity of the Manila host is identified as the
            # one that has 'manila-host' as the prefix.
            # Wrt. GlusterFS' parsing of auth.ssl-allow, please see code from
            # https://github.com/gluster/glusterfs/blob/v3.6.2/
            # xlators/protocol/auth/login/src/login.c#L80
            # until end of gf_auth() function
            old_access_list = re.split('[ ,]', ssl_allow_opt)
            glusterfs_server_CN_pattern = '\Aglusterfs-server'
            manila_host_CN_pattern = '\Amanila-host'
            regex = re.compile(
                '%(pattern1)s|%(pattern2)s' % {
                    'pattern1': glusterfs_server_CN_pattern,
                    'pattern2': manila_host_CN_pattern})
            access_to = ','.join(filter(regex.match, old_access_list))
            gluster_actions.append(('set', AUTH_SSL_ALLOW, access_to))

        for option, value in (
            (NFS_EXPORT_VOL, 'off'), (CLIENT_SSL, 'on'), (SERVER_SSL, 'on')
        ):
            gluster_actions.append(('set', option, value))

        if not gluster_mgr_parent:
            # TODO(deepakcs) Remove this once ssl options can be
            # set dynamically.
            gluster_actions.append(('stop', '--mode=script'))
        gluster_actions.append(('start',))

        for action in gluster_actions:
            try:
                gluster_mgr.gluster_call(
                    'volume', action[0], gluster_mgr.volume, *action[1:])
            except exception.ProcessExecutionError as exc:
                msg = (_("Error in gluster volume set during volume setup. "
                         "volume: %(volname)s, option: %(option)s, "
                         "value: %(value)s, error: %(error)s") %
                       {'volname': gluster_mgr.volume,
                        'option': option, 'value': value, 'error': exc.stderr})
                LOG.error(msg)
                raise exception.GlusterfsException(msg)

        return gluster_mgr.export

    @utils.synchronized("glusterfs_native_access", external=False)
    def _allow_access_via_manager(self, gluster_mgr, context, share, access,
                                  share_server=None):
        """Allow access to a share using certs.

        Add the SSL CN (Common Name) that's allowed to access the server.
        """

        if access['access_type'] != ACCESS_TYPE_CERT:
            raise exception.InvalidShareAccess(_("Only 'cert' access type "
                                                 "allowed"))

        ssl_allow_opt = gluster_mgr.get_gluster_vol_option(AUTH_SSL_ALLOW)
        # wrt. GlusterFS' parsing of auth.ssl-allow, please see code from
        # https://github.com/gluster/glusterfs/blob/v3.6.2/
        # xlators/protocol/auth/login/src/login.c#L80
        # until end of gf_auth() function
        ssl_allow = re.split('[ ,]', ssl_allow_opt)
        access_to = access['access_to']
        if access_to in ssl_allow:
            LOG.warn(_LW("Access to %(share)s at %(export)s is already "
                         "granted for %(access_to)s. GlusterFS volume "
                         "options might have been changed externally."),
                     {'share': share['id'], 'export': gluster_mgr.qualified,
                      'access_to': access_to})
            return

        ssl_allow.append(access_to)
        ssl_allow_opt = ','.join(ssl_allow)
        try:
            gluster_mgr.gluster_call(
                'volume', 'set', gluster_mgr.volume,
                AUTH_SSL_ALLOW, ssl_allow_opt)
        except exception.ProcessExecutionError as exc:
            msg = (_("Error in gluster volume set during allow access. "
                     "Volume: %(volname)s, Option: %(option)s, "
                     "access_to: %(access_to)s, Error: %(error)s") %
                   {'volname': gluster_mgr.volume,
                    'option': AUTH_SSL_ALLOW, 'access_to': access_to,
                    'error': exc.stderr})
            LOG.error(msg)
            raise exception.GlusterfsException(msg)

        # TODO(deepakcs) Remove this once ssl options can be
        # set dynamically.
        common._restart_gluster_vol(gluster_mgr)

    @utils.synchronized("glusterfs_native_access", external=False)
    def _deny_access_via_manager(self, gluster_mgr, context, share, access,
                                 share_server=None):
        """Deny access to a share that's using cert based auth.

        Remove the SSL CN (Common Name) that's allowed to access the server.
        """

        if access['access_type'] != ACCESS_TYPE_CERT:
            raise exception.InvalidShareAccess(_("Only 'cert' access type "
                                                 "allowed for access "
                                                 "removal."))

        ssl_allow_opt = gluster_mgr.get_gluster_vol_option(AUTH_SSL_ALLOW)
        ssl_allow = re.split('[ ,]', ssl_allow_opt)
        access_to = access['access_to']
        if access_to not in ssl_allow:
            LOG.warn(_LW("Access to %(share)s at %(export)s is already "
                         "denied for %(access_to)s. GlusterFS volume "
                         "options might have been changed externally."),
                     {'share': share['id'], 'export': gluster_mgr.qualified,
                      'access_to': access_to})
            return

        ssl_allow.remove(access_to)
        ssl_allow_opt = ','.join(ssl_allow)
        try:
            gluster_mgr.gluster_call(
                'volume', 'set', gluster_mgr.volume,
                AUTH_SSL_ALLOW, ssl_allow_opt)
        except exception.ProcessExecutionError as exc:
            msg = (_("Error in gluster volume set during deny access. "
                     "Volume: %(volname)s, Option: %(option)s, "
                     "access_to: %(access_to)s, Error: %(error)s") %
                   {'volname': gluster_mgr.volume,
                    'option': AUTH_SSL_ALLOW, 'access_to': access_to,
                    'error': exc.stderr})
            LOG.error(msg)
            raise exception.GlusterfsException(msg)

        # TODO(deepakcs) Remove this once ssl options can be
        # set dynamically.
        common._restart_gluster_vol(gluster_mgr)

    def _update_share_stats(self):
        """Send stats info for the GlusterFS volume."""

        data = dict(
            share_backend_name=self.backend_name,
            vendor_name='Red Hat',
            driver_version='1.1',
            storage_protocol='glusterfs',
            reserved_percentage=self.configuration.reserved_share_percentage)

        # We don't use a service mount to get stats data.
        # Instead we use glusterfs quota feature and use that to limit
        # the share to its expected share['size'].

        # TODO(deepakcs): Change below once glusterfs supports volume
        # specific stats via the gluster cli.
        data['total_capacity_gb'] = 'unknown'
        data['free_capacity_gb'] = 'unknown'

        super(GlusterfsNativeShareDriver, self)._update_share_stats(data)

    def get_network_allocations_number(self):
        return 0
