Add PrepResizeAtSourceTask
This change adds a new conductor sub-task which will make a synchronous RPC call (using long_rpc_timeout) to the new method "prep_snapshot_based_resize_at_source" on the source compute. If the instance is not volume-backed, the sub-task will create an image and pass the image ID to the compute method to upload the snapshot data. If the migration fails at this point, any snapshot image created should be deleted. Recovering the guest on the source host should be as simple as hard rebooting the server (which is allowed with servers in ERROR status). Part of blueprint cross-cell-resize Change-Id: I5bfcac018c1d1196d4efcb321213eb5a1d4c7a6b
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user