# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#
#    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 ddt
import mock

from os_brick import exception
from os_brick.initiator.connectors import rbd
from os_brick.initiator import linuxrbd
from os_brick.privileged import rootwrap as priv_rootwrap
from os_brick.tests.initiator import test_connector
from os_brick import utils


@ddt.ddt
class RBDConnectorTestCase(test_connector.ConnectorTestCase):

    def setUp(self):
        super(RBDConnectorTestCase, self).setUp()

        self.user = 'fake_user'
        self.pool = 'fake_pool'
        self.volume = 'fake_volume'
        self.clustername = 'fake_ceph'
        self.hosts = ['192.168.10.2']
        self.ports = ['6789']
        self.keyring = "[client.cinder]\n  key = test\n"

        self.connection_properties = {
            'auth_username': self.user,
            'name': '%s/%s' % (self.pool, self.volume),
            'cluster_name': self.clustername,
            'hosts': self.hosts,
            'ports': self.ports,
            'keyring': self.keyring,
        }

    def test_get_search_path(self):
        rbd_connector = rbd.RBDConnector(None)
        path = rbd_connector.get_search_path()
        self.assertIsNone(path)

    @mock.patch('os_brick.initiator.linuxrbd.rbd')
    @mock.patch('os_brick.initiator.linuxrbd.rados')
    def test_get_volume_paths(self, mock_rados, mock_rbd):
        rbd_connector = rbd.RBDConnector(None)
        expected = []
        actual = rbd_connector.get_volume_paths(self.connection_properties)
        self.assertEqual(expected, actual)

    def test_get_connector_properties(self):
        props = rbd.RBDConnector.get_connector_properties(
            'sudo', multipath=True, enforce_multipath=True)

        expected_props = {'do_local_attach': False}
        self.assertEqual(expected_props, props)

    @mock.patch('os_brick.initiator.linuxrbd.rbd')
    @mock.patch('os_brick.initiator.linuxrbd.rados')
    @mock.patch.object(rbd.RBDConnector, '_create_ceph_conf')
    @mock.patch('os.path.exists')
    def test_connect_volume(self, mock_path, mock_conf, mock_rados, mock_rbd):
        """Test the connect volume case."""
        rbd_connector = rbd.RBDConnector(None)
        mock_path.return_value = False
        mock_conf.return_value = "/tmp/fake_dir/fake_ceph.conf"
        device_info = rbd_connector.connect_volume(self.connection_properties)

        # Ensure rados is instantiated correctly
        mock_rados.Rados.assert_called_once_with(
            clustername=self.clustername,
            rados_id=utils.convert_str(self.user),
            conffile='/tmp/fake_dir/fake_ceph.conf')

        # Ensure correct calls to connect to cluster
        self.assertEqual(1, mock_rados.Rados.return_value.connect.call_count)
        mock_rados.Rados.return_value.open_ioctx.assert_called_once_with(
            utils.convert_str(self.pool))

        # Ensure rbd image is instantiated correctly
        mock_rbd.Image.assert_called_once_with(
            mock_rados.Rados.return_value.open_ioctx.return_value,
            utils.convert_str(self.volume), read_only=False,
            snapshot=None)

        # Ensure expected object is returned correctly
        self.assertIsInstance(device_info['path'],
                              linuxrbd.RBDVolumeIOWrapper)

    @mock.patch('os_brick.initiator.linuxrbd.rbd')
    @mock.patch('os_brick.initiator.linuxrbd.rados')
    @mock.patch.object(rbd.RBDConnector, '_create_ceph_conf')
    @mock.patch('os.path.exists')
    def test_provided_keyring(self, mock_path, mock_conf, mock_rados,
                              mock_rbd):
        conn = rbd.RBDConnector(None)
        mock_path.return_value = False
        mock_conf.return_value = "/tmp/fake_dir/fake_ceph.conf"
        self.connection_properties['keyring'] = self.keyring
        conn.connect_volume(self.connection_properties)
        mock_conf.assert_called_once_with(self.hosts, self.ports,
                                          self.clustername, self.user,
                                          self.keyring)

    def test_keyring_is_none(self):
        conn = rbd.RBDConnector(None)
        keyring = None
        keyring_data = "[client.cinder]\n  key = test\n"
        mockopen = mock.mock_open(read_data=keyring_data)
        mockopen.return_value.__exit__ = mock.Mock()
        with mock.patch('os_brick.initiator.connectors.rbd.open', mockopen,
                        create=True):
            self.assertEqual(
                conn._check_or_get_keyring_contents(keyring, 'cluster',
                                                    'user'), keyring_data)
            self.assertEqual(
                conn._check_or_get_keyring_contents(keyring, 'cluster',
                                                    None), '')

    def test_keyring_raise_error(self):
        conn = rbd.RBDConnector(None)
        keyring = None
        mockopen = mock.mock_open()
        mockopen.return_value = ""
        with mock.patch('os_brick.initiator.connectors.rbd.open', mockopen,
                        create=True) as mock_keyring_file:
            mock_keyring_file.side_effect = IOError
            self.assertRaises(exception.BrickException,
                              conn._check_or_get_keyring_contents, keyring,
                              'cluster', 'user')

    @ddt.data((['192.168.1.1', '192.168.1.2'],
               ['192.168.1.1', '192.168.1.2']),
              (['3ffe:1900:4545:3:200:f8ff:fe21:67cf',
                'fe80:0:0:0:200:f8ff:fe21:67cf'],
               ['[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
                '[fe80:0:0:0:200:f8ff:fe21:67cf]']),
              (['foobar', 'fizzbuzz'], ['foobar', 'fizzbuzz']),
              (['192.168.1.1',
                '3ffe:1900:4545:3:200:f8ff:fe21:67cf',
                'hello, world!'],
               ['192.168.1.1',
                '[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
                'hello, world!']))
    @ddt.unpack
    def test_sanitize_mon_host(self, hosts_in, hosts_out):
        conn = rbd.RBDConnector(None)
        self.assertEqual(hosts_out, conn._sanitize_mon_hosts(hosts_in))

    @mock.patch('os_brick.initiator.connectors.rbd.tempfile.mkstemp')
    def test_create_ceph_conf(self, mock_mkstemp):
        mockopen = mock.mock_open()
        fd = mock.sentinel.fd
        tmpfile = mock.sentinel.tmpfile
        mock_mkstemp.return_value = (fd, tmpfile)

        with mock.patch('os.fdopen', mockopen, create=True):
            rbd_connector = rbd.RBDConnector(None)
            conf_path = rbd_connector._create_ceph_conf(
                self.hosts, self.ports, self.clustername, self.user,
                self.keyring)
        self.assertEqual(conf_path, tmpfile)
        mock_mkstemp.assert_called_once_with(prefix='brickrbd_')

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_connect_local_volume(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'auth_username': 'fake_user',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        device_info = rbd_connector.connect_volume(conn)
        execute_call1 = mock.call('which', 'rbd')
        cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user',
               '--mon_host', '192.168.10.2:6789']
        execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
        mock_execute.assert_has_calls([execute_call1, execute_call2])
        expected_info = {'path': '/dev/rbd/pool/image',
                         'type': 'block'}
        self.assertEqual(expected_info, device_info)

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    @mock.patch('os.path.exists')
    @mock.patch('os.path.islink')
    @mock.patch('os.path.realpath')
    def test_connect_local_volume_dev_exist(self, mock_realpath, mock_islink,
                                            mock_exists, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'auth_username': 'fake_user',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        mock_realpath.return_value = '/dev/rbd0'
        mock_islink.return_value = True
        mock_exists.return_value = True
        device_info = rbd_connector.connect_volume(conn)
        execute_call1 = mock.call('which', 'rbd')
        cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user',
               '--mon_host', '192.168.10.2:6789']
        execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
        mock_execute.assert_has_calls([execute_call1])
        self.assertFalse(execute_call2 in mock_execute.mock_calls)
        expected_info = {'path': '/dev/rbd/pool/image',
                         'type': 'block'}
        self.assertEqual(expected_info, device_info)

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_connect_local_volume_without_mons(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'auth_username': 'fake_user'}
        device_info = rbd_connector.connect_volume(conn)
        execute_call1 = mock.call('which', 'rbd')
        cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user']
        execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
        mock_execute.assert_has_calls([execute_call1, execute_call2])
        expected_info = {'path': '/dev/rbd/pool/image',
                         'type': 'block'}
        self.assertEqual(expected_info, device_info)

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_connect_local_volume_without_auth(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        self.assertRaises(exception.BrickException,
                          rbd_connector.connect_volume,
                          conn)

    @mock.patch('os_brick.initiator.linuxrbd.rbd')
    @mock.patch('os_brick.initiator.linuxrbd.rados')
    @mock.patch.object(linuxrbd.RBDVolumeIOWrapper, 'close')
    def test_disconnect_volume(self, volume_close, mock_rados, mock_rbd):
        """Test the disconnect volume case."""
        rbd_connector = rbd.RBDConnector(None)
        device_info = rbd_connector.connect_volume(self.connection_properties)
        rbd_connector.disconnect_volume(
            self.connection_properties, device_info)

        self.assertEqual(1, volume_close.call_count)

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_disconnect_local_volume(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'auth_username': 'fake_user',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        mock_execute.side_effect = [("""
{"0":{"pool":"pool","device":"/dev/rbd0","name":"image"},
 "1":{"pool":"pool","device":"/dev/rdb1","name":"image_2"}}""", None),
                                    (None, None)]
        show_cmd = ['rbd', 'showmapped', '--format=json', '--id', 'fake_user',
                    '--mon_host', '192.168.10.2:6789']
        unmap_cmd = ['rbd', 'unmap', '/dev/rbd0', '--id', 'fake_user',
                     '--mon_host', '192.168.10.2:6789']

        rbd_connector.disconnect_volume(conn, None)

        # Assert that showmapped is used before we unmap the root device
        mock_execute.assert_has_calls([
            mock.call(*show_cmd, root_helper=None, run_as_root=True),
            mock.call(*unmap_cmd, root_helper=None, run_as_root=True)])

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_disconnect_local_volume_no_mapping(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/not_mapped',
                'auth_username': 'fake_user',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        mock_execute.return_value = ("""
{"0":{"pool":"pool","device":"/dev/rbd0","name":"pool-image"},
 "1":{"pool":"pool","device":"/dev/rdb1","name":"pool-image_2"}}""", None)
        show_cmd = ['rbd', 'showmapped', '--format=json', '--id', 'fake_user',
                    '--mon_host', '192.168.10.2:6789']
        rbd_connector.disconnect_volume(conn, None)

        # Assert that only showmapped is called when no mappings are found
        mock_execute.called_once_with(*show_cmd, root_helper=None,
                                      run_as_root=True)

    @mock.patch.object(priv_rootwrap, 'execute', return_value=None)
    def test_disconnect_local_volume_no_mappings(self, mock_execute):
        rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
        conn = {'name': 'pool/image',
                'auth_username': 'fake_user',
                'hosts': ['192.168.10.2'],
                'ports': ['6789']}
        mock_execute.return_value = ("{}", None)
        show_cmd = ['rbd', 'showmapped', '--format=json', '--id', 'fake_user',
                    '--mon_host', '192.168.10.2:6789']
        rbd_connector.disconnect_volume(conn, None)

        # Assert that only showmapped is called when no mappings are found
        mock_execute.called_once_with(*show_cmd, root_helper=None,
                                      run_as_root=True)

    def test_extend_volume(self):
        rbd_connector = rbd.RBDConnector(None)
        self.assertRaises(NotImplementedError,
                          rbd_connector.extend_volume,
                          self.connection_properties)
