# Copyright 2014 IBM Corp.
#
#    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.
"""
GPFS Driver for shares.

Config Requirements:
  GPFS file system must have quotas enabled (`mmchfs -Q yes`).
Notes:
  GPFS independent fileset is used for each share.

TODO(nileshb): add support for share server creation/deletion/handling.

Limitation:
  While using remote GPFS node, with Ganesha NFS, 'gpfs_ssh_private_key'
  for remote login to the GPFS node must be specified and there must be
  a passwordless authentication already setup between the Manila share
  service and the remote GPFS node.

"""
import abc
import math
import os
import re
import shlex
import socket

from oslo_config import cfg
from oslo_config import types
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import importutils
from oslo_utils import strutils
from oslo_utils import units

from manila.common import constants
from manila import exception
from manila.i18n import _
from manila.share import driver
from manila.share.drivers.helpers import NFSHelper
from manila.share import share_types
from manila import ssh_utils
from manila import utils

LOG = log.getLogger(__name__)

# matches multiple comma separated avpairs on a line.  values with an embedded
# comma must be wrapped in quotation marks
AVPATTERN = re.compile(r'\s*(?P<attr>\w+)\s*=\s*(?P<val>'
                       r'(["][a-zA-Z0-9_, ]+["])|(\w+))\s*[,]?')

ERR_FILE_NOT_FOUND = 2

gpfs_share_opts = [
    cfg.HostAddressOpt('gpfs_share_export_ip',
                       help='IP to be added to GPFS export string.'),
    cfg.StrOpt('gpfs_mount_point_base',
               default='$state_path/mnt',
               help='Base folder where exported shares are located.'),
    cfg.StrOpt('gpfs_nfs_server_type',
               default='CES',
               help=('NFS Server type. Valid choices are "CES" (Ganesha NFS) '
                     'or "KNFS" (Kernel NFS).')),
    cfg.ListOpt('gpfs_nfs_server_list',
                help=('A list of the fully qualified NFS server names that '
                      'make up the OpenStack Manila configuration.')),
    cfg.BoolOpt('is_gpfs_node',
                default=False,
                help=('True:when Manila services are running on one of the '
                      'Spectrum Scale node. '
                      'False:when Manila services are not running on any of '
                      'the Spectrum Scale node.')),
    cfg.PortOpt('gpfs_ssh_port',
                default=22,
                help='GPFS server SSH port.'),
    cfg.StrOpt('gpfs_ssh_login',
               help='GPFS server SSH login name.'),
    cfg.StrOpt('gpfs_ssh_password',
               secret=True,
               help='GPFS server SSH login password. '
                    'The password is not needed, if \'gpfs_ssh_private_key\' '
                    'is configured.'),
    cfg.StrOpt('gpfs_ssh_private_key',
               help='Path to GPFS server SSH private key for login.'),
    cfg.Opt('gpfs_share_helpers',
            type=types.Dict(key_value_separator='='),
            default={
                'KNFS': 'manila.share.drivers.ibm.gpfs.KNFSHelper',
                'CES': 'manila.share.drivers.ibm.gpfs.CESHelper',
            },
            help='Specify list of share export helpers.'),
]


CONF = cfg.CONF
CONF.register_opts(gpfs_share_opts)


