# Copyright (c) 2012 Citrix Systems, Inc.
#
#    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 mock

from jacket.compute import exception
from jacket.compute import test
from jacket.tests.compute.unit.virt.xenapi import stubs
from jacket.compute.virt.xenapi import vm_utils
from jacket.compute.virt.xenapi import volume_utils
from jacket.compute.virt.xenapi import volumeops


class VolumeOpsTestBase(stubs.XenAPITestBaseNoDB):
    def setUp(self):
        super(VolumeOpsTestBase, self).setUp()
        self._setup_mock_volumeops()

    def _setup_mock_volumeops(self):
        self.session = stubs.FakeSessionForVolumeTests('fake_uri')
        self.ops = volumeops.VolumeOps(self.session)


class VolumeDetachTestCase(VolumeOpsTestBase):
    def test_detach_volume_call(self):
        registered_calls = []

        def regcall(label):
            def side_effect(*args, **kwargs):
                registered_calls.append(label)
            return side_effect

        ops = volumeops.VolumeOps('session')
        self.mox.StubOutWithMock(volumeops.vm_utils, 'lookup')
        self.mox.StubOutWithMock(volumeops.volume_utils, 'find_vbd_by_number')
        self.mox.StubOutWithMock(volumeops.vm_utils, 'is_vm_shutdown')
        self.mox.StubOutWithMock(volumeops.vm_utils, 'unplug_vbd')
        self.mox.StubOutWithMock(volumeops.vm_utils, 'destroy_vbd')
        self.mox.StubOutWithMock(volumeops.volume_utils, 'get_device_number')
        self.mox.StubOutWithMock(volumeops.volume_utils, 'find_sr_from_vbd')
        self.mox.StubOutWithMock(volumeops.volume_utils, 'purge_sr')

        volumeops.vm_utils.lookup('session', 'instance_1').AndReturn(
            'vmref')

        volumeops.volume_utils.get_device_number('mountpoint').AndReturn(
            'devnumber')

        volumeops.volume_utils.find_vbd_by_number(
            'session', 'vmref', 'devnumber').AndReturn('vbdref')

        volumeops.vm_utils.is_vm_shutdown('session', 'vmref').AndReturn(
            False)

        volumeops.vm_utils.unplug_vbd('session', 'vbdref', 'vmref')

        volumeops.vm_utils.destroy_vbd('session', 'vbdref').WithSideEffects(
            regcall('destroy_vbd'))

        volumeops.volume_utils.find_sr_from_vbd(
            'session', 'vbdref').WithSideEffects(
                regcall('find_sr_from_vbd')).AndReturn('srref')

        volumeops.volume_utils.purge_sr('session', 'srref')

        self.mox.ReplayAll()

        ops.detach_volume(
            dict(driver_volume_type='iscsi', data='conn_data'),
            'instance_1', 'mountpoint')

        self.assertEqual(
            ['find_sr_from_vbd', 'destroy_vbd'], registered_calls)

    @mock.patch.object(volumeops.VolumeOps, "_detach_vbds_and_srs")
    @mock.patch.object(volume_utils, "find_vbd_by_number")
    @mock.patch.object(vm_utils, "vm_ref_or_raise")
    def test_detach_volume(self, mock_vm, mock_vbd, mock_detach):
        mock_vm.return_value = "vm_ref"
        mock_vbd.return_value = "vbd_ref"

        self.ops.detach_volume({}, "name", "/dev/xvdd")

        mock_vm.assert_called_once_with(self.session, "name")
        mock_vbd.assert_called_once_with(self.session, "vm_ref", 3)
        mock_detach.assert_called_once_with("vm_ref", ["vbd_ref"])

    @mock.patch.object(volumeops.VolumeOps, "_detach_vbds_and_srs")
    @mock.patch.object(volume_utils, "find_vbd_by_number")
    @mock.patch.object(vm_utils, "vm_ref_or_raise")
    def test_detach_volume_skips_error_skip_attach(self, mock_vm, mock_vbd,
                                                   mock_detach):
        mock_vm.return_value = "vm_ref"
        mock_vbd.return_value = None

        self.ops.detach_volume({}, "name", "/dev/xvdd")

        self.assertFalse(mock_detach.called)

    @mock.patch.object(volumeops.VolumeOps, "_detach_vbds_and_srs")
    @mock.patch.object(volume_utils, "find_vbd_by_number")
    @mock.patch.object(vm_utils, "vm_ref_or_raise")
    def test_detach_volume_raises(self, mock_vm, mock_vbd,
                                  mock_detach):
        mock_vm.return_value = "vm_ref"
        mock_vbd.side_effect = test.TestingException

        self.assertRaises(test.TestingException,
                          self.ops.detach_volume, {}, "name", "/dev/xvdd")
        self.assertFalse(mock_detach.called)

    @mock.patch.object(volume_utils, "purge_sr")
    @mock.patch.object(vm_utils, "destroy_vbd")
    @mock.patch.object(volume_utils, "find_sr_from_vbd")
    @mock.patch.object(vm_utils, "unplug_vbd")
    @mock.patch.object(vm_utils, "is_vm_shutdown")
    def test_detach_vbds_and_srs_not_shutdown(self, mock_shutdown, mock_unplug,
            mock_find_sr, mock_destroy, mock_purge):
        mock_shutdown.return_value = False
        mock_find_sr.return_value = "sr_ref"

        self.ops._detach_vbds_and_srs("vm_ref", ["vbd_ref"])

        mock_shutdown.assert_called_once_with(self.session, "vm_ref")
        mock_find_sr.assert_called_once_with(self.session, "vbd_ref")
        mock_unplug.assert_called_once_with(self.session, "vbd_ref", "vm_ref")
        mock_destroy.assert_called_once_with(self.session, "vbd_ref")
        mock_purge.assert_called_once_with(self.session, "sr_ref")

    @mock.patch.object(volume_utils, "purge_sr")
    @mock.patch.object(vm_utils, "destroy_vbd")
    @mock.patch.object(volume_utils, "find_sr_from_vbd")
    @mock.patch.object(vm_utils, "unplug_vbd")
    @mock.patch.object(vm_utils, "is_vm_shutdown")
    def test_detach_vbds_and_srs_is_shutdown(self, mock_shutdown, mock_unplug,
            mock_find_sr, mock_destroy, mock_purge):
        mock_shutdown.return_value = True
        mock_find_sr.return_value = "sr_ref"

        self.ops._detach_vbds_and_srs("vm_ref", ["vbd_ref_1", "vbd_ref_2"])

        expected = [mock.call(self.session, "vbd_ref_1"),
                    mock.call(self.session, "vbd_ref_2")]
        self.assertEqual(expected, mock_destroy.call_args_list)
        mock_purge.assert_called_with(self.session, "sr_ref")
        self.assertFalse(mock_unplug.called)

    @mock.patch.object(volumeops.VolumeOps, "_detach_vbds_and_srs")
    @mock.patch.object(volumeops.VolumeOps, "_get_all_volume_vbd_refs")
    def test_detach_all_no_volumes(self, mock_get_all, mock_detach):
        mock_get_all.return_value = []

        self.ops.detach_all("vm_ref")

        mock_get_all.assert_called_once_with("vm_ref")
        self.assertFalse(mock_detach.called)

    @mock.patch.object(volumeops.VolumeOps, "_detach_vbds_and_srs")
    @mock.patch.object(volumeops.VolumeOps, "_get_all_volume_vbd_refs")
    def test_detach_all_volumes(self, mock_get_all, mock_detach):
        mock_get_all.return_value = ["1"]

        self.ops.detach_all("vm_ref")

        mock_get_all.assert_called_once_with("vm_ref")
        mock_detach.assert_called_once_with("vm_ref", ["1"])

    def test_get_all_volume_vbd_refs_no_vbds(self):
        with mock.patch.object(self.session.VM, "get_VBDs") as mock_get:
            with mock.patch.object(self.session.VBD,
                                   "get_other_config") as mock_conf:
                mock_get.return_value = []

                result = self.ops._get_all_volume_vbd_refs("vm_ref")

                self.assertEqual([], list(result))
                mock_get.assert_called_once_with("vm_ref")
                self.assertFalse(mock_conf.called)

    def test_get_all_volume_vbd_refs_no_volumes(self):
        with mock.patch.object(self.session.VM, "get_VBDs") as mock_get:
            with mock.patch.object(self.session.VBD,
                                   "get_other_config") as mock_conf:
                mock_get.return_value = ["1"]
                mock_conf.return_value = {}

                result = self.ops._get_all_volume_vbd_refs("vm_ref")

                self.assertEqual([], list(result))
                mock_get.assert_called_once_with("vm_ref")
                mock_conf.assert_called_once_with("1")

    def test_get_all_volume_vbd_refs_with_volumes(self):
        with mock.patch.object(self.session.VM, "get_VBDs") as mock_get:
            with mock.patch.object(self.session.VBD,
                                   "get_other_config") as mock_conf:
                mock_get.return_value = ["1", "2"]
                mock_conf.return_value = {"osvol": True}

                result = self.ops._get_all_volume_vbd_refs("vm_ref")

                self.assertEqual(["1", "2"], list(result))
                mock_get.assert_called_once_with("vm_ref")


