Merge "Add PrepResizeAtSourceTask"
This commit is contained in:
commit
964d7dc879
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue