nova/nova/tests/unit/conductor/tasks/test_cross_cell_migrate.py

1652 lines
82 KiB
Python

# 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 copy
import mock
from oslo_messaging import exceptions as messaging_exceptions
from oslo_utils.fixture import uuidsentinel as uuids
from oslo_utils import timeutils
from nova.compute import instance_actions
from nova.compute import power_state
from nova.compute import task_states
from nova.compute import utils as compute_utils
from nova.compute import vm_states
from nova.conductor.tasks import cross_cell_migrate
from nova import context as nova_context
from nova import exception
from nova.network import model as network_model
from nova import objects
from nova.objects import base as obj_base
from nova.objects import fields
from nova.objects import instance as instance_obj
from nova import test
from nova.tests.unit.db.main import test_api as test_db_api
from nova.tests.unit import fake_block_device
from nova.tests.unit import fake_instance
from nova.tests.unit.objects import test_compute_node
from nova.tests.unit.objects import test_instance_device_metadata
from nova.tests.unit.objects import test_instance_numa
from nova.tests.unit.objects import test_instance_pci_requests
from nova.tests.unit.objects import test_keypair
from nova.tests.unit.objects import test_migration
from nova.tests.unit.objects import test_pci_device
from nova.tests.unit.objects import test_service
from nova.tests.unit.objects import test_vcpu_model
class ObjectComparatorMixin(test_db_api.ModelsObjectComparatorMixin):
"""Mixin class to aid in comparing two objects."""
def _compare_objs(self, obj1, obj2, ignored_keys=None):
# We can always ignore id since it is not deterministic when records
# are copied over to the target cell database.
if ignored_keys is None:
ignored_keys = []
if 'id' not in ignored_keys:
ignored_keys.append('id')
prim1 = obj1.obj_to_primitive()['nova_object.data']
prim2 = obj2.obj_to_primitive()['nova_object.data']
if isinstance(obj1, obj_base.ObjectListBase):
self.assertEqual(len(obj1), len(obj2))
prim1 = [o['nova_object.data'] for o in prim1['objects']]
prim2 = [o['nova_object.data'] for o in prim2['objects']]
self._assertEqualListsOfObjects(
prim1, prim2, ignored_keys=ignored_keys)
else:
self._assertEqualObjects(prim1, prim2, ignored_keys=ignored_keys)
class TargetDBSetupTaskTestCase(test.TestCase, ObjectComparatorMixin):
def setUp(self):
super(TargetDBSetupTaskTestCase, self).setUp()
cells = list(self.cell_mappings.values())
self.source_cell = cells[0]
self.target_cell = cells[1]
# Pass is_admin=True because of the funky DB API
# _check_instance_exists_in_project check when creating instance tags.
self.source_context = nova_context.RequestContext(
user_id='fake-user', project_id='fake-project', is_admin=True)
self.target_context = self.source_context.elevated() # copy source
nova_context.set_target_cell(self.source_context, self.source_cell)
nova_context.set_target_cell(self.target_context, self.target_cell)
def _create_instance_data(self):
"""Creates an instance record and associated data like BDMs, VIFs,
migrations, etc in the source cell and returns the Instance object.
The idea is to create as many things from the
Instance.INSTANCE_OPTIONAL_ATTRS list as possible.
:returns: The created Instance and Migration objects
"""
# Create the nova-compute services record first.
fake_service = test_service._fake_service()
fake_service.pop('version', None) # version field is immutable
fake_service.pop('id', None) # cannot create with an id set
service = objects.Service(self.source_context, **fake_service)
service.create()
# Create the compute node using the service.
fake_compute_node = copy.copy(test_compute_node.fake_compute_node)
fake_compute_node['host'] = service.host
fake_compute_node['hypervisor_hostname'] = service.host
fake_compute_node['stats'] = {} # the object requires a dict
fake_compute_node['service_id'] = service.id
fake_compute_node.pop('id', None) # cannot create with an id set
compute_node = objects.ComputeNode(
self.source_context, **fake_compute_node)
compute_node.create()
# Build an Instance object with basic fields set.
updates = {
'metadata': {'foo': 'bar'},
'system_metadata': {'roles': ['member']},
'host': compute_node.host,
'node': compute_node.hypervisor_hostname
}
inst = fake_instance.fake_instance_obj(self.source_context, **updates)
delattr(inst, 'id') # cannot create an instance with an id set
# Now we have to dirty all of the fields because fake_instance_obj
# uses Instance._from_db_object to create the Instance object we have
# but _from_db_object calls obj_reset_changes() which resets all of
# the fields that were on the object, including the basic stuff like
# the 'host' field, which means those fields don't get set in the DB.
# TODO(mriedem): This should live in fake_instance_obj with a
# make_creatable kwarg.
for field in inst.obj_fields:
if field in inst:
setattr(inst, field, getattr(inst, field))
# Make sure at least one expected basic field is dirty on the Instance.
self.assertIn('host', inst.obj_what_changed())
# Set the optional fields on the instance before creating it.
inst.pci_requests = objects.InstancePCIRequests(requests=[
objects.InstancePCIRequest(
**test_instance_pci_requests.fake_pci_requests[0])])
inst.numa_topology = objects.InstanceNUMATopology(
cells=test_instance_numa.fake_obj_numa_topology.cells)
inst.trusted_certs = objects.TrustedCerts(ids=[uuids.cert])
inst.vcpu_model = test_vcpu_model.fake_vcpumodel
inst.keypairs = objects.KeyPairList(objects=[
objects.KeyPair(**test_keypair.fake_keypair)])
inst.device_metadata = (
test_instance_device_metadata.get_fake_obj_device_metadata(
self.source_context))
# FIXME(mriedem): db.instance_create does not handle tags
inst.obj_reset_changes(['tags'])
inst.create()
bdm = {
'instance_uuid': inst.uuid,
'source_type': 'volume',
'destination_type': 'volume',
'volume_id': uuids.volume_id,
'volume_size': 1,
'device_name': '/dev/vda',
}
bdm = objects.BlockDeviceMapping(
self.source_context,
**fake_block_device.FakeDbBlockDeviceDict(bdm_dict=bdm))
delattr(bdm, 'id') # cannot create a bdm with an id set
bdm.obj_reset_changes(['id'])
bdm.create()
vif = objects.VirtualInterface(
self.source_context, address='de:ad:be:ef:ca:fe', uuid=uuids.port,
instance_uuid=inst.uuid)
vif.create()
info_cache = objects.InstanceInfoCache().new(
self.source_context, inst.uuid)
info_cache.network_info = network_model.NetworkInfo([
network_model.VIF(id=vif.uuid, address=vif.address)])
info_cache.save(update_cells=False)
objects.TagList.create(self.source_context, inst.uuid, ['test'])
try:
raise test.TestingException('test-fault')
except test.TestingException as fault:
compute_utils.add_instance_fault_from_exc(
self.source_context, inst, fault)
objects.InstanceAction().action_start(
self.source_context, inst.uuid, 'resize', want_result=False)
objects.InstanceActionEvent().event_start(
self.source_context, inst.uuid, 'migrate_server',
want_result=False)
# Create a fake migration for the cross-cell resize operation.
migration = objects.Migration(
self.source_context,
**test_migration.fake_db_migration(
instance_uuid=inst.uuid, cross_cell_move=True,
migration_type='resize'))
delattr(migration, 'id') # cannot create a migration with an id set
migration.obj_reset_changes(['id'])
migration.create()
# Create an old non-resize migration to make sure it is copied to the
# target cell database properly.
old_migration = objects.Migration(
self.source_context,
**test_migration.fake_db_migration(
instance_uuid=inst.uuid, migration_type='live-migration',
status='completed', uuid=uuids.old_migration))
delattr(old_migration, 'id') # cannot create a migration with an id
old_migration.obj_reset_changes(['id'])
old_migration.create()
fake_pci_device = copy.copy(test_pci_device.fake_db_dev)
fake_pci_device['extra_info'] = {} # the object requires a dict
fake_pci_device['compute_node_id'] = compute_node.id
pci_device = objects.PciDevice.create(
self.source_context, fake_pci_device)
pci_device.allocate(inst) # sets the status and instance_uuid fields
pci_device.save()
# Return a fresh copy of the instance from the DB with as many joined
# fields loaded as possible.
expected_attrs = copy.copy(instance_obj.INSTANCE_OPTIONAL_ATTRS)
# Cannot load fault from get_by_uuid.
expected_attrs.remove('fault')
inst = objects.Instance.get_by_uuid(
self.source_context, inst.uuid, expected_attrs=expected_attrs)
return inst, migration
def test_execute_and_rollback(self):
"""Happy path test which creates an instance with related records
in a source cell and then executes TargetDBSetupTask to create those
same records in a target cell. Runs rollback to make sure the target
cell instance is deleted.
"""
source_cell_instance, migration = self._create_instance_data()
instance_uuid = source_cell_instance.uuid
task = cross_cell_migrate.TargetDBSetupTask(
self.source_context, source_cell_instance, migration,
self.target_context)
target_cell_instance = task.execute()[0]
# The instance in the target cell should be hidden.
self.assertTrue(target_cell_instance.hidden,
'Target cell instance should be hidden')
# Assert that the various records created in _create_instance_data are
# found in the target cell database. We ignore 'hidden' because the
# values are explicitly different between source and target DB. The
# pci_devices and services fields are not set on the target instance
# during TargetDBSetupTask.execute so we ignore those here and verify
# them below. tags are also special in that we have to lazy-load them
# on target_cell_instance so we check those explicitly below as well.
ignored_keys = ['hidden', 'pci_devices', 'services', 'tags']
self._compare_objs(source_cell_instance, target_cell_instance,
ignored_keys=ignored_keys)
# Explicitly compare flavor fields to make sure they are created and
# loaded properly.
for flavor_field in ('old_', 'new_', ''):
source_field = getattr(
source_cell_instance, flavor_field + 'flavor')
target_field = getattr(
target_cell_instance, flavor_field + 'flavor')
# old/new may not be set
if source_field is None or target_field is None:
self.assertIsNone(source_field)
self.assertIsNone(target_field)
else:
self._compare_objs(source_field, target_field)
# Compare PCI requests
self.assertIsNotNone(target_cell_instance.pci_requests)
self._compare_objs(source_cell_instance.pci_requests,
target_cell_instance.pci_requests)
# Compare requested instance NUMA topology
self.assertIsNotNone(target_cell_instance.numa_topology)
self._compare_objs(source_cell_instance.numa_topology,
target_cell_instance.numa_topology)
# Compare trusted certs
self.assertIsNotNone(target_cell_instance.trusted_certs)
self._compare_objs(source_cell_instance.trusted_certs,
target_cell_instance.trusted_certs)
# Compare vcpu_model
self.assertIsNotNone(target_cell_instance.vcpu_model)
self._compare_objs(source_cell_instance.vcpu_model,
target_cell_instance.vcpu_model)
# Compare keypairs
self.assertEqual(1, len(target_cell_instance.keypairs))
self._compare_objs(source_cell_instance.keypairs,
target_cell_instance.keypairs)
# Compare device_metadata
self.assertIsNotNone(target_cell_instance.device_metadata)
self._compare_objs(source_cell_instance.device_metadata,
target_cell_instance.device_metadata)
# Compare BDMs
target_bdms = target_cell_instance.get_bdms()
self.assertEqual(1, len(target_bdms))
self._compare_objs(source_cell_instance.get_bdms(), target_bdms)
self.assertEqual(source_cell_instance.uuid,
target_bdms[0].instance_uuid)
# Compare VIFs
source_vifs = objects.VirtualInterfaceList.get_by_instance_uuid(
self.source_context, instance_uuid)
target_vifs = objects.VirtualInterfaceList.get_by_instance_uuid(
self.target_context, instance_uuid)
self.assertEqual(1, len(target_vifs))
self._compare_objs(source_vifs, target_vifs)
# Compare info cache (there should be a single vif in the target)
self.assertEqual(1, len(target_cell_instance.info_cache.network_info))
self.assertEqual(target_vifs[0].uuid,
target_cell_instance.info_cache.network_info[0]['id'])
self._compare_objs(source_cell_instance.info_cache,
target_cell_instance.info_cache)
# Compare tags
self.assertEqual(1, len(target_cell_instance.tags))
self._compare_objs(source_cell_instance.tags,
target_cell_instance.tags)
# Assert that the fault from the source is not in the target.
self.assertIsNone(target_cell_instance.fault)
# Compare instance actions and events
source_actions = objects.InstanceActionList.get_by_instance_uuid(
self.source_context, instance_uuid)
target_actions = objects.InstanceActionList.get_by_instance_uuid(
self.target_context, instance_uuid)
self._compare_objs(source_actions, target_actions)
# The InstanceActionEvent.action_id is per-cell DB so we need to get
# the events per action and compare them but ignore the action_id.
source_events = objects.InstanceActionEventList.get_by_action(
self.source_context, source_actions[0].id)
target_events = objects.InstanceActionEventList.get_by_action(
self.target_context, target_actions[0].id)
self._compare_objs(source_events, target_events,
ignored_keys=['action_id'])
# Compare migrations
filters = {'instance_uuid': instance_uuid}
source_migrations = objects.MigrationList.get_by_filters(
self.source_context, filters)
target_migrations = objects.MigrationList.get_by_filters(
self.target_context, filters)
# There should be two migrations in the target cell.
self.assertEqual(2, len(target_migrations))
self._compare_objs(source_migrations, target_migrations)
# One should be a live-migration type (make sure Migration._from-db_obj
# did not set the migration_type for us).
migration_types = [mig.migration_type for mig in target_migrations]
self.assertIn('resize', migration_types)
self.assertIn('live-migration', migration_types)
# pci_devices and services should not have been copied over since they
# are specific to the compute node in the source cell database
for field in ('pci_devices', 'services'):
source_value = getattr(source_cell_instance, field)
self.assertEqual(
1, len(source_value),
'Unexpected number of %s in source cell instance' % field)
target_value = getattr(target_cell_instance, field)
self.assertEqual(
0, len(target_value),
'Unexpected number of %s in target cell instance' % field)
# Rollback the task and assert the instance and its related data are
# gone from the target cell database. Use a modified context to make
# sure the instance was hard-deleted.
task.rollback(test.TestingException('error'))
read_deleted_ctxt = self.target_context.elevated(read_deleted='yes')
self.assertRaises(exception.InstanceNotFound,
objects.Instance.get_by_uuid,
read_deleted_ctxt, target_cell_instance.uuid)
class CrossCellMigrationTaskTestCase(test.NoDBTestCase):
def setUp(self):
super(CrossCellMigrationTaskTestCase, self).setUp()
source_context = nova_context.get_context()
host_selection = objects.Selection(
service_host='target.host.com', cell_uuid=uuids.cell_uuid)
migration = objects.Migration(
id=1, cross_cell_move=False, source_compute='source.host.com')
instance = objects.Instance()
self.task = cross_cell_migrate.CrossCellMigrationTask(
source_context,
instance,
objects.Flavor(),
mock.sentinel.request_spec,
migration,
mock.sentinel.compute_rpcapi,
host_selection,
mock.sentinel.alternate_hosts)
def test_execute_and_rollback(self):
"""Basic test to just hit execute and rollback."""
# Mock out the things that execute calls
with test.nested(
mock.patch.object(self.task.source_migration, 'save'),
mock.patch.object(self.task, '_perform_external_api_checks'),
mock.patch.object(self.task, '_setup_target_cell_db'),
mock.patch.object(self.task, '_prep_resize_at_dest'),
mock.patch.object(self.task, '_prep_resize_at_source'),
mock.patch.object(self.task, '_finish_resize_at_dest'),
) as (
mock_migration_save, mock_perform_external_api_checks,
mock_setup_target_cell_db, mock_prep_resize_at_dest,
mock_prep_resize_at_source, mock_finish_resize_at_dest,
):
mock_setup_target_cell_db.return_value = (
mock.sentinel.target_cell_migration,
mock.sentinel.target_cell_mapping)
self.task.execute()
# Assert the calls
self.assertTrue(self.task.source_migration.cross_cell_move,
'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()
mock_prep_resize_at_dest.assert_called_once_with(
mock.sentinel.target_cell_migration)
mock_prep_resize_at_source.assert_called_once_with()
mock_finish_resize_at_dest.assert_called_once_with(
mock_prep_resize_at_dest.return_value,
mock.sentinel.target_cell_mapping,
mock_prep_resize_at_source.return_value)
# Now rollback the completed sub-tasks
self.task.rollback(test.TestingException('error'))
def test_perform_external_api_checks_ok(self):
"""Tests the happy path scenario where neutron APIs are new enough for
what we need.
"""
with mock.patch.object(
self.task.network_api, 'has_port_binding_extension',
return_value=True) as mock_neutron_check:
self.task._perform_external_api_checks()
mock_neutron_check.assert_called_once_with(self.task.context)
def test_perform_external_api_checks_old_neutron(self):
"""Tests the case that neutron API is old."""
with mock.patch.object(
self.task.network_api, 'has_port_binding_extension',
return_value=False):
ex = self.assertRaises(exception.MigrationPreCheckError,
self.task._perform_external_api_checks)
self.assertIn('Required networking service API extension', str(ex))
@mock.patch('nova.conductor.tasks.cross_cell_migrate.LOG.exception')
def test_rollback_idempotent(self, mock_log_exception):
"""Tests that the rollback routine hits all completed tasks even if
one or more of them fail their own rollback routine.
"""
# Mock out some completed tasks
for x in range(3):
task = mock.Mock()
# The 2nd task will fail its rollback.
if x == 1:
task.rollback.side_effect = test.TestingException('sub-task')
self.task._completed_tasks[str(x)] = task
# Run execute but mock _execute to fail somehow.
error = test.TestingException('main task')
with mock.patch.object(self.task, '_execute', side_effect=error):
# The TestingException from the main task should be raised.
ex = self.assertRaises(test.TestingException, self.task.execute)
self.assertEqual('main task', str(ex))
# And all three sub-task rollbacks should have been called.
for subtask in self.task._completed_tasks.values():
subtask.rollback.assert_called_once_with(error)
# 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 along with the target cell mapping.
self.assertIs(result[0], mock.sentinel.target_cell_migration)
self.assertIs(result[1], mock_get_cell_mapping.return_value)
# 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)
@mock.patch.object(cross_cell_migrate.PrepResizeAtDestTask, 'execute')
@mock.patch('nova.availability_zones.get_host_availability_zone',
return_value='cell2-az1')
def test_prep_resize_at_dest(self, mock_get_az, mock_task_execute):
"""Tests setting up and executing PrepResizeAtDestTask"""
# _setup_target_cell_db set the _target_cell_context and
# _target_cell_instance variables so fake those out here
self.task._target_cell_context = mock.sentinel.target_cell_context
target_inst = objects.Instance(
vm_state=vm_states.ACTIVE, system_metadata={})
self.task._target_cell_instance = target_inst
target_cell_migration = objects.Migration(
# use unique ids for comparisons
id=self.task.source_migration.id + 1)
self.assertNotIn('migration_context', self.task.instance)
mock_task_execute.return_value = objects.MigrationContext(
migration_id=target_cell_migration.id)
with test.nested(
mock.patch.object(self.task,
'_update_migration_from_dest_after_claim'),
mock.patch.object(self.task.instance, 'save'),
mock.patch.object(target_inst, 'save')
) as (
_upd_mig, source_inst_save, target_inst_save
):
retval = self.task._prep_resize_at_dest(target_cell_migration)
self.assertIs(retval, _upd_mig.return_value)
mock_task_execute.assert_called_once_with()
mock_get_az.assert_called_once_with(
self.task.context, self.task.host_selection.service_host)
self.assertIn('PrepResizeAtDestTask', self.task._completed_tasks)
self.assertIsInstance(
self.task._completed_tasks['PrepResizeAtDestTask'],
cross_cell_migrate.PrepResizeAtDestTask)
# The new_flavor should be set on the target cell instance along with
# the AZ and old_vm_state.
self.assertIs(target_inst.new_flavor, self.task.flavor)
self.assertEqual(vm_states.ACTIVE,
target_inst.system_metadata['old_vm_state'])
self.assertEqual(mock_get_az.return_value,
target_inst.availability_zone)
# A clone of the MigrationContext returned from execute() should be
# stored on the source instance with the internal context targeted
# at the source cell context and the migration_id updated.
self.assertIsNotNone(self.task.instance.migration_context)
self.assertEqual(self.task.source_migration.id,
self.task.instance.migration_context.migration_id)
source_inst_save.assert_called_once_with()
_upd_mig.assert_called_once_with(target_cell_migration)
@mock.patch('nova.objects.Migration.get_by_uuid')
def test_update_migration_from_dest_after_claim(self, get_by_uuid):
"""Tests the _update_migration_from_dest_after_claim method."""
self.task._target_cell_context = mock.sentinel.target_cell_context
target_cell_migration = objects.Migration(
uuid=uuids.migration, cross_cell_move=True,
dest_compute='dest-compute', dest_node='dest-node',
dest_host='192.168.159.176')
get_by_uuid.return_value = target_cell_migration.obj_clone()
with mock.patch.object(self.task.source_migration, 'save') as save:
retval = self.task._update_migration_from_dest_after_claim(
target_cell_migration)
# The returned target cell migration should be the one we pulled from
# the target cell database.
self.assertIs(retval, get_by_uuid.return_value)
get_by_uuid.assert_called_once_with(
self.task._target_cell_context, target_cell_migration.uuid)
# The source cell migration on the task should have been updated.
source_cell_migration = self.task.source_migration
self.assertEqual('dest-compute', source_cell_migration.dest_compute)
self.assertEqual('dest-node', source_cell_migration.dest_node)
self.assertEqual('192.168.159.176', source_cell_migration.dest_host)
save.assert_called_once_with()
@mock.patch.object(cross_cell_migrate.PrepResizeAtSourceTask, 'execute')
def test_prep_resize_at_source(self, mock_task_execute):
"""Tests setting up and executing PrepResizeAtSourceTask"""
snapshot_id = self.task._prep_resize_at_source()
self.assertIs(snapshot_id, mock_task_execute.return_value)
self.assertIn('PrepResizeAtSourceTask', self.task._completed_tasks)
self.assertIsInstance(
self.task._completed_tasks['PrepResizeAtSourceTask'],
cross_cell_migrate.PrepResizeAtSourceTask)
@mock.patch.object(cross_cell_migrate.FinishResizeAtDestTask, 'execute')
def test_finish_resize_at_dest(self, mock_task_execute):
"""Tests setting up and executing FinishResizeAtDestTask"""
target_cell_migration = objects.Migration()
target_cell_mapping = objects.CellMapping()
self.task._finish_resize_at_dest(
target_cell_migration, target_cell_mapping, uuids.snapshot_id)
mock_task_execute.assert_called_once_with()
self.assertIn('FinishResizeAtDestTask', self.task._completed_tasks)
self.assertIsInstance(
self.task._completed_tasks['FinishResizeAtDestTask'],
cross_cell_migrate.FinishResizeAtDestTask)
class PrepResizeAtDestTaskTestCase(test.NoDBTestCase):
def setUp(self):
super(PrepResizeAtDestTaskTestCase, self).setUp()
host_selection = objects.Selection(
service_host='fake-host', nodename='fake-host',
limits=objects.SchedulerLimits())
self.task = cross_cell_migrate.PrepResizeAtDestTask(
nova_context.get_context(),
objects.Instance(uuid=uuids.instance),
objects.Flavor(),
objects.Migration(),
objects.RequestSpec(),
compute_rpcapi=mock.Mock(),
host_selection=host_selection,
network_api=mock.Mock(),
volume_api=mock.Mock())
def test_create_port_bindings(self):
"""Happy path test for creating port bindings"""
with mock.patch.object(
self.task.network_api, 'bind_ports_to_host') as mock_bind:
self.task._create_port_bindings()
self.assertIs(self.task._bindings_by_port_id, mock_bind.return_value)
mock_bind.assert_called_once_with(
self.task.context, self.task.instance,
self.task.host_selection.service_host)
def test_create_port_bindings_port_binding_failed(self):
"""Tests that bind_ports_to_host raises PortBindingFailed which
results in a MigrationPreCheckError.
"""
with mock.patch.object(
self.task.network_api, 'bind_ports_to_host',
side_effect=exception.PortBindingFailed(
port_id=uuids.port_id)) as mock_bind:
self.assertRaises(exception.MigrationPreCheckError,
self.task._create_port_bindings)
self.assertEqual({}, self.task._bindings_by_port_id)
mock_bind.assert_called_once_with(
self.task.context, self.task.instance,
self.task.host_selection.service_host)
@mock.patch('nova.objects.BlockDeviceMapping.save')
def test_create_volume_attachments(self, mock_bdm_save):
"""Happy path test for creating volume attachments"""
# Two BDMs: one as a local image and one as an attached data volume;
# only the volume BDM should be processed and returned.
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(
source_type='image', destination_type='local'),
objects.BlockDeviceMapping(
source_type='volume', destination_type='volume',
volume_id=uuids.volume_id,
instance_uuid=self.task.instance.uuid)])
with test.nested(
mock.patch.object(
self.task.instance, 'get_bdms', return_value=bdms),
mock.patch.object(
self.task.volume_api, 'attachment_create',
return_value={'id': uuids.attachment_id}),
) as (
mock_get_bdms, mock_attachment_create
):
volume_bdms = self.task._create_volume_attachments()
mock_attachment_create.assert_called_once_with(
self.task.context, uuids.volume_id, self.task.instance.uuid)
# The created attachment ID should be saved for rollbacks.
self.assertEqual(1, len(self.task._created_volume_attachment_ids))
self.assertEqual(
uuids.attachment_id, self.task._created_volume_attachment_ids[0])
# Only the volume BDM should have been processed and returned.
self.assertEqual(1, len(volume_bdms))
self.assertIs(bdms[1], volume_bdms[0])
# The volume BDM attachment_id should have been updated.
self.assertEqual(uuids.attachment_id, volume_bdms[0].attachment_id)
def test_execute(self):
"""Happy path for executing the task"""
def fake_create_port_bindings():
self.task._bindings_by_port_id = mock.sentinel.bindings
with test.nested(
mock.patch.object(self.task, '_create_port_bindings',
side_effect=fake_create_port_bindings),
mock.patch.object(self.task, '_create_volume_attachments'),
mock.patch.object(
self.task.compute_rpcapi, 'prep_snapshot_based_resize_at_dest')
) as (
_create_port_bindings, _create_volume_attachments,
prep_snapshot_based_resize_at_dest
):
# Execute the task. The return value should be the MigrationContext
# returned from prep_snapshot_based_resize_at_dest.
self.assertEqual(
prep_snapshot_based_resize_at_dest.return_value,
self.task.execute())
_create_port_bindings.assert_called_once_with()
_create_volume_attachments.assert_called_once_with()
prep_snapshot_based_resize_at_dest.assert_called_once_with(
self.task.context, self.task.instance, self.task.flavor,
self.task.host_selection.nodename, self.task.target_migration,
self.task.host_selection.limits, self.task.request_spec,
self.task.host_selection.service_host)
def test_execute_messaging_timeout(self):
"""Tests the case that prep_snapshot_based_resize_at_dest raises
MessagingTimeout which results in a MigrationPreCheckError.
"""
with test.nested(
mock.patch.object(self.task, '_create_port_bindings'),
mock.patch.object(self.task, '_create_volume_attachments'),
mock.patch.object(
self.task.compute_rpcapi, 'prep_snapshot_based_resize_at_dest',
side_effect=messaging_exceptions.MessagingTimeout)
) as (
_create_port_bindings, _create_volume_attachments,
prep_snapshot_based_resize_at_dest
):
ex = self.assertRaises(
exception.MigrationPreCheckError, self.task.execute)
self.assertIn(
'RPC timeout while checking if we can cross-cell migrate to '
'host: fake-host', str(ex))
_create_port_bindings.assert_called_once_with()
_create_volume_attachments.assert_called_once_with()
prep_snapshot_based_resize_at_dest.assert_called_once_with(
self.task.context, self.task.instance, self.task.flavor,
self.task.host_selection.nodename, self.task.target_migration,
self.task.host_selection.limits, self.task.request_spec,
self.task.host_selection.service_host)
@mock.patch('nova.conductor.tasks.cross_cell_migrate.LOG.exception')
def test_rollback(self, mock_log_exception):
"""Tests rollback to make sure it idempotently handles cleaning up
port bindings and volume attachments even if one in the set fails for
each.
"""
# Make sure we have two port bindings and two volume attachments
# because we are going to make the first of each fail and we want to
# make sure we still try to delete the other.
self.task._bindings_by_port_id = {
uuids.port_id1: mock.sentinel.binding1,
uuids.port_id2: mock.sentinel.binding2
}
self.task._created_volume_attachment_ids = [
uuids.attachment_id1, uuids.attachment_id2
]
with test.nested(
mock.patch.object(
self.task.network_api, 'delete_port_binding',
# First call fails, second is OK.
side_effect=(exception.PortBindingDeletionFailed, None)),
mock.patch.object(
self.task.volume_api, 'attachment_delete',
# First call fails, second is OK.
side_effect=(exception.CinderConnectionFailed, None)),
) as (
delete_port_binding, attachment_delete
):
self.task.rollback(test.TestingException('error'))
# Should have called both delete methods twice in any order.
host = self.task.host_selection.service_host
delete_port_binding.assert_has_calls([
mock.call(self.task.context, port_id, host)
for port_id in self.task._bindings_by_port_id],
any_order=True)
attachment_delete.assert_has_calls([
mock.call(self.task.context, attachment_id)
for attachment_id in self.task._created_volume_attachment_ids],
any_order=True)
# Should have logged both exceptions.
self.assertEqual(2, mock_log_exception.call_count)
class PrepResizeAtSourceTaskTestCase(test.NoDBTestCase):
def setUp(self):
super(PrepResizeAtSourceTaskTestCase, self).setUp()
self.task = cross_cell_migrate.PrepResizeAtSourceTask(
nova_context.get_context(),
objects.Instance(
uuid=uuids.instance,
vm_state=vm_states.ACTIVE,
display_name='fake-server',
system_metadata={},
host='source.host.com',
flavor=objects.Flavor(),
),
objects.Flavor(),
objects.Migration(),
objects.RequestSpec(),
compute_rpcapi=mock.Mock(),
image_api=mock.Mock())
@mock.patch('nova.compute.utils.create_image')
@mock.patch('nova.objects.Instance.save')
def test_execute_volume_backed(self, instance_save, create_image):
"""Tests execution with a volume-backed server so no snapshot image
is created.
"""
self.task.request_spec.is_bfv = True
# No image should be created so no image is returned.
self.assertIsNone(self.task.execute())
self.assertIsNone(self.task._image_id)
create_image.assert_not_called()
self.task.compute_rpcapi.prep_snapshot_based_resize_at_source.\
assert_called_once_with(
self.task.context, self.task.instance, self.task.migration,
snapshot_id=None)
# The instance should have been updated.
instance_save.assert_called_once_with(
expected_task_state=task_states.RESIZE_PREP)
self.assertIs(self.task.instance.old_flavor, self.task.instance.flavor)
self.assertIs(self.task.instance.new_flavor, self.task.flavor)
self.assertEqual(
task_states.RESIZE_MIGRATING, self.task.instance.task_state)
self.assertEqual(self.task.instance.vm_state,
self.task.instance.system_metadata['old_vm_state'])
@mock.patch('nova.compute.utils.create_image',
return_value={'id': uuids.snapshot_id})
@mock.patch('nova.objects.Instance.save')
def test_execute_image_backed(self, instance_save, create_image):
"""Tests execution with an image-backed server so a snapshot image
is created.
"""
self.task.request_spec.is_bfv = False
self.task.instance.image_ref = uuids.old_image_ref
# An image should be created so an image ID is returned.
self.assertEqual(uuids.snapshot_id, self.task.execute())
self.assertEqual(uuids.snapshot_id, self.task._image_id)
create_image.assert_called_once_with(
self.task.context, self.task.instance, 'fake-server-resize-temp',
'snapshot', self.task.image_api)
self.task.compute_rpcapi.prep_snapshot_based_resize_at_source.\
assert_called_once_with(
self.task.context, self.task.instance, self.task.migration,
snapshot_id=uuids.snapshot_id)
# The instance should have been updated.
instance_save.assert_called_once_with(
expected_task_state=task_states.RESIZE_PREP)
self.assertIs(self.task.instance.old_flavor, self.task.instance.flavor)
self.assertIs(self.task.instance.new_flavor, self.task.flavor)
self.assertEqual(
task_states.RESIZE_MIGRATING, self.task.instance.task_state)
self.assertEqual(self.task.instance.vm_state,
self.task.instance.system_metadata['old_vm_state'])
@mock.patch('nova.compute.utils.delete_image')
def test_rollback(self, delete_image):
"""Tests rollback when there is an image and when there is not."""
# First test when there is no image_id so we do not try to delete it.
self.task.rollback(test.TestingException('error'))
delete_image.assert_not_called()
# Now set an image and we should try to delete it.
self.task._image_id = uuids.image_id
self.task.rollback(test.TestingException('error'))
delete_image.assert_called_once_with(
self.task.context, self.task.instance, self.task.image_api,
self.task._image_id)
class FinishResizeAtDestTaskTestCase(test.TestCase):
"""Tests for FinishResizeAtDestTask which rely on a database"""
def _create_instance(self, ctxt, create_instance_mapping=False, **updates):
"""Create a fake instance with the given cell-targeted context
:param ctxt: Cell-targeted RequestContext
:param create_instance_mapping: If True, create an InstanceMapping
for the instance pointed at the cell in which the ctxt is targeted,
otherwise no InstanceMapping is created.
:param updates: Additional fields to set on the Instance object.
:returns: Instance object that was created.
"""
inst = fake_instance.fake_instance_obj(ctxt, **updates)
delattr(inst, 'id') # make it creatable
# Now we have to dirty all of the fields because fake_instance_obj
# uses Instance._from_db_object to create the Instance object we have
# but _from_db_object calls obj_reset_changes() which resets all of
# the fields that were on the object, including the basic stuff like
# the 'host' field, which means those fields don't get set in the DB.
# TODO(mriedem): This should live in fake_instance_obj
for field in inst.obj_fields:
if field in inst:
setattr(inst, field, getattr(inst, field))
# FIXME(mriedem): db.instance_create does not handle tags
inst.obj_reset_changes(['tags'])
inst.create()
if create_instance_mapping:
# Find the cell mapping from the context.
self.assertIsNotNone(ctxt.cell_uuid,
'ctxt must be targeted to a cell.')
for cell in self.cell_mappings.values():
if cell.uuid == ctxt.cell_uuid:
break
else:
raise Exception('Unable to find CellMapping with UUID %s' %
ctxt.cell_uuid)
mapping = objects.InstanceMapping(
ctxt, instance_uuid=inst.uuid,
project_id=inst.project_id, cell_mapping=cell)
mapping.create()
return inst
def setUp(self):
super(FinishResizeAtDestTaskTestCase, self).setUp()
cells = list(self.cell_mappings.values())
source_cell = cells[0]
target_cell = cells[1]
self.source_context = nova_context.RequestContext(
user_id='fake-user', project_id='fake-project', is_admin=True)
self.target_context = self.source_context.elevated() # copy source
nova_context.set_target_cell(self.source_context, source_cell)
nova_context.set_target_cell(self.target_context, target_cell)
# Create the source cell instance.
source_instance = self._create_instance(
self.source_context, create_instance_mapping=True,
hidden=False)
# Create the instance action record in the source cell which is needed
# by the EventReporter.
objects.InstanceAction.action_start(
self.source_context, source_instance.uuid,
instance_actions.RESIZE, want_result=False)
# Create the target cell instance which would normally be a clone of
# the source cell instance but the only thing these tests care about
# is that the UUID matches. The target cell instance is also hidden.
target_instance = self._create_instance(
self.target_context, hidden=True, uuid=source_instance.uuid)
target_migration = objects.Migration(dest_compute='target.host.com')
self.task = cross_cell_migrate.FinishResizeAtDestTask(
self.target_context, target_instance, target_migration,
source_instance, compute_rpcapi=mock.Mock(),
target_cell_mapping=target_cell, snapshot_id=uuids.snapshot_id,
request_spec=objects.RequestSpec())
def test_execute(self):
"""Tests the happy path scenario for the task execution."""
with test.nested(
mock.patch.object(
self.task.compute_rpcapi,
'finish_snapshot_based_resize_at_dest'),
mock.patch.object(self.task.instance, 'refresh')
) as (
finish_resize, refresh
):
self.task.execute()
# _finish_snapshot_based_resize_at_dest will set the instance
# task_state to resize_migrated, save the change, and call the
# finish_snapshot_based_resize_at_dest method.
target_instance = self.task.instance
self.assertEqual(task_states.RESIZE_MIGRATED,
self.task.instance.task_state)
finish_resize.assert_called_once_with(
self.task.context, target_instance, self.task.migration,
self.task.snapshot_id, self.task.request_spec)
refresh.assert_called_once_with()
# _update_instance_mapping will swap the hidden fields and update
# the instance mapping to point at the target cell.
self.assertFalse(target_instance.hidden,
'Target cell instance should not be hidden')
source_instance = self.task.source_cell_instance
source_instance.refresh()
self.assertTrue(source_instance.hidden,
'Source cell instance should be hidden')
mapping = objects.InstanceMapping.get_by_instance_uuid(
self.task.context, target_instance.uuid)
self.assertEqual(self.target_context.cell_uuid,
mapping.cell_mapping.uuid)
@mock.patch('nova.objects.InstanceMapping.save')
def test_finish_snapshot_based_resize_at_dest_fails(self, mock_im_save):
"""Tests when the finish_snapshot_based_resize_at_dest compute method
raises an error.
"""
with test.nested(
mock.patch.object(self.task.compute_rpcapi,
'finish_snapshot_based_resize_at_dest',
side_effect=test.TestingException('oops')),
mock.patch.object(self.task, '_copy_latest_fault'),
) as (
finish_resize, copy_fault
):
self.assertRaises(test.TestingException,
self.task._finish_snapshot_based_resize_at_dest)
# The source cell instance should be in error state.
source_instance = self.task.source_cell_instance
source_instance.refresh()
self.assertEqual(vm_states.ERROR, source_instance.vm_state)
self.assertIsNone(source_instance.task_state)
# And the latest fault and instance action event should have been
# copied from the target cell DB to the source cell DB.
copy_fault.assert_called_once_with(self.source_context)
# Assert the event was recorded in the source cell DB.
event_name = 'compute_finish_snapshot_based_resize_at_dest'
action = objects.InstanceAction.get_by_request_id(
source_instance._context, source_instance.uuid,
source_instance._context.request_id)
self.assertIsNotNone(action, 'InstanceAction not found.')
events = objects.InstanceActionEventList.get_by_action(
source_instance._context, action.id)
self.assertEqual(1, len(events), events)
self.assertEqual(event_name, events[0].event)
self.assertEqual('Error', events[0].result)
self.assertIn('_finish_snapshot_based_resize_at_dest',
events[0].traceback)
self.assertEqual(self.task.migration.dest_compute, events[0].host)
# Assert the instance mapping was never updated.
mock_im_save.assert_not_called()
def test_copy_latest_fault(self):
"""Tests _copy_latest_fault working as expected"""
# Inject a fault in the target cell database.
try:
raise test.TestingException('test-fault')
except test.TestingException as fault:
compute_utils.add_instance_fault_from_exc(
self.target_context, self.task.instance, fault)
self.task._copy_latest_fault(self.source_context)
# Now make sure that fault shows up in the source cell DB (it will
# get lazy-loaded here).
fault = self.task.source_cell_instance.fault
self.assertIsNotNone(fault, 'Fault not copied to source cell DB')
# And it's the fault we expect.
self.assertEqual('TestingException', fault.message)
@mock.patch('nova.conductor.tasks.cross_cell_migrate.LOG.exception')
def test_copy_latest_fault_error(self, mock_log):
"""Tests that _copy_latest_fault errors are swallowed"""
with mock.patch('nova.objects.InstanceFault.get_latest_for_instance',
side_effect=test.TestingException):
self.task._copy_latest_fault(self.source_context)
# The source cell should not have a fault.
self.assertIsNone(self.task.source_cell_instance.fault)
# The error should have been logged.
mock_log.assert_called_once()
self.assertIn('Failed to copy instance fault from target cell DB',
mock_log.call_args[0][0])
class UtilityTestCase(test.NoDBTestCase):
"""Tests utility methods in the cross_cell_migrate module."""
@mock.patch('nova.objects.HostMapping.get_by_host',
return_value=objects.HostMapping(
cell_mapping=objects.CellMapping(uuid=uuids.cell)))
@mock.patch('nova.objects.Instance.get_by_uuid')
def test_get_inst_and_cell_map_from_source(self, mock_get_inst,
mock_get_by_host):
target_cell_context = nova_context.get_admin_context()
# Stub out Instance.get_by_uuid to make sure a copy of the context is
# targeted at the source cell mapping.
def stub_get_by_uuid(ctxt, *args, **kwargs):
self.assertIsNot(ctxt, target_cell_context)
self.assertEqual(uuids.cell, ctxt.cell_uuid)
return mock.sentinel.instance
mock_get_inst.side_effect = stub_get_by_uuid
inst, cell_mapping = (
cross_cell_migrate.get_inst_and_cell_map_from_source(
target_cell_context, 'source-host', uuids.instance))
self.assertIs(inst, mock.sentinel.instance)
self.assertIs(cell_mapping, mock_get_by_host.return_value.cell_mapping)
mock_get_by_host.assert_called_once_with(
target_cell_context, 'source-host')
mock_get_inst.assert_called_once_with(
test.MatchType(nova_context.RequestContext), uuids.instance,
expected_attrs=['flavor', 'info_cache', 'system_metadata'])
class ConfirmResizeTaskTestCase(test.NoDBTestCase):
def setUp(self):
super(ConfirmResizeTaskTestCase, self).setUp()
context = nova_context.get_admin_context()
compute_rpcapi = mock.Mock()
self.task = cross_cell_migrate.ConfirmResizeTask(
context,
objects.Instance(context, uuid=uuids.instance,
host='target-host', vm_state=vm_states.RESIZED,
system_metadata={
'old_vm_state': vm_states.ACTIVE}),
objects.Migration(context, uuid=uuids.migration,
dest_compute='target-host',
source_compute='source-host',
status='confirming'),
mock.sentinel.legacy_notifier,
compute_rpcapi)
@mock.patch('nova.conductor.tasks.cross_cell_migrate.'
'get_inst_and_cell_map_from_source')
def test_execute(self, mock_get_instance):
source_cell_instance = objects.Instance(
mock.MagicMock(), uuid=uuids.instance)
source_cell_instance.destroy = mock.Mock()
mock_get_instance.return_value = (
source_cell_instance, objects.CellMapping())
with test.nested(
mock.patch.object(self.task, '_send_resize_confirm_notification'),
mock.patch.object(self.task, '_cleanup_source_host'),
mock.patch.object(self.task, '_finish_confirm_in_target_cell')
) as (
_send_resize_confirm_notification, _cleanup_source_host,
_finish_confirm_in_target_cell
):
self.task.execute()
mock_get_instance.assert_called_once_with(
self.task.context, self.task.migration.source_compute,
self.task.instance.uuid)
self.assertEqual(2, _send_resize_confirm_notification.call_count)
_send_resize_confirm_notification.assert_has_calls([
mock.call(source_cell_instance, fields.NotificationPhase.START),
mock.call(self.task.instance, fields.NotificationPhase.END)])
_cleanup_source_host.assert_called_once_with(source_cell_instance)
source_cell_instance.destroy.assert_called_once_with(
hard_delete=True)
_finish_confirm_in_target_cell.assert_called_once_with()
@mock.patch('nova.conductor.tasks.cross_cell_migrate.'
'get_inst_and_cell_map_from_source',
side_effect=exception.InstanceNotFound(
instance_id=uuids.instance))
@mock.patch('nova.objects.Migration.save')
@mock.patch('nova.objects.RequestSpec.get_by_instance_uuid')
@mock.patch('nova.scheduler.utils.set_vm_state_and_notify')
def test_rollback(self, mock_set_state_notify, mock_get_reqspec,
mock_mig_save, mock_get_instance):
self.assertRaises(exception.InstanceNotFound, self.task.execute)
mock_get_instance.assert_called_once_with(
self.task.context, self.task.migration.source_compute,
self.task.instance.uuid)
self.assertEqual('error', self.task.migration.status)
mock_mig_save.assert_called_once_with()
mock_get_reqspec.assert_called_once_with(
self.task.context, self.task.instance.uuid)
mock_set_state_notify.assert_called_once_with(
self.task.context, self.task.instance.uuid, 'compute_task',
'migrate_server',
{'vm_state': vm_states.ERROR, 'task_state': None},
mock_get_instance.side_effect,
mock_get_reqspec.return_value)
@mock.patch('nova.compute.utils.notify_about_instance_usage')
@mock.patch('nova.compute.utils.notify_about_instance_action')
def test_send_resize_confirm_notification(self, mock_versioned_notify,
mock_legacy_notify):
self.flags(host='fake-conductor-host')
instance = self.task.instance
self.task._send_resize_confirm_notification(instance, 'fake-phase')
mock_legacy_notify.assert_called_once_with(
self.task.legacy_notifier, instance._context, instance,
'resize.confirm.fake-phase')
mock_versioned_notify.assert_called_once_with(
instance._context, instance, 'fake-conductor-host',
action=fields.NotificationAction.RESIZE_CONFIRM,
phase='fake-phase')
@mock.patch('nova.objects.InstanceAction.action_start')
@mock.patch('nova.objects.Migration.get_by_uuid')
@mock.patch('nova.objects.InstanceActionEvent') # stub EventReporter calls
def test_cleanup_source_host(
self, mock_action_event, mock_get_mig, mock_action_start):
instance = objects.Instance(nova_context.get_admin_context(),
uuid=uuids.instance,
flavor=objects.Flavor())
self.task._cleanup_source_host(instance)
mock_action_start.assert_called_once_with(
instance._context, instance.uuid, instance_actions.CONFIRM_RESIZE,
want_result=False)
mock_get_mig.assert_called_once_with(
instance._context, self.task.migration.uuid)
self.task.compute_rpcapi.confirm_snapshot_based_resize_at_source.\
assert_called_once_with(instance._context, instance,
mock_get_mig.return_value)
mock_action_event.event_start.assert_called_once_with(
self.task.context, uuids.instance,
'compute_confirm_snapshot_based_resize_at_source',
want_result=False, host=mock_get_mig.return_value.source_compute)
mock_action_event.event_finish_with_failure.assert_called_once_with(
self.task.context, uuids.instance,
'compute_confirm_snapshot_based_resize_at_source',
exc_val=None, exc_tb=None, want_result=False)
@mock.patch('nova.objects.Migration.save')
@mock.patch('nova.objects.Instance.save')
@mock.patch('nova.objects.Instance.drop_migration_context')
def test_finish_confirm_in_target_cell(self, mock_drop_ctx, mock_inst_save,
mock_mig_save):
with mock.patch.object(
self.task, '_set_vm_and_task_state') as mock_set_state:
self.task._finish_confirm_in_target_cell()
self.assertEqual('confirmed', self.task.migration.status)
mock_mig_save.assert_called_once_with()
self.assertNotIn('old_vm_state', self.task.instance.system_metadata)
self.assertIsNone(self.task.instance.old_flavor)
self.assertIsNone(self.task.instance.new_flavor)
mock_set_state.assert_called_once_with()
mock_drop_ctx.assert_called_once_with()
mock_inst_save.assert_called_once_with(expected_task_state=[
None, task_states.DELETING, task_states.SOFT_DELETING])
def test_set_vm_and_task_state_shutdown(self):
self.task.instance.power_state = power_state.SHUTDOWN
self.task._set_vm_and_task_state()
self.assertEqual(vm_states.STOPPED, self.task.instance.vm_state)
self.assertIsNone(self.task.instance.task_state)
def test_set_vm_and_task_state_active(self):
self.task.instance.power_state = power_state.RUNNING
self.task._set_vm_and_task_state()
self.assertEqual(vm_states.ACTIVE, self.task.instance.vm_state)
self.assertIsNone(self.task.instance.task_state)
class RevertResizeTaskTestCase(test.NoDBTestCase, ObjectComparatorMixin):
def setUp(self):
super(RevertResizeTaskTestCase, self).setUp()
target_cell_context = nova_context.get_admin_context()
target_cell_context.cell_uuid = uuids.target_cell
instance = fake_instance.fake_instance_obj(
target_cell_context, **{
'vm_state': vm_states.RESIZED,
'task_state': task_states.RESIZE_REVERTING,
'expected_attrs': ['system_metadata', 'flavor']
})
migration = objects.Migration(
target_cell_context, uuid=uuids.migration, status='reverting',
source_compute='source-host', dest_compute='dest-host')
legacy_notifier = mock.MagicMock()
compute_rpcapi = mock.MagicMock()
self.task = cross_cell_migrate.RevertResizeTask(
target_cell_context, instance, migration, legacy_notifier,
compute_rpcapi)
def _generate_source_cell_instance(self):
source_cell_context = nova_context.get_admin_context()
source_cell_context.cell_uuid = uuids.source_cell
source_cell_instance = self.task.instance.obj_clone()
source_cell_instance._context = source_cell_context
return source_cell_instance
@mock.patch('nova.conductor.tasks.cross_cell_migrate.'
'get_inst_and_cell_map_from_source')
@mock.patch('nova.objects.InstanceActionEvent') # Stub EventReport calls.
def test_execute(self, mock_action_event, mock_get_instance):
"""Happy path test for the execute method."""
# Setup mocks.
source_cell_instance = self._generate_source_cell_instance()
source_cell_context = source_cell_instance._context
source_cell_mapping = objects.CellMapping(source_cell_context,
uuid=uuids.source_cell)
mock_get_instance.return_value = (source_cell_instance,
source_cell_mapping)
def stub_update_instance_in_source_cell(*args, **kwargs):
# Ensure _update_instance_mapping is not called before
# _update_instance_in_source_cell.
_update_instance_mapping.assert_not_called()
return mock.sentinel.source_cell_migration
with test.nested(
mock.patch.object(self.task, '_send_resize_revert_notification'),
mock.patch.object(self.task, '_update_instance_in_source_cell',
side_effect=stub_update_instance_in_source_cell),
mock.patch.object(self.task, '_update_instance_mapping'),
mock.patch.object(self.task.instance, 'destroy'),
mock.patch.object(source_cell_instance, 'refresh'),
) as (
_send_resize_revert_notification, _update_instance_in_source_cell,
_update_instance_mapping, mock_inst_destroy, mock_inst_refresh,
):
# Run the code.
self.task.execute()
# Should have sent a start and end notification.
self.assertEqual(2, _send_resize_revert_notification.call_count,
_send_resize_revert_notification.calls)
_send_resize_revert_notification.assert_has_calls([
mock.call(self.task.instance, fields.NotificationPhase.START),
mock.call(source_cell_instance, fields.NotificationPhase.END),
])
mock_get_instance.assert_called_once_with(
self.task.context, self.task.migration.source_compute,
self.task.instance.uuid)
_update_instance_in_source_cell.assert_called_once_with(
source_cell_instance)
_update_instance_mapping.assert_called_once_with(
source_cell_instance, source_cell_mapping)
# _source_cell_instance and _source_cell_migration should have been
# set for rollbacks
self.assertIs(self.task._source_cell_instance, source_cell_instance)
self.assertIs(self.task._source_cell_migration,
mock.sentinel.source_cell_migration)
# Cleanup at dest host.
self.task.compute_rpcapi.revert_snapshot_based_resize_at_dest.\
assert_called_once_with(self.task.context, self.task.instance,
self.task.migration)
# EventReporter should have been used.
event_name = 'compute_revert_snapshot_based_resize_at_dest'
mock_action_event.event_start.assert_called_once_with(
source_cell_context, source_cell_instance.uuid, event_name,
want_result=False, host=self.task.migration.dest_compute)
mock_action_event.event_finish_with_failure.assert_called_once_with(
source_cell_context, source_cell_instance.uuid, event_name,
exc_val=None, exc_tb=None, want_result=False)
mock_action_event.event_finish.assert_called_once_with(
source_cell_context, source_cell_instance.uuid,
'conductor_revert_snapshot_based_resize', want_result=False)
# Destroy the instance in the target cell.
mock_inst_destroy.assert_called_once_with(hard_delete=True)
# Cleanup at source host.
self.task.compute_rpcapi.\
finish_revert_snapshot_based_resize_at_source.\
assert_called_once_with(
source_cell_context, source_cell_instance,
mock.sentinel.source_cell_migration)
# Refresh the source cell instance so we have the latest data.
mock_inst_refresh.assert_called_once_with()
@mock.patch('nova.conductor.tasks.cross_cell_migrate.RevertResizeTask.'
'_execute')
@mock.patch('nova.objects.RequestSpec.get_by_instance_uuid')
@mock.patch('nova.scheduler.utils.set_vm_state_and_notify')
def test_rollback_target_cell(
self, mock_set_state_notify, mock_get_reqspec, mock_execute):
"""Tests the case that we did not update the instance mapping
so we set the target cell migration to error status.
"""
error = test.TestingException('zoinks!')
mock_execute.side_effect = error
with mock.patch.object(self.task.migration, 'save') as mock_save:
self.assertRaises(test.TestingException, self.task.execute)
self.assertEqual('error', self.task.migration.status)
mock_save.assert_called_once_with()
mock_get_reqspec.assert_called_once_with(
self.task.context, self.task.instance.uuid)
mock_set_state_notify.assert_called_once_with(
self.task.instance._context, self.task.instance.uuid,
'compute_task', 'migrate_server',
{'vm_state': vm_states.ERROR, 'task_state': None}, error,
mock_get_reqspec.return_value)
self.assertIn('The instance is mapped to the target cell',
self.stdlog.logger.output)
@mock.patch('nova.conductor.tasks.cross_cell_migrate.RevertResizeTask.'
'_execute')
@mock.patch('nova.objects.RequestSpec.get_by_instance_uuid')
@mock.patch('nova.scheduler.utils.set_vm_state_and_notify')
def test_rollback_source_cell(
self, mock_set_state_notify, mock_get_reqspec, mock_execute):
"""Tests the case that we did update the instance mapping
so we set the source cell migration to error status.
"""
source_cell_instance = self._generate_source_cell_instance()
source_cell_context = source_cell_instance._context
self.task._source_cell_instance = source_cell_instance
self.task._source_cell_migration = objects.Migration(
source_cell_context, status='reverting', dest_compute='dest-host')
error = test.TestingException('jinkies!')
mock_execute.side_effect = error
with mock.patch.object(self.task._source_cell_migration,
'save') as mock_save:
self.assertRaises(test.TestingException, self.task.execute)
self.assertEqual('error', self.task._source_cell_migration.status)
mock_save.assert_called_once_with()
mock_get_reqspec.assert_called_once_with(
self.task.context, self.task.instance.uuid)
mock_set_state_notify.assert_called_once_with(
source_cell_context, source_cell_instance.uuid,
'compute_task', 'migrate_server',
{'vm_state': vm_states.ERROR, 'task_state': None}, error,
mock_get_reqspec.return_value)
self.assertIn('The instance is mapped to the source cell',
self.stdlog.logger.output)
@mock.patch('nova.compute.utils.notify_about_instance_usage')
@mock.patch('nova.compute.utils.notify_about_instance_action')
def test_send_resize_revert_notification(self, mock_notify_action,
mock_notify_usage):
self.flags(host='fake-conductor-host')
instance = self.task.instance
self.task._send_resize_revert_notification(instance, 'foo')
# Assert the legacy notification was sent.
mock_notify_usage.assert_called_once_with(
self.task.legacy_notifier, instance._context, instance,
'resize.revert.foo')
# Assert the versioned notification was sent.
mock_notify_action.assert_called_once_with(
instance._context, instance, 'fake-conductor-host',
action=fields.NotificationAction.RESIZE_REVERT, phase='foo')
def test_update_instance_in_source_cell(self):
# Setup mocks.
source_cell_instance = self._generate_source_cell_instance()
source_cell_instance.task_state = None
self.task.instance.system_metadata = {'old_vm_state': vm_states.ACTIVE}
with test.nested(
mock.patch.object(source_cell_instance, 'save'),
mock.patch.object(self.task, '_update_bdms_in_source_cell'),
mock.patch.object(self.task,
'_update_instance_actions_in_source_cell'),
mock.patch.object(self.task, '_update_migration_in_source_cell')
) as (
mock_inst_save, _update_bdms_in_source_cell,
_update_instance_actions_in_source_cell,
_update_migration_in_source_cell
):
# Run the code.
source_cell_migration = self.task._update_instance_in_source_cell(
source_cell_instance)
# The returned object should be the updated migration object from the
# source cell database.
self.assertIs(source_cell_migration,
_update_migration_in_source_cell.return_value)
# Fields on the source cell instance should have been updated.
self.assertEqual(vm_states.ACTIVE,
source_cell_instance.system_metadata['old_vm_state'])
self.assertEqual(task_states.RESIZE_REVERTING,
source_cell_instance.task_state)
mock_inst_save.assert_called_once_with()
_update_bdms_in_source_cell.assert_called_once_with(
source_cell_instance._context)
_update_instance_actions_in_source_cell.assert_called_once_with(
source_cell_instance._context)
_update_migration_in_source_cell.assert_called_once_with(
source_cell_instance._context)
@mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid')
@mock.patch('nova.conductor.tasks.cross_cell_migrate.'
'clone_creatable_object')
def test_update_bdms_in_source_cell(self, mock_clone, mock_get_bdms):
"""Test updating BDMs from the target cell to the source cell."""
source_cell_context = nova_context.get_admin_context()
source_cell_context.cell_uuid = uuids.source_cell
# Setup fake bdms.
bdm1 = objects.BlockDeviceMapping(
source_cell_context, uuid=uuids.bdm1, volume_id='vol1',
attachment_id=uuids.attach1)
bdm2 = objects.BlockDeviceMapping(
source_cell_context, uuid=uuids.bdm2, volume_id='vol2',
attachment_id=uuids.attach2)
source_bdms = objects.BlockDeviceMappingList(objects=[bdm1, bdm2])
# With the target BDMs bdm1 from the source is gone and bdm3 is new
# to simulate bdm1 being detached and bdm3 being attached while the
# instance was in VERIFY_RESIZE status.
bdm3 = objects.BlockDeviceMapping(
self.task.context, uuid=uuids.bdm3, volume_id='vol3',
attachment_id=uuids.attach1)
target_bdms = objects.BlockDeviceMappingList(objects=[bdm2, bdm3])
def stub_get_bdms(ctxt, *args, **kwargs):
if ctxt.cell_uuid == uuids.source_cell:
return source_bdms
return target_bdms
mock_get_bdms.side_effect = stub_get_bdms
def stub_mock_clone(ctxt, obj, *args, **kwargs):
# We want to make assertions on our mocks so do not create a copy.
return obj
mock_clone.side_effect = stub_mock_clone
with test.nested(
mock.patch.object(self.task.volume_api, 'attachment_create',
return_value={'id': uuids.attachment_id}),
mock.patch.object(self.task.volume_api, 'attachment_delete'),
mock.patch.object(bdm3, 'create'),
mock.patch.object(bdm1, 'destroy')
) as (
mock_attachment_create, mock_attachment_delete,
mock_bdm_create, mock_bdm_destroy
):
self.task._update_bdms_in_source_cell(source_cell_context)
# Should have gotten BDMs from the source and target cell (order does
# not matter).
self.assertEqual(2, mock_get_bdms.call_count, mock_get_bdms.calls)
mock_get_bdms.assert_has_calls([
mock.call(source_cell_context, self.task.instance.uuid),
mock.call(self.task.context, self.task.instance.uuid)],
any_order=True)
# Since bdm3 was new in the target cell an attachment should have been
# created for it in the source cell.
mock_attachment_create.assert_called_once_with(
source_cell_context, bdm3.volume_id, self.task.instance.uuid)
self.assertEqual(uuids.attachment_id, bdm3.attachment_id)
# And bdm3 should have been created in the source cell.
mock_bdm_create.assert_called_once_with()
# Since bdm1 was not in the target cell it should be destroyed in the
# source cell since we can assume it was detached from the target host
# in the target cell while the instance was in VERIFY_RESIZE status.
mock_attachment_delete.assert_called_once_with(
bdm1._context, bdm1.attachment_id)
mock_bdm_destroy.assert_called_once_with()
@mock.patch('nova.objects.BlockDeviceMapping.destroy')
def test_delete_orphan_source_cell_bdms_attach_delete_fails(self, destroy):
"""Tests attachment_delete failing but not being fatal."""
source_cell_context = nova_context.get_admin_context()
bdm1 = objects.BlockDeviceMapping(
source_cell_context, volume_id='vol1', attachment_id=uuids.attach1)
bdm2 = objects.BlockDeviceMapping(
source_cell_context, volume_id='vol2', attachment_id=uuids.attach2)
source_cell_bdms = objects.BlockDeviceMappingList(objects=[bdm1, bdm2])
with mock.patch.object(self.task.volume_api,
'attachment_delete') as attachment_delete:
# First call to attachment_delete fails, second is OK.
attachment_delete.side_effect = [
test.TestingException('cinder is down'), None]
self.task._delete_orphan_source_cell_bdms(source_cell_bdms)
attachment_delete.assert_has_calls([
mock.call(bdm1._context, bdm1.attachment_id),
mock.call(bdm2._context, bdm2.attachment_id)])
self.assertEqual(2, destroy.call_count, destroy.mock_calls)
self.assertIn('cinder is down', self.stdlog.logger.output)
@mock.patch('nova.objects.InstanceAction.get_by_request_id')
@mock.patch('nova.objects.InstanceActionEventList.get_by_action')
@mock.patch('nova.objects.InstanceAction.create')
@mock.patch('nova.objects.InstanceActionEvent.create')
def test_update_instance_actions_in_source_cell(
self, mock_event_create, mock_action_create, mock_get_events,
mock_get_action):
"""Tests copying instance actions from the target to source cell."""
source_cell_context = nova_context.get_admin_context()
source_cell_context.cell_uuid = uuids.source_cell
# Setup a fake action and fake event.
action = objects.InstanceAction(
id=1, action=instance_actions.REVERT_RESIZE,
instance_uuid=self.task.instance.uuid,
request_id=self.task.context.request_id)
mock_get_action.return_value = action
event = objects.InstanceActionEvent(
id=2, action_id=action.id,
event='conductor_revert_snapshot_based_resize')
mock_get_events.return_value = objects.InstanceActionEventList(
objects=[event])
# Run the code.
self.task._update_instance_actions_in_source_cell(source_cell_context)
# Should have created a clone of the action and event.
mock_get_action.assert_called_once_with(
self.task.context, self.task.instance.uuid,
self.task.context.request_id)
mock_get_events.assert_called_once_with(self.task.context, action.id)
mock_action_create.assert_called_once_with()
mock_event_create.assert_called_once_with(
action.instance_uuid, action.request_id)
def test_update_source_obj_from_target_cell(self):
# Create a fake source object to be updated.
t1 = timeutils.utcnow()
source_obj = objects.Migration(id=1, created_at=t1, updated_at=t1,
uuid=uuids.migration,
status='post-migrating')
t2 = timeutils.utcnow()
target_obj = objects.Migration(id=2, created_at=t2, updated_at=t2,
uuid=uuids.migration,
status='reverting',
# Add a field that is not in source_obj.
migration_type='resize')
# Run the copy code.
self.task._update_source_obj_from_target_cell(source_obj, target_obj)
# First make sure that id, created_at and updated_at are not changed.
ignored_keys = ['id', 'created_at', 'updated_at']
for field in ignored_keys:
self.assertNotEqual(getattr(source_obj, field),
getattr(target_obj, field))
# Now make sure the rest of the fields are the same.
self._compare_objs(source_obj, target_obj, ignored_keys=ignored_keys)
def test_update_source_obj_from_target_cell_nested_object(self):
"""Tests that calling _update_source_obj_from_target_cell with an
object that has nested object fields will raise ObjectActionError.
"""
source = objects.Instance(flavor=objects.Flavor(flavorid='a'))
target = objects.Instance(flavor=objects.Flavor(flavorid='b'))
ex = self.assertRaises(exception.ObjectActionError,
self.task._update_source_obj_from_target_cell,
source, target)
self.assertIn('nested objects are not supported', str(ex))
@mock.patch('nova.objects.Migration.get_by_uuid')
def test_update_migration_in_source_cell(self, mock_get_migration):
"""Tests updating the migration record in the source cell from the
target cell.
"""
source_cell_context = nova_context.get_admin_context()
with mock.patch.object(
self.task,
'_update_source_obj_from_target_cell') as mock_update_obj:
source_cell_migration = \
self.task._update_migration_in_source_cell(source_cell_context)
mock_get_migration.assert_called_once_with(source_cell_context,
self.task.migration.uuid)
mock_update_obj.assert_called_once_with(source_cell_migration,
self.task.migration)
self.assertIs(source_cell_migration, mock_get_migration.return_value)
source_cell_migration.save.assert_called_once_with()
@mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid')
def test_update_instance_mapping(self, get_inst_map):
"""Tests updating the instance mapping from the target to source cell.
"""
source_cell_instance = self._generate_source_cell_instance()
source_cell_context = source_cell_instance._context
source_cell_mapping = objects.CellMapping(source_cell_context,
uuid=uuids.source_cell)
inst_map = objects.InstanceMapping(
cell_mapping=objects.CellMapping(uuids.target_cell))
get_inst_map.return_value = inst_map
with test.nested(
mock.patch.object(source_cell_instance, 'save'),
mock.patch.object(self.task.instance, 'save'),
mock.patch.object(inst_map, 'save')
) as (
source_inst_save, target_inst_save, inst_map_save
):
self.task._update_instance_mapping(source_cell_instance,
source_cell_mapping)
get_inst_map.assert_called_once_with(self.task.context,
self.task.instance.uuid)
# The source cell instance should not be hidden.
self.assertFalse(source_cell_instance.hidden)
source_inst_save.assert_called_once_with()
# The instance mapping should point at the source cell.
self.assertIs(source_cell_mapping, inst_map.cell_mapping)
inst_map_save.assert_called_once_with()
# The target cell instance should be hidden.
self.assertTrue(self.task.instance.hidden)
target_inst_save.assert_called_once_with()