# Copyright (c) 2014 VMware, 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.

"""
Unit tests for datastore module.
"""

import mock
from oslo_utils import units
from oslo_vmware import exceptions

from cinder import test
from cinder.volume.drivers.vmware import datastore as ds_sel
from cinder.volume.drivers.vmware import exceptions as vmdk_exceptions


class DatastoreTest(test.TestCase):
    """Unit tests for Datastore."""

    def setUp(self):
        super(DatastoreTest, self).setUp()
        self._session = mock.Mock()
        self._vops = mock.Mock()
        self._ds_sel = ds_sel.DatastoreSelector(self._vops, self._session)

    @mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
    def test_get_profile_id(self, get_profile_id_by_name):
        profile_id = mock.sentinel.profile_id
        get_profile_id_by_name.return_value = profile_id
        profile_name = mock.sentinel.profile_name

        self.assertEqual(profile_id, self._ds_sel.get_profile_id(profile_name))
        get_profile_id_by_name.assert_called_once_with(self._session,
                                                       profile_name)

    @mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
    def test_get_profile_id_with_invalid_profile(self, get_profile_id_by_name):
        get_profile_id_by_name.return_value = None
        profile_name = mock.sentinel.profile_name

        self.assertRaises(vmdk_exceptions.ProfileNotFoundException,
                          self._ds_sel.get_profile_id,
                          profile_name)
        get_profile_id_by_name.assert_called_once_with(self._session,
                                                       profile_name)

    def _create_datastore(self, moref):
        return mock.Mock(value=moref)

    def _create_summary(
            self, ds, free_space=units.Mi, _type=ds_sel.DatastoreType.VMFS,
            capacity=2 * units.Mi):
        return mock.Mock(datastore=ds, freeSpace=free_space, type=_type,
                         capacity=capacity)

    def _create_host(self, value):
        host = mock.Mock(spec=['_type', 'value'])
        host._type = 'HostSystem'
        host.value = value
        return host

    def test_filter_datastores_with_unsupported_type(self):
        ds_1 = self._create_datastore('ds-1')
        ds_2 = self._create_datastore('ds-2')
        datastores = [ds_1, ds_2]

        self._vops.get_summary.side_effect = [
            self._create_summary(ds_1),
            self._create_summary(ds_2, _type='foo')]

        res = self._ds_sel._filter_datastores(
            datastores, units.Ki, None, None, None)

        self.assertEqual(1, len(res))
        self.assertEqual(ds_1, res[0].datastore)

    @mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
                '_filter_by_profile')
    def test_filter_datastores(self, filter_by_profile):
        # Test with empty datastore list.
        datastores = []
        size_bytes = 2 * units.Mi
        profile_id = mock.sentinel.profile_id
        hard_anti_affinity_datastores = None
        hard_affinity_ds_types = None

        self.assertEqual([], self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types))

        # Test with single datastore with hard anti-affinity.
        ds_1 = self._create_datastore('ds-1')
        datastores = [ds_1]
        hard_anti_affinity_datastores = [ds_1.value]

        self.assertEqual([], self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types))

        # Extend previous case with a profile non-compliant datastore.
        ds_2 = self._create_datastore('ds-2')
        datastores.append(ds_2)
        filter_by_profile.return_value = []

        self.assertEqual([], self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types))
        filter_by_profile.assert_called_once_with([ds_2], profile_id)

        # Extend previous case with a less free space datastore.
        ds_3 = self._create_datastore('ds-3')
        datastores.append(ds_3)
        filter_by_profile.return_value = [ds_3]

        free_space_list = [units.Mi]
        type_list = [ds_sel.DatastoreType.NFS]
        self._vops.get_summary.side_effect = (
            lambda ds: self._create_summary(ds,
                                            free_space_list.pop(0),
                                            type_list.pop(0)))

        self.assertEqual([], self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types))

        # Extend previous case with a datastore not satisfying hard affinity
        # datastore type requirement.
        ds_4 = self._create_datastore('ds-4')
        datastores.append(ds_4)
        filter_by_profile.return_value = [ds_3, ds_4]

        free_space_list = [units.Mi, 4 * units.Mi]
        type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN]
        hard_affinity_ds_types = [ds_sel.DatastoreType.NFS]

        self.assertEqual([], self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types))

        # Modify the previous case to remove hard affinity datastore type
        # requirement.
        free_space_list = [units.Mi, 4 * units.Mi]
        type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN]
        hard_affinity_ds_types = None

        res = self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types)
        self.assertTrue(len(res) == 1)
        self.assertEqual(ds_4, res[0].datastore)

        # Extend the previous case by adding a datastore satisfying
        # hard affinity datastore type requirement.
        ds_5 = self._create_datastore('ds-5')
        datastores.append(ds_5)
        filter_by_profile.return_value = [ds_3, ds_4, ds_5]

        free_space_list = [units.Mi, 4 * units.Mi, 5 * units.Mi]
        type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN,
                     ds_sel.DatastoreType.VMFS]
        hard_affinity_ds_types = [ds_sel.DatastoreType.VMFS]

        res = self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types)
        self.assertTrue(len(res) == 1)
        self.assertEqual(ds_5, res[0].datastore)

        # Modify the previous case to have two datastores satisfying
        # hard affinity datastore type requirement.
        free_space_list = [units.Mi, 4 * units.Mi, 5 * units.Mi]
        type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN,
                     ds_sel.DatastoreType.VSAN]
        hard_affinity_ds_types = [ds_sel.DatastoreType.VSAN]

        res = self._ds_sel._filter_datastores(
            datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
            hard_affinity_ds_types)
        self.assertTrue(len(res) == 2)
        self.assertEqual(ds_4, res[0].datastore)
        self.assertEqual(ds_5, res[1].datastore)

        # Clear side effects.
        self._vops.get_summary.side_effect = None

    def test_select_best_summary(self):
        # No tie-- all datastores with different host mount count.
        summary_1 = self._create_summary(mock.sentinel.ds_1,
                                         free_space=units.Mi,
                                         capacity=2 * units.Mi)
        summary_2 = self._create_summary(mock.sentinel.ds_2,
                                         free_space=units.Mi,
                                         capacity=3 * units.Mi)
        summary_3 = self._create_summary(mock.sentinel.ds_3,
                                         free_space=units.Mi,
                                         capacity=4 * units.Mi)

        host_1 = self._create_host('host-1')
        host_2 = self._create_host('host-2')
        host_3 = self._create_host('host-3')

        connected_hosts = {mock.sentinel.ds_1: [host_1.value],
                           mock.sentinel.ds_2: [host_1.value, host_2.value],
                           mock.sentinel.ds_3: [host_1.value, host_2.value,
                                                host_3.value]}
        self._vops.get_connected_hosts.side_effect = (
            lambda summary: connected_hosts[summary])

        summaries = [summary_1, summary_2, summary_3]
        (best_summary, best_utilization) = self._ds_sel._select_best_summary(
            summaries)

        self.assertEqual(summary_3, best_summary)
        self.assertEqual(3 / 4.0, best_utilization)

        # Tie-- two datastores with max host mount count.
        summary_4 = self._create_summary(mock.sentinel.ds_4,
                                         free_space=2 * units.Mi,
                                         capacity=4 * units.Mi)
        connected_hosts[mock.sentinel.ds_4] = (
            connected_hosts[mock.sentinel.ds_3])
        summaries.append(summary_4)
        (best_summary, best_utilization) = self._ds_sel._select_best_summary(
            summaries)

        self.assertEqual(summary_4, best_summary)
        self.assertEqual(1 / 2.0, best_utilization)

        # Clear side effects.
        self._vops.get_connected_hosts.side_effect = None

    @mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
                'get_profile_id')
    @mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
                '_filter_datastores')
    def test_select_datastore(self, filter_datastores, get_profile_id):
        # Test with no hosts.
        size_bytes = units.Ki
        req = {self._ds_sel.SIZE_BYTES: size_bytes}
        self._vops.get_hosts.return_value = mock.Mock(objects=[])

        self.assertEqual((), self._ds_sel.select_datastore(req))
        self._vops.get_hosts.assert_called_once_with()

        # Test with single host with no valid datastores.
        host_1 = self._create_host('host-1')
        self._vops.get_hosts.return_value = mock.Mock(
            objects=[mock.Mock(obj=host_1)])
        self._vops.continue_retrieval.return_value = None
        self._vops.get_dss_rp.side_effect = exceptions.VimException('error')

        self.assertEqual((), self._ds_sel.select_datastore(req))
        self._vops.get_dss_rp.assert_called_once_with(host_1)

        # Test with three hosts and vCenter connection problem while fetching
        # datastores for the second host.
        self._vops.get_dss_rp.reset_mock()
        host_2 = self._create_host('host-2')
        host_3 = self._create_host('host-3')
        self._vops.get_hosts.return_value = mock.Mock(
            objects=[mock.Mock(obj=host_1),
                     mock.Mock(obj=host_2),
                     mock.Mock(obj=host_3)])
        self._vops.get_dss_rp.side_effect = [
            exceptions.VimException('no valid datastores'),
            exceptions.VimConnectionException('connection error')]

        self.assertRaises(exceptions.VimConnectionException,
                          self._ds_sel.select_datastore,
                          req)
        get_dss_rp_exp_calls = [mock.call(host_1), mock.call(host_2)]
        self.assertEqual(get_dss_rp_exp_calls,
                         self._vops.get_dss_rp.call_args_list)

        # Modify previous case to return datastores for second and third host,
        # where none of them meet the requirements which include a storage
        # profile and affinity requirements.
        aff_ds_types = [ds_sel.DatastoreType.VMFS]
        req[ds_sel.DatastoreSelector.HARD_AFFINITY_DS_TYPE] = aff_ds_types

        ds_1a = mock.sentinel.ds_1a
        anti_affinity_ds = [ds_1a]
        req[ds_sel.DatastoreSelector.HARD_ANTI_AFFINITY_DS] = anti_affinity_ds

        profile_name = mock.sentinel.profile_name
        req[ds_sel.DatastoreSelector.PROFILE_NAME] = profile_name

        profile_id = mock.sentinel.profile_id
        get_profile_id.return_value = profile_id

        ds_2a = mock.sentinel.ds_2a
        ds_2b = mock.sentinel.ds_2b
        ds_3a = mock.sentinel.ds_3a

        self._vops.get_dss_rp.reset_mock()
        rp_2 = mock.sentinel.rp_2
        rp_3 = mock.sentinel.rp_3
        self._vops.get_dss_rp.side_effect = [
            exceptions.VimException('no valid datastores'),
            ([ds_2a, ds_2b], rp_2),
            ([ds_3a], rp_3)]

        filter_datastores.return_value = []

        self.assertEqual((), self._ds_sel.select_datastore(req))
        get_profile_id.assert_called_once_with(profile_name)
        get_dss_rp_exp_calls.append(mock.call(host_3))
        self.assertEqual(get_dss_rp_exp_calls,
                         self._vops.get_dss_rp.call_args_list)
        filter_datastores_exp_calls = [
            mock.call([ds_2a, ds_2b], size_bytes, profile_id, anti_affinity_ds,
                      aff_ds_types),
            mock.call([ds_3a], size_bytes, profile_id, anti_affinity_ds,
                      aff_ds_types)]
        self.assertEqual(filter_datastores_exp_calls,
                         filter_datastores.call_args_list)

        # Modify previous case to have a non-empty summary list after filtering
        # with preferred utilization threshold unset.
        self._vops.get_dss_rp.side_effect = [
            exceptions.VimException('no valid datastores'),
            ([ds_2a, ds_2b], rp_2),
            ([ds_3a], rp_3)]

        summary_2b = self._create_summary(ds_2b, free_space=0.5 * units.Mi,
                                          capacity=units.Mi)
        filter_datastores.side_effect = [[summary_2b]]
        self._vops.get_connected_hosts.return_value = [host_1]

        self.assertEqual((host_2, rp_2, summary_2b),
                         self._ds_sel.select_datastore(req))

        # Modify previous case to have a preferred utilization threshold
        # satsified by one datastore.
        self._vops.get_dss_rp.side_effect = [
            exceptions.VimException('no valid datastores'),
            ([ds_2a, ds_2b], rp_2),
            ([ds_3a], rp_3)]

        req[ds_sel.DatastoreSelector.PREF_UTIL_THRESH] = 0.4
        summary_3a = self._create_summary(ds_3a, free_space=0.7 * units.Mi,
                                          capacity=units.Mi)
        filter_datastores.side_effect = [[summary_2b], [summary_3a]]

        self.assertEqual((host_3, rp_3, summary_3a),
                         self._ds_sel.select_datastore(req))

        # Modify previous case to have a preferred utilization threshold
        # which cannot be satisfied.
        self._vops.get_dss_rp.side_effect = [
            exceptions.VimException('no valid datastores'),
            ([ds_2a, ds_2b], rp_2),
            ([ds_3a], rp_3)]
        filter_datastores.side_effect = [[summary_2b], [summary_3a]]

        req[ds_sel.DatastoreSelector.PREF_UTIL_THRESH] = 0.2
        summary_2b.freeSpace = 0.75 * units.Mi

        self.assertEqual((host_2, rp_2, summary_2b),
                         self._ds_sel.select_datastore(req))

        # Clear side effects.
        self._vops.get_dss_rp.side_effect = None

    @mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
                '_filter_datastores')
    def test_select_datastore_with_single_host(self, filter_datastores):
        host = self._create_host('host-1')
        req = {self._ds_sel.SIZE_BYTES: units.Gi}

        ds = mock.sentinel.ds
        rp = mock.sentinel.rp
        self._vops.get_dss_rp.return_value = ([ds], rp)

        summary = self._create_summary(ds, free_space=2 * units.Gi,
                                       capacity=3 * units.Gi)
        filter_datastores.return_value = [summary]
        self._vops.get_connected_hosts.return_value = [host.value]

        self.assertEqual((host, rp, summary),
                         self._ds_sel.select_datastore(req, [host]))

        # reset mocks
        self._vops.get_dss_rp.reset_mock()
        self._vops.get_dss_rp.return_value = None
        self._vops.get_connected_hosts.reset_mock()
        self._vops.get_connected_hosts.return_value = None

    def test_select_datastore_with_empty_host_list(self):
        size_bytes = units.Ki
        req = {self._ds_sel.SIZE_BYTES: size_bytes}
        self._vops.get_hosts.return_value = mock.Mock(objects=[])

        self.assertEqual((), self._ds_sel.select_datastore(req, hosts=[]))
        self._vops.get_hosts.assert_called_once_with()

    @mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
    @mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
                '_filter_by_profile')
    def test_is_datastore_compliant(self, filter_by_profile,
                                    get_profile_id_by_name):
        # Test with empty profile.
        profile_name = None
        datastore = mock.sentinel.datastore
        self.assertTrue(self._ds_sel.is_datastore_compliant(datastore,
                                                            profile_name))

        # Test with invalid profile.
        profile_name = mock.sentinel.profile_name
        get_profile_id_by_name.return_value = None
        self.assertRaises(vmdk_exceptions.ProfileNotFoundException,
                          self._ds_sel.is_datastore_compliant,
                          datastore,
                          profile_name)
        get_profile_id_by_name.assert_called_once_with(self._session,
                                                       profile_name)

        # Test with valid profile and non-compliant datastore.
        get_profile_id_by_name.reset_mock()
        profile_id = mock.sentinel.profile_id
        get_profile_id_by_name.return_value = profile_id
        filter_by_profile.return_value = []
        self.assertFalse(self._ds_sel.is_datastore_compliant(datastore,
                                                             profile_name))
        get_profile_id_by_name.assert_called_once_with(self._session,
                                                       profile_name)
        filter_by_profile.assert_called_once_with([datastore], profile_id)

        # Test with valid profile and compliant datastore.
        get_profile_id_by_name.reset_mock()
        filter_by_profile.reset_mock()
        filter_by_profile.return_value = [datastore]
        self.assertTrue(self._ds_sel.is_datastore_compliant(datastore,
                                                            profile_name))
        get_profile_id_by_name.assert_called_once_with(self._session,
                                                       profile_name)
        filter_by_profile.assert_called_once_with([datastore], profile_id)

    def test_get_all_hosts(self):
        host_1 = self._create_host('host-1')
        host_2 = self._create_host('host-2')
        hosts = mock.Mock(objects=[mock.Mock(obj=host_1),
                                   mock.Mock(obj=host_2)])

        self._vops.get_hosts.return_value = hosts
        self._vops.continue_retrieval.return_value = None
        # host_1 is usable and host_2 is not usable
        self._vops.is_host_usable.side_effect = [True, False]

        ret = self._ds_sel._get_all_hosts()

        self.assertEqual([host_1], ret)
        self._vops.get_hosts.assert_called_once_with()
        self._vops.continue_retrieval.assert_called_once_with(hosts)
        exp_calls = [mock.call(host_1), mock.call(host_2)]
        self.assertEqual(exp_calls, self._vops.is_host_usable.call_args_list)
