Merge "Add PrepResizeAtSourceTask"

This commit is contained in:
Zuul 2019-10-21 11:31:32 +00:00 committed by Gerrit Code Review
commit 964d7dc879
2 changed files with 201 additions and 7 deletions

View File

@ -17,10 +17,13 @@ from oslo_log import log as logging
import oslo_messaging as messaging
from nova import availability_zones
from nova.compute import task_states
from nova.compute import utils as compute_utils
from nova.conductor.tasks import base
from nova import context as nova_context
from nova import exception
from nova.i18n import _
from nova import image as nova_image
from nova import network
from nova.network.neutronv2 import constants as neutron_constants
from nova import objects
@ -363,6 +366,87 @@ class PrepResizeAtDestTask(base.TaskBase):
instance=self.instance)
class PrepResizeAtSourceTask(base.TaskBase):
"""Task to prepare the instance at the source host for the resize.
Will power off the instance at the source host, create and upload a
snapshot image for a non-volume-backed server, and disconnect volumes and
networking from the source host.
The vm_state is recorded with the "old_vm_state" key in the
instance.system_metadata field prior to powering off the instance so the
revert flow can determine if the guest should be running or stopped.
Returns the snapshot image ID, if one was created, from the ``execute``
method.
Upon successful completion, the instance.task_state will be
``resize_migrated`` and the migration.status will be ``post-migrating``.
"""
def __init__(self, context, instance, migration, request_spec,
compute_rpcapi, image_api):
"""Initializes this PrepResizeAtSourceTask instance.
:param context: nova auth context targeted at the source cell
:param instance: Instance object from the source cell
:param migration: Migration object from the source cell
:param request_spec: RequestSpec object for the resize operation
:param compute_rpcapi: instance of nova.compute.rpcapi.ComputeAPI
:param image_api: instance of nova.image.api.API
"""
super(PrepResizeAtSourceTask, self).__init__(context, instance)
self.migration = migration
self.request_spec = request_spec
self.compute_rpcapi = compute_rpcapi
self.image_api = image_api
self._image_id = None
def _execute(self):
# Save off the vm_state so we can use that later on the source host
# if the resize is reverted - it is used to determine if the reverted
# guest should be powered on.
self.instance.system_metadata['old_vm_state'] = self.instance.vm_state
self.instance.task_state = task_states.RESIZE_MIGRATING
# If the instance is not volume-backed, create a snapshot of the root
# disk.
if not self.request_spec.is_bfv:
# Create an empty image.
name = '%s-resize-temp' % self.instance.display_name
image_meta = compute_utils.create_image(
self.context, self.instance, name, 'snapshot', self.image_api)
self._image_id = image_meta['id']
LOG.debug('Created snapshot image %s for cross-cell resize.',
self._image_id, instance=self.instance)
self.instance.save(expected_task_state=task_states.RESIZE_PREP)
# RPC call the source host to prepare for resize.
self.compute_rpcapi.prep_snapshot_based_resize_at_source(
self.context, self.instance, self.migration,
snapshot_id=self._image_id)
return self._image_id
def rollback(self):
# If we created a snapshot image, attempt to delete it.
if self._image_id:
compute_utils.delete_image(
self.context, self.instance, self.image_api, self._image_id)
# If the compute service successfully powered off the guest but failed
# to snapshot (or timed out during the snapshot), then the
# _sync_power_states periodic task should mark the instance as stopped
# and the user can start/reboot it.
# If the compute service powered off the instance, snapshot it and
# destroyed the guest and then a failure occurred, the instance should
# have been set to ERROR status (by the compute service) so the user
# has to hard reboot or rebuild it.
LOG.error('Preparing for cross-cell resize at the source host %s '
'failed. The instance may need to be hard rebooted.',
self.instance.host, instance=self.instance)
class CrossCellMigrationTask(base.TaskBase):
"""Orchestrates a cross-cell cold migration (resize)."""
@ -401,6 +485,7 @@ class CrossCellMigrationTask(base.TaskBase):
self.network_api = network.API()
self.volume_api = cinder.API()
self.image_api = nova_image.API()
# Keep an ordered dict of the sub-tasks completed so we can call their
# rollback routines if something fails.
@ -534,6 +619,21 @@ class CrossCellMigrationTask(base.TaskBase):
return target_cell_migration
def _prep_resize_at_source(self):
"""Executes PrepResizeAtSourceTask
:return: The image snapshot ID if the instance is not volume-backed,
else None.
"""
LOG.debug('Preparing source host %s for cross-cell resize.',
self.source_migration.source_compute, instance=self.instance)
prep_source_task = PrepResizeAtSourceTask(
self.context, self.instance, self.source_migration,
self.request_spec, self.compute_rpcapi, self.image_api)
snapshot_id = prep_source_task.execute()
self._completed_tasks['PrepResizeAtSourceTask'] = prep_source_task
return snapshot_id
def _execute(self):
"""Execute high-level orchestration of the cross-cell resize"""
# We are committed to a cross-cell move at this point so update the
@ -560,12 +660,10 @@ class CrossCellMigrationTask(base.TaskBase):
target_cell_migration = self._prep_resize_at_dest(
target_cell_migration)
# TODO(mriedem): If image-backed, snapshot the server from source host
# and store it in the migration_context for spawn. Should we do this
# in PrepResizeAtDestTask? Re-using compute_rpcapi.snapshot_instance()
# would be nice but it sets the task_state=None and sends different
# notifications from a normal resize (but do those matter?).
# TODO(mriedem): Stop the server on the source host.
# Prepare the instance at the source host (stop it, optionally snapshot
# it, disconnect volumes and VIFs, etc).
self._prep_resize_at_source()
# TODO(mriedem): Copy data to dest cell DB.
# TODO(mriedem): Update instance mapping to dest cell DB.
# TODO(mriedem): Spawn in target cell host:

View File

@ -17,6 +17,7 @@ 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
@ -379,7 +380,8 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase):
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)
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,
@ -399,9 +401,11 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase):
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
@ -412,6 +416,7 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase):
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()
@ -569,6 +574,16 @@ class CrossCellMigrationTaskTestCase(test.NoDBTestCase):
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):
@ -747,3 +762,84 @@ class PrepResizeAtDestTaskTestCase(test.NoDBTestCase):
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)