# 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.

import os
from unittest import mock

import ddt
from oslo_config import cfg

from manila import context
from manila import exception
from manila.share import configuration as config
from manila.share.drivers.glusterfs import common
from manila.share.drivers.glusterfs import layout_directory
from manila import test
from manila.tests import fake_share
from manila.tests import fake_utils


CONF = cfg.CONF


fake_gluster_manager_attrs = {
    'export': '127.0.0.1:/testvol',
    'host': '127.0.0.1',
    'qualified': 'testuser@127.0.0.1:/testvol',
    'user': 'testuser',
    'volume': 'testvol',
    'path_to_private_key': '/fakepath/to/privatekey',
    'remote_server_password': 'fakepassword',
    'components': {'user': 'testuser', 'host': '127.0.0.1',
                   'volume': 'testvol', 'path': None}
}

fake_local_share_path = '/mnt/nfs/testvol/fakename'

fake_path_to_private_key = '/fakepath/to/privatekey'
fake_remote_server_password = 'fakepassword'


@ddt.ddt
class GlusterfsDirectoryMappedLayoutTestCase(test.TestCase):
    """Tests GlusterfsDirectoryMappedLayout."""

    def setUp(self):
        super(GlusterfsDirectoryMappedLayoutTestCase, self).setUp()
        fake_utils.stub_out_utils_execute(self)
        self._execute = fake_utils.fake_execute
        self._context = context.get_admin_context()
        self.addCleanup(fake_utils.fake_execute_set_repliers, [])
        self.addCleanup(fake_utils.fake_execute_clear_log)

        CONF.set_default('glusterfs_target', '127.0.0.1:/testvol')
        CONF.set_default('glusterfs_mount_point_base', '/mnt/nfs')
        CONF.set_default('glusterfs_server_password',
                         fake_remote_server_password)
        CONF.set_default('glusterfs_path_to_private_key',
                         fake_path_to_private_key)

        self.fake_driver = mock.Mock()
        self.mock_object(self.fake_driver, '_execute',
                         self._execute)
        self.fake_driver.GLUSTERFS_VERSION_MIN = (3, 6)
        self.fake_conf = config.Configuration(None)
        self.mock_object(common.GlusterManager, 'make_gluster_call')
        self._layout = layout_directory.GlusterfsDirectoryMappedLayout(
            self.fake_driver, configuration=self.fake_conf)
        self._layout.gluster_manager = mock.Mock(**fake_gluster_manager_attrs)
        self.share = fake_share.fake_share(share_proto='NFS')

    def test_do_setup(self):
        fake_gluster_manager = mock.Mock(**fake_gluster_manager_attrs)
        self.mock_object(fake_gluster_manager, 'get_gluster_version',
                         mock.Mock(return_value=('3', '5')))
        methods = ('_check_mount_glusterfs', '_ensure_gluster_vol_mounted')
        for method in methods:
            self.mock_object(self._layout, method)
        self.mock_object(common, 'GlusterManager',
                         mock.Mock(return_value=fake_gluster_manager))

        self._layout.do_setup(self._context)

        self.assertEqual(fake_gluster_manager, self._layout.gluster_manager)
        common.GlusterManager.assert_called_once_with(
            self._layout.configuration.glusterfs_target, self._execute,
            self._layout.configuration.glusterfs_path_to_private_key,
            self._layout.configuration.glusterfs_server_password,
            requires={'volume': True})
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'enable')
        self._layout._check_mount_glusterfs.assert_called_once_with()
        self._layout._ensure_gluster_vol_mounted.assert_called_once_with()

    def test_do_setup_glusterfs_target_not_set(self):
        self._layout.configuration.glusterfs_target = None
        self.assertRaises(exception.GlusterfsException, self._layout.do_setup,
                          self._context)

    def test_do_setup_error_enabling_creation_share_specific_size(self):
        attrs = {'volume': 'testvol',
                 'gluster_call.side_effect': exception.GlusterfsException,
                 'get_vol_option.return_value': 'off'}
        fake_gluster_manager = mock.Mock(**attrs)
        self.mock_object(layout_directory.LOG, 'exception')
        methods = ('_check_mount_glusterfs', '_ensure_gluster_vol_mounted')
        for method in methods:
            self.mock_object(self._layout, method)
        self.mock_object(common, 'GlusterManager',
                         mock.Mock(return_value=fake_gluster_manager))

        self.assertRaises(exception.GlusterfsException, self._layout.do_setup,
                          self._context)

        self.assertEqual(fake_gluster_manager, self._layout.gluster_manager)
        common.GlusterManager.assert_called_once_with(
            self._layout.configuration.glusterfs_target, self._execute,
            self._layout.configuration.glusterfs_path_to_private_key,
            self._layout.configuration.glusterfs_server_password,
            requires={'volume': True})
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'enable')
        (self._layout.gluster_manager.get_vol_option.
         assert_called_once_with('features.quota'))
        layout_directory.LOG.exception.assert_called_once_with(mock.ANY)
        self._layout._check_mount_glusterfs.assert_called_once_with()
        self.assertFalse(self._layout._ensure_gluster_vol_mounted.called)

    def test_do_setup_error_already_enabled_creation_share_specific_size(self):
        attrs = {'volume': 'testvol',
                 'gluster_call.side_effect': exception.GlusterfsException,
                 'get_vol_option.return_value': 'on'}
        fake_gluster_manager = mock.Mock(**attrs)
        self.mock_object(layout_directory.LOG, 'error')
        methods = ('_check_mount_glusterfs', '_ensure_gluster_vol_mounted')
        for method in methods:
            self.mock_object(self._layout, method)
        self.mock_object(common, 'GlusterManager',
                         mock.Mock(return_value=fake_gluster_manager))

        self._layout.do_setup(self._context)

        self.assertEqual(fake_gluster_manager, self._layout.gluster_manager)
        common.GlusterManager.assert_called_once_with(
            self._layout.configuration.glusterfs_target, self._execute,
            self._layout.configuration.glusterfs_path_to_private_key,
            self._layout.configuration.glusterfs_server_password,
            requires={'volume': True})
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'enable')
        (self._layout.gluster_manager.get_vol_option.
         assert_called_once_with('features.quota'))
        self.assertFalse(layout_directory.LOG.error.called)
        self._layout._check_mount_glusterfs.assert_called_once_with()
        self._layout._ensure_gluster_vol_mounted.assert_called_once_with()

    def test_share_manager(self):
        self._layout._glustermanager = mock.Mock()

        self._layout._share_manager(self.share)

        self._layout._glustermanager.assert_called_once_with(
            {'user': 'testuser', 'host': '127.0.0.1',
             'volume': 'testvol', 'path': '/fakename'})

    def test_ensure_gluster_vol_mounted(self):
        common._mount_gluster_vol = mock.Mock()

        self._layout._ensure_gluster_vol_mounted()

        self.assertTrue(common._mount_gluster_vol.called)

    def test_ensure_gluster_vol_mounted_error(self):
        common._mount_gluster_vol = (
            mock.Mock(side_effect=exception.GlusterfsException))

        self.assertRaises(exception.GlusterfsException,
                          self._layout._ensure_gluster_vol_mounted)

    def test_get_local_share_path(self):
        with mock.patch.object(os, 'access', return_value=True):

            ret = self._layout._get_local_share_path(self.share)

            self.assertEqual('/mnt/nfs/testvol/fakename', ret)

    def test_local_share_path_not_exists(self):
        with mock.patch.object(os, 'access', return_value=False):

            self.assertRaises(exception.GlusterfsException,
                              self._layout._get_local_share_path,
                              self.share)

    def test_update_share_stats(self):
        test_statvfs = mock.Mock(f_frsize=4096, f_blocks=524288,
                                 f_bavail=524288)
        self._layout._get_mount_point_for_gluster_vol = (
            mock.Mock(return_value='/mnt/nfs/testvol'))
        some_no = 42
        not_some_no = some_no + 1
        os_stat = (lambda path: mock.Mock(st_dev=some_no) if path == '/mnt/nfs'
                   else mock.Mock(st_dev=not_some_no))
        with mock.patch.object(os, 'statvfs', return_value=test_statvfs):
            with mock.patch.object(os, 'stat', os_stat):

                ret = self._layout._update_share_stats()

                test_data = {
                    'total_capacity_gb': 2,
                    'free_capacity_gb': 2,
                }
                self.assertEqual(test_data, ret)

    def test_update_share_stats_gluster_mnt_unavailable(self):
        self._layout._get_mount_point_for_gluster_vol = (
            mock.Mock(return_value='/mnt/nfs/testvol'))
        some_no = 42
        with mock.patch.object(os, 'stat',
                               return_value=mock.Mock(st_dev=some_no)):

            self.assertRaises(exception.GlusterfsException,
                              self._layout._update_share_stats)

    @ddt.data((), (None,))
    def test_create_share(self, extra_args):
        exec_cmd1 = 'mkdir %s' % fake_local_share_path
        expected_exec = [exec_cmd1, ]
        expected_ret = 'testuser@127.0.0.1:/testvol/fakename'
        self.mock_object(
            self._layout, '_get_local_share_path',
            mock.Mock(return_value=fake_local_share_path))
        gmgr = mock.Mock()
        self.mock_object(
            self._layout, '_glustermanager', mock.Mock(return_value=gmgr))
        self.mock_object(
            self._layout.driver, '_setup_via_manager',
            mock.Mock(return_value=expected_ret))

        ret = self._layout.create_share(self._context, self.share, *extra_args)

        self._layout._get_local_share_path.called_once_with(self.share)
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'limit-usage', '/fakename', '1GB')
        self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
        self._layout._glustermanager.assert_called_once_with(
            {'user': 'testuser', 'host': '127.0.0.1',
             'volume': 'testvol', 'path': '/fakename'})
        self._layout.driver._setup_via_manager.assert_called_once_with(
            {'share': self.share, 'manager': gmgr})
        self.assertEqual(expected_ret, ret)

    @ddt.data(exception.ProcessExecutionError, exception.GlusterfsException)
    def test_create_share_unable_to_create_share(self, trouble):
        def exec_runner(*ignore_args, **ignore_kw):
            raise trouble

        self.mock_object(
            self._layout, '_get_local_share_path',
            mock.Mock(return_value=fake_local_share_path))
        self.mock_object(self._layout, '_cleanup_create_share')
        self.mock_object(layout_directory.LOG, 'error')
        expected_exec = ['mkdir %s' % fake_local_share_path]
        fake_utils.fake_execute_set_repliers([(expected_exec[0],
                                               exec_runner)])

        self.assertRaises(
            exception.GlusterfsException, self._layout.create_share,
            self._context, self.share)

        self._layout._get_local_share_path.called_once_with(self.share)
        self._layout._cleanup_create_share.assert_called_once_with(
            fake_local_share_path, self.share['name'])
        layout_directory.LOG.error.assert_called_once_with(
            mock.ANY, mock.ANY)

    def test_create_share_unable_to_create_share_weird(self):
        def exec_runner(*ignore_args, **ignore_kw):
            raise RuntimeError

        self.mock_object(
            self._layout, '_get_local_share_path',
            mock.Mock(return_value=fake_local_share_path))
        self.mock_object(self._layout, '_cleanup_create_share')
        self.mock_object(layout_directory.LOG, 'error')
        expected_exec = ['mkdir %s' % fake_local_share_path]
        fake_utils.fake_execute_set_repliers([(expected_exec[0],
                                               exec_runner)])

        self.assertRaises(
            RuntimeError, self._layout.create_share,
            self._context, self.share)

        self._layout._get_local_share_path.called_once_with(self.share)
        self.assertFalse(self._layout._cleanup_create_share.called)

    def test_cleanup_create_share_local_share_path_exists(self):
        expected_exec = ['rm -rf %s' % fake_local_share_path]
        self.mock_object(os.path, 'exists', mock.Mock(return_value=True))

        ret = self._layout._cleanup_create_share(fake_local_share_path,
                                                 self.share['name'])

        os.path.exists.assert_called_once_with(fake_local_share_path)
        self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
        self.assertIsNone(ret)

    def test_cleanup_create_share_cannot_cleanup_unusable_share(self):
        def exec_runner(*ignore_args, **ignore_kw):
            raise exception.ProcessExecutionError

        expected_exec = ['rm -rf %s' % fake_local_share_path]
        fake_utils.fake_execute_set_repliers([(expected_exec[0],
                                               exec_runner)])
        self.mock_object(layout_directory.LOG, 'error')
        self.mock_object(os.path, 'exists', mock.Mock(return_value=True))

        self.assertRaises(exception.GlusterfsException,
                          self._layout._cleanup_create_share,
                          fake_local_share_path, self.share['name'])

        os.path.exists.assert_called_once_with(fake_local_share_path)
        layout_directory.LOG.error.assert_called_once_with(mock.ANY, mock.ANY)

    def test_cleanup_create_share_local_share_path_does_not_exist(self):
        self.mock_object(os.path, 'exists', mock.Mock(return_value=False))

        ret = self._layout._cleanup_create_share(fake_local_share_path,
                                                 self.share['name'])

        os.path.exists.assert_called_once_with(fake_local_share_path)
        self.assertIsNone(ret)

    def test_delete_share(self):
        self._layout._get_local_share_path = (
            mock.Mock(return_value='/mnt/nfs/testvol/fakename'))

        self._layout.delete_share(self._context, self.share)

        self.assertEqual(['rm -rf /mnt/nfs/testvol/fakename'],
                         fake_utils.fake_execute_get_log())

    def test_cannot_delete_share(self):
        self._layout._get_local_share_path = (
            mock.Mock(return_value='/mnt/nfs/testvol/fakename'))

        def exec_runner(*ignore_args, **ignore_kw):
            raise exception.ProcessExecutionError

        expected_exec = ['rm -rf %s' % (self._layout._get_local_share_path())]
        fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])

        self.assertRaises(exception.ProcessExecutionError,
                          self._layout.delete_share, self._context, self.share)

        self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())

    def test_delete_share_can_be_called_with_extra_arg_share_server(self):
        self._layout._get_local_share_path = mock.Mock()

        share_server = None
        ret = self._layout.delete_share(self._context, self.share,
                                        share_server)

        self.assertIsNone(ret)
        self._layout._get_local_share_path.assert_called_once_with(self.share)

    def test_ensure_share(self):
        self.assertIsNone(self._layout.ensure_share(self._context, self.share))

    def test_extend_share(self):
        self._layout.extend_share(self.share, 3)

        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'limit-usage', '/fakename', '3GB')

    def test_shrink_share(self):
        self.mock_object(self._layout, '_get_directory_usage',
                         mock.Mock(return_value=10.0))

        self._layout.shrink_share(self.share, 11)
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'limit-usage', '/fakename', '11GB')

    def test_shrink_share_data_loss(self):
        self.mock_object(self._layout, '_get_directory_usage',
                         mock.Mock(return_value=10.0))
        shrink_on_gluster = self.mock_object(self._layout,
                                             '_set_directory_quota')

        self.assertRaises(exception.ShareShrinkingPossibleDataLoss,
                          self._layout.shrink_share, self.share, 9)
        shrink_on_gluster.assert_not_called()

    def test_set_directory_quota(self):
        self._layout._set_directory_quota(self.share, 3)

        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'limit-usage', '/fakename', '3GB')

    def test_set_directory_quota_unable_to_set(self):
        self.mock_object(self._layout.gluster_manager, 'gluster_call',
                         mock.Mock(side_effect=exception.GlusterfsException))

        self.assertRaises(exception.GlusterfsException,
                          self._layout._set_directory_quota, self.share, 3)

        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            'volume', 'quota', 'testvol', 'limit-usage', '/fakename', '3GB')

    def test_get_directory_usage(self):

        def xml_output(*ignore_args, **ignore_kwargs):
            return """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cliOutput>
  <opRet>0</opRet>
  <opErrno>0</opErrno>
  <opErrstr/>
  <volQuota>
    <limit>
      <used_space>10737418240</used_space>
    </limit>
  </volQuota>
</cliOutput>""", ''

        self.mock_object(self._layout.gluster_manager, 'gluster_call',
                         mock.Mock(side_effect=xml_output))

        ret = self._layout._get_directory_usage(self.share)

        self.assertEqual(10.0, ret)
        share_dir = '/' + self.share['name']
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            '--xml', 'volume', 'quota', self._layout.gluster_manager.volume,
            'list', share_dir)

    def test_get_directory_usage_unable_to_get(self):
        self.mock_object(self._layout.gluster_manager, 'gluster_call',
                         mock.Mock(side_effect=exception.GlusterfsException))

        self.assertRaises(exception.GlusterfsException,
                          self._layout._get_directory_usage, self.share)

        share_dir = '/' + self.share['name']
        self._layout.gluster_manager.gluster_call.assert_called_once_with(
            '--xml', 'volume', 'quota', self._layout.gluster_manager.volume,
            'list', share_dir)

    @ddt.data(
        ('create_share_from_snapshot', ('context', 'share', 'snapshot'),
         {'share_server': None}),
        ('create_snapshot', ('context', 'snapshot'), {'share_server': None}),
        ('delete_snapshot', ('context', 'snapshot'), {'share_server': None}),
        ('manage_existing', ('share', 'driver_options'), {}),
        ('unmanage', ('share',), {}))
    def test_nonimplemented_methods(self, method_invocation):
        method, args, kwargs = method_invocation
        self.assertRaises(NotImplementedError, getattr(self._layout, method),
                          *args, **kwargs)
