diff --git a/nova/conductor/tasks/cross_cell_migrate.py b/nova/conductor/tasks/cross_cell_migrate.py index e713b7e97a..ed3688bec8 100644 --- a/nova/conductor/tasks/cross_cell_migrate.py +++ b/nova/conductor/tasks/cross_cell_migrate.py @@ -11,10 +11,12 @@ # under the License. import collections +import copy from oslo_log import log as logging from nova.conductor.tasks import base +from nova import context as nova_context from nova import exception from nova.i18n import _ from nova import network @@ -226,6 +228,9 @@ class CrossCellMigrationTask(base.TaskBase): self.host_selection = host_selection self.alternate_hosts = alternate_hosts + self._target_cell_instance = None + self._target_cell_context = None + self.network_api = network.API() self.volume_api = cinder.API() @@ -233,6 +238,41 @@ class CrossCellMigrationTask(base.TaskBase): # rollback routines if something fails. self._completed_tasks = collections.OrderedDict() + def _get_target_cell_mapping(self): + """Get the target host CellMapping for the selected host + + :returns: nova.objects.CellMapping for the cell of the selected target + host + :raises: nova.exception.CellMappingNotFound if the cell mapping for + the selected target host cannot be found (this should not happen + if the scheduler just selected it) + """ + return objects.CellMapping.get_by_uuid( + self.context, self.host_selection.cell_uuid) + + def _setup_target_cell_db(self): + """Creates the instance and its related records in the target cell + + Upon successful completion the self._target_cell_context and + self._target_cell_instance variables are set. + + :returns: The active Migration object from the target cell DB. + """ + LOG.debug('Setting up the target cell database for the instance and ' + 'its related records.', instance=self.instance) + target_cell_mapping = self._get_target_cell_mapping() + # Clone the context targeted at the source cell and then target the + # clone at the target cell. + self._target_cell_context = copy.copy(self.context) + nova_context.set_target_cell( + self._target_cell_context, target_cell_mapping) + task = TargetDBSetupTask( + self.context, self.instance, self.migration, + self._target_cell_context) + self._target_cell_instance, target_cell_migration = task.execute() + self._completed_tasks['TargetDBSetupTask'] = task + return target_cell_migration + def _perform_external_api_checks(self): """Performs checks on external service APIs for support. @@ -268,6 +308,15 @@ class CrossCellMigrationTask(base.TaskBase): # Make sure neutron and cinder APIs we need are available. self._perform_external_api_checks() + # Before preparing the target host create the instance record data + # in the target cell database since we cannot do anything in the + # target cell without having an instance record there. Remember that + # we lose the cell-targeting on the request context over RPC so we + # cannot simply pass the source cell context and instance over RPC + # to the target compute host and assume changes get mirrored back to + # the source cell database. + self._setup_target_cell_db() + def rollback(self): """Rollback based on how sub-tasks completed diff --git a/nova/tests/unit/conductor/tasks/test_cross_cell_migrate.py b/nova/tests/unit/conductor/tasks/test_cross_cell_migrate.py index 9031167a5c..c1540b7745 100644 --- a/nova/tests/unit/conductor/tasks/test_cross_cell_migrate.py +++ b/nova/tests/unit/conductor/tasks/test_cross_cell_migrate.py @@ -392,8 +392,10 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase): with test.nested( mock.patch.object(self.task.migration, 'save'), mock.patch.object(self.task, '_perform_external_api_checks'), + mock.patch.object(self.task, '_setup_target_cell_db'), ) as ( mock_migration_save, mock_perform_external_api_checks, + mock_setup_target_cell_db, ): self.task.execute() # Assert the calls @@ -401,6 +403,7 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase): 'Migration.cross_cell_move should be True.') mock_migration_save.assert_called_once_with() mock_perform_external_api_checks.assert_called_once_with() + mock_setup_target_cell_db.assert_called_once_with() # Now rollback the completed sub-tasks self.task.rollback() @@ -467,3 +470,36 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase): # The 2nd task rollback should have raised and been logged. mock_log_exception.assert_called_once() self.assertEqual('1', mock_log_exception.call_args[0][1]) + + @mock.patch('nova.objects.CellMapping.get_by_uuid') + @mock.patch('nova.context.set_target_cell') + @mock.patch.object(cross_cell_migrate.TargetDBSetupTask, 'execute') + def test_setup_target_cell_db(self, mock_target_db_set_task_execute, + mock_set_target_cell, mock_get_cell_mapping): + """Tests setting up and executing TargetDBSetupTask""" + mock_target_db_set_task_execute.return_value = ( + mock.sentinel.target_cell_instance, + mock.sentinel.target_cell_migration) + result = self.task._setup_target_cell_db() + mock_target_db_set_task_execute.assert_called_once_with() + mock_get_cell_mapping.assert_called_once_with( + self.task.context, self.task.host_selection.cell_uuid) + # The target_cell_context should be set on the main task but as a copy + # of the source context. + self.assertIsNotNone(self.task._target_cell_context) + self.assertIsNot(self.task._target_cell_context, self.task.context) + # The target cell context should have been targeted to the target + # cell mapping. + mock_set_target_cell.assert_called_once_with( + self.task._target_cell_context, mock_get_cell_mapping.return_value) + # The resulting migration record from TargetDBSetupTask should have + # been returned. + self.assertIs(result, mock.sentinel.target_cell_migration) + # The target_cell_instance should be set on the main task. + self.assertIsNotNone(self.task._target_cell_instance) + self.assertIs(self.task._target_cell_instance, + mock.sentinel.target_cell_instance) + # And the completed task should have been recorded for rollbacks. + self.assertIn('TargetDBSetupTask', self.task._completed_tasks) + self.assertIsInstance(self.task._completed_tasks['TargetDBSetupTask'], + cross_cell_migrate.TargetDBSetupTask)