# Copyright 2011 OpenStack LLC.
# 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.
"""
Tests For Zone Aware Scheduler.
"""

import json

import nova.db

from nova import exception
from nova import test
from nova.scheduler import driver
from nova.scheduler import zone_aware_scheduler
from nova.scheduler import zone_manager


def _host_caps(multiplier):
    # Returns host capabilities in the following way:
    # host1 = memory:free 10 (100max)
    #         disk:available 100 (1000max)
    # hostN = memory:free 10 + 10N
    #         disk:available 100 + 100N
    # in other words: hostN has more resources than host0
    # which means ... don't go above 10 hosts.
    return {'host_name-description': 'XenServer %s' % multiplier,
            'host_hostname': 'xs-%s' % multiplier,
            'host_memory_total': 100,
            'host_memory_overhead': 10,
            'host_memory_free': 10 + multiplier * 10,
            'host_memory_free-computed': 10 + multiplier * 10,
            'host_other-config': {},
            'host_ip_address': '192.168.1.%d' % (100 + multiplier),
            'host_cpu_info': {},
            'disk_available': 100 + multiplier * 100,
            'disk_total': 1000,
            'disk_used': 0,
            'host_uuid': 'xxx-%d' % multiplier,
            'host_name-label': 'xs-%s' % multiplier}


def fake_zone_manager_service_states(num_hosts):
    states = {}
    for x in xrange(num_hosts):
        states['host%02d' % (x + 1)] = {'compute': _host_caps(x)}
    return states


class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler):
    # No need to stub anything at the moment
    pass


class FakeZoneManager(zone_manager.ZoneManager):
    def __init__(self):
        self.service_states = {
            'host1': {
                'compute': {'host_memory_free': 1073741824},
            },
            'host2': {
                'compute': {'host_memory_free': 2147483648},
            },
            'host3': {
                'compute': {'host_memory_free': 3221225472},
            },
        }


class FakeEmptyZoneManager(zone_manager.ZoneManager):
    def __init__(self):
        self.service_states = {}


def fake_empty_call_zone_method(context, method, specs, zones):
    return []


# Hmm, I should probably be using mox for this.
was_called = False


def fake_provision_resource(context, item, instance_id, request_spec, kwargs):
    global was_called
    was_called = True


def fake_ask_child_zone_to_create_instance(context, zone_info,
                                           request_spec, kwargs):
    global was_called
    was_called = True


def fake_provision_resource_locally(context, build_plan, request_spec, kwargs):
    global was_called
    was_called = True


def fake_provision_resource_from_blob(context, item, instance_id,
                                      request_spec, kwargs):
    global was_called
    was_called = True


def fake_decrypt_blob_returns_local_info(blob):
    return {'foo': True}  # values aren't important.


def fake_decrypt_blob_returns_child_info(blob):
    return {'child_zone': True,
            'child_blob': True}  # values aren't important. Keys are.


def fake_call_zone_method(context, method, specs, zones):
    return [
        (1, [
            dict(weight=1, blob='AAAAAAA'),
            dict(weight=111, blob='BBBBBBB'),
            dict(weight=112, blob='CCCCCCC'),
            dict(weight=113, blob='DDDDDDD'),
        ]),
        (2, [
            dict(weight=120, blob='EEEEEEE'),
            dict(weight=2, blob='FFFFFFF'),
            dict(weight=122, blob='GGGGGGG'),
            dict(weight=123, blob='HHHHHHH'),
        ]),
        (3, [
            dict(weight=130, blob='IIIIIII'),
            dict(weight=131, blob='JJJJJJJ'),
            dict(weight=132, blob='KKKKKKK'),
            dict(weight=3, blob='LLLLLLL'),
        ]),
    ]


def fake_zone_get_all(context):
    return [
        dict(id=1, api_url='zone1',
             username='admin', password='password',
             weight_offset=0.0, weight_scale=1.0),
        dict(id=2, api_url='zone2',
             username='admin', password='password',
             weight_offset=1000.0, weight_scale=1.0),
        dict(id=3, api_url='zone3',
             username='admin', password='password',
             weight_offset=0.0, weight_scale=1000.0),
    ]


