# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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 Scheduler
"""

import mock
from oslo_config import cfg
from oslo_utils import timeutils

from manila import context
from manila import db
from manila import exception
from manila.scheduler import driver
from manila.scheduler import manager
from manila.scheduler import simple
from manila.share import rpcapi as share_rpcapi
from manila import test
from manila.tests import db_utils
from manila import utils

CONF = cfg.CONF


class SchedulerManagerTestCase(test.TestCase):
    """Test case for scheduler manager."""

    manager_cls = manager.SchedulerManager
    driver_cls = driver.Scheduler
    driver_cls_name = 'manila.scheduler.driver.Scheduler'

    def setUp(self):
        super(SchedulerManagerTestCase, self).setUp()
        self.flags(scheduler_driver=self.driver_cls_name)
        self.manager = self.manager_cls()
        self.context = context.RequestContext('fake_user', 'fake_project')
        self.topic = 'fake_topic'
        self.fake_args = (1, 2, 3)
        self.fake_kwargs = {'cat': 'meow', 'dog': 'woof'}

    def test_1_correct_init(self):
        # Correct scheduler driver
        manager = self.manager
        self.assertTrue(isinstance(manager.driver, self.driver_cls))

    def test_update_service_capabilities(self):
        service_name = 'fake_service'
        host = 'fake_host'
        with mock.patch.object(self.manager.driver,
                               'update_service_capabilities', mock.Mock()):
            self.manager.update_service_capabilities(
                self.context, service_name=service_name, host=host)
            self.manager.driver.update_service_capabilities.\
                assert_called_once_with(service_name, host, {})
        with mock.patch.object(self.manager.driver,
                               'update_service_capabilities', mock.Mock()):
            capabilities = {'fake_capability': 'fake_value'}
            self.manager.update_service_capabilities(
                self.context, service_name=service_name, host=host,
                capabilities=capabilities)
            self.manager.driver.update_service_capabilities.\
                assert_called_once_with(service_name, host, capabilities)

    @mock.patch.object(db, 'share_update', mock.Mock())
    def test_create_share_exception_puts_share_in_error_state(self):
        """Test that a NoValideHost exception for create_share.

        Puts the share in 'error' state and eats the exception.
        """
        def raise_no_valid_host(*args, **kwargs):
            raise exception.NoValidHost(reason="")

        fake_share_id = 1

        request_spec = {'share_id': fake_share_id}
        with mock.patch.object(self.manager.driver,
                               'schedule_create_share',
                               mock.Mock(side_effect=raise_no_valid_host)):
            self.mock_object(manager.LOG, 'error')
            self.manager.create_share_instance(
                self.context, request_spec=request_spec, filter_properties={})
            db.share_update.assert_called_once_with(
                self.context, fake_share_id, {'status': 'error'})
            self.manager.driver.schedule_create_share.assert_called_once_with(
                self.context, request_spec, {})
            manager.LOG.error.assert_called_once_with(mock.ANY, mock.ANY)

    def test_get_pools(self):
        """Ensure get_pools exists and calls driver.get_pools."""
        mock_get_pools = self.mock_object(self.manager.driver, 'get_pools',
                                          mock.Mock(return_value='fake_pools'))

        result = self.manager.get_pools(self.context, filters='fake_filters')

        mock_get_pools.assert_called_once_with(self.context, 'fake_filters')
        self.assertEqual('fake_pools', result)

    @mock.patch.object(db, 'consistency_group_update', mock.Mock())
    def test_create_cg_no_valid_host_puts_cg_in_error_state(self):
        """Test that NoValidHost is raised for create_consistency_group.

        Puts the share in 'error' state and eats the exception.
        """
        def raise_no_valid_host(*args, **kwargs):
            raise exception.NoValidHost(reason="")

        fake_cg_id = 1
        cg_id = fake_cg_id
        request_spec = {"consistency_group_id": cg_id}
        with mock.patch.object(self.manager.driver,
                               'schedule_create_consistency_group',
                               mock.Mock(side_effect=raise_no_valid_host)):
            self.manager.create_consistency_group(self.context,
                                                  fake_cg_id,
                                                  request_spec=request_spec,
                                                  filter_properties={})
            db.consistency_group_update.assert_called_once_with(
                self.context, fake_cg_id, {'status': 'error'})
            self.manager.driver.schedule_create_consistency_group\
                .assert_called_once_with(self.context, cg_id,
                                         request_spec, {})

    @mock.patch.object(db, 'consistency_group_update', mock.Mock())
    def test_create_cg_exception_puts_cg_in_error_state(self):
        """Test that exceptions for create_consistency_group.

        Puts the share in 'error' state and raises the exception.
        """

        fake_cg_id = 1
        cg_id = fake_cg_id
        request_spec = {"consistency_group_id": cg_id}
        with mock.patch.object(self.manager.driver,
                               'schedule_create_consistency_group',
                               mock.Mock(side_effect=exception.NotFound)):
            self.assertRaises(exception.NotFound,
                              self.manager.create_consistency_group,
                              self.context, fake_cg_id,
                              request_spec=request_spec,
                              filter_properties={})

    def test_migrate_share_to_host(self):

        share = db_utils.create_share()
        host = 'fake@backend#pool'

        self.mock_object(db, 'share_get', mock.Mock(return_value=share))
        self.mock_object(share_rpcapi.ShareAPI, 'migrate_share')
        self.mock_object(driver.Scheduler, 'host_passes_filters',
                         mock.Mock(return_value=host))

        self.manager.migrate_share_to_host(self.context, share['id'], host,
                                           False, {}, None)

    def test_migrate_share_to_host_no_valid_host(self):

        share = db_utils.create_share()
        host = 'fake@backend#pool'

        self.mock_object(
            driver.Scheduler, 'host_passes_filters',
            mock.Mock(side_effect=[exception.NoValidHost('fake')]))

        self.manager.migrate_share_to_host(self.context, share['id'], host,
                                           False, {}, None)


class SchedulerTestCase(test.TestCase):
    """Test case for base scheduler driver class."""

    # So we can subclass this test and re-use tests if we need.
    driver_cls = driver.Scheduler

    def setUp(self):
        super(SchedulerTestCase, self).setUp()
        self.driver = self.driver_cls()
        self.context = context.RequestContext('fake_user', 'fake_project')
        self.topic = 'fake_topic'

    def test_update_service_capabilities(self):
        service_name = 'fake_service'
        host = 'fake_host'
        capabilities = {'fake_capability': 'fake_value'}
        with mock.patch.object(self.driver.host_manager,
                               'update_service_capabilities', mock.Mock()):
            self.driver.update_service_capabilities(
                service_name, host, capabilities)
            self.driver.host_manager.update_service_capabilities.\
                assert_called_once_with(service_name, host, capabilities)

    def test_hosts_up(self):
        service1 = {'host': 'host1'}
        service2 = {'host': 'host2'}
        services = [service1, service2]

        def fake_service_is_up(*args, **kwargs):
            if args[0]['host'] == 'host1':
                return False
            return True

        with mock.patch.object(db, 'service_get_all_by_topic',
                               mock.Mock(return_value=services)):
            with mock.patch.object(utils, 'service_is_up',
                                   mock.Mock(side_effect=fake_service_is_up)):
                result = self.driver.hosts_up(self.context, self.topic)
                self.assertEqual(result, ['host2'])
                db.service_get_all_by_topic.assert_called_once_with(
                    self.context, self.topic)


class SchedulerDriverBaseTestCase(SchedulerTestCase):
    """Test cases for base scheduler driver class methods.

    These can't fail if the driver is changed.
    """

    def test_unimplemented_schedule(self):
        fake_args = (1, 2, 3)
        fake_kwargs = {'cat': 'meow'}

        self.assertRaises(NotImplementedError, self.driver.schedule,
                          self.context, self.topic, 'schedule_something',
                          *fake_args, **fake_kwargs)


class SchedulerDriverModuleTestCase(test.TestCase):
    """Test case for scheduler driver module methods."""

    def setUp(self):
        super(SchedulerDriverModuleTestCase, self).setUp()
        self.context = context.RequestContext('fake_user', 'fake_project')

    @mock.patch.object(db, 'share_update', mock.Mock())
    def test_share_host_update_db(self):
        with mock.patch.object(timeutils, 'utcnow',
                               mock.Mock(return_value='fake-now')):
            driver.share_update_db(self.context, 31337, 'fake_host')
            db.share_update.assert_called_once_with(
                self.context, 31337,
                {'host': 'fake_host', 'scheduled_at': 'fake-now'})


class SimpleSchedulerSharesTestCase(test.TestCase):
    """Test case for simple scheduler create share method."""

    def setUp(self):
        super(SimpleSchedulerSharesTestCase, self).setUp()
        self.mock_object(share_rpcapi, 'ShareAPI')
        self.driver = simple.SimpleScheduler()

        self.context = context.RequestContext('fake_user', 'fake_project')
        self.admin_context = context.RequestContext('fake_admin_user',
                                                    'fake_project')
        self.admin_context.is_admin = True

    @mock.patch.object(utils, 'service_is_up', mock.Mock(return_value=True))
    def test_create_share_if_two_services_up(self):
        share_id = 'fake'
        fake_share = {'id': share_id, 'size': 1}
        fake_service_1 = {'disabled': False, 'host': 'fake_host1'}
        fake_service_2 = {'disabled': False, 'host': 'fake_host2'}
        fake_result = [(fake_service_1, 2), (fake_service_2, 1)]
        fake_request_spec = {
            'share_id': share_id,
            'share_properties': fake_share,
        }
        self.mock_object(db, 'service_get_all_share_sorted',
                         mock.Mock(return_value=fake_result))
        self.mock_object(driver, 'share_update_db',
                         mock.Mock(return_value=db_utils.create_share()))

        self.driver.schedule_create_share(self.context,
                                          fake_request_spec, {})
        utils.service_is_up.assert_called_once_with(utils.IsAMatcher(dict))
        db.service_get_all_share_sorted.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext))
        driver.share_update_db.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext), share_id, 'fake_host1')

    def test_create_share_if_services_not_available(self):
        share_id = 'fake'
        fake_share = {'id': share_id, 'size': 1}
        fake_result = []
        fake_request_spec = {
            'share_id': share_id,
            'share_properties': fake_share,
        }
        with mock.patch.object(db, 'service_get_all_share_sorted',
                               mock.Mock(return_value=fake_result)):
            self.assertRaises(exception.NoValidHost,
                              self.driver.schedule_create_share,
                              self.context, fake_request_spec, {})
            db.service_get_all_share_sorted.assert_called_once_with(
                utils.IsAMatcher(context.RequestContext))

    def test_create_share_if_max_gigabytes_exceeded(self):
        share_id = 'fake'
        fake_share = {'id': share_id, 'size': 10001}
        fake_service_1 = {'disabled': False, 'host': 'fake_host1'}
        fake_service_2 = {'disabled': False, 'host': 'fake_host2'}
        fake_result = [(fake_service_1, 5), (fake_service_2, 7)]
        fake_request_spec = {
            'share_id': share_id,
            'share_properties': fake_share,
        }
        with mock.patch.object(db, 'service_get_all_share_sorted',
                               mock.Mock(return_value=fake_result)):
            self.assertRaises(exception.NoValidHost,
                              self.driver.schedule_create_share,
                              self.context, fake_request_spec, {})
            db.service_get_all_share_sorted.assert_called_once_with(
                utils.IsAMatcher(context.RequestContext))

    @mock.patch.object(utils, 'service_is_up', mock.Mock(return_value=True))
    def test_create_share_availability_zone(self):
        share_id = 'fake'
        fake_share = {
            'id': share_id,
            'size': 1,
        }
        fake_instance = {
            'availability_zone_id': 'fake',
        }
        fake_service_1 = {
            'disabled': False, 'host': 'fake_host1',
            'availability_zone_id': 'fake',
        }
        fake_service_2 = {
            'disabled': False, 'host': 'fake_host2',
            'availability_zone_id': 'super_fake',
        }
        fake_result = [(fake_service_1, 0), (fake_service_2, 1)]
        fake_request_spec = {
            'share_id': share_id,
            'share_properties': fake_share,
            'share_instance_properties': fake_instance,
        }
        self.mock_object(db, 'service_get_all_share_sorted',
                         mock.Mock(return_value=fake_result))
        self.mock_object(driver, 'share_update_db',
                         mock.Mock(return_value=db_utils.create_share()))

        self.driver.schedule_create_share(self.context,
                                          fake_request_spec, {})
        utils.service_is_up.assert_called_once_with(fake_service_1)
        driver.share_update_db.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext), share_id,
            fake_service_1['host'])
        db.service_get_all_share_sorted.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext))

    @mock.patch.object(utils, 'service_is_up', mock.Mock(return_value=True))
    def test_create_share_availability_zone_on_host(self):
        share_id = 'fake'
        fake_share = {
            'id': share_id,
            'availability_zone': 'fake:fake',
            'size': 1,
        }
        fake_service = {'disabled': False, 'host': 'fake'}
        fake_request_spec = {
            'share_id': share_id,
            'share_properties': fake_share,
        }
        self.mock_object(db, 'service_get_all_share_sorted',
                         mock.Mock(return_value=[(fake_service, 1)]))
        self.mock_object(driver, 'share_update_db',
                         mock.Mock(return_value=db_utils.create_share()))

        self.driver.schedule_create_share(self.admin_context,
                                          fake_request_spec, {})
        utils.service_is_up.assert_called_once_with(fake_service)
        db.service_get_all_share_sorted.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext))
        driver.share_update_db.assert_called_once_with(
            utils.IsAMatcher(context.RequestContext), share_id, 'fake')
