Sfoglia il codice sorgente

Merge pull request #249 from smiclea/test-deploy-replica

Add unit test for `deploy_replica_instances`
Daniel Vincze 3 anni fa
parent
commit
83cd5cfe6a

+ 182 - 0
coriolis/tests/conductor/rpc/data/deploy_replica_instance_config.yml

@@ -0,0 +1,182 @@
+- config:
+    skip_os_morphing: False
+    has_os_morphing_minion: True
+  expected_tasks:
+    - type: 'VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY'
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: 'CREATE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY']
+    - type: 'DEPLOY_REPLICA_INSTANCE_RESOURCES'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: 'ATTACH_VOLUMES_TO_OSMORPHING_MINION'
+      depends_on: ['VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY', 'DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: 'COLLECT_OS_MORPHING_INFO'
+      depends_on: ['ATTACH_VOLUMES_TO_OSMORPHING_MINION']
+    - type: 'OS_MORPHING'
+      depends_on: ['COLLECT_OS_MORPHING_INFO']
+    - type: 'DETACH_VOLUMES_FROM_OSMORPHING_MINION'
+      depends_on: ['ATTACH_VOLUMES_TO_OSMORPHING_MINION', 'OS_MORPHING']
+      on_error: True
+    - type: 'RELEASE_OSMORPHING_MINION'
+      depends_on: ['VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY', 'DETACH_VOLUMES_FROM_OSMORPHING_MINION']
+      on_error: True
+    - type: 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['OS_MORPHING', 'RELEASE_OSMORPHING_MINION']
+    - type: 'DELETE_REPLICA_TARGET_DISK_SNAPSHOTS'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: 'CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: 'RESTORE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+
+- config:
+    skip_os_morphing: False
+    has_os_morphing_minion: False
+  expected_tasks:
+    - type: 'CREATE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: 'DEPLOY_REPLICA_INSTANCE_RESOURCES'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: 'DEPLOY_OS_MORPHING_RESOURCES'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: 'OS_MORPHING'
+      depends_on: ['DEPLOY_OS_MORPHING_RESOURCES']
+    - type: 'DELETE_OS_MORPHING_RESOURCES'
+      depends_on: ['DEPLOY_OS_MORPHING_RESOURCES', 'OS_MORPHING']
+      on_error: True
+    - type: 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['OS_MORPHING', 'DELETE_OS_MORPHING_RESOURCES']
+    - type: 'DELETE_REPLICA_TARGET_DISK_SNAPSHOTS'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: 'CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: 'RESTORE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+
+- config:
+    skip_os_morphing: True
+  expected_tasks:
+    - type: 'CREATE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: 'DEPLOY_REPLICA_INSTANCE_RESOURCES'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: 'DELETE_REPLICA_TARGET_DISK_SNAPSHOTS'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: 'CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: 'RESTORE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+
+- config:
+    skip_os_morphing: True
+    get_optimal_flavor: True
+  expected_tasks:
+    - type: 'CREATE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: 'DEPLOY_REPLICA_INSTANCE_RESOURCES'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: 'GET_OPTIMAL_FLAVOR'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['GET_OPTIMAL_FLAVOR']
+    - type: 'DELETE_REPLICA_TARGET_DISK_SNAPSHOTS'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: 'CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: 'RESTORE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+
+- config:
+    skip_os_morphing: True
+    clone_disks: True
+  expected_tasks:
+    - type: 'CREATE_REPLICA_DISK_SNAPSHOTS'
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: 'DEPLOY_REPLICA_INSTANCE_RESOURCES'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: 'DELETE_REPLICA_TARGET_DISK_SNAPSHOTS'
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+    - type: 'CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT'
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+
+- config:
+    get_optimal_flavor: True
+    skip_os_morphing: False
+  expected_tasks:
+    - type: CREATE_REPLICA_DISK_SNAPSHOTS
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: DEPLOY_REPLICA_INSTANCE_RESOURCES
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: DEPLOY_OS_MORPHING_RESOURCES
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: OS_MORPHING
+      depends_on: ['DEPLOY_OS_MORPHING_RESOURCES']
+    - type: DELETE_OS_MORPHING_RESOURCES
+      depends_on: ['DEPLOY_OS_MORPHING_RESOURCES', 'OS_MORPHING']
+      on_error: True
+    - type: GET_OPTIMAL_FLAVOR
+      depends_on: ['OS_MORPHING', 'DELETE_OS_MORPHING_RESOURCES']
+    - type: FINALIZE_REPLICA_INSTANCE_DEPLOYMENT
+      depends_on: ['GET_OPTIMAL_FLAVOR']
+    - type: DELETE_REPLICA_TARGET_DISK_SNAPSHOTS
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: RESTORE_REPLICA_DISK_SNAPSHOTS
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True
+
+- config:
+    get_optimal_flavor: True
+    skip_os_morphing: False
+    has_os_morphing_minion: True
+  expected_tasks:
+    - type: VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY
+      depends_on: ['VALIDATE_REPLICA_DEPLOYMENT_INPUTS']
+    - type: DEPLOY_REPLICA_INSTANCE_RESOURCES
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS']
+    - type: ATTACH_VOLUMES_TO_OSMORPHING_MINION
+      depends_on: ['VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY', 'DEPLOY_REPLICA_INSTANCE_RESOURCES']
+    - type: COLLECT_OS_MORPHING_INFO
+      depends_on: ['ATTACH_VOLUMES_TO_OSMORPHING_MINION']
+    - type: OS_MORPHING
+      depends_on: ['COLLECT_OS_MORPHING_INFO']
+    - type: DETACH_VOLUMES_FROM_OSMORPHING_MINION
+      depends_on: ['ATTACH_VOLUMES_TO_OSMORPHING_MINION', 'OS_MORPHING']
+      on_error: True
+    - type: RELEASE_OSMORPHING_MINION
+      depends_on: ['VALIDATE_OSMORPHING_MINION_POOL_COMPATIBILITY', 'DETACH_VOLUMES_FROM_OSMORPHING_MINION']
+      on_error: True
+    - type: GET_OPTIMAL_FLAVOR
+      depends_on: ['OS_MORPHING', 'RELEASE_OSMORPHING_MINION']
+    - type: FINALIZE_REPLICA_INSTANCE_DEPLOYMENT
+      depends_on: ['GET_OPTIMAL_FLAVOR']
+    - type: DELETE_REPLICA_TARGET_DISK_SNAPSHOTS
+      depends_on: ['CREATE_REPLICA_DISK_SNAPSHOTS', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: False
+    - type: CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT
+      depends_on: ['DEPLOY_REPLICA_INSTANCE_RESOURCES', 'FINALIZE_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error_only: True
+    - type: RESTORE_REPLICA_DISK_SNAPSHOTS
+      depends_on: ['CLEANUP_FAILED_REPLICA_INSTANCE_DEPLOYMENT']
+      on_error: True

+ 282 - 2
coriolis/tests/conductor/rpc/test_server.py

@@ -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
+        )