# 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 Manager
"""

from importlib import reload
from unittest import mock

import ddt
from oslo_config import cfg

from manila.common import constants
from manila import context
from manila import db
from manila import exception
from manila.message import message_field
from manila import quota
from manila.scheduler.drivers import base
from manila.scheduler.drivers import filter
from manila.scheduler import manager
from manila.share import rpcapi as share_rpcapi
from manila.share import share_types
from manila import test
from manila.tests import db_utils
from manila.tests import fake_share as fakes

CONF = cfg.CONF


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

    driver_cls = base.Scheduler
    driver_cls_name = 'manila.scheduler.drivers.base.Scheduler'

    def setUp(self):
        super(SchedulerManagerTestCase, self).setUp()
        self.periodic_tasks = []

        def _periodic_task(*args, **kwargs):
            def decorator(f):
                self.periodic_tasks.append(f)
                return f
            return mock.Mock(side_effect=decorator)

        self.mock_periodic_task = self.mock_object(
            manager.periodic_task, 'periodic_task',
            mock.Mock(side_effect=_periodic_task))
        reload(manager)

        self.flags(scheduler_driver=self.driver_cls_name)
        self.manager = manager.SchedulerManager()
        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 raise_no_valid_host(self, *args, **kwargs):
        raise exception.NoValidHost(reason="")

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

    @ddt.data('manila.scheduler.filter_scheduler.FilterScheduler',
              'manila.scheduler.drivers.filter.FilterScheduler')
    def test_scheduler_driver_mapper(self, driver_class):

        test_manager = manager.SchedulerManager(scheduler_driver=driver_class)

        self.assertIsInstance(test_manager.driver, filter.FilterScheduler)

    def test_init_host_with_rpc(self):

        self.mock_object(context,
                         'get_admin_context',
                         mock.Mock(return_value='fake_admin_context'))
        self.mock_object(self.manager, 'request_service_capabilities')

        self.manager.init_host_with_rpc()

        self.manager.request_service_capabilities.assert_called_once_with(
            'fake_admin_context')

    def test_get_host_list(self):

        self.mock_object(self.manager.driver, 'get_host_list')

        self.manager.get_host_list(context)

        self.manager.driver.get_host_list.assert_called_once_with()

    def test_get_service_capabilities(self):

        self.mock_object(self.manager.driver, 'get_service_capabilities')

        self.manager.get_service_capabilities(context)

        self.manager.driver.get_service_capabilities.assert_called_once_with()

    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, {}, None))
        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, None))

    @mock.patch.object(db, 'share_update', mock.Mock())
    @mock.patch('manila.message.api.API.create')
    def test_create_share_exception_puts_share_in_error_state(
            self, _mock_message_create):
        """Test NoValidHost exception for create_share.

        Puts the share in 'error' state and eats the exception.
        """
        fake_share_id = 1

        request_spec = {'share_id': fake_share_id}
        ex = exception.NoValidHost(reason='')
        with mock.patch.object(
                self.manager.driver, 'schedule_create_share',
                mock.Mock(side_effect=ex)):
            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)

            _mock_message_create.assert_called_once_with(
                self.context,
                message_field.Action.ALLOCATE_HOST,
                self.context.project_id, resource_type='SHARE',
                exception=ex, resource_id=fake_share_id)

    @mock.patch.object(db, 'share_update', mock.Mock())
    def test_create_share_other_exception_puts_share_in_error_state(self):
        """Test any exception except NoValidHost for create_share.

        Puts the share in 'error' state and re-raises the exception.
        """
        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=exception.QuotaError)):
            self.mock_object(manager.LOG, 'error')

            self.assertRaises(exception.QuotaError,
                              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)

    @mock.patch.object(quota.QUOTAS, 'expire')
    def test__expire_reservations(self, mock_expire):
        self.manager._expire_reservations(self.context)

        mock_expire.assert_called_once_with(self.context)

    @mock.patch('manila.message.api.API.cleanup_expired_messages')
    def test__clean_expired_messages(self, mock_expire):
        self.manager._clean_expired_messages(self.context)

        mock_expire.assert_called_once_with(self.context)

    def test_periodic_tasks(self):
        self.assertEqual(2, self.mock_periodic_task.call_count)

        self.assertEqual(2, len(self.periodic_tasks))
        self.assertEqual(
            self.periodic_tasks[0].__name__,
            self.manager._expire_reservations.__name__)
        self.assertEqual(
            self.periodic_tasks[1].__name__,
            self.manager._clean_expired_messages.__name__)

    def test_get_pools(self):
        """Ensure get_pools exists and calls base_scheduler.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',
                                               False)
        self.assertEqual('fake_pools', result)

    @mock.patch.object(db, 'share_group_update', mock.Mock())
    def test_create_group_no_valid_host_puts_group_in_error_state(self):
        """Test that NoValidHost is raised for create_share_group.

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

        fake_group_id = 1
        group_id = fake_group_id
        request_spec = {"share_group_id": group_id}
        with mock.patch.object(
                self.manager.driver, 'schedule_create_share_group',
                mock.Mock(side_effect=self.raise_no_valid_host)):
            self.manager.create_share_group(self.context,
                                            fake_group_id,
                                            request_spec=request_spec,
                                            filter_properties={})
            db.share_group_update.assert_called_once_with(
                self.context, fake_group_id, {'status': 'error'})
            (self.manager.driver.schedule_create_share_group.
                assert_called_once_with(self.context, group_id, request_spec,
                                        {}))

    @mock.patch.object(db, 'share_group_update', mock.Mock())
    def test_create_group_exception_puts_group_in_error_state(self):
        """Test that exceptions for create_share_group.

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

        fake_group_id = 1
        group_id = fake_group_id
        request_spec = {"share_group_id": group_id}
        with mock.patch.object(self.manager.driver,
                               'schedule_create_share_group',
                               mock.Mock(side_effect=exception.NotFound)):
            self.assertRaises(exception.NotFound,
                              self.manager.create_share_group,
                              self.context, fake_group_id,
                              request_spec=request_spec,
                              filter_properties={})

    def test_migrate_share_to_host(self):

        class fake_host(object):
            host = 'fake@backend#pool'

        share = db_utils.create_share()
        host = fake_host()

        self.mock_object(db, 'share_get', mock.Mock(return_value=share))
        self.mock_object(share_rpcapi.ShareAPI, 'migration_start',
                         mock.Mock(side_effect=TypeError))
        self.mock_object(base.Scheduler,
                         'host_passes_filters',
                         mock.Mock(return_value=host))
        self.mock_object(
            share_types,
            'revert_allocated_share_type_quotas_during_migration')

        self.assertRaises(
            TypeError, self.manager.migrate_share_to_host,
            self.context, share['id'], 'fake@backend#pool', False, True, True,
            False, True, 'fake_net_id', 'fake_type_id', {}, None)

        db.share_get.assert_called_once_with(self.context, share['id'])
        base.Scheduler.host_passes_filters.assert_called_once_with(
            self.context, 'fake@backend#pool', {}, None)
        share_rpcapi.ShareAPI.migration_start.assert_called_once_with(
            self.context, share, host.host, False, True, True, False, True,
            'fake_net_id', 'fake_type_id')
        (share_types.revert_allocated_share_type_quotas_during_migration.
            assert_called_once_with(self.context, share, 'fake_type_id'))

    @ddt.data(exception.NoValidHost(reason='fake'), TypeError)
    def test_migrate_share_to_host_exception(self, exc):

        share = db_utils.create_share(status=constants.STATUS_MIGRATING)
        host = 'fake@backend#pool'
        request_spec = {'share_id': share['id']}

        self.mock_object(db, 'share_get', mock.Mock(return_value=share))
        self.mock_object(
            base.Scheduler, 'host_passes_filters',
            mock.Mock(side_effect=exc))
        self.mock_object(db, 'share_update')
        self.mock_object(db, 'share_instance_update')
        self.mock_object(
            share_types,
            'revert_allocated_share_type_quotas_during_migration')

        capture = (exception.NoValidHost if
                   isinstance(exc, exception.NoValidHost) else TypeError)

        self.assertRaises(
            capture, self.manager.migrate_share_to_host,
            self.context, share['id'], host, False, True, True, False, True,
            'fake_net_id', 'fake_type_id', request_spec, None)

        base.Scheduler.host_passes_filters.assert_called_once_with(
            self.context, host, request_spec, None)
        db.share_get.assert_called_once_with(self.context, share['id'])
        db.share_update.assert_called_once_with(
            self.context, share['id'],
            {'task_state': constants.TASK_STATE_MIGRATION_ERROR})
        db.share_instance_update.assert_called_once_with(
            self.context, share.instance['id'],
            {'status': constants.STATUS_AVAILABLE})
        (share_types.revert_allocated_share_type_quotas_during_migration.
            assert_called_once_with(self.context, share, 'fake_type_id'))

    def test_manage_share(self):

        share = db_utils.create_share()

        self.mock_object(db, 'share_get', mock.Mock(return_value=share))
        self.mock_object(share_rpcapi.ShareAPI, 'manage_share')
        self.mock_object(base.Scheduler, 'host_passes_filters')

        self.manager.manage_share(self.context, share['id'], 'driver_options',
                                  {}, None)

    def test_manage_share_exception(self):

        share = db_utils.create_share()

        db_update = self.mock_object(db, 'share_update', mock.Mock())
        self.mock_object(
            base.Scheduler, 'host_passes_filters',
            mock.Mock(side_effect=exception.NoValidHost('fake')))

        share_id = share['id']

        self.assertRaises(
            exception.NoValidHost, self.manager.manage_share,
            self.context, share['id'], 'driver_options',
            {'share_id': share_id}, None)
        db_update.assert_called_once_with(
            self.context, share_id,
            {'status': constants.STATUS_MANAGE_ERROR, 'size': 0})

    def test_create_share_replica_exception_path(self):
        """Test 'raisable' exceptions for create_share_replica."""
        db_update = self.mock_object(db, 'share_replica_update')
        self.mock_object(db, 'share_snapshot_instance_get_all_with_filters',
                         mock.Mock(return_value=[{'id': '123'}]))
        snap_update = self.mock_object(db, 'share_snapshot_instance_update')
        request_spec = fakes.fake_replica_request_spec()
        replica_id = request_spec.get('share_instance_properties').get('id')
        expected_updates = {
            'status': constants.STATUS_ERROR,
            'replica_state': constants.STATUS_ERROR,
        }
        with mock.patch.object(self.manager.driver, 'schedule_create_replica',
                               mock.Mock(side_effect=exception.NotFound)):

            self.assertRaises(exception.NotFound,
                              self.manager.create_share_replica,
                              self.context,
                              request_spec=request_spec,
                              filter_properties={})
            db_update.assert_called_once_with(
                self.context, replica_id, expected_updates)
            snap_update.assert_called_once_with(
                self.context, '123', {'status': constants.STATUS_ERROR})

    def test_create_share_replica_no_valid_host(self):
        """Test the NoValidHost exception for create_share_replica."""
        db_update = self.mock_object(db, 'share_replica_update')
        request_spec = fakes.fake_replica_request_spec()
        replica_id = request_spec.get('share_instance_properties').get('id')
        expected_updates = {
            'status': constants.STATUS_ERROR,
            'replica_state': constants.STATUS_ERROR,
        }
        with mock.patch.object(
                self.manager.driver, 'schedule_create_replica',
                mock.Mock(side_effect=self.raise_no_valid_host)):

            retval = self.manager.create_share_replica(
                self.context, request_spec=request_spec, filter_properties={})

            self.assertIsNone(retval)
            db_update.assert_called_once_with(
                self.context, replica_id, expected_updates)

    def test_create_share_replica(self):
        """Test happy path for create_share_replica."""
        db_update = self.mock_object(db, 'share_replica_update')
        mock_scheduler_driver_call = self.mock_object(
            self.manager.driver, 'schedule_create_replica')
        request_spec = fakes.fake_replica_request_spec()

        retval = self.manager.create_share_replica(
            self.context, request_spec=request_spec, filter_properties={})

        mock_scheduler_driver_call.assert_called_once_with(
            self.context, request_spec, {})
        self.assertFalse(db_update.called)
        self.assertIsNone(retval)