class AttachVolumeTestCase(VolumeOpsTestBase):
    @mock.patch.object(volumeops.VolumeOps, "_attach_volume")
    @mock.patch.object(vm_utils, "vm_ref_or_raise")
    def test_attach_volume_default_hotplug(self, mock_get_vm, mock_attach):
        mock_get_vm.return_value = "vm_ref"

        self.ops.attach_volume({}, "instance_name", "/dev/xvda")

        mock_attach.assert_called_once_with({}, "vm_ref", "instance_name", 0,
                                            True)

    @mock.patch.object(volumeops.VolumeOps, "_attach_volume")
    @mock.patch.object(vm_utils, "vm_ref_or_raise")
    def test_attach_volume_hotplug(self, mock_get_vm, mock_attach):
        mock_get_vm.return_value = "vm_ref"

        self.ops.attach_volume({}, "instance_name", "/dev/xvda", False)

        mock_attach.assert_called_once_with({}, "vm_ref", "instance_name", 0,
                                            False)

    @mock.patch.object(volumeops.VolumeOps, "_attach_volume")
    def test_attach_volume_default_hotplug_connect_volume(self, mock_attach):
        self.ops.connect_volume({})
        mock_attach.assert_called_once_with({})

    @mock.patch.object(volumeops.VolumeOps, "_check_is_supported_driver_type")
    @mock.patch.object(volumeops.VolumeOps, "_connect_to_volume_provider")
    @mock.patch.object(volumeops.VolumeOps, "_connect_hypervisor_to_volume")
    @mock.patch.object(volumeops.VolumeOps, "_attach_volume_to_vm")
    def test_attach_volume_with_defaults(self, mock_attach, mock_hypervisor,
                                         mock_provider, mock_driver):
        connection_info = {"data": {}}
        with mock.patch.object(self.session.VDI, "get_uuid") as mock_vdi:
            mock_provider.return_value = ("sr_ref", "sr_uuid")
            mock_vdi.return_value = "vdi_uuid"

            result = self.ops._attach_volume(connection_info)

            self.assertEqual(result, ("sr_uuid", "vdi_uuid"))

        mock_driver.assert_called_once_with(connection_info)
        mock_provider.assert_called_once_with({}, None)
        mock_hypervisor.assert_called_once_with("sr_ref", {})
        self.assertFalse(mock_attach.called)

    @mock.patch.object(volumeops.VolumeOps, "_check_is_supported_driver_type")
    @mock.patch.object(volumeops.VolumeOps, "_connect_to_volume_provider")
    @mock.patch.object(volumeops.VolumeOps, "_connect_hypervisor_to_volume")
    @mock.patch.object(volumeops.VolumeOps, "_attach_volume_to_vm")
    def test_attach_volume_with_hot_attach(self, mock_attach, mock_hypervisor,
                                           mock_provider, mock_driver):
        connection_info = {"data": {}}
        with mock.patch.object(self.session.VDI, "get_uuid") as mock_vdi:
            mock_provider.return_value = ("sr_ref", "sr_uuid")
            mock_hypervisor.return_value = "vdi_ref"
            mock_vdi.return_value = "vdi_uuid"

            result = self.ops._attach_volume(connection_info, "vm_ref",
                        "name", 2, True)

            self.assertEqual(result, ("sr_uuid", "vdi_uuid"))

        mock_driver.assert_called_once_with(connection_info)
        mock_provider.assert_called_once_with({}, "name")
        mock_hypervisor.assert_called_once_with("sr_ref", {})
        mock_attach.assert_called_once_with("vdi_ref", "vm_ref", "name", 2,
                                            True)

    @mock.patch.object(volume_utils, "forget_sr")
    @mock.patch.object(volumeops.VolumeOps, "_check_is_supported_driver_type")
    @mock.patch.object(volumeops.VolumeOps, "_connect_to_volume_provider")
    @mock.patch.object(volumeops.VolumeOps, "_connect_hypervisor_to_volume")
    @mock.patch.object(volumeops.VolumeOps, "_attach_volume_to_vm")
    def test_attach_volume_cleanup(self, mock_attach, mock_hypervisor,
                                   mock_provider, mock_driver, mock_forget):
        connection_info = {"data": {}}
        mock_provider.return_value = ("sr_ref", "sr_uuid")
        mock_hypervisor.side_effect = test.TestingException

        self.assertRaises(test.TestingException,
                          self.ops._attach_volume, connection_info)

        mock_driver.assert_called_once_with(connection_info)
        mock_provider.assert_called_once_with({}, None)
        mock_hypervisor.assert_called_once_with("sr_ref", {})
        mock_forget.assert_called_once_with(self.session, "sr_ref")
        self.assertFalse(mock_attach.called)

    def test_check_is_supported_driver_type_pass_iscsi(self):
        conn_info = {"driver_volume_type": "iscsi"}
        self.ops._check_is_supported_driver_type(conn_info)

    def test_check_is_supported_driver_type_pass_xensm(self):
        conn_info = {"driver_volume_type": "xensm"}
        self.ops._check_is_supported_driver_type(conn_info)

    def test_check_is_supported_driver_type_pass_bad(self):
        conn_info = {"driver_volume_type": "bad"}
        self.assertRaises(exception.VolumeDriverNotFound,
                          self.ops._check_is_supported_driver_type, conn_info)

    @mock.patch.object(volume_utils, "introduce_sr")
    @mock.patch.object(volume_utils, "find_sr_by_uuid")
    @mock.patch.object(volume_utils, "parse_sr_info")
    def test_connect_to_volume_provider_new_sr(self, mock_parse, mock_find_sr,
                                               mock_introduce_sr):
        mock_parse.return_value = ("uuid", "label", "params")
        mock_find_sr.return_value = None
        mock_introduce_sr.return_value = "sr_ref"

        ref, uuid = self.ops._connect_to_volume_provider({}, "name")

        self.assertEqual("sr_ref", ref)
        self.assertEqual("uuid", uuid)
        mock_parse.assert_called_once_with({}, "Disk-for:name")
        mock_find_sr.assert_called_once_with(self.session, "uuid")
        mock_introduce_sr.assert_called_once_with(self.session, "uuid",
                                                  "label", "params")

    @mock.patch.object(volume_utils, "introduce_sr")
    @mock.patch.object(volume_utils, "find_sr_by_uuid")
    @mock.patch.object(volume_utils, "parse_sr_info")
    def test_connect_to_volume_provider_old_sr(self, mock_parse, mock_find_sr,
                                               mock_introduce_sr):
        mock_parse.return_value = ("uuid", "label", "params")
        mock_find_sr.return_value = "sr_ref"

        ref, uuid = self.ops._connect_to_volume_provider({}, "name")

        self.assertEqual("sr_ref", ref)
        self.assertEqual("uuid", uuid)
        mock_parse.assert_called_once_with({}, "Disk-for:name")
        mock_find_sr.assert_called_once_with(self.session, "uuid")
        self.assertFalse(mock_introduce_sr.called)

    @mock.patch.object(volume_utils, "introduce_vdi")
    def test_connect_hypervisor_to_volume_regular(self, mock_intro):
        mock_intro.return_value = "vdi"

        result = self.ops._connect_hypervisor_to_volume("sr", {})

        self.assertEqual("vdi", result)
        mock_intro.assert_called_once_with(self.session, "sr")

    @mock.patch.object(volume_utils, "introduce_vdi")
    def test_connect_hypervisor_to_volume_vdi(self, mock_intro):
        mock_intro.return_value = "vdi"

        conn = {"vdi_uuid": "id"}
        result = self.ops._connect_hypervisor_to_volume("sr", conn)

        self.assertEqual("vdi", result)
        mock_intro.assert_called_once_with(self.session, "sr",
                                           vdi_uuid="id")

    @mock.patch.object(volume_utils, "introduce_vdi")
    def test_connect_hypervisor_to_volume_lun(self, mock_intro):
        mock_intro.return_value = "vdi"

        conn = {"target_lun": "lun"}
        result = self.ops._connect_hypervisor_to_volume("sr", conn)

        self.assertEqual("vdi", result)
        mock_intro.assert_called_once_with(self.session, "sr",
                                           target_lun="lun")

    @mock.patch.object(volume_utils, "introduce_vdi")
    @mock.patch.object(volumeops.LOG, 'debug')
    def test_connect_hypervisor_to_volume_mask_password(self, mock_debug,
                                                        mock_intro):
        # Tests that the connection_data is scrubbed before logging.
        data = {'auth_password': 'verybadpass'}
        self.ops._connect_hypervisor_to_volume("sr", data)
        self.assertTrue(mock_debug.called, 'LOG.debug was not called')
        password_logged = False
        for call in mock_debug.call_args_list:
            # The call object is a tuple of (args, kwargs)
            if 'verybadpass' in call[0]:
                password_logged = True
                break
        self.assertFalse(password_logged, 'connection_data was not scrubbed')

    @mock.patch.object(vm_utils, "is_vm_shutdown")
    @mock.patch.object(vm_utils, "create_vbd")
    def test_attach_volume_to_vm_plug(self, mock_vbd, mock_shutdown):
        mock_vbd.return_value = "vbd"
        mock_shutdown.return_value = False

        with mock.patch.object(self.session.VBD, "plug") as mock_plug:
            self.ops._attach_volume_to_vm("vdi", "vm", "name", 2, True)
            mock_plug.assert_called_once_with("vbd", "vm")

        mock_vbd.assert_called_once_with(self.session, "vm", "vdi", 2,
                                         bootable=False, osvol=True)
        mock_shutdown.assert_called_once_with(self.session, "vm")

    @mock.patch.object(vm_utils, "is_vm_shutdown")
    @mock.patch.object(vm_utils, "create_vbd")
    def test_attach_volume_to_vm_no_plug(self, mock_vbd, mock_shutdown):
        mock_vbd.return_value = "vbd"
        mock_shutdown.return_value = True

        with mock.patch.object(self.session.VBD, "plug") as mock_plug:
            self.ops._attach_volume_to_vm("vdi", "vm", "name", 2, True)
            self.assertFalse(mock_plug.called)

        mock_vbd.assert_called_once_with(self.session, "vm", "vdi", 2,
                                         bootable=False, osvol=True)
        mock_shutdown.assert_called_once_with(self.session, "vm")

    @mock.patch.object(vm_utils, "is_vm_shutdown")
    @mock.patch.object(vm_utils, "create_vbd")
    def test_attach_volume_to_vm_no_hotplug(self, mock_vbd, mock_shutdown):
        mock_vbd.return_value = "vbd"

        with mock.patch.object(self.session.VBD, "plug") as mock_plug:
            self.ops._attach_volume_to_vm("vdi", "vm", "name", 2, False)
            self.assertFalse(mock_plug.called)

        mock_vbd.assert_called_once_with(self.session, "vm", "vdi", 2,
                                         bootable=False, osvol=True)
        self.assertFalse(mock_shutdown.called)


