# Copyright (c) 2014 Rackspace Hosting
# 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 mock
from oslo_utils import timeutils
from six.moves import range

from nova import exception
from nova import objects
from nova.scheduler import caching_scheduler
from nova.scheduler import host_manager
from nova.tests.unit.scheduler import test_scheduler
from nova.tests import uuidsentinel as uuids

ENABLE_PROFILER = False


class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase):
    """Test case for Caching Scheduler."""

    driver_cls = caching_scheduler.CachingScheduler

    @mock.patch.object(caching_scheduler.CachingScheduler,
                       "_get_up_hosts")
    def test_run_periodic_tasks_loads_hosts(self, mock_up_hosts):
        mock_up_hosts.return_value = []
        context = mock.Mock()

        self.driver.run_periodic_tasks(context)

        self.assertTrue(mock_up_hosts.called)
        self.assertEqual([], self.driver.all_host_states)
        context.elevated.assert_called_with()

    @mock.patch.object(caching_scheduler.CachingScheduler,
                       "_get_up_hosts")
    def test_get_all_host_states_returns_cached_value(self, mock_up_hosts):
        self.driver.all_host_states = {uuids.cell: []}

        self.driver._get_all_host_states(self.context, None,
            mock.sentinel.provider_uuids)

        self.assertFalse(mock_up_hosts.called)
        self.assertEqual({uuids.cell: []}, self.driver.all_host_states)

    @mock.patch.object(caching_scheduler.CachingScheduler,
                       "_get_up_hosts")
    def test_get_all_host_states_loads_hosts(self, mock_up_hosts):
        host_state = self._get_fake_host_state()
        mock_up_hosts.return_value = {uuids.cell: [host_state]}

        result = self.driver._get_all_host_states(self.context, None,
            mock.sentinel.provider_uuids)

        self.assertTrue(mock_up_hosts.called)
        self.assertEqual({uuids.cell: [host_state]},
                         self.driver.all_host_states)
        self.assertEqual([host_state], list(result))

    def test_get_up_hosts(self):
        with mock.patch.object(self.driver.host_manager,
                               "get_all_host_states") as mock_get_hosts:
            host_state = self._get_fake_host_state()
            mock_get_hosts.return_value = [host_state]

            result = self.driver._get_up_hosts(self.context)

            self.assertTrue(mock_get_hosts.called)
            self.assertEqual({uuids.cell: [host_state]}, result)

    def test_select_destination_raises_with_no_hosts(self):
        spec_obj = self._get_fake_request_spec()
        self.driver.all_host_states = {uuids.cell: []}

        self.assertRaises(exception.NoValidHost,
                self.driver.select_destinations,
                self.context, spec_obj, [spec_obj.instance_uuid],
                {}, {})

    @mock.patch('nova.db.instance_extra_get_by_instance_uuid',
                return_value={'numa_topology': None,
                              'pci_requests': None})
    def test_select_destination_works(self, mock_get_extra):
        spec_obj = self._get_fake_request_spec()
        fake_host = self._get_fake_host_state()
        self.driver.all_host_states = {uuids.cell: [fake_host]}

        result = self._test_select_destinations(spec_obj)

        self.assertEqual(1, len(result))
        self.assertEqual(result[0][0].service_host, fake_host.host)

    def _test_select_destinations(self, spec_obj):
        provider_summaries = {}
        for cell_hosts in self.driver.all_host_states.values():
            for hs in cell_hosts:
                provider_summaries[hs.uuid] = hs

        return self.driver.select_destinations(
                self.context, spec_obj, [spec_obj.instance_uuid], {},
                provider_summaries)

    def _get_fake_request_spec(self):
        # NOTE(sbauza): Prevent to stub the Flavor.get_by_id call just by
        # directly providing a Flavor object
        flavor = objects.Flavor(
            flavorid="small",
            memory_mb=512,
            root_gb=1,
            ephemeral_gb=1,
            vcpus=1,
            swap=0,
        )
        instance_properties = {
            "os_type": "linux",
            "project_id": "1234",
        }
        request_spec = objects.RequestSpec(
            flavor=flavor,
            num_instances=1,
            ignore_hosts=None,
            force_hosts=None,
            force_nodes=None,
            retry=None,
            availability_zone=None,
            image=None,
            instance_group=None,
            pci_requests=None,
            numa_topology=None,
            instance_uuid='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
            **instance_properties
        )
        return request_spec

    def _get_fake_host_state(self, index=0):
        host_state = host_manager.HostState(
            'host_%s' % index,
            'node_%s' % index,
            uuids.cell)
        host_state.uuid = getattr(uuids, 'host_%s' % index)
        host_state.free_ram_mb = 50000
        host_state.total_usable_ram_mb = 50000
        host_state.free_disk_mb = 4096
        host_state.total_usable_disk_gb = 4
        host_state.service = {
            "disabled": False,
            "updated_at": timeutils.utcnow(),
            "created_at": timeutils.utcnow(),
        }
        host_state.cpu_allocation_ratio = 16.0
        host_state.ram_allocation_ratio = 1.5
        host_state.disk_allocation_ratio = 1.0
        host_state.metrics = objects.MonitorMetricList(objects=[])
        return host_state

    @mock.patch('nova.db.instance_extra_get_by_instance_uuid',
                return_value={'numa_topology': None,
                              'pci_requests': None})
    def test_performance_check_select_destination(self, mock_get_extra):
        hosts = 2
        requests = 1

        self.flags(service_down_time=240)

        spec_obj = self._get_fake_request_spec()
        host_states = []
        for x in range(hosts):
            host_state = self._get_fake_host_state(x)
            host_states.append(host_state)
        self.driver.all_host_states = {uuids.cell: host_states}
        provider_summaries = {hs.uuid: hs for hs in host_states}

        def run_test():
            a = timeutils.utcnow()

            for x in range(requests):
                self.driver.select_destinations(self.context, spec_obj,
                        [spec_obj.instance_uuid], {}, provider_summaries)

            b = timeutils.utcnow()
            c = b - a

            seconds = (c.days * 24 * 60 * 60 + c.seconds)
            microseconds = seconds * 1000 + c.microseconds / 1000.0
            per_request_ms = microseconds / requests
            return per_request_ms

        per_request_ms = None
        if ENABLE_PROFILER:
            import pycallgraph
            from pycallgraph import output
            config = pycallgraph.Config(max_depth=10)
            config.trace_filter = pycallgraph.GlobbingFilter(exclude=[
                'pycallgraph.*',
                'unittest.*',
                'testtools.*',
                'nova.tests.unit.*',
            ])
            graphviz = output.GraphvizOutput(output_file='scheduler.png')

            with pycallgraph.PyCallGraph(output=graphviz):
                per_request_ms = run_test()

        else:
            per_request_ms = run_test()

        # This has proved to be around 1 ms on a random dev box
        # But this is here so you can do simply performance testing easily.
        self.assertLess(per_request_ms, 1000)

    def test_request_single_cell(self):
        spec_obj = self._get_fake_request_spec()
        spec_obj.requested_destination = objects.Destination(
            cell=objects.CellMapping(uuid=uuids.cell2))
        host_states_cell1 = [self._get_fake_host_state(i)
                             for i in range(1, 5)]
        host_states_cell2 = [self._get_fake_host_state(i)
                             for i in range(5, 10)]

        self.driver.all_host_states = {
            uuids.cell1: host_states_cell1,
            uuids.cell2: host_states_cell2,
        }
        provider_summaries = {
            cn.uuid: cn for cn in host_states_cell1 + host_states_cell2
        }

        d = self.driver.select_destinations(self.context, spec_obj,
                [spec_obj.instance_uuid], {}, provider_summaries)
        self.assertIn(d[0][0].service_host,
                [hs.host for hs in host_states_cell2])

    @mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
    @mock.patch("nova.scheduler.caching_scheduler.CachingScheduler."
                "_get_sorted_hosts")
    @mock.patch("nova.scheduler.caching_scheduler.CachingScheduler."
                "_get_all_host_states")
    def test_alternates_same_cell(self, mock_get_all_hosts, mock_sorted,
            mock_consume):
        """Tests getting hosts plus alternates where the hosts are spread
        across two cells.
        """
        all_host_states = []
        for num in range(10):
            host_name = "host%s" % num
            cell_uuid = uuids.cell1 if num % 2 else uuids.cell2
            hs = host_manager.HostState(host_name, "node%s" % num,
                    cell_uuid)
            hs.uuid = getattr(uuids, host_name)
            all_host_states.append(hs)

        mock_get_all_hosts.return_value = all_host_states
        # There are two instances, so _get_sorted_hosts will be called once
        # per instance, and then once again before picking alternates.
        mock_sorted.side_effect = [all_host_states,
                                   list(reversed(all_host_states)),
                                   all_host_states]
        total_returned = 3
        self.flags(max_attempts=total_returned, group="scheduler")
        instance_uuids = [uuids.inst1, uuids.inst2]
        num_instances = len(instance_uuids)

        spec_obj = objects.RequestSpec(
                num_instances=num_instances,
                flavor=objects.Flavor(memory_mb=512,
                                      root_gb=512,
                                      ephemeral_gb=0,
                                      swap=0,
                                      vcpus=1),
                project_id=uuids.project_id,
                instance_group=None)

        dests = self.driver._schedule(self.context, spec_obj,
                instance_uuids, None, None, return_alternates=True)
        # There should be max_attempts hosts per instance (1 selected, 2 alts)
        self.assertEqual(total_returned, len(dests[0]))
        self.assertEqual(total_returned, len(dests[1]))
        # Verify that the two selected hosts are not in the same cell.
        self.assertNotEqual(dests[0][0].cell_uuid, dests[1][0].cell_uuid)
        for dest in dests:
            selected_host = dest[0]
            selected_cell_uuid = selected_host.cell_uuid
            for alternate in dest[1:]:
                self.assertEqual(alternate.cell_uuid, selected_cell_uuid)


if __name__ == '__main__':
    # A handy tool to help profile the schedulers performance
    ENABLE_PROFILER = True
    import testtools
    suite = testtools.ConcurrentTestSuite()
    test = "test_performance_check_select_destination"
    test_case = CachingSchedulerTestCase(test)
    suite.addTest(test_case)
    runner = testtools.TextTestResult.TextTestRunner()
    runner.run(suite)
