Files
nova/nova/tests/unit/api/openstack/compute/test_server_migrations.py
T
Andrea Rosa fa00292546 Abort an ongoing live migration
This change adds a DELETE call on the server-migrations object to cancel
a running live migration of a specific instance.
TO perform the cancellation the virtualization driver needs to support
it, in case that the feature is not supported we return an error.
We allow a cancellation of a migration only if the migration is
running at the moment of the request and if the migration type is equal
to 'live-migration'.
In this change we implement this feature for the libvirt driver.
When the cancellation of a live migration succeeded we rollback the live
migration and we set the state of the Migration object equals to
'cancelled'.
The implementation of this change is based on the work done by the
implementation of the feature called 'force live migration':
https://review.openstack.org/245921

DocImpact
ApiImpact

Implements blueprint: abort-live-migration
Change-Id: I1ff861e54997a069894b542bd764ac3ef1b3dbb2
2016-02-26 15:11:41 +00:00

388 lines
16 KiB
Python

# Copyright 2016 OpenStack Foundation
# All Rights Reserved.
#
# 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 datetime
import mock
import webob
from nova.api.openstack.compute import server_migrations
from nova import exception
from nova import objects
from nova.objects import base
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids
SERVER_UUID = uuids.server_uuid
fake_migrations = [
{
'id': 1234,
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'running',
'instance_uuid': SERVER_UUID,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 123456,
'memory_processed': 12345,
'memory_remaining': 120000,
'disk_total': 234567,
'disk_processed': 23456,
'disk_remaining': 230000,
'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
{
'id': 5678,
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'running',
'instance_uuid': SERVER_UUID,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 456789,
'memory_processed': 56789,
'memory_remaining': 45000,
'disk_total': 96789,
'disk_processed': 6789,
'disk_remaining': 96000,
'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
}
]
migrations_obj = base.obj_make_list(
'fake-context',
objects.MigrationList(),
objects.Migration,
fake_migrations)
class ServerMigrationsTestsV21(test.NoDBTestCase):
wsgi_api_version = '2.22'
def setUp(self):
super(ServerMigrationsTestsV21, self).setUp()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)
self.context = self.req.environ['nova.context']
self.controller = server_migrations.ServerMigrationsController()
self.compute_api = self.controller.compute_api
def test_force_complete_succeeded(self):
@mock.patch.object(self.compute_api, 'live_migrate_force_complete')
@mock.patch.object(self.compute_api, 'get')
def _do_test(compute_api_get, live_migrate_force_complete):
self.controller._force_complete(self.req, '1', '1',
body={'force_complete': None})
live_migrate_force_complete.assert_called_once_with(
self.context, compute_api_get(), '1')
_do_test()
def _test_force_complete_failed_with_exception(self, fake_exc,
expected_exc):
@mock.patch.object(self.compute_api, 'live_migrate_force_complete',
side_effect=fake_exc)
@mock.patch.object(self.compute_api, 'get')
def _do_test(compute_api_get, live_migrate_force_complete):
self.assertRaises(expected_exc,
self.controller._force_complete,
self.req, '1', '1',
body={'force_complete': None})
_do_test()
def test_force_complete_instance_not_migrating(self):
self._test_force_complete_failed_with_exception(
exception.InstanceInvalidState(instance_uuid='', state='',
attr='', method=''),
webob.exc.HTTPConflict)
def test_force_complete_migration_not_found(self):
self._test_force_complete_failed_with_exception(
exception.MigrationNotFoundByStatus(instance_id='', status=''),
webob.exc.HTTPBadRequest)
def test_force_complete_instance_is_locked(self):
self._test_force_complete_failed_with_exception(
exception.InstanceIsLocked(instance_uuid=''),
webob.exc.HTTPConflict)
def test_force_complete_invalid_migration_state(self):
self._test_force_complete_failed_with_exception(
exception.InvalidMigrationState(migration_id='', instance_uuid='',
state='', method=''),
webob.exc.HTTPBadRequest)
def test_force_complete_instance_not_found(self):
self._test_force_complete_failed_with_exception(
exception.InstanceNotFound(instance_id=''),
webob.exc.HTTPNotFound)
def test_force_complete_unexpected_error(self):
self._test_force_complete_failed_with_exception(
exception.NovaException(), webob.exc.HTTPInternalServerError)
class ServerMigrationsTestsV223(ServerMigrationsTestsV21):
wsgi_api_version = '2.23'
def setUp(self):
super(ServerMigrationsTestsV223, self).setUp()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version,
use_admin_context=True)
self.context = self.req.environ['nova.context']
@mock.patch('nova.compute.api.API.get_migrations_in_progress_by_instance')
@mock.patch('nova.compute.api.API.get')
def test_index(self, m_get_instance, m_get_mig):
migrations = [server_migrations.output(mig) for mig in migrations_obj]
migrations_in_progress = {'migrations': migrations}
for mig in migrations_in_progress['migrations']:
self.assertIn('id', mig)
self.assertNotIn('deleted', mig)
self.assertNotIn('deleted_at', mig)
m_get_mig.return_value = migrations_obj
response = self.controller.index(self.req, SERVER_UUID)
self.assertEqual(migrations_in_progress, response)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get')
def test_index_invalid_instance(self, m_get_instance):
m_get_instance.side_effect = exception.InstanceNotFound(instance_id=1)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.index,
self.req, SERVER_UUID)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show(self, m_get_instance, m_get_mig):
migrations = [server_migrations.output(mig) for mig in migrations_obj]
m_get_mig.return_value = migrations_obj[0]
response = self.controller.show(self.req, SERVER_UUID,
migrations_obj[0].id)
self.assertEqual(migrations[0], response['migration'])
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_non_progress(self, m_get_instance, m_get_mig):
non_progress_mig = copy.deepcopy(migrations_obj[0])
non_progress_mig.status = "reverted"
m_get_mig.return_value = non_progress_mig
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
non_progress_mig.id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_not_live_migration(self, m_get_instance,
m_get_mig):
non_progress_mig = copy.deepcopy(migrations_obj[0])
non_progress_mig.migration_type = "resize"
m_get_mig.return_value = non_progress_mig
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
non_progress_mig.id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_not_exist(self, m_get_instance, m_get_mig):
m_get_mig.side_effect = exception.MigrationNotFoundForInstance(
migration_id=migrations_obj[0].id,
instance_id=SERVER_UUID)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
migrations_obj[0].id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get')
def test_show_migration_invalid_instance(self, m_get_instance):
m_get_instance.side_effect = exception.InstanceNotFound(instance_id=1)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
migrations_obj[0].id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
class ServerMigrationsTestsV224(ServerMigrationsTestsV21):
wsgi_api_version = '2.24'
def setUp(self):
super(ServerMigrationsTestsV224, self).setUp()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version,
use_admin_context=True)
self.context = self.req.environ['nova.context']
def test_cancel_live_migration_succeeded(self):
@mock.patch.object(self.compute_api, 'live_migrate_abort')
@mock.patch.object(self.compute_api, 'get')
def _do_test(mock_get, mock_abort):
self.controller.delete(self.req, 'server-id', 'migration-id')
mock_abort.assert_called_once_with(self.context,
mock_get(),
'migration-id')
_do_test()
def _test_cancel_live_migration_failed(self, fake_exc, expected_exc):
@mock.patch.object(self.compute_api, 'live_migrate_abort',
side_effect=fake_exc)
@mock.patch.object(self.compute_api, 'get')
def _do_test(mock_get, mock_abort):
self.assertRaises(expected_exc,
self.controller.delete,
self.req,
'server-id',
'migration-id')
_do_test()
def test_cancel_live_migration_invalid_state(self):
self._test_cancel_live_migration_failed(
exception.InstanceInvalidState(instance_uuid='',
state='',
attr='',
method=''),
webob.exc.HTTPConflict)
def test_cancel_live_migration_migration_not_found(self):
self._test_cancel_live_migration_failed(
exception.MigrationNotFoundForInstance(migration_id='',
instance_id=''),
webob.exc.HTTPNotFound)
def test_cancel_live_migration_invalid_migration_state(self):
self._test_cancel_live_migration_failed(
exception.InvalidMigrationState(migration_id='',
instance_uuid='',
state='',
method=''),
webob.exc.HTTPBadRequest)
def test_cancel_live_migration_instance_not_found(self):
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete,
self.req,
'server-id',
'migration-id')
class ServerMigrationsPolicyEnforcementV21(test.NoDBTestCase):
wsgi_api_version = '2.22'
def setUp(self):
super(ServerMigrationsPolicyEnforcementV21, self).setUp()
self.controller = server_migrations.ServerMigrationsController()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)
def test_migrate_live_policy_failed(self):
rule_name = "os_compute_api:servers:migrations:force_complete"
self.policy.set_rules({rule_name: "project:non_fake"})
body_args = {'force_complete': None}
exc = self.assertRaises(
exception.PolicyNotAuthorized,
self.controller._force_complete, self.req,
fakes.FAKE_UUID, fakes.FAKE_UUID,
body=body_args)
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
class ServerMigrationsPolicyEnforcementV223(
ServerMigrationsPolicyEnforcementV21):
wsgi_api_version = '2.23'
def setUp(self):
super(ServerMigrationsPolicyEnforcementV223, self).setUp()
def test_migration_index_failed(self):
rule_name = "os_compute_api:servers:migrations:index"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index, self.req,
fakes.FAKE_UUID)
self.assertEqual("Policy doesn't allow %s to be performed." %
rule_name, exc.format_message())
def test_migration_show_failed(self):
rule_name = "os_compute_api:servers:migrations:show"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, self.req,
fakes.FAKE_UUID, 1)
self.assertEqual("Policy doesn't allow %s to be performed." %
rule_name, exc.format_message())
class ServerMigrationsPolicyEnforcementV224(
ServerMigrationsPolicyEnforcementV223):
wsgi_api_version = '2.24'
def setUp(self):
super(ServerMigrationsPolicyEnforcementV224, self).setUp()
def test_migrate_delete_failed(self):
rule_name = "os_compute_api:servers:migrations:delete"
self.policy.set_rules({rule_name: "project:non_fake"})
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.delete, self.req,
fakes.FAKE_UUID, '10')