class ZoneAwareSchedulerTestCase(test.TestCase):
    """Test case for Zone Aware Scheduler."""

    def test_zone_aware_scheduler(self):
        """
        Create a nested set of FakeZones, try to build multiple instances
        and ensure that a select call returns the appropriate build plan.
        """
        sched = FakeZoneAwareScheduler()
        self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
        self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)

        zm = FakeZoneManager()
        sched.set_zone_manager(zm)

        fake_context = {}
        build_plan = sched.select(fake_context,
                {'instance_type': {'memory_mb': 512},
                    'num_instances': 4})

        # 4 from local zones, 12 from remotes
        self.assertEqual(16, len(build_plan))

        hostnames = [plan_item['hostname']
                     for plan_item in build_plan if 'hostname' in plan_item]
        # 4 local hosts
        self.assertEqual(4, len(hostnames))

    def test_adjust_child_weights(self):
        """Make sure the weights returned by child zones are
        properly adjusted based on the scale/offset in the zone
        db entries.
        """
        sched = FakeZoneAwareScheduler()
        child_results = fake_call_zone_method(None, None, None, None)
        zones = fake_zone_get_all(None)
        sched._adjust_child_weights(child_results, zones)
        scaled = [130000, 131000, 132000, 3000]
        for zone, results in child_results:
            for item in results:
                w = item['weight']
                if zone == 'zone1':  # No change
                    self.assertTrue(w < 1000.0)
                if zone == 'zone2':  # Offset +1000
                    self.assertTrue(w >= 1000.0 and w < 2000)
                if zone == 'zone3':  # Scale x1000
                    self.assertEqual(scaled.pop(0), w)

    def test_empty_zone_aware_scheduler(self):
        """
        Ensure empty hosts & child_zones result in NoValidHosts exception.
        """
        sched = FakeZoneAwareScheduler()
        self.stubs.Set(sched, '_call_zone_method', fake_empty_call_zone_method)
        self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)

        zm = FakeEmptyZoneManager()
        sched.set_zone_manager(zm)

        fake_context = {}
        self.assertRaises(driver.NoValidHost, sched.schedule_run_instance,
                          fake_context, 1,
                          dict(host_filter=None, instance_type={}))

    def test_schedule_do_not_schedule_with_hint(self):
        """
        Check the local/child zone routing in the run_instance() call.
        If the zone_blob hint was passed in, don't re-schedule.
        """
        global was_called
        sched = FakeZoneAwareScheduler()
        was_called = False
        self.stubs.Set(sched, '_provision_resource', fake_provision_resource)
        request_spec = {
                'instance_properties': {},
                'instance_type': {},
                'filter_driver': 'nova.scheduler.host_filter.AllHostsFilter',
                'blob': "Non-None blob data",
            }

        result = sched.schedule_run_instance(None, 1, request_spec)
        self.assertEquals(None, result)
        self.assertTrue(was_called)

    def test_provision_resource_local(self):
        """Provision a resource locally or remotely."""
        global was_called
        sched = FakeZoneAwareScheduler()
        was_called = False
        self.stubs.Set(sched, '_provision_resource_locally',
                       fake_provision_resource_locally)

        request_spec = {'hostname': "foo"}
        sched._provision_resource(None, request_spec, 1, request_spec, {})
        self.assertTrue(was_called)

    def test_provision_resource_remote(self):
        """Provision a resource locally or remotely."""
        global was_called
        sched = FakeZoneAwareScheduler()
        was_called = False
        self.stubs.Set(sched, '_provision_resource_from_blob',
                       fake_provision_resource_from_blob)

        request_spec = {}
        sched._provision_resource(None, request_spec, 1, request_spec, {})
        self.assertTrue(was_called)

    def test_provision_resource_from_blob_empty(self):
        """Provision a resource locally or remotely given no hints."""
        global was_called
        sched = FakeZoneAwareScheduler()
        request_spec = {}
        self.assertRaises(zone_aware_scheduler.InvalidBlob,
                          sched._provision_resource_from_blob,
                          None, {}, 1, {}, {})

    def test_provision_resource_from_blob_with_local_blob(self):
        """
        Provision a resource locally or remotely when blob hint passed in.
        """
        global was_called
        sched = FakeZoneAwareScheduler()
        was_called = False
        self.stubs.Set(sched, '_decrypt_blob',
                       fake_decrypt_blob_returns_local_info)
        self.stubs.Set(sched, '_provision_resource_locally',
                       fake_provision_resource_locally)

        request_spec = {'blob': "Non-None blob data"}

        sched._provision_resource_from_blob(None, request_spec, 1,
                                            request_spec, {})
        self.assertTrue(was_called)

    def test_provision_resource_from_blob_with_child_blob(self):
        """
        Provision a resource locally or remotely when child blob hint
        passed in.
        """
        global was_called
        sched = FakeZoneAwareScheduler()
        self.stubs.Set(sched, '_decrypt_blob',
                       fake_decrypt_blob_returns_child_info)
        was_called = False
        self.stubs.Set(sched, '_ask_child_zone_to_create_instance',
                       fake_ask_child_zone_to_create_instance)

        request_spec = {'blob': "Non-None blob data"}

        sched._provision_resource_from_blob(None, request_spec, 1,
                                            request_spec, {})
        self.assertTrue(was_called)

    def test_provision_resource_from_blob_with_immediate_child_blob(self):
        """
        Provision a resource locally or remotely when blob hint passed in
        from an immediate child.
        """
        global was_called
        sched = FakeZoneAwareScheduler()
        was_called = False
        self.stubs.Set(sched, '_ask_child_zone_to_create_instance',
                       fake_ask_child_zone_to_create_instance)

        request_spec = {'child_blob': True, 'child_zone': True}

        sched._provision_resource_from_blob(None, request_spec, 1,
                                            request_spec, {})
        self.assertTrue(was_called)

    def test_decrypt_blob(self):
        """Test that the decrypt method works."""

        fixture = FakeZoneAwareScheduler()
        test_data = {"foo": "bar"}

        class StubDecryptor(object):
            def decryptor(self, key):
                return lambda blob: blob

        self.stubs.Set(zone_aware_scheduler, 'crypto',
                       StubDecryptor())

        self.assertEqual(fixture._decrypt_blob(test_data),
                         json.dumps(test_data))
