fa00292546
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
388 lines
16 KiB
Python
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')
|