class GPFSShareDriver(driver.ExecuteMixin, driver.GaneshaMixin,
                      driver.ShareDriver):

    """GPFS Share Driver.

    Executes commands relating to Shares.
    Supports creation of shares on a GPFS cluster.

    API version history:

        1.0 - Initial version.
        1.1 - Added extend_share functionality
        2.0 - Added CES support for NFS Ganesha
    """

    def __init__(self, *args, **kwargs):
        """Do initialization."""
        super(GPFSShareDriver, self).__init__(False, *args, **kwargs)
        self._helpers = {}
        self.configuration.append_config_values(gpfs_share_opts)
        self.backend_name = self.configuration.safe_get(
            'share_backend_name') or "IBM Storage System"
        self.sshpool = None
        self.ssh_connections = {}
        self._gpfs_execute = None
        if self.configuration.is_gpfs_node:
            self.GPFS_PATH = ''
        else:
            self.GPFS_PATH = '/usr/lpp/mmfs/bin/'

    def do_setup(self, context):
        """Any initialization the share driver does while starting."""
        super(GPFSShareDriver, self).do_setup(context)
        if self.configuration.is_gpfs_node:
            self._gpfs_execute = self._gpfs_local_execute
        else:
            self._gpfs_execute = self._gpfs_remote_execute
        self._setup_helpers()

    def _gpfs_local_execute(self, *cmd, **kwargs):
        if 'run_as_root' not in kwargs:
            kwargs.update({'run_as_root': True})
        if 'ignore_exit_code' in kwargs:
            check_exit_code = kwargs.pop('ignore_exit_code')
            check_exit_code.append(0)
            kwargs.update({'check_exit_code': check_exit_code})

        return utils.execute(*cmd, **kwargs)

    def _gpfs_remote_execute(self, *cmd, **kwargs):
        host = self.configuration.gpfs_share_export_ip
        check_exit_code = kwargs.pop('check_exit_code', True)
        ignore_exit_code = kwargs.pop('ignore_exit_code', None)

        return self._run_ssh(host, cmd, ignore_exit_code, check_exit_code)

    def _sanitize_command(self, cmd_list):
        # pylint: disable=too-many-function-args
        return ' '.join(shlex.quote(cmd_arg) for cmd_arg in cmd_list)

    def _run_ssh(self, host, cmd_list, ignore_exit_code=None,
                 check_exit_code=True):
        command = self._sanitize_command(cmd_list)
        if not self.sshpool:
            gpfs_ssh_login = self.configuration.gpfs_ssh_login
            password = self.configuration.gpfs_ssh_password
            privatekey = self.configuration.gpfs_ssh_private_key
            gpfs_ssh_port = self.configuration.gpfs_ssh_port
            ssh_conn_timeout = self.configuration.ssh_conn_timeout
            min_size = self.configuration.ssh_min_pool_conn
            max_size = self.configuration.ssh_max_pool_conn

            self.sshpool = ssh_utils.SSHPool(host,
                                             gpfs_ssh_port,
                                             ssh_conn_timeout,
                                             gpfs_ssh_login,
                                             password=password,
                                             privatekey=privatekey,
                                             min_size=min_size,
                                             max_size=max_size)
        try:
            with self.sshpool.item() as ssh:
                return self._gpfs_ssh_execute(
                    ssh,
                    command,
                    ignore_exit_code=ignore_exit_code,
                    check_exit_code=check_exit_code)

        except Exception as e:
            with excutils.save_and_reraise_exception():
                msg = (_('Error running SSH command: %(cmd)s. '
                         'Error: %(excmsg)s.') %
                       {'cmd': command, 'excmsg': e})
                LOG.error(msg)
                raise exception.GPFSException(msg)

    def _gpfs_ssh_execute(self, ssh, cmd, ignore_exit_code=None,
                          check_exit_code=True):
        sanitized_cmd = strutils.mask_password(cmd)
        LOG.debug('Running cmd (SSH): %s', sanitized_cmd)

        stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
        channel = stdout_stream.channel

        stdout = stdout_stream.read()
        sanitized_stdout = strutils.mask_password(stdout)
        stderr = stderr_stream.read()
        sanitized_stderr = strutils.mask_password(stderr)

        stdin_stream.close()

        exit_status = channel.recv_exit_status()

        # exit_status == -1 if no exit code was returned
        if exit_status != -1:
            LOG.debug('Result was %s', exit_status)
            if ((check_exit_code and exit_status != 0)
                and
                (ignore_exit_code is None or
                 exit_status not in ignore_exit_code)):
                raise exception.ProcessExecutionError(exit_code=exit_status,
                                                      stdout=sanitized_stdout,
                                                      stderr=sanitized_stderr,
                                                      cmd=sanitized_cmd)

        return (sanitized_stdout, sanitized_stderr)

    def _check_gpfs_state(self):
        try:
            out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmgetstate', '-Y')
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to check GPFS state. Error: %(excmsg)s.') %
                   {'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)
        lines = out.splitlines()
        try:
            state_token = lines[0].split(':').index('state')
            gpfs_state = lines[1].split(':')[state_token]
        except (IndexError, ValueError) as e:
            msg = (_('Failed to check GPFS state. Error: %(excmsg)s.') %
                   {'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)
        if gpfs_state != 'active':
            return False
        return True

    def _is_dir(self, path):
        try:
            output, __ = self._gpfs_execute('stat', '--format=%F', path,
                                            run_as_root=False)
        except exception.ProcessExecutionError as e:
            msg = (_('%(path)s is not a directory. Error: %(excmsg)s') %
                   {'path': path, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        return output.strip() == 'directory'

    def _is_gpfs_path(self, directory):
        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmlsattr', directory)
        except exception.ProcessExecutionError as e:
            msg = (_('%(dir)s is not on GPFS filesystem. Error: %(excmsg)s.') %
                   {'dir': directory, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        return True

    def _setup_helpers(self):
        """Initializes protocol-specific NAS drivers."""
        self._helpers = {}
        helpers = self.configuration.gpfs_share_helpers
        for share_proto, import_str in helpers.items():
            helper = importutils.import_class(import_str)
            self._helpers[share_proto.upper()] = helper(self._gpfs_execute,
                                                        self.configuration)

    def _local_path(self, sharename):
        """Get local path for a share or share snapshot by name."""
        return os.path.join(self.configuration.gpfs_mount_point_base,
                            sharename)

    def _get_gpfs_device(self):
        fspath = self.configuration.gpfs_mount_point_base
        try:
            (out, __) = self._gpfs_execute('df', fspath)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to get GPFS device for %(fspath)s.'
                   'Error: %(excmsg)s') %
                   {'fspath': fspath, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        lines = out.splitlines()
        fs = lines[1].split()[0]
        return fs

    def _create_share(self, shareobj):
        """Create a linked fileset file in GPFS.

        Note:  GPFS file system must have quotas enabled
        (mmchfs -Q yes).
        """
        sharename = shareobj['name']
        sizestr = '%sG' % shareobj['size']
        sharepath = self._local_path(sharename)
        fsdev = self._get_gpfs_device()

        # create fileset for the share, link it to root path and set max size
        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmcrfileset', fsdev,
                               sharename, '--inode-space', 'new')
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to create fileset on %(fsdev)s for '
                     'the share %(sharename)s. Error: %(excmsg)s.') %
                   {'fsdev': fsdev, 'sharename': sharename,
                    'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmlinkfileset', fsdev,
                               sharename, '-J', sharepath)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to link fileset for the share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
                               sharename, '--block', '0:' + sizestr)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to set quota for the share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        try:
            self._gpfs_execute('chmod', '777', sharepath)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to set permissions for share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _delete_share(self, shareobj):
        """Remove container by removing GPFS fileset."""
        sharename = shareobj['name']
        fsdev = self._get_gpfs_device()
        # ignore error, when the fileset does not exist
        # it may happen, when the share creation failed, the share is in
        # 'error' state, and the fileset was never created
        # we want to ignore that error condition while deleting the fileset,
        # i.e. 'Fileset name share-xyz not found', with error code '2'
        # and mark the deletion successful
        ignore_exit_code = [ERR_FILE_NOT_FOUND]

        # unlink and delete the share's fileset
        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmunlinkfileset', fsdev,
                               sharename, '-f',
                               ignore_exit_code=ignore_exit_code)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed unlink fileset for share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmdelfileset', fsdev,
                               sharename, '-f',
                               ignore_exit_code=ignore_exit_code)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed delete fileset for share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _get_available_capacity(self, path):
        """Calculate available space on path."""
        try:
            out, __ = self._gpfs_execute('df', '-P', '-B', '1', path)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to check available capacity for %(path)s.'
                     'Error: %(excmsg)s.') %
                   {'path': path, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

        out = out.splitlines()[1]
        size = int(out.split()[1])
        available = int(out.split()[3])
        return available, size

    def _create_share_snapshot(self, snapshot):
        """Create a snapshot of the share."""
        sharename = snapshot['share_name']
        snapshotname = snapshot['name']
        fsdev = self._get_gpfs_device()
        LOG.debug(
            'Attempting to create a snapshot %(snap)s from share %(share)s '
            'on device %(dev)s.',
            {'share': sharename, 'snap': snapshotname, 'dev': fsdev}
        )

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmcrsnapshot', fsdev,
                               snapshot['name'], '-j', sharename)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to create snapshot %(snapshot)s. '
                     'Error: %(excmsg)s.') %
                   {'snapshot': snapshot['name'], 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _delete_share_snapshot(self, snapshot):
        """Delete a snapshot of the share."""
        sharename = snapshot['share_name']
        fsdev = self._get_gpfs_device()

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmdelsnapshot', fsdev,
                               snapshot['name'], '-j', sharename)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to delete snapshot %(snapshot)s. '
                     'Error: %(excmsg)s.') %
                   {'snapshot': snapshot['name'], 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _create_share_from_snapshot(self, share, snapshot, share_path):
        """Create share from a share snapshot."""
        self._create_share(share)
        snapshot_path = self._get_snapshot_path(snapshot)
        snapshot_path = snapshot_path + "/"
        try:
            self._gpfs_execute('rsync', '-rp', snapshot_path, share_path)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to create share %(share)s from '
                     'snapshot %(snapshot)s. Error: %(excmsg)s.') %
                   {'share': share['name'], 'snapshot': snapshot['name'],
                    'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _extend_share(self, shareobj, new_size):
        sharename = shareobj['name']
        sizestr = '%sG' % new_size
        fsdev = self._get_gpfs_device()
        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
                               sharename, '--block', '0:' + sizestr)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to set quota for the share %(sharename)s. '
                     'Error: %(excmsg)s.') %
                   {'sharename': sharename, 'excmsg': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def get_network_allocations_number(self):
        return 0

    def create_share(self, ctx, share, share_server=None):
        """Create GPFS directory that will be represented as share."""
        self._create_share(share)
        share_path = self._get_share_path(share)
        location = self._get_helper(share).create_export(share_path)
        return location

    def create_share_from_snapshot(self, ctx, share, snapshot,
                                   share_server=None, parent_share=None):
        """Is called to create share from a snapshot."""
        share_path = self._get_share_path(share)
        self._create_share_from_snapshot(share, snapshot, share_path)
        location = self._get_helper(share).create_export(share_path)
        return location

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

    def delete_share(self, ctx, share, share_server=None):
        """Remove and cleanup share storage."""
        location = self._get_share_path(share)
        self._get_helper(share).remove_export(location, share)
        self._delete_share(share)

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

    def extend_share(self, share, new_size, share_server=None):
        """Extends the quota on the share fileset."""
        self._extend_share(share, new_size)

    def ensure_share(self, ctx, share, share_server=None):
        """Ensure that storage are mounted and exported."""

    def update_access(self, context, share, access_rules, add_rules,
                      delete_rules, update_rules, share_server=None):
        """Update access rules for given share."""
        helper = self._get_helper(share)
        location = self._get_share_path(share)

        for access in delete_rules:
            helper.deny_access(location, share, access)

        for access in add_rules:
            helper.allow_access(location, share, access)

        if not (add_rules or delete_rules):
            helper.resync_access(location, share, access_rules)

    def check_for_setup_error(self):
        """Returns an error if prerequisites aren't met."""
        if not self._check_gpfs_state():
            msg = (_('GPFS is not active.'))
            LOG.error(msg)
            raise exception.GPFSException(msg)

        if not self.configuration.gpfs_share_export_ip:
            msg = (_('gpfs_share_export_ip must be specified.'))
            LOG.error(msg)
            raise exception.InvalidParameterValue(err=msg)

        gpfs_base_dir = self.configuration.gpfs_mount_point_base
        if not gpfs_base_dir.startswith('/'):
            msg = (_('%s must be an absolute path.') % gpfs_base_dir)
            LOG.error(msg)
            raise exception.GPFSException(msg)

        if not self._is_dir(gpfs_base_dir):
            msg = (_('%s is not a directory.') % gpfs_base_dir)
            LOG.error(msg)
            raise exception.GPFSException(msg)

        if not self._is_gpfs_path(gpfs_base_dir):
            msg = (_('%s is not on GPFS. Perhaps GPFS not mounted.')
                   % gpfs_base_dir)
            LOG.error(msg)
            raise exception.GPFSException(msg)

        if self.configuration.gpfs_nfs_server_type not in ("KNFS", "CES"):
            msg = (_('Invalid gpfs_nfs_server_type value: %s. '
                     'Valid values are: "KNFS", "CES".')
                   % self.configuration.gpfs_nfs_server_type)
            LOG.error(msg)
            raise exception.InvalidParameterValue(err=msg)

        if ((not self.configuration.gpfs_nfs_server_list) and
                (self.configuration.gpfs_nfs_server_type != 'CES')):
            msg = (_('Missing value for gpfs_nfs_server_list.'))
            LOG.error(msg)
            raise exception.InvalidParameterValue(err=msg)

    def _is_share_valid(self, fsdev, location):
        try:
            out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsfileset', fsdev,
                                         '-J', location, '-L', '-Y')
        except exception.ProcessExecutionError:
            msg = (_('Given share path %(share_path)s does not exist at '
                     'mount point %(mount_point)s.')
                   % {'share_path': location, 'mount_point': fsdev})
            LOG.exception(msg)
            raise exception.ManageInvalidShare(reason=msg)

        lines = out.splitlines()
        try:
            validation_token = lines[0].split(':').index('allocInodes')
            alloc_inodes = lines[1].split(':')[validation_token]
        except (IndexError, ValueError):
            msg = (_('Failed to check share at %s.') % location)
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        return alloc_inodes != '0'

    def _get_share_name(self, fsdev, location):
        try:
            out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsfileset', fsdev,
                                         '-J', location, '-L', '-Y')
        except exception.ProcessExecutionError:
            msg = (_('Given share path %(share_path)s does not exist at '
                     'mount point %(mount_point)s.')
                   % {'share_path': location, 'mount_point': fsdev})
            LOG.exception(msg)
            raise exception.ManageInvalidShare(reason=msg)

        lines = out.splitlines()
        try:
            validation_token = lines[0].split(':').index('filesetName')
            share_name = lines[1].split(':')[validation_token]
        except (IndexError, ValueError):
            msg = (_('Failed to check share at %s.') % location)
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        return share_name

    def _manage_existing(self, fsdev, share, old_share_name):
        new_share_name = share['name']
        new_export_location = self._local_path(new_share_name)
        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmunlinkfileset', fsdev,
                               old_share_name, '-f')
        except exception.ProcessExecutionError:
            msg = _('Failed to unlink fileset for share %s.') % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)
        LOG.debug('Unlinked the fileset of share %s.', old_share_name)

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmchfileset', fsdev,
                               old_share_name, '-j', new_share_name)
        except exception.ProcessExecutionError:
            msg = _('Failed to rename fileset for share %s.') % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)
        LOG.debug('Renamed the fileset from %(old_share)s to %(new_share)s.',
                  {'old_share': old_share_name, 'new_share': new_share_name})

        try:
            self._gpfs_execute(self.GPFS_PATH + 'mmlinkfileset', fsdev,
                               new_share_name, '-J', new_export_location)
        except exception.ProcessExecutionError:
            msg = _('Failed to link fileset for the share %s.'
                    ) % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)
        LOG.debug('Linked the fileset of share %(share_name)s at location '
                  '%(export_location)s.',
                  {'share_name': new_share_name,
                   'export_location': new_export_location})

        try:
            self._gpfs_execute('chmod', '777', new_export_location)
        except exception.ProcessExecutionError:
            msg = _('Failed to set permissions for share %s.') % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)
        LOG.debug('Changed the permission of share %s.', new_share_name)

        try:
            out, __ = self._gpfs_execute(self.GPFS_PATH + 'mmlsquota', '-j',
                                         new_share_name, '-Y', fsdev)
        except exception.ProcessExecutionError:
            msg = _('Failed to check size for share %s.') % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        lines = out.splitlines()
        try:
            quota_limit = lines[0].split(':').index('blockLimit')
            quota_status = lines[1].split(':')[quota_limit]
        except (IndexError, ValueError):
            msg = _('Failed to check quota for share %s.') % new_share_name
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        share_size = int(quota_status)
        # Note: since share_size returns integer value in KB,
        # we are checking whether share is less than 1GiB.
        # (units.Mi * KB = 1GB)
        if share_size < units.Mi:
            try:
                self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev + ':' +
                                   new_share_name, '--block', '0:1G')
            except exception.ProcessExecutionError:
                msg = _('Failed to set quota for share %s.') % new_share_name
                LOG.exception(msg)
                raise exception.GPFSException(msg)
            LOG.info('Existing share %(shr)s has size %(size)s KB '
                     'which is below 1GiB, so extended it to 1GiB.',
                     {'shr': new_share_name, 'size': share_size})
            share_size = 1
        else:
            orig_share_size = share_size
            share_size = int(math.ceil(float(share_size) / units.Mi))
            if orig_share_size != share_size * units.Mi:
                try:
                    self._gpfs_execute(self.GPFS_PATH + 'mmsetquota', fsdev +
                                       ':' + new_share_name, '--block', '0:' +
                                       str(share_size) + 'G')
                except exception.ProcessExecutionError:
                    msg = _('Failed to set quota for share %s.'
                            ) % new_share_name
                    LOG.exception(msg)
                    raise exception.GPFSException(msg)

        new_export_location = self._get_helper(share).create_export(
            new_export_location)
        return share_size, new_export_location

    def manage_existing(self, share, driver_options):

        old_export = share['export_location'].split(':')
        try:
            ces_ip = old_export[0]
            old_export_location = old_export[1]
        except IndexError:
            msg = _('Incorrect export path. Expected format: '
                    'IP:/gpfs_mount_point_base/share_id.')
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        if ces_ip not in self.configuration.gpfs_nfs_server_list:
            msg = _('The CES IP %s is not present in the '
                    'configuration option "gpfs_nfs_server_list".') % ces_ip
            raise exception.ShareBackendException(msg=msg)

        fsdev = self._get_gpfs_device()
        if not self._is_share_valid(fsdev, old_export_location):
            err_msg = _('Given share path %s does not have a valid '
                        'share.') % old_export_location
            raise exception.ManageInvalidShare(reason=err_msg)

        share_name = self._get_share_name(fsdev, old_export_location)

        out = self._get_helper(share)._has_client_access(old_export_location)
        if out:
            err_msg = _('Clients have access to %s share currently. Evict any '
                        'clients before trying again.') % share_name
            raise exception.ManageInvalidShare(reason=err_msg)

        share_size, new_export_location = self._manage_existing(
            fsdev, share, share_name)
        return {"size": share_size, "export_locations": new_export_location}

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

        data = dict(
            share_backend_name=self.backend_name,
            vendor_name='IBM',
            storage_protocol='NFS',
            reserved_percentage=self.configuration.reserved_share_percentage,
            reserved_snapshot_percentage=(
                self.configuration.reserved_share_from_snapshot_percentage
                or self.configuration.reserved_share_percentage),
            reserved_share_extend_percentage=(
                self.configuration.reserved_share_extend_percentage
                or self.configuration.reserved_share_percentage))

        free, capacity = self._get_available_capacity(
            self.configuration.gpfs_mount_point_base)

        data['total_capacity_gb'] = math.ceil(capacity / units.Gi)
        data['free_capacity_gb'] = math.ceil(free / units.Gi)

        super(GPFSShareDriver, self)._update_share_stats(data)

    def _get_helper(self, share):
        if share['share_proto'] == 'NFS':
            return self._helpers[self.configuration.gpfs_nfs_server_type]
        else:
            msg = (_('Share protocol %s not supported by GPFS driver.')
                   % share['share_proto'])
            LOG.error(msg)
            raise exception.InvalidShare(reason=msg)

    def _get_share_path(self, share):
        """Returns share path on storage provider."""
        return os.path.join(self.configuration.gpfs_mount_point_base,
                            share['name'])

    def _get_snapshot_path(self, snapshot):
        """Returns share path on storage provider."""
        snapshot_dir = ".snapshots"
        return os.path.join(self.configuration.gpfs_mount_point_base,
                            snapshot["share_name"], snapshot_dir,
                            snapshot["name"])


class NASHelperBase(metaclass=abc.ABCMeta):
    """Interface to work with share."""

    def __init__(self, execute, config_object):
        self.configuration = config_object
        self._execute = execute

    def create_export(self, local_path):
        """Construct location of new export."""
        return ':'.join([self.configuration.gpfs_share_export_ip, local_path])

    def get_export_options(self, share, access, helper):
        """Get the export options."""
        extra_specs = share_types.get_extra_specs_from_share(share)
        if helper == 'KNFS':
            export_options = extra_specs.get('knfs:export_options')
        elif helper == 'CES':
            export_options = extra_specs.get('ces:export_options')
        else:
            export_options = None

        options = self._get_validated_opt_list(export_options)
        options.append(self.get_access_option(access))
        return ','.join(options)

    def _validate_export_options(self, options):
        """Validate the export options."""
        options_not_allowed = self._get_options_not_allowed()
        invalid_options = [
            option for option in options if option in options_not_allowed
        ]

        if invalid_options:
            raise exception.InvalidInput(reason='Invalid export_option %s as '
                                                'it is set by access_type.'
                                                % invalid_options)

    def _get_validated_opt_list(self, export_options):
        """Validate the export options and return an option list."""
        if export_options:
            options = export_options.lower().split(',')
            self._validate_export_options(options)
        else:
            options = []
        return options

    @abc.abstractmethod
    def get_access_option(self, access):
        """Get access option string based on access level."""

    @abc.abstractmethod
    def _get_options_not_allowed(self):
        """Get access options that are not allowed in extra-specs."""

    @abc.abstractmethod
    def remove_export(self, local_path, share):
        """Remove export."""

    @abc.abstractmethod
    def allow_access(self, local_path, share, access):
        """Allow access to the host."""

    @abc.abstractmethod
    def deny_access(self, local_path, share, access):
        """Deny access to the host."""

    @abc.abstractmethod
    def resync_access(self, local_path, share, access_rules):
        """Re-sync all access rules for given share."""


class KNFSHelper(NASHelperBase):
    """Wrapper for Kernel NFS Commands."""

    def __init__(self, execute, config_object):
        super(KNFSHelper, self).__init__(execute, config_object)
        self._execute = execute
        try:
            self._execute('exportfs', check_exit_code=True, run_as_root=True)
        except exception.ProcessExecutionError as e:
            msg = (_('NFS server not found. Error: %s.') % e)
            LOG.error(msg)
            raise exception.GPFSException(msg)

    def _has_client_access(self, local_path, access_to=None):
        try:
            out, __ = self._execute('exportfs', run_as_root=True)
        except exception.ProcessExecutionError:
            msg = _('Failed to check exports on the systems.')
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        if access_to:
            if (re.search(re.escape(local_path) + r'[\s\n]*'
                          + re.escape(access_to), out)):
                return True
        else:
            if re.findall(local_path + '\\b', ''.join(out)):
                return True
        return False

    def _publish_access(self, *cmd, **kwargs):
        check_exit_code = kwargs.get('check_exit_code', True)

        outs = []
        localserver_iplist = socket.gethostbyname_ex(socket.gethostname())[2]
        for server in self.configuration.gpfs_nfs_server_list:
            if server in localserver_iplist:
                run_command = cmd
                run_local = True
            else:
                sshlogin = self.configuration.gpfs_ssh_login
                remote_login = sshlogin + '@' + server
                run_command = ['ssh', remote_login] + list(cmd)
                run_local = False
            try:
                out = utils.execute(*run_command,
                                    run_as_root=run_local,
                                    check_exit_code=check_exit_code)
            except exception.ProcessExecutionError:
                raise
            outs.append(out)
        return outs

    def _verify_denied_access(self, local_path, share, ip):
        try:
            cmd = ['exportfs']
            outs = self._publish_access(*cmd)
        except exception.ProcessExecutionError:
            msg = _('Failed to verify denied access for '
                    'share %s.') % share['name']
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        for stdout, stderr in outs:
            if stderr and stderr.strip():
                msg = ('Log/ignore stderr during _validate_denied_access for '
                       'share %(sharename)s. Return code OK. '
                       'Stderr: %(stderr)s' % {'sharename': share['name'],
                                               'stderr': stderr})
                LOG.debug(msg)

            gpfs_ips = NFSHelper.get_host_list(stdout, local_path)
            if ip in gpfs_ips:
                msg = (_('Failed to deny access for share %(sharename)s. '
                         'IP %(ip)s still has access.') %
                       {'sharename': share['name'],
                        'ip': ip})
                LOG.error(msg)
                raise exception.GPFSException(msg)

    def remove_export(self, local_path, share):
        """Remove export."""

    def get_access_option(self, access):
        """Get access option string based on access level."""
        return access['access_level']

    def _get_options_not_allowed(self):
        """Get access options that are not allowed in extra-specs."""
        return list(constants.ACCESS_LEVELS)

    def _get_exports(self):
        """Get exportfs output."""
        try:
            out, __ = self._execute('exportfs', run_as_root=True)
        except exception.ProcessExecutionError as e:
            msg = (_('Failed to check exports on the systems. '
                     ' Error: %s.') % e)
            LOG.error(msg)
            raise exception.GPFSException(msg)
        return out

    def allow_access(self, local_path, share, access, error_on_exists=True):
        """Allow access to one or more vm instances."""

        if access['access_type'] != 'ip':
            raise exception.InvalidShareAccess(reason='Only ip access type '
                                                      'supported.')

        if error_on_exists:
            # check if present in export
            out = re.search(
                re.escape(local_path) + r'[\s\n]*'
                + re.escape(access['access_to']), self._get_exports())

            if out is not None:
                access_type = access['access_type']
                access_to = access['access_to']
                raise exception.ShareAccessExists(access_type=access_type,
                                                  access=access_to)

        export_opts = self.get_export_options(share, access, 'KNFS')
        cmd = ['exportfs', '-o', export_opts,
               ':'.join([access['access_to'], local_path])]
        try:
            self._publish_access(*cmd)
        except exception.ProcessExecutionError:
            msg = _('Failed to allow access for share %s.') % share['name']
            LOG.exception(msg)
            raise exception.GPFSException(msg)

    def _deny_ip(self, local_path, share, ip):
        """Remove access for one or more vm instances."""
        cmd = ['exportfs', '-u', ':'.join([ip, local_path])]
        try:
            # Can get exit code 0 for success or 1 for already gone (also
            # potentially get 1 due to exportfs bug). So allow
            # _publish_access to continue with [0, 1] and then verify after
            # it is done.
            self._publish_access(*cmd, check_exit_code=[0, 1])
        except exception.ProcessExecutionError:
            msg = _('Failed to deny access for share %s.') % share['name']
            LOG.exception(msg)
            raise exception.GPFSException(msg)

        # Error code (0 or 1) makes deny IP success indeterminate.
        # So, verify that the IP access was completely removed.
        self._verify_denied_access(local_path, share, ip)

    def deny_access(self, local_path, share, access):
        """Remove access for one or more vm instances."""
        self._deny_ip(local_path, share, access['access_to'])

    def _remove_other_access(self, local_path, share, access_rules):
        """Remove any client access that is not in access_rules."""
        exports = self._get_exports()
        gpfs_ips = set(NFSHelper.get_host_list(exports, local_path))
        manila_ips = set([x['access_to'] for x in access_rules])
        remove_ips = gpfs_ips - manila_ips
        for ip in remove_ips:
            self._deny_ip(local_path, share, ip)

    def resync_access(self, local_path, share, access_rules):
        """Re-sync all access rules for given share."""
        for access in access_rules:
            self.allow_access(local_path, share, access, error_on_exists=False)
        self._remove_other_access(local_path, share, access_rules)


