# Copyright 2011 OpenStack Foundation
# 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 Filter Scheduler.
"""

import mock

from cinder import context
from cinder import exception
from cinder.scheduler import filter_scheduler
from cinder.scheduler import host_manager
from cinder.tests.unit.scheduler import fakes
from cinder.tests.unit.scheduler import test_scheduler
from cinder.volume import utils


class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
    """Test case for Filter Scheduler."""

    driver_cls = filter_scheduler.FilterScheduler

    def test_create_consistencygroup_no_hosts(self):
        # Ensure empty hosts result in NoValidHosts exception.
        sched = fakes.FakeFilterScheduler()

        fake_context = context.RequestContext('user', 'project')
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 0},
                        'volume_type': {'name': 'Type1',
                                        'extra_specs': {}}}
        request_spec2 = {'volume_properties': {'project_id': 1,
                                               'size': 0},
                         'volume_type': {'name': 'Type2',
                                         'extra_specs': {}}}
        request_spec_list = [request_spec, request_spec2]
        self.assertRaises(exception.NoValidHost,
                          sched.schedule_create_consistencygroup,
                          fake_context, 'faki-id1', request_spec_list, {})

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_schedule_consistencygroup(self,
                                       _mock_service_get_all_by_topic):
        # Make sure _schedule_group() can find host successfully.
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fake_context = context.RequestContext('user', 'project',
                                              is_admin=True)

        fakes.mock_host_manager_db_calls(_mock_service_get_all_by_topic)

        specs = {'capabilities:consistencygroup_support': '<is> True'}
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 0},
                        'volume_type': {'name': 'Type1',
                                        'extra_specs': specs}}
        request_spec2 = {'volume_properties': {'project_id': 1,
                                               'size': 0},
                         'volume_type': {'name': 'Type2',
                                         'extra_specs': specs}}
        request_spec_list = [request_spec, request_spec2]
        weighed_host = sched._schedule_group(fake_context,
                                             request_spec_list,
                                             {})
        self.assertIsNotNone(weighed_host.obj)
        self.assertTrue(_mock_service_get_all_by_topic.called)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_schedule_consistencygroup_no_cg_support_in_extra_specs(
            self,
            _mock_service_get_all_by_topic):
        # Make sure _schedule_group() can find host successfully even
        # when consistencygroup_support is not specified in volume type's
        # extra specs
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fake_context = context.RequestContext('user', 'project',
                                              is_admin=True)

        fakes.mock_host_manager_db_calls(_mock_service_get_all_by_topic)

        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 0},
                        'volume_type': {'name': 'Type1',
                                        'extra_specs': {}}}
        request_spec2 = {'volume_properties': {'project_id': 1,
                                               'size': 0},
                         'volume_type': {'name': 'Type2',
                                         'extra_specs': {}}}
        request_spec_list = [request_spec, request_spec2]
        weighed_host = sched._schedule_group(fake_context,
                                             request_spec_list,
                                             {})
        self.assertIsNotNone(weighed_host.obj)
        self.assertTrue(_mock_service_get_all_by_topic.called)

    def test_create_volume_no_hosts(self):
        # Ensure empty hosts/child_zones result in NoValidHosts exception.
        sched = fakes.FakeFilterScheduler()

        fake_context = context.RequestContext('user', 'project')
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_id': ['fake-id1']}
        self.assertRaises(exception.NoValidHost, sched.schedule_create_volume,
                          fake_context, request_spec, {})

    def test_create_volume_no_hosts_invalid_req(self):
        sched = fakes.FakeFilterScheduler()

        fake_context = context.RequestContext('user', 'project')

        # request_spec is missing 'volume_id'
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_type': {'name': 'LVM_iSCSI'}}
        self.assertRaises(exception.NoValidHost,
                          sched.schedule_create_volume,
                          fake_context,
                          request_spec,
                          {})

    def test_create_volume_no_volume_type(self):
        sched = fakes.FakeFilterScheduler()

        fake_context = context.RequestContext('user', 'project')

        # request_spec is missing 'volume_type'
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_id': ['fake-id1']}
        self.assertRaises(exception.InvalidVolumeType,
                          sched.schedule_create_volume,
                          fake_context,
                          request_spec,
                          {})

    @mock.patch('cinder.scheduler.host_manager.HostManager.'
                'get_all_host_states')
    def test_create_volume_non_admin(self, _mock_get_all_host_states):
        # Test creating a volume locally using create_volume, passing
        # a non-admin context.  DB actions should work.
        self.was_admin = False

        def fake_get(ctxt):
            # Make sure this is called with admin context, even though
            # we're using user context below.
            self.was_admin = ctxt.is_admin
            return {}

        sched = fakes.FakeFilterScheduler()
        _mock_get_all_host_states.side_effect = fake_get

        fake_context = context.RequestContext('user', 'project')

        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_id': ['fake-id1']}
        self.assertRaises(exception.NoValidHost, sched.schedule_create_volume,
                          fake_context, request_spec, {})
        self.assertTrue(self.was_admin)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_schedule_happy_day(self, _mock_service_get_all_by_topic):
        # Make sure there's nothing glaringly wrong with _schedule()
        # by doing a happy day pass through.
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fake_context = context.RequestContext('user', 'project',
                                              is_admin=True)

        fakes.mock_host_manager_db_calls(_mock_service_get_all_by_topic)

        request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}
        weighed_host = sched._schedule(fake_context, request_spec, {})
        self.assertIsNotNone(weighed_host.obj)
        self.assertTrue(_mock_service_get_all_by_topic.called)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_create_volume_clear_host_different_with_cg(self,
                                                        _mock_service_get_all):
        # Ensure we clear those hosts whose backend is not same as
        # consistencygroup's backend.
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fakes.mock_host_manager_db_calls(_mock_service_get_all)
        fake_context = context.RequestContext('user', 'project')
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'CG_backend': 'host@lvmdriver'}
        weighed_host = sched._schedule(fake_context, request_spec, {})
        self.assertIsNone(weighed_host)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_create_volume_host_same_as_cg(self, _mock_service_get_all):
        # Ensure we don't clear the host whose backend is same as
        # consistencygroup's backend.
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fakes.mock_host_manager_db_calls(_mock_service_get_all)
        fake_context = context.RequestContext('user', 'project')
        request_spec = {'volume_properties': {'project_id': 1,
                                              'size': 1},
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'CG_backend': 'host1'}
        weighed_host = sched._schedule(fake_context, request_spec, {})
        self.assertEqual('host1#lvm1', weighed_host.obj.host)

    def test_max_attempts(self):
        self.flags(scheduler_max_attempts=4)

        sched = fakes.FakeFilterScheduler()
        self.assertEqual(4, sched._max_attempts())

    def test_invalid_max_attempts(self):
        self.flags(scheduler_max_attempts=0)

        self.assertRaises(exception.InvalidParameterValue,
                          fakes.FakeFilterScheduler)

    def test_retry_disabled(self):
        # Retry info should not get populated when re-scheduling is off.
        self.flags(scheduler_max_attempts=1)
        sched = fakes.FakeFilterScheduler()

        request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}
        filter_properties = {}

        sched._schedule(self.context, request_spec,
                        filter_properties=filter_properties)

        # Should not have retry info in the populated filter properties.
        self.assertNotIn("retry", filter_properties)

    def test_retry_attempt_one(self):
        # Test retry logic on initial scheduling attempt.
        self.flags(scheduler_max_attempts=2)
        sched = fakes.FakeFilterScheduler()

        request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}
        filter_properties = {}

        sched._schedule(self.context, request_spec,
                        filter_properties=filter_properties)

        num_attempts = filter_properties['retry']['num_attempts']
        self.assertEqual(1, num_attempts)

    def test_retry_attempt_two(self):
        # Test retry logic when re-scheduling.
        self.flags(scheduler_max_attempts=2)
        sched = fakes.FakeFilterScheduler()

        request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}

        retry = dict(num_attempts=1)
        filter_properties = dict(retry=retry)

        sched._schedule(self.context, request_spec,
                        filter_properties=filter_properties)

        num_attempts = filter_properties['retry']['num_attempts']
        self.assertEqual(2, num_attempts)

    def test_retry_exceeded_max_attempts(self):
        # Test for necessary explosion when max retries is exceeded.
        self.flags(scheduler_max_attempts=2)
        sched = fakes.FakeFilterScheduler()

        request_spec = {'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}

        retry = dict(num_attempts=2)
        filter_properties = dict(retry=retry)

        self.assertRaises(exception.NoValidHost, sched._schedule, self.context,
                          request_spec, filter_properties=filter_properties)

    def test_add_retry_host(self):
        retry = dict(num_attempts=1, hosts=[])
        filter_properties = dict(retry=retry)
        host = "fakehost"

        sched = fakes.FakeFilterScheduler()
        sched._add_retry_host(filter_properties, host)

        hosts = filter_properties['retry']['hosts']
        self.assertEqual(1, len(hosts))
        self.assertEqual(host, hosts[0])

    def test_post_select_populate(self):
        # Test addition of certain filter props after a node is selected.
        retry = {'hosts': [], 'num_attempts': 1}
        filter_properties = {'retry': retry}
        sched = fakes.FakeFilterScheduler()

        host_state = host_manager.HostState('host')
        host_state.total_capacity_gb = 1024
        sched._post_select_populate_filter_properties(filter_properties,
                                                      host_state)

        self.assertEqual('host',
                         filter_properties['retry']['hosts'][0])

        self.assertEqual(1024, host_state.total_capacity_gb)

    def _host_passes_filters_setup(self, mock_obj):
        sched = fakes.FakeFilterScheduler()
        sched.host_manager = fakes.FakeHostManager()
        fake_context = context.RequestContext('user', 'project',
                                              is_admin=True)

        fakes.mock_host_manager_db_calls(mock_obj)

        return (sched, fake_context)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_host_passes_filters_happy_day(self, _mock_service_get_topic):
        """Do a successful pass through of with host_passes_filters()."""
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}
        ret_host = sched.host_passes_filters(ctx, 'host1#lvm1',
                                             request_spec, {})
        self.assertEqual('host1', utils.extract_host(ret_host.host))
        self.assertTrue(_mock_service_get_topic.called)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_host_passes_filters_default_pool_happy_day(
            self, _mock_service_get_topic):
        """Do a successful pass through of with host_passes_filters()."""
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1}}
        ret_host = sched.host_passes_filters(ctx, 'host5#_pool0',
                                             request_spec, {})
        self.assertEqual('host5', utils.extract_host(ret_host.host))
        self.assertTrue(_mock_service_get_topic.called)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_host_passes_filters_no_capacity(self, _mock_service_get_topic):
        """Fail the host due to insufficient capacity."""
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI'},
                        'volume_properties': {'project_id': 1,
                                              'size': 1024}}
        self.assertRaises(exception.NoValidHost,
                          sched.host_passes_filters,
                          ctx, 'host1#lvm1', request_spec, {})
        self.assertTrue(_mock_service_get_topic.called)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_retype_policy_never_migrate_pass(self, _mock_service_get_topic):
        # Retype should pass if current host passes filters and
        # policy=never. host4 doesn't have enough space to hold an additional
        # 200GB, but it is already the host of this volume and should not be
        # counted twice.
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        extra_specs = {'volume_backend_name': 'lvm4'}
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI',
                                        'extra_specs': extra_specs},
                        'volume_properties': {'project_id': 1,
                                              'size': 200,
                                              'host': 'host4#lvm4'}}
        host_state = sched.find_retype_host(ctx, request_spec,
                                            filter_properties={},
                                            migration_policy='never')
        self.assertEqual('host4', utils.extract_host(host_state.host))

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_retype_with_pool_policy_never_migrate_pass(
            self, _mock_service_get_topic):
        # Retype should pass if current host passes filters and
        # policy=never. host4 doesn't have enough space to hold an additional
        # 200GB, but it is already the host of this volume and should not be
        # counted twice.
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        extra_specs = {'volume_backend_name': 'lvm3'}
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI',
                                        'extra_specs': extra_specs},
                        'volume_properties': {'project_id': 1,
                                              'size': 200,
                                              'host': 'host3#lvm3'}}
        host_state = sched.find_retype_host(ctx, request_spec,
                                            filter_properties={},
                                            migration_policy='never')
        self.assertEqual('host3#lvm3', host_state.host)

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_retype_policy_never_migrate_fail(self, _mock_service_get_topic):
        # Retype should fail if current host doesn't pass filters and
        # policy=never.
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        extra_specs = {'volume_backend_name': 'lvm1'}
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI',
                                        'extra_specs': extra_specs},
                        'volume_properties': {'project_id': 1,
                                              'size': 200,
                                              'host': 'host4'}}
        self.assertRaises(exception.NoValidHost, sched.find_retype_host, ctx,
                          request_spec, filter_properties={},
                          migration_policy='never')

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_retype_policy_demand_migrate_pass(self, _mock_service_get_topic):
        # Retype should pass if current host fails filters but another host
        # is suitable when policy=on-demand.
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        extra_specs = {'volume_backend_name': 'lvm1'}
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI',
                                        'extra_specs': extra_specs},
                        'volume_properties': {'project_id': 1,
                                              'size': 200,
                                              'host': 'host4'}}
        host_state = sched.find_retype_host(ctx, request_spec,
                                            filter_properties={},
                                            migration_policy='on-demand')
        self.assertEqual('host1', utils.extract_host(host_state.host))

    @mock.patch('cinder.db.service_get_all_by_topic')
    def test_retype_policy_demand_migrate_fail(self, _mock_service_get_topic):
        # Retype should fail if current host doesn't pass filters and
        # no other suitable candidates exist even if policy=on-demand.
        sched, ctx = self._host_passes_filters_setup(
            _mock_service_get_topic)
        extra_specs = {'volume_backend_name': 'lvm1'}
        request_spec = {'volume_id': 1,
                        'volume_type': {'name': 'LVM_iSCSI',
                                        'extra_specs': extra_specs},
                        'volume_properties': {'project_id': 1,
                                              'size': 2048,
                                              'host': 'host4'}}
        self.assertRaises(exception.NoValidHost, sched.find_retype_host, ctx,
                          request_spec, filter_properties={},
                          migration_policy='on-demand')
