#    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 mox3 import mox
import oslo_messaging as messaging

from nova.compute import power_state
from nova.compute import rpcapi as compute_rpcapi
from nova.conductor.tasks import live_migrate
from nova import exception
from nova import objects
from nova.scheduler import client as scheduler_client
from nova.scheduler import utils as scheduler_utils
from nova import servicegroup
from nova import test
from nova.tests.unit import fake_instance
from nova import utils


class LiveMigrationTaskTestCase(test.NoDBTestCase):
    def setUp(self):
        super(LiveMigrationTaskTestCase, self).setUp()
        self.context = "context"
        self.instance_host = "host"
        self.instance_uuid = "uuid"
        self.instance_image = "image_ref"
        db_instance = fake_instance.fake_db_instance(
                host=self.instance_host,
                uuid=self.instance_uuid,
                power_state=power_state.RUNNING,
                memory_mb=512,
                image_ref=self.instance_image)
        self.instance = objects.Instance._from_db_object(
                self.context, objects.Instance(), db_instance)
        self.instance.system_metadata = {'image_hw_disk_bus': 'scsi'}
        self.destination = "destination"
        self.block_migration = "bm"
        self.disk_over_commit = "doc"
        self.migration = objects.Migration()
        self._generate_task()

    def _generate_task(self):
        self.task = live_migrate.LiveMigrationTask(self.context,
            self.instance, self.destination, self.block_migration,
            self.disk_over_commit, self.migration, compute_rpcapi.ComputeAPI(),
            servicegroup.API(), scheduler_client.SchedulerClient())

    def test_execute_with_destination(self):
        self.mox.StubOutWithMock(self.task, '_check_host_is_up')
        self.mox.StubOutWithMock(self.task, '_check_requested_destination')
        self.mox.StubOutWithMock(self.task.compute_rpcapi, 'live_migration')

        self.task._check_host_is_up(self.instance_host)
        self.task._check_requested_destination()
        self.task.compute_rpcapi.live_migration(self.context,
                host=self.instance_host,
                instance=self.instance,
                dest=self.destination,
                block_migration=self.block_migration,
                migration=self.migration,
                migrate_data=None).AndReturn("bob")

        self.mox.ReplayAll()
        self.assertEqual("bob", self.task.execute())

    def test_execute_without_destination(self):
        self.destination = None
        self._generate_task()
        self.assertIsNone(self.task.destination)

        self.mox.StubOutWithMock(self.task, '_check_host_is_up')
        self.mox.StubOutWithMock(self.task, '_find_destination')
        self.mox.StubOutWithMock(self.task.compute_rpcapi, 'live_migration')

        self.task._check_host_is_up(self.instance_host)
        self.task._find_destination().AndReturn("found_host")
        self.task.compute_rpcapi.live_migration(self.context,
                host=self.instance_host,
                instance=self.instance,
                dest="found_host",
                block_migration=self.block_migration,
                migration=self.migration,
                migrate_data=None).AndReturn("bob")

        self.mox.ReplayAll()
        with mock.patch.object(self.migration, 'save') as mock_save:
            self.assertEqual("bob", self.task.execute())
            self.assertTrue(mock_save.called)
            self.assertEqual('found_host', self.migration.dest_compute)

    def test_check_instance_is_active_passes_when_paused(self):
        self.task.instance['power_state'] = power_state.PAUSED
        self.task._check_instance_is_active()

    def test_check_instance_is_active_fails_when_shutdown(self):
        self.task.instance['power_state'] = power_state.SHUTDOWN
        self.assertRaises(exception.InstanceInvalidState,
                          self.task._check_instance_is_active)

    def test_check_instance_host_is_up(self):
        self.mox.StubOutWithMock(objects.Service, 'get_by_compute_host')
        self.mox.StubOutWithMock(self.task.servicegroup_api, 'service_is_up')

        objects.Service.get_by_compute_host(self.context,
                                            "host").AndReturn("service")
        self.task.servicegroup_api.service_is_up("service").AndReturn(True)

        self.mox.ReplayAll()
        self.task._check_host_is_up("host")

    def test_check_instance_host_is_up_fails_if_not_up(self):
        self.mox.StubOutWithMock(objects.Service, 'get_by_compute_host')
        self.mox.StubOutWithMock(self.task.servicegroup_api, 'service_is_up')

        objects.Service.get_by_compute_host(self.context,
                                            "host").AndReturn("service")
        self.task.servicegroup_api.service_is_up("service").AndReturn(False)

        self.mox.ReplayAll()
        self.assertRaises(exception.ComputeServiceUnavailable,
                self.task._check_host_is_up, "host")

    def test_check_instance_host_is_up_fails_if_not_found(self):
        self.mox.StubOutWithMock(objects.Service, 'get_by_compute_host')

        objects.Service.get_by_compute_host(
            self.context, "host").AndRaise(exception.NotFound)

        self.mox.ReplayAll()
        self.assertRaises(exception.ComputeServiceUnavailable,
            self.task._check_host_is_up, "host")

    def test_check_requested_destination(self):
        self.mox.StubOutWithMock(objects.Service, 'get_by_compute_host')
        self.mox.StubOutWithMock(self.task, '_get_compute_info')
        self.mox.StubOutWithMock(self.task.servicegroup_api, 'service_is_up')
        self.mox.StubOutWithMock(self.task.compute_rpcapi,
                                 'check_can_live_migrate_destination')

        objects.Service.get_by_compute_host(
            self.context, self.destination).AndReturn("service")
        self.task.servicegroup_api.service_is_up("service").AndReturn(True)
        hypervisor_details = {
            "hypervisor_type": "a",
            "hypervisor_version": 6.1,
            "free_ram_mb": 513
        }
        self.task._get_compute_info(self.destination)\
                .AndReturn(hypervisor_details)
        self.task._get_compute_info(self.instance_host)\
                .AndReturn(hypervisor_details)
        self.task._get_compute_info(self.destination)\
                .AndReturn(hypervisor_details)

        self.task.compute_rpcapi.check_can_live_migrate_destination(
                self.context, self.instance, self.destination,
                self.block_migration, self.disk_over_commit).AndReturn(
                        "migrate_data")

        self.mox.ReplayAll()
        self.task._check_requested_destination()
        self.assertEqual("migrate_data", self.task.migrate_data)

    def test_check_requested_destination_fails_with_same_dest(self):
        self.task.destination = "same"
        self.task.source = "same"
        self.assertRaises(exception.UnableToMigrateToSelf,
                          self.task._check_requested_destination)

    def test_check_requested_destination_fails_when_destination_is_up(self):
        self.mox.StubOutWithMock(objects.Service, 'get_by_compute_host')

        objects.Service.get_by_compute_host(
            self.context, self.destination).AndRaise(exception.NotFound)

        self.mox.ReplayAll()
        self.assertRaises(exception.ComputeServiceUnavailable,
                          self.task._check_requested_destination)

    def test_check_requested_destination_fails_with_not_enough_memory(self):
        self.mox.StubOutWithMock(self.task, '_check_host_is_up')
        self.mox.StubOutWithMock(objects.ComputeNode,
                                 'get_first_node_by_host_for_old_compat')

        self.task._check_host_is_up(self.destination)
        objects.ComputeNode.get_first_node_by_host_for_old_compat(self.context,
            self.destination).AndReturn({"free_ram_mb": 511})

        self.mox.ReplayAll()
        self.assertRaises(exception.MigrationPreCheckError,
                          self.task._check_requested_destination)

    def test_check_requested_destination_fails_with_hypervisor_diff(self):
        self.mox.StubOutWithMock(self.task, '_check_host_is_up')
        self.mox.StubOutWithMock(self.task,
                '_check_destination_has_enough_memory')
        self.mox.StubOutWithMock(self.task, '_get_compute_info')

        self.task._check_host_is_up(self.destination)
        self.task._check_destination_has_enough_memory()
        self.task._get_compute_info(self.instance_host).AndReturn({
            "hypervisor_type": "b"
        })
        self.task._get_compute_info(self.destination).AndReturn({
            "hypervisor_type": "a"
        })

        self.mox.ReplayAll()
        self.assertRaises(exception.InvalidHypervisorType,
                          self.task._check_requested_destination)

    def test_check_requested_destination_fails_with_hypervisor_too_old(self):
        self.mox.StubOutWithMock(self.task, '_check_host_is_up')
        self.mox.StubOutWithMock(self.task,
                '_check_destination_has_enough_memory')
        self.mox.StubOutWithMock(self.task, '_get_compute_info')

        self.task._check_host_is_up(self.destination)
        self.task._check_destination_has_enough_memory()
        self.task._get_compute_info(self.instance_host).AndReturn({
            "hypervisor_type": "a",
            "hypervisor_version": 7
        })
        self.task._get_compute_info(self.destination).AndReturn({
            "hypervisor_type": "a",
            "hypervisor_version": 6
        })

        self.mox.ReplayAll()
        self.assertRaises(exception.DestinationHypervisorTooOld,
                          self.task._check_requested_destination)

    def test_find_destination_works(self):
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')
        self.mox.StubOutWithMock(self.task, '_call_livem_checks_on_host')

        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")
        self.task._call_livem_checks_on_host("host1")

        self.mox.ReplayAll()
        self.assertEqual("host1", self.task._find_destination())

    def test_find_destination_no_image_works(self):
        self.instance['image_ref'] = ''

        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')
        self.mox.StubOutWithMock(self.task, '_call_livem_checks_on_host')

        scheduler_utils.build_request_spec(
            self.context,
            {'properties': {'hw_disk_bus': 'scsi'}},
            mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")
        self.task._call_livem_checks_on_host("host1")

        self.mox.ReplayAll()
        self.assertEqual("host1", self.task._find_destination())

    def _test_find_destination_retry_hypervisor_raises(self, error):
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')
        self.mox.StubOutWithMock(self.task, '_call_livem_checks_on_host')

        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")\
                .AndRaise(error)

        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host, "host1"]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host2'}])
        self.task._check_compatible_with_source_hypervisor("host2")
        self.task._call_livem_checks_on_host("host2")

        self.mox.ReplayAll()
        self.assertEqual("host2", self.task._find_destination())

    def test_find_destination_retry_with_old_hypervisor(self):
        self._test_find_destination_retry_hypervisor_raises(
                exception.DestinationHypervisorTooOld)

    def test_find_destination_retry_with_invalid_hypervisor_type(self):
        self._test_find_destination_retry_hypervisor_raises(
                exception.InvalidHypervisorType)

    def test_find_destination_retry_with_invalid_livem_checks(self):
        self.flags(migrate_max_retries=1)
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')
        self.mox.StubOutWithMock(self.task, '_call_livem_checks_on_host')

        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")
        self.task._call_livem_checks_on_host("host1")\
                .AndRaise(exception.Invalid)

        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host, "host1"]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host2'}])
        self.task._check_compatible_with_source_hypervisor("host2")
        self.task._call_livem_checks_on_host("host2")

        self.mox.ReplayAll()
        self.assertEqual("host2", self.task._find_destination())

    def test_find_destination_retry_with_failed_migration_pre_checks(self):
        self.flags(migrate_max_retries=1)
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')
        self.mox.StubOutWithMock(self.task, '_call_livem_checks_on_host')

        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")
        self.task._call_livem_checks_on_host("host1")\
                .AndRaise(exception.MigrationPreCheckError("reason"))

        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host, "host1"]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host2'}])
        self.task._check_compatible_with_source_hypervisor("host2")
        self.task._call_livem_checks_on_host("host2")

        self.mox.ReplayAll()
        self.assertEqual("host2", self.task._find_destination())

    def test_find_destination_retry_exceeds_max(self):
        self.flags(migrate_max_retries=0)
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        self.mox.StubOutWithMock(self.task,
                '_check_compatible_with_source_hypervisor')

        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
                        [{'host': 'host1'}])
        self.task._check_compatible_with_source_hypervisor("host1")\
                .AndRaise(exception.DestinationHypervisorTooOld)

        self.mox.ReplayAll()
        self.assertRaises(exception.MaxRetriesExceeded,
                          self.task._find_destination)

    def test_find_destination_when_runs_out_of_hosts(self):
        self.mox.StubOutWithMock(utils, 'get_image_from_system_metadata')
        self.mox.StubOutWithMock(scheduler_utils, 'build_request_spec')
        self.mox.StubOutWithMock(scheduler_utils, 'setup_instance_group')
        self.mox.StubOutWithMock(self.task.scheduler_client,
                                 'select_destinations')
        utils.get_image_from_system_metadata(
            self.instance.system_metadata).AndReturn("image")
        scheduler_utils.build_request_spec(self.context, mox.IgnoreArg(),
                                           mox.IgnoreArg()).AndReturn({})
        scheduler_utils.setup_instance_group(
            self.context, {}, {'ignore_hosts': [self.instance_host]})
        self.task.scheduler_client.select_destinations(self.context,
                mox.IgnoreArg(), mox.IgnoreArg()).AndRaise(
                        exception.NoValidHost(reason=""))

        self.mox.ReplayAll()
        self.assertRaises(exception.NoValidHost, self.task._find_destination)

    def test_call_livem_checks_on_host(self):
        with mock.patch.object(self.task.compute_rpcapi,
            'check_can_live_migrate_destination',
            side_effect=messaging.MessagingTimeout):
            self.assertRaises(exception.MigrationPreCheckError,
                self.task._call_livem_checks_on_host, {})