class CESHelper(NASHelperBase):
    """Wrapper for NFS by Spectrum Scale CES"""

    def __init__(self, execute, config_object):
        super(CESHelper, self).__init__(execute, config_object)
        self._execute = execute
        if self.configuration.is_gpfs_node:
            self.GPFS_PATH = ''
        else:
            self.GPFS_PATH = '/usr/lpp/mmfs/bin/'

    def _execute_mmnfs_command(self, cmd, err_msg):
        try:
            out, __ = self._execute(self.GPFS_PATH + 'mmnfs', 'export', *cmd)
        except exception.ProcessExecutionError as e:
            msg = (_('%(err_msg)s Error: %(e)s.')
                   % {'err_msg': err_msg, 'e': e})
            LOG.error(msg)
            raise exception.GPFSException(msg)
        return out

    @staticmethod
    def _fix_export_data(data, headers):
        """Export data split by ':' may need fixing if client had colons."""

        # If an IPv6 client shows up then ':' delimiters don't work.
        # So use header positions to get data before/after Clients.
        # Then what is left in between can be joined back into a client IP.
        client_index = headers.index('Clients')
        # reverse_client_index is distance from end.
        reverse_client_index = len(headers) - (client_index + 1)
        after_client_index = len(data) - reverse_client_index

        before_client = data[:client_index]
        client = data[client_index: after_client_index]
        after_client = data[after_client_index:]

        result_data = before_client
        result_data.append(':'.join(client))  # Fixes colons in client IP
        result_data.extend(after_client)
        return result_data

    def _get_nfs_client_exports(self, local_path):
        """Get the current NFS client export details from GPFS."""

        out = self._execute_mmnfs_command(
            ('list', '-n', local_path, '-Y'),
            'Failed to get exports from the system.')

        # Remove the header line and use the headers to describe the data
        lines = out.splitlines()
        for line in lines:
            data = line.split(':')
            if "HEADER" in data:
                headers = data
                lines.remove(line)
                break
        else:
            msg = _('Failed to parse exports for path %s. '
                    'No HEADER found.') % local_path
            LOG.error(msg)
            raise exception.GPFSException(msg)

        exports = []
        for line in lines:
            data = line.split(':')
            if len(data) < 3:
                continue  # Skip empty lines (and anything less than minimal).

            result_data = self._fix_export_data(data, headers)
            exports.append(dict(zip(headers, result_data)))

        return exports

    def _has_client_access(self, local_path, access_to=None):
        """Check path for any export or for one with a specific IP address."""
        gpfs_clients = self._get_nfs_client_exports(local_path)
        return gpfs_clients and (access_to is None or access_to in [
            x['Clients'] for x in gpfs_clients])

    def remove_export(self, local_path, share):
        """Remove export."""
        if self._has_client_access(local_path):
            err_msg = ('Failed to remove export for share %s.'
                       % share['name'])
            self._execute_mmnfs_command(('remove', local_path), err_msg)

    def _get_options_not_allowed(self):
        """Get access options that are not allowed in extra-specs."""
        return ['access_type=ro', 'access_type=rw']

    def get_access_option(self, access):
        """Get access option string based on access level."""
        if access['access_level'] == constants.ACCESS_LEVEL_RO:
            return 'access_type=ro'
        else:
            return 'access_type=rw'

    def allow_access(self, local_path, share, access):
        """Allow access to the host."""

        if access['access_type'] != 'ip':
            raise exception.InvalidShareAccess(reason='Only ip access type '
                                                      'supported.')
        has_exports = self._has_client_access(local_path)

        export_opts = self.get_export_options(share, access, 'CES')

        if not has_exports:
            cmd = ['add', local_path, '-c',
                   access['access_to'] +
                   '(' + export_opts + ')']
        else:
            cmd = ['change', local_path, '--nfsadd',
                   access['access_to'] +
                   '(' + export_opts + ')']

        err_msg = ('Failed to allow access for share %s.'
                   % share['name'])
        self._execute_mmnfs_command(cmd, err_msg)

    def deny_access(self, local_path, share, access, force=False):
        """Deny access to the host."""
        has_export = self._has_client_access(local_path, access['access_to'])

        if has_export:
            err_msg = ('Failed to remove access for share %s.'
                       % share['name'])
            self._execute_mmnfs_command(('change', local_path,
                                         '--nfsremove', access['access_to']),
                                        err_msg)

    def _get_client_opts(self, access, opts_list):
        """Get client options string for access rule and NFS options."""
        nfs_opts = ','.join([self.get_access_option(access)] + opts_list)

        return '%(ip)s(%(nfs_opts)s)' % {'ip': access['access_to'],
                                         'nfs_opts': nfs_opts}

    def _get_share_opts(self, share):
        """Get a list of NFS options from the share's share type."""
        extra_specs = share_types.get_extra_specs_from_share(share)
        opts_list = self._get_validated_opt_list(
            extra_specs.get('ces:export_options'))
        return opts_list

    def _nfs_change(self, local_path, share, access_rules, gpfs_clients):
        """Bulk add/update/remove of access rules for share."""
        opts_list = self._get_share_opts(share)

        # Create a map of existing client access rules from GPFS.
        # Key from 'Clients' is an IP address or
        # Value from 'Access_Type' is RW|RO (case varies)
        gpfs_map = {
            x['Clients']: x['Access_Type'].lower() for x in gpfs_clients}
        gpfs_ips = set(gpfs_map.keys())

        manila_ips = set([x['access_to'] for x in access_rules])
        add_ips = manila_ips - gpfs_ips
        update_ips = gpfs_ips.intersection(manila_ips)
        remove_ips = gpfs_ips - manila_ips

        adds = []
        updates = []
        if add_ips or update_ips:
            for access in access_rules:
                ip = access['access_to']
                if ip in add_ips:
                    adds.append(self._get_client_opts(access, opts_list))
                elif (ip in update_ips
                      and access['access_level'] != gpfs_map[ip]):
                    updates.append(self._get_client_opts(access, opts_list))

        if remove_ips or adds or updates:
            cmd = ['change', local_path]
            if remove_ips:
                cmd.append('--nfsremove')
                cmd.append(','.join(remove_ips))
            if adds:
                cmd.append('--nfsadd')
                cmd.append(';'.join(adds))
            if updates:
                cmd.append('--nfschange')
                cmd.append(';'.join(updates))
            err_msg = ('Failed to resync access for share %s.' % share['name'])
            self._execute_mmnfs_command(cmd, err_msg)

    def _nfs_add(self, access_rules, local_path, share):
        """Bulk add of access rules to share."""
        if not access_rules:
            return

        opts_list = self._get_share_opts(share)
        client_options = []
        for access in access_rules:
            client_options.append(self._get_client_opts(access, opts_list))

        cmd = ['add', local_path, '-c', ';'.join(client_options)]
        err_msg = ('Failed to resync access for share %s.' % share['name'])
        self._execute_mmnfs_command(cmd, err_msg)

    def resync_access(self, local_path, share, access_rules):
        """Re-sync all access rules for given share."""
        gpfs_clients = self._get_nfs_client_exports(local_path)
        if not gpfs_clients:
            self._nfs_add(access_rules, local_path, share)
        else:
            self._nfs_change(local_path, share, access_rules, gpfs_clients)