class FindBadVolumeTestCase(VolumeOpsTestBase):
    @mock.patch.object(volumeops.VolumeOps, "_get_all_volume_vbd_refs")
    def test_find_bad_volumes_no_vbds(self, mock_get_all):
        mock_get_all.return_value = []

        result = self.ops.find_bad_volumes("vm_ref")

        mock_get_all.assert_called_once_with("vm_ref")
        self.assertEqual([], result)

    @mock.patch.object(volume_utils, "find_sr_from_vbd")
    @mock.patch.object(volumeops.VolumeOps, "_get_all_volume_vbd_refs")
    def test_find_bad_volumes_no_bad_vbds(self, mock_get_all, mock_find_sr):
        mock_get_all.return_value = ["1", "2"]
        mock_find_sr.return_value = "sr_ref"

        with mock.patch.object(self.session.SR, "scan") as mock_scan:
            result = self.ops.find_bad_volumes("vm_ref")

            mock_get_all.assert_called_once_with("vm_ref")
            expected_find = [mock.call(self.session, "1"),
                             mock.call(self.session, "2")]
            self.assertEqual(expected_find, mock_find_sr.call_args_list)
            expected_scan = [mock.call("sr_ref"), mock.call("sr_ref")]
            self.assertEqual(expected_scan, mock_scan.call_args_list)
            self.assertEqual([], result)

    @mock.patch.object(volume_utils, "find_sr_from_vbd")
    @mock.patch.object(volumeops.VolumeOps, "_get_all_volume_vbd_refs")
    def test_find_bad_volumes_bad_vbds(self, mock_get_all, mock_find_sr):
        mock_get_all.return_value = ["vbd_ref"]
        mock_find_sr.return_value = "sr_ref"

        class FakeException(Exception):
            details = ['SR_BACKEND_FAILURE_40', "", "", ""]