Add RevertResizeTask
This adds the RevertResizeTask which will be used to orchestrate the revert of a cross-cell resize. It is responsible for updating the instance mapping and hidden fields to hide the target cell instance and show the source cell instance, clean up the target host / cell, destroy the target cell instance, and (re)spawn the instance in the source cell. Part of blueprint cross-cell-resize Change-Id: I7069f8807f353fa63c96494395bbbf7cc44562fb
This commit is contained in:
@@ -904,7 +904,10 @@ def get_instance_from_source_cell(
|
||||
target cell database
|
||||
:param source_compute: name of the source compute service host
|
||||
:param instance_uuid: UUID of the instance
|
||||
:returns: Instance object from the source cell database.
|
||||
:returns: 2-item tuple of:
|
||||
|
||||
- Instance object from the source cell database.
|
||||
- CellMapping object of the source cell mapping
|
||||
"""
|
||||
# We can get the source cell via the host mapping based on the
|
||||
# source_compute in the migration object.
|
||||
@@ -918,9 +921,10 @@ def get_instance_from_source_cell(
|
||||
# Now get the instance from the source cell DB using the source
|
||||
# cell context which will make the source cell instance permanently
|
||||
# targeted to the source cell database.
|
||||
return objects.Instance.get_by_uuid(
|
||||
instance = objects.Instance.get_by_uuid(
|
||||
source_cell_context, instance_uuid,
|
||||
expected_attrs=['flavor', 'info_cache', 'system_metadata'])
|
||||
return instance, source_cell_mapping
|
||||
|
||||
|
||||
class ConfirmResizeTask(base.TaskBase):
|
||||
@@ -1059,7 +1063,7 @@ class ConfirmResizeTask(base.TaskBase):
|
||||
def _execute(self):
|
||||
# First get the instance from the source cell so we can cleanup.
|
||||
source_cell_instance = get_instance_from_source_cell(
|
||||
self.context, self.migration.source_compute, self.instance.uuid)
|
||||
self.context, self.migration.source_compute, self.instance.uuid)[0]
|
||||
# Send the resize.confirm.start notification(s) using the source
|
||||
# cell instance since we start there.
|
||||
self._send_resize_confirm_notification(
|
||||
@@ -1103,3 +1107,357 @@ class ConfirmResizeTask(base.TaskBase):
|
||||
scheduler_utils.set_vm_state_and_notify(
|
||||
self.context, self.instance.uuid, 'compute_task',
|
||||
'migrate_server', updates, ex, request_spec)
|
||||
|
||||
|
||||
class RevertResizeTask(base.TaskBase):
|
||||
"""Task to orchestrate a cross-cell resize revert operation.
|
||||
|
||||
This task is responsible for coordinating the cleanup of the resources
|
||||
in the target cell and restoring the server and its related resources
|
||||
(e.g. networking and volumes) in the source cell.
|
||||
|
||||
Upon successful completion the instance mapping should point back at the
|
||||
source cell, the source cell instance should no longer be hidden and the
|
||||
instance in the target cell should be destroyed.
|
||||
"""
|
||||
|
||||
def __init__(self, context, instance, migration, legacy_notifier,
|
||||
compute_rpcapi):
|
||||
"""Initialize this RevertResizeTask instance
|
||||
|
||||
:param context: nova auth request context targeted at the target cell
|
||||
:param instance: Instance object in "resized" status from the target
|
||||
cell with task_state "resize_reverting"
|
||||
:param migration: Migration object from the target cell for the resize
|
||||
operation expected to have status "reverting"
|
||||
:param legacy_notifier: LegacyValidatingNotifier for sending legacy
|
||||
unversioned notifications
|
||||
:param compute_rpcapi: instance of nova.compute.rpcapi.ComputeAPI
|
||||
"""
|
||||
super(RevertResizeTask, self).__init__(context, instance)
|
||||
self.migration = migration
|
||||
self.legacy_notifier = legacy_notifier
|
||||
self.compute_rpcapi = compute_rpcapi
|
||||
|
||||
self._source_cell_migration = None
|
||||
|
||||
self.volume_api = cinder.API()
|
||||
|
||||
def _send_resize_revert_notification(self, instance, phase):
|
||||
"""Sends an unversioned and versioned resize.revert.(phase)
|
||||
notification.
|
||||
|
||||
:param instance: The instance whose resize is being reverted.
|
||||
:param phase: The phase for the resize.revert operation (either
|
||||
"start" or "end").
|
||||
"""
|
||||
ctxt = instance._context
|
||||
# Send the legacy unversioned notification.
|
||||
compute_utils.notify_about_instance_usage(
|
||||
self.legacy_notifier, ctxt, instance, 'resize.revert.%s' % phase)
|
||||
# Send the versioned notification.
|
||||
compute_utils.notify_about_instance_action(
|
||||
ctxt, instance, instance.host, # TODO(mriedem): Use CONF.host?
|
||||
action=fields.NotificationAction.RESIZE_REVERT,
|
||||
phase=phase)
|
||||
|
||||
@staticmethod
|
||||
def _update_source_obj_from_target_cell(source_obj, target_obj):
|
||||
"""Updates the object from the source cell using the target cell object
|
||||
|
||||
All fields on the source object are updated from the target object
|
||||
except for the ``id`` and ``created_at`` fields since those value must
|
||||
not change during an update. The ``updated_at`` field is also skipped
|
||||
because saving changes to ``source_obj`` will automatically update the
|
||||
``updated_at`` field.
|
||||
|
||||
It is expected that the two objects represent the same thing but from
|
||||
different cell databases, so for example, a uuid field (if one exists)
|
||||
should not change.
|
||||
|
||||
Note that the changes to ``source_obj`` are not persisted in this
|
||||
method.
|
||||
|
||||
:param source_obj: Versioned object from the source cell database
|
||||
:param target_obj: Versioned object from the target cell database
|
||||
"""
|
||||
ignore_fields = ['created_at', 'id', 'updated_at']
|
||||
for field in source_obj.obj_fields:
|
||||
if field in target_obj and field not in ignore_fields:
|
||||
setattr(source_obj, field, getattr(target_obj, field))
|
||||
|
||||
def _update_bdms_in_source_cell(self, source_cell_context):
|
||||
"""Update BlockDeviceMapppings in the source cell database.
|
||||
|
||||
It is possible to attach/detach volumes to/from a resized instance,
|
||||
which would create/delete BDM records in the target cell, so we have
|
||||
to recreate newly attached BDMs in the source cell database and
|
||||
delete any old BDMs that were detached while resized in the target
|
||||
cell.
|
||||
|
||||
:param source_cell_context: nova auth request context targeted at the
|
||||
source cell database
|
||||
"""
|
||||
# TODO(mriedem): Need functional test wrinkle for this. Attach volume2
|
||||
# while resized, detach volume1 while resized, and make sure those are
|
||||
# the same when the revert is done.
|
||||
bdms_from_source_cell = (
|
||||
objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
source_cell_context, self.instance.uuid))
|
||||
source_cell_bdms_by_uuid = {
|
||||
bdm.uuid: bdm for bdm in bdms_from_source_cell}
|
||||
bdms_from_target_cell = (
|
||||
objects.BlockDeviceMappingList.get_by_instance_uuid(
|
||||
self.context, self.instance.uuid))
|
||||
# Copy new/updated BDMs from the target cell DB to the source cell DB.
|
||||
for bdm in bdms_from_target_cell:
|
||||
if bdm.uuid in source_cell_bdms_by_uuid:
|
||||
# Remove this BDM from the list since we want to preserve it
|
||||
# along with its attachment_id.
|
||||
source_cell_bdms_by_uuid.pop(bdm.uuid)
|
||||
else:
|
||||
# Newly attached BDM while in the target cell, so create it
|
||||
# in the source cell.
|
||||
source_bdm = clone_creatable_object(source_cell_context, bdm)
|
||||
# revert_snapshot_based_resize_at_dest is going to delete the
|
||||
# attachment for this BDM so we need to create a new empty
|
||||
# attachment to reserve this volume so that
|
||||
# finish_revert_snapshot_based_resize_at_source can use it.
|
||||
attach_ref = self.volume_api.attachment_create(
|
||||
source_cell_context, bdm.volume_id, self.instance.uuid)
|
||||
source_bdm.attachment_id = attach_ref['id']
|
||||
LOG.debug('Creating BlockDeviceMapping with volume ID %s '
|
||||
'and attachment %s in the source cell database '
|
||||
'since the volume was attached while the server was '
|
||||
'resized.', bdm.volume_id, attach_ref['id'],
|
||||
instance=self.instance)
|
||||
source_bdm.create()
|
||||
# If there are any source bdms left that were not processed from the
|
||||
# target cell bdms, it means those source bdms were detached while
|
||||
# resized in the target cell, and we need to delete them from the
|
||||
# source cell so they don't re-appear once the revert is complete.
|
||||
self._delete_orphan_source_cell_bdms(source_cell_bdms_by_uuid.values())
|
||||
|
||||
def _delete_orphan_source_cell_bdms(self, source_cell_bdms):
|
||||
"""Deletes orphaned BDMs and volume attachments from the source cell.
|
||||
|
||||
If any volumes were detached while the server was resized into the
|
||||
target cell they are destroyed here so they do not show up again once
|
||||
the instance is mapped back to the source cell.
|
||||
|
||||
:param source_cell_bdms: Iterator of BlockDeviceMapping objects.
|
||||
"""
|
||||
for bdm in source_cell_bdms:
|
||||
LOG.debug('Destroying BlockDeviceMapping with volume ID %s and '
|
||||
'attachment ID %s from source cell database during '
|
||||
'cross-cell resize revert since the volume was detached '
|
||||
'while the server was resized.', bdm.volume_id,
|
||||
bdm.attachment_id, instance=self.instance)
|
||||
# First delete the (empty) attachment, created by
|
||||
# prep_snapshot_based_resize_at_source, so it is not leaked.
|
||||
try:
|
||||
self.volume_api.attachment_delete(
|
||||
bdm._context, bdm.attachment_id)
|
||||
except Exception as e:
|
||||
LOG.error('Failed to delete attachment %s for volume %s. The '
|
||||
'attachment may be leaked and needs to be manually '
|
||||
'cleaned up. Error: %s', bdm.attachment_id,
|
||||
bdm.volume_id, e, instance=self.instance)
|
||||
bdm.destroy()
|
||||
|
||||
def _update_instance_actions_in_source_cell(self, source_cell_context):
|
||||
"""Update instance action records in the source cell database
|
||||
|
||||
We need to copy the REVERT_RESIZE instance action and related events
|
||||
from the target cell to the source cell. Otherwise the revert operation
|
||||
in the source compute service will not be able to lookup the correct
|
||||
instance action to track events.
|
||||
|
||||
:param source_cell_context: nova auth request context targeted at the
|
||||
source cell database
|
||||
"""
|
||||
# FIXME(mriedem): This is a hack to just get revert working on
|
||||
# the source; we need to re-create any actions created in the target
|
||||
# cell DB after the instance was moved while it was in
|
||||
# VERIFY_RESIZE status, like if volumes were attached/detached.
|
||||
# Can we use a changes-since filter for that, i.e. find the last
|
||||
# instance action for the instance in the source cell database and then
|
||||
# get all instance actions from the target cell database that were
|
||||
# created after that time.
|
||||
action = objects.InstanceAction.get_by_request_id(
|
||||
self.context, self.instance.uuid, self.context.request_id)
|
||||
new_action = clone_creatable_object(source_cell_context, action)
|
||||
new_action.create()
|
||||
# Also create the events under this action.
|
||||
events = objects.InstanceActionEventList.get_by_action(
|
||||
self.context, action.id)
|
||||
for event in events:
|
||||
new_event = clone_creatable_object(source_cell_context, event)
|
||||
new_event.create(action.instance_uuid, action.request_id)
|
||||
|
||||
def _update_migration_in_source_cell(self, source_cell_context):
|
||||
"""Update the migration record in the source cell database.
|
||||
|
||||
Updates the migration record in the source cell database based on the
|
||||
current information about the migration in the target cell database.
|
||||
|
||||
:param source_cell_context: nova auth request context targeted at the
|
||||
source cell database
|
||||
:return: Migration object of the updated source cell database migration
|
||||
record
|
||||
"""
|
||||
source_cell_migration = objects.Migration.get_by_uuid(
|
||||
source_cell_context, self.migration.uuid)
|
||||
# The only change we really expect here is the status changing to
|
||||
# "reverting".
|
||||
self._update_source_obj_from_target_cell(
|
||||
source_cell_migration, self.migration)
|
||||
source_cell_migration.save()
|
||||
return source_cell_migration
|
||||
|
||||
def _update_instance_in_source_cell(self, instance):
|
||||
"""Updates the instance and related records in the source cell DB.
|
||||
|
||||
Before reverting in the source cell we need to copy the
|
||||
latest state information from the target cell database where the
|
||||
instance lived before the revert. This is because data about the
|
||||
instance could have changed while it was in VERIFY_RESIZE status, like
|
||||
attached volumes.
|
||||
|
||||
:param instance: Instance object from the source cell database
|
||||
:return: Migration object of the updated source cell database migration
|
||||
record
|
||||
"""
|
||||
LOG.debug('Updating instance-related records in the source cell '
|
||||
'database based on target cell database information.',
|
||||
instance=instance)
|
||||
# Copy information from the target cell instance that we need in the
|
||||
# source cell instance for doing the revert on the source compute host.
|
||||
instance.system_metadata['old_vm_state'] = (
|
||||
self.instance.system_metadata.get('old_vm_state'))
|
||||
instance.old_flavor = instance.flavor
|
||||
instance.task_state = task_states.RESIZE_REVERTING
|
||||
instance.save()
|
||||
|
||||
source_cell_context = instance._context
|
||||
self._update_bdms_in_source_cell(source_cell_context)
|
||||
self._update_instance_actions_in_source_cell(source_cell_context)
|
||||
source_cell_migration = self._update_migration_in_source_cell(
|
||||
source_cell_context)
|
||||
|
||||
# NOTE(mriedem): We do not have to worry about ports changing while
|
||||
# resized since the API does not allow attach/detach interface while
|
||||
# resized. Same for tags.
|
||||
return source_cell_migration
|
||||
|
||||
def _update_instance_mapping(
|
||||
self, source_cell_instance, source_cell_mapping):
|
||||
"""Swaps the hidden field value on the source and target cell instance
|
||||
and updates the instance mapping to point at the source cell.
|
||||
|
||||
:param source_cell_instance: Instance object from the source cell DB
|
||||
:param source_cell_mapping: CellMapping object for the source cell
|
||||
"""
|
||||
LOG.debug('Marking instance in target cell as hidden and updating '
|
||||
'instance mapping to point at source cell %s.',
|
||||
source_cell_mapping.identity, instance=source_cell_instance)
|
||||
# Get the instance mapping first to make the window of time where both
|
||||
# instances are hidden=False as small as possible.
|
||||
instance_mapping = objects.InstanceMapping.get_by_instance_uuid(
|
||||
self.context, self.instance.uuid)
|
||||
# Mark the source cell instance record as hidden=False so it will show
|
||||
# up when listing servers. Note that because of how the API filters
|
||||
# duplicate instance records, even if the user is listing servers at
|
||||
# this exact moment only one copy of the instance will be returned.
|
||||
source_cell_instance.hidden = False
|
||||
source_cell_instance.save()
|
||||
# Update the instance mapping to point at the source cell. We do this
|
||||
# before cleaning up the target host/cell because that is really best
|
||||
# effort and if something fails on the target we want the user to
|
||||
# now interact with the instance in the source cell with the original
|
||||
# flavor because they are ultimately trying to revert and get back
|
||||
# there, so if they hard reboot/rebuild after an error (for example)
|
||||
# that should happen in the source cell.
|
||||
instance_mapping.cell_mapping = source_cell_mapping
|
||||
instance_mapping.save()
|
||||
# Mark the target cell instance record as hidden=True to hide it from
|
||||
# the user when listing servers.
|
||||
self.instance.hidden = True
|
||||
self.instance.save()
|
||||
|
||||
def _execute(self):
|
||||
# Send the resize.revert.start notification(s) using the target
|
||||
# cell instance since we start there.
|
||||
self._send_resize_revert_notification(
|
||||
self.instance, fields.NotificationPhase.START)
|
||||
|
||||
source_cell_instance, source_cell_mapping = (
|
||||
get_instance_from_source_cell(
|
||||
self.context, self.migration.source_compute,
|
||||
self.instance.uuid))
|
||||
|
||||
# Update the source cell database information based on the target cell
|
||||
# database, i.e. the instance/migration/BDMs/action records. Do all of
|
||||
# this before updating the instance mapping in case it fails.
|
||||
source_cell_migration = self._update_instance_in_source_cell(
|
||||
source_cell_instance)
|
||||
|
||||
# Swap the instance.hidden values and update the instance mapping to
|
||||
# point at the source cell. From here on out the user will see and
|
||||
# operate on the instance in the source cell.
|
||||
self._update_instance_mapping(
|
||||
source_cell_instance, source_cell_mapping)
|
||||
# Save off the source cell migration record for rollbacks.
|
||||
self._source_cell_migration = source_cell_migration
|
||||
|
||||
# Clean the instance from the target host.
|
||||
LOG.debug('Calling destination host %s to revert cross-cell resize.',
|
||||
self.migration.dest_compute, instance=self.instance)
|
||||
# Use the EventReport context manager to create the same event that
|
||||
# the dest compute will create but in the source cell DB so we do not
|
||||
# have to explicitly copy it over from target to source DB.
|
||||
event_name = 'compute_revert_snapshot_based_resize_at_dest'
|
||||
with compute_utils.EventReporter(
|
||||
source_cell_instance._context, event_name,
|
||||
self.migration.dest_compute, self.instance.uuid):
|
||||
self.compute_rpcapi.revert_snapshot_based_resize_at_dest(
|
||||
self.context, self.instance, self.migration)
|
||||
# NOTE(mriedem): revert_snapshot_based_resize_at_dest updates the
|
||||
# target cell instance so if we need to do something with it here
|
||||
# in the future before destroying it, it should be refreshed.
|
||||
|
||||
# Destroy the instance and its related records from the target cell DB.
|
||||
LOG.info('Deleting instance record from target cell %s',
|
||||
self.context.cell_uuid, instance=source_cell_instance)
|
||||
# This needs to be a hard delete because if we retry the resize to the
|
||||
# target cell we could hit a duplicate entry unique constraint error.
|
||||
self.instance.destroy(hard_delete=True)
|
||||
|
||||
# Launch the guest at the source host with the old flavor.
|
||||
LOG.debug('Calling source host %s to finish reverting cross-cell '
|
||||
'resize.', self.migration.source_compute,
|
||||
instance=self.instance)
|
||||
self.compute_rpcapi.finish_revert_snapshot_based_resize_at_source(
|
||||
source_cell_instance._context, source_cell_instance,
|
||||
source_cell_migration)
|
||||
# finish_revert_snapshot_based_resize_at_source updates the source cell
|
||||
# instance so refresh it here so we have the latest copy.
|
||||
source_cell_instance.refresh()
|
||||
|
||||
# Send the resize.revert.end notification using the instance from
|
||||
# the source cell since we end there.
|
||||
self._send_resize_revert_notification(
|
||||
source_cell_instance, fields.NotificationPhase.END)
|
||||
|
||||
def rollback(self, ex):
|
||||
# If we have updated the instance mapping to point at the source
|
||||
# cell we update the migration from the source cell, otherwise we
|
||||
# update the migration in the target cell.
|
||||
migration = self._source_cell_migration \
|
||||
if self._source_cell_migration else self.migration
|
||||
migration.status = 'error'
|
||||
migration.save()
|
||||
|
||||
# TODO(mriedem): Will have to think about setting the instance to
|
||||
# ERROR status and/or recording a fault and sending an
|
||||
# error notification (create new resize_revert.error notification?).
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
import six
|
||||
|
||||
from nova.compute import instance_actions
|
||||
@@ -45,8 +46,29 @@ 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):
|
||||
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()
|
||||
@@ -203,24 +225,6 @@ class TargetDBSetupTaskTestCase(
|
||||
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
|
||||
@@ -1117,9 +1121,10 @@ class UtilityTestCase(test.NoDBTestCase):
|
||||
self.assertEqual(uuids.cell, ctxt.cell_uuid)
|
||||
return mock.sentinel.instance
|
||||
mock_get_inst.side_effect = stub_get_by_uuid
|
||||
inst = cross_cell_migrate.get_instance_from_source_cell(
|
||||
inst, cell_mapping = cross_cell_migrate.get_instance_from_source_cell(
|
||||
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(
|
||||
@@ -1147,11 +1152,13 @@ class ConfirmResizeTaskTestCase(test.NoDBTestCase):
|
||||
compute_rpcapi)
|
||||
|
||||
@mock.patch('nova.conductor.tasks.cross_cell_migrate.'
|
||||
'get_instance_from_source_cell',
|
||||
return_value=objects.Instance(
|
||||
mock.MagicMock(), uuid=uuids.instance))
|
||||
'get_instance_from_source_cell')
|
||||
def test_execute(self, mock_get_instance):
|
||||
mock_get_instance.return_value.destroy = mock.Mock()
|
||||
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'),
|
||||
@@ -1166,12 +1173,10 @@ class ConfirmResizeTaskTestCase(test.NoDBTestCase):
|
||||
self.task.instance.uuid)
|
||||
self.assertEqual(2, _send_resize_confirm_notification.call_count)
|
||||
_send_resize_confirm_notification.assert_has_calls([
|
||||
mock.call(mock_get_instance.return_value,
|
||||
fields.NotificationPhase.START),
|
||||
mock.call(source_cell_instance, fields.NotificationPhase.START),
|
||||
mock.call(self.task.instance, fields.NotificationPhase.END)])
|
||||
_cleanup_source_host.assert_called_once_with(
|
||||
mock_get_instance.return_value)
|
||||
mock_get_instance.return_value.destroy.assert_called_once_with(
|
||||
_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()
|
||||
|
||||
@@ -1270,3 +1275,369 @@ class ConfirmResizeTaskTestCase(test.NoDBTestCase):
|
||||
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_instance_from_source_cell')
|
||||
@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_migration should have been set for rollbacks
|
||||
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)
|
||||
# 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()
|
||||
|
||||
def test_rollback_target_cell(self):
|
||||
"""Tests the case that we did not update the instance mapping
|
||||
so we set the target cell migration to error status.
|
||||
"""
|
||||
with mock.patch.object(self.task.migration, 'save') as mock_save:
|
||||
self.task.rollback(test.TestingException('zoinks!'))
|
||||
self.assertEqual('error', self.task.migration.status)
|
||||
mock_save.assert_called_once_with()
|
||||
|
||||
def test_rollback_source_cell(self):
|
||||
"""Tests the case that we did update the instance mapping
|
||||
so we set the source cell migration to error status.
|
||||
"""
|
||||
self.task._source_cell_migration = objects.Migration(
|
||||
status='reverting')
|
||||
with mock.patch.object(self.task._source_cell_migration,
|
||||
'save') as mock_save:
|
||||
self.task.rollback(test.TestingException('jinkies!'))
|
||||
self.assertEqual('error', self.task._source_cell_migration.status)
|
||||
mock_save.assert_called_once_with()
|
||||
|
||||
@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):
|
||||
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, instance.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.assertIs(source_cell_instance.old_flavor,
|
||||
source_cell_instance.flavor)
|
||||
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)
|
||||
|
||||
@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()
|
||||
|
||||
Reference in New Issue
Block a user