|
|
@@ -2,11 +2,10 @@
|
|
|
# All Rights Reserved.
|
|
|
|
|
|
import copy
|
|
|
+import ddt
|
|
|
import uuid
|
|
|
from unittest import mock
|
|
|
|
|
|
-import ddt
|
|
|
-
|
|
|
from coriolis import constants, exception
|
|
|
from coriolis.conductor.rpc import server
|
|
|
from coriolis.db import api as db_api
|
|
|
@@ -14,6 +13,7 @@ from coriolis.db.sqlalchemy import models
|
|
|
from coriolis.licensing import client as licensing_client
|
|
|
from coriolis.tests import test_base, testutils
|
|
|
from coriolis.worker.rpc import client as rpc_worker_client
|
|
|
+from oslo_concurrency import lockutils
|
|
|
|
|
|
|
|
|
@ddt.ddt
|
|
|
@@ -1276,3 +1276,283 @@ class ConductorServerEndpointTestCase(test_base.CoriolisBaseTestCase):
|
|
|
self.server._check_valid_replica_tasks_execution(
|
|
|
mock_replica
|
|
|
)
|
|
|
+
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_get_replica'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_check_reservation_for_transfer'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_check_replica_running_executions'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_check_valid_replica_tasks_execution'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ 'get_endpoint'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_get_provider_types'
|
|
|
+ )
|
|
|
+ @mock.patch.object(models, "Migration")
|
|
|
+ @mock.patch.object(uuid, "uuid4", return_value="migration_id")
|
|
|
+ @mock.patch.object(copy, "deepcopy")
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_check_minion_pools_for_action'
|
|
|
+ )
|
|
|
+ @mock.patch.object(models, "TasksExecution")
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_get_instance_scripts'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_create_task'
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ '_check_execution_tasks_sanity'
|
|
|
+ )
|
|
|
+ @mock.patch.object(db_api, 'add_migration')
|
|
|
+ @mock.patch.object(lockutils, 'lock')
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ "_minion_manager_client"
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ "_set_tasks_execution_status"
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ "_begin_tasks"
|
|
|
+ )
|
|
|
+ @mock.patch.object(
|
|
|
+ server.ConductorServerEndpoint,
|
|
|
+ "get_migration"
|
|
|
+ )
|
|
|
+ @ddt.file_data("data/deploy_replica_instance_config.yml")
|
|
|
+ @ddt.unpack
|
|
|
+ def test_deploy_replica_instance(
|
|
|
+ self,
|
|
|
+ mock_get_migration,
|
|
|
+ mock_begin_tasks,
|
|
|
+ mock_set_tasks_execution_status,
|
|
|
+ mock_minion_manager_client,
|
|
|
+ mock_lock,
|
|
|
+ mock_add_migration,
|
|
|
+ mock_check_execution_tasks_sanity,
|
|
|
+ mock_create_task,
|
|
|
+ mock_get_instance_scripts,
|
|
|
+ mock_tasks_execution,
|
|
|
+ mock_check_minion_pools_for_action,
|
|
|
+ mock_deepcopy, # pylint: disable=unused-argument
|
|
|
+ mock_uuid4, # pylint: disable=unused-argument
|
|
|
+ mock_migration,
|
|
|
+ mock_get_provider_types,
|
|
|
+ mock_get_endpoint,
|
|
|
+ mock_check_valid_replica_tasks_execution,
|
|
|
+ mock_check_replica_running_executions,
|
|
|
+ mock_check_reservation_for_transfer,
|
|
|
+ mock_get_replica,
|
|
|
+ config,
|
|
|
+ expected_tasks,
|
|
|
+ ):
|
|
|
+ skip_os_morphing = config.get('skip_os_morphing', False)
|
|
|
+ has_os_morphing_minion = config.get('has_os_morphing_minion', False)
|
|
|
+ get_optimal_flavor = config.get('get_optimal_flavor', False)
|
|
|
+ clone_disks = config.get('clone_disks', False)
|
|
|
+
|
|
|
+ if get_optimal_flavor:
|
|
|
+ mock_get_provider_types.return_value = [
|
|
|
+ constants.PROVIDER_TYPE_INSTANCE_FLAVOR
|
|
|
+ ]
|
|
|
+
|
|
|
+ instance_osmorphing_minion_pool_mappings = None
|
|
|
+ if not skip_os_morphing and has_os_morphing_minion:
|
|
|
+ instance_osmorphing_minion_pool_mappings = {
|
|
|
+ mock.sentinel.instance1: mock.sentinel.pool1,
|
|
|
+ mock.sentinel.instance2: mock.sentinel.pool2,
|
|
|
+ }
|
|
|
+
|
|
|
+ mock_get_replica.return_value = mock.Mock(
|
|
|
+ instances=[mock.sentinel.instance1, mock.sentinel.instance2],
|
|
|
+ info={
|
|
|
+ mock.sentinel.instance1: {
|
|
|
+ 'volumes_info': mock.sentinel.volumes_info1
|
|
|
+ },
|
|
|
+ mock.sentinel.instance2: {
|
|
|
+ 'volumes_info': {}
|
|
|
+ },
|
|
|
+ },
|
|
|
+ instance_osmorphing_minion_pool_mappings={}
|
|
|
+ )
|
|
|
+
|
|
|
+ def call_deploy_replica_instance():
|
|
|
+ return self.server.deploy_replica_instances(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock.sentinel.replica_id,
|
|
|
+ clone_disks=clone_disks,
|
|
|
+ force=False,
|
|
|
+ instance_osmorphing_minion_pool_mappings=(
|
|
|
+ instance_osmorphing_minion_pool_mappings),
|
|
|
+ skip_os_morphing=skip_os_morphing,
|
|
|
+ user_scripts=mock.sentinel.user_scripts,
|
|
|
+ )
|
|
|
+
|
|
|
+ # One of the instances has no volumes info
|
|
|
+ self.assertRaises(
|
|
|
+ exception.InvalidReplicaState,
|
|
|
+ call_deploy_replica_instance,
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_get_endpoint.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_get_replica.return_value.destination_endpoint_id
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_get_replica.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock.sentinel.replica_id,
|
|
|
+ include_task_info=True,
|
|
|
+ )
|
|
|
+ mock_check_reservation_for_transfer.assert_called_once_with(
|
|
|
+ mock_get_replica.return_value,
|
|
|
+ licensing_client.RESERVATION_TYPE_REPLICA
|
|
|
+ )
|
|
|
+ mock_check_replica_running_executions.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_get_replica.return_value
|
|
|
+ )
|
|
|
+ mock_check_valid_replica_tasks_execution.assert_called_once_with(
|
|
|
+ mock_get_replica.return_value,
|
|
|
+ False
|
|
|
+ )
|
|
|
+ mock_get_provider_types.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_get_endpoint.return_value
|
|
|
+ )
|
|
|
+
|
|
|
+ # add the missing volumes info
|
|
|
+ mock_get_replica.return_value.info[mock.sentinel.instance2] = {
|
|
|
+ 'volumes_info': mock.sentinel.volumes_info2
|
|
|
+ }
|
|
|
+
|
|
|
+ def create_task_side_effect(
|
|
|
+ instance,
|
|
|
+ task_type,
|
|
|
+ execution,
|
|
|
+ depends_on=None,
|
|
|
+ on_error=False,
|
|
|
+ on_error_only=False
|
|
|
+ ):
|
|
|
+ return mock.Mock(
|
|
|
+ id=task_type,
|
|
|
+ type=task_type,
|
|
|
+ instance=instance,
|
|
|
+ execution=execution,
|
|
|
+ depends_on=depends_on,
|
|
|
+ on_error=on_error,
|
|
|
+ on_error_only=on_error_only,
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_create_task.side_effect = create_task_side_effect
|
|
|
+
|
|
|
+ # no longer raises exception
|
|
|
+ migration = call_deploy_replica_instance()
|
|
|
+
|
|
|
+ mock_check_minion_pools_for_action.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_migration.return_value
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual(
|
|
|
+ mock_tasks_execution.return_value.status,
|
|
|
+ constants.EXECUTION_STATUS_UNEXECUTED
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ mock_tasks_execution.return_value.type,
|
|
|
+ constants.EXECUTION_TYPE_REPLICA_DEPLOY
|
|
|
+ )
|
|
|
+
|
|
|
+ for instance in mock_get_replica.return_value.instances:
|
|
|
+ mock_get_instance_scripts.assert_any_call(
|
|
|
+ mock.sentinel.user_scripts,
|
|
|
+ instance,
|
|
|
+ )
|
|
|
+ mock_create_task.assert_any_call(
|
|
|
+ instance,
|
|
|
+ constants.TASK_TYPE_VALIDATE_REPLICA_DEPLOYMENT_INPUTS,
|
|
|
+ mock_tasks_execution.return_value,
|
|
|
+ )
|
|
|
+
|
|
|
+ # tasks defined in the yaml config
|
|
|
+ for task in expected_tasks:
|
|
|
+ kwargs = {}
|
|
|
+ if 'on_error' in task:
|
|
|
+ kwargs = {'on_error': task['on_error']}
|
|
|
+ if 'on_error_only' in task:
|
|
|
+ kwargs = {'on_error_only': task['on_error_only']}
|
|
|
+ mock_create_task.assert_has_calls([
|
|
|
+ mock.call(
|
|
|
+ instance,
|
|
|
+ task['type'],
|
|
|
+ mock_tasks_execution.return_value,
|
|
|
+ depends_on=task['depends_on'],
|
|
|
+ **kwargs,
|
|
|
+ )
|
|
|
+ ])
|
|
|
+
|
|
|
+ mock_check_execution_tasks_sanity.assert_called_once_with(
|
|
|
+ mock_tasks_execution.return_value,
|
|
|
+ mock_migration.return_value.info,
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_add_migration.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_migration.return_value,
|
|
|
+ )
|
|
|
+
|
|
|
+ if not skip_os_morphing and has_os_morphing_minion:
|
|
|
+ mock_lock.assert_any_call(
|
|
|
+ constants.MIGRATION_LOCK_NAME_FORMAT
|
|
|
+ % mock_migration.return_value.id,
|
|
|
+ external=True,
|
|
|
+ )
|
|
|
+ mock_minion_manager_client\
|
|
|
+ .allocate_minion_machines_for_migration\
|
|
|
+ .assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_migration.return_value,
|
|
|
+ include_transfer_minions=False,
|
|
|
+ include_osmorphing_minions=True
|
|
|
+ )
|
|
|
+ mock_set_tasks_execution_status.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_tasks_execution.return_value,
|
|
|
+ constants.EXECUTION_STATUS_AWAITING_MINION_ALLOCATIONS
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ mock_begin_tasks.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_migration.return_value,
|
|
|
+ mock_tasks_execution.return_value,
|
|
|
+ )
|
|
|
+
|
|
|
+ mock_get_migration.assert_called_once_with(
|
|
|
+ mock.sentinel.context,
|
|
|
+ mock_migration.return_value.id,
|
|
|
+ )
|
|
|
+
|
|
|
+ self.assertEqual(
|
|
|
+ migration,
|
|
|
+ mock_get_migration.return_value
|
|
|
+ )
|