846 lines
40 KiB
Python
846 lines
40 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
|
|
import six
|
|
|
|
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 instance as instance_obj
|
|
from nova import test
|
|
from nova.tests.unit.db import 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 TargetDBSetupTaskTestCase(
|
|
test.TestCase, test_db_api.ModelsObjectComparatorMixin):
|
|
|
|
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 _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)
|
|
|
|
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()
|
|
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'),
|
|
) as (
|
|
mock_migration_save, mock_perform_external_api_checks,
|
|
mock_setup_target_cell_db, mock_prep_resize_at_dest,
|
|
mock_prep_resize_at_source,
|
|
):
|
|
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_setup_target_cell_db.return_value)
|
|
mock_prep_resize_at_source.assert_called_once_with()
|
|
# Now rollback the completed sub-tasks
|
|
self.task.rollback()
|
|
|
|
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, 'supports_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, 'supports_port_binding_extension',
|
|
return_value=False):
|
|
ex = self.assertRaises(exception.MigrationPreCheckError,
|
|
self.task._perform_external_api_checks)
|
|
self.assertIn('Required networking service API extension',
|
|
six.text_type(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.
|
|
with mock.patch.object(self.task, '_execute',
|
|
side_effect=test.TestingException('main task')):
|
|
# The TestingException from the main task should be raised.
|
|
ex = self.assertRaises(test.TestingException, self.task.execute)
|
|
self.assertEqual('main task', six.text_type(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()
|
|
# 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)
|
|
|
|
@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('migration_context', self.task.instance)
|
|
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)
|
|
|
|
|
|
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', six.text_type(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()
|
|
# 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'),
|
|
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.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.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()
|
|
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()
|
|
delete_image.assert_called_once_with(
|
|
self.task.context, self.task.instance, self.task.image_api,
|
|
self.task._image_id)
|