diff --git a/doc/api_samples/server-migrations/force_complete.json b/doc/api_samples/server-migrations/force_complete.json new file mode 100644 index 000000000000..e2adb7b5a0ea --- /dev/null +++ b/doc/api_samples/server-migrations/force_complete.json @@ -0,0 +1,3 @@ +{ + "force_complete": null +} diff --git a/doc/api_samples/server-migrations/live-migrate-server.json b/doc/api_samples/server-migrations/live-migrate-server.json new file mode 100644 index 000000000000..251863d7855e --- /dev/null +++ b/doc/api_samples/server-migrations/live-migrate-server.json @@ -0,0 +1,7 @@ +{ + "os-migrateLive": { + "host": "01c0cadef72d47e28a672a76060d492c", + "block_migration": false, + "disk_over_commit": false + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index f22db3db2b74..2f23b09a08f8 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.21", + "version": "2.22", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index bb902524ae3d..c37948ab98cd 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.21", + "version": "2.22", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/notification_samples/service-update.json b/doc/notification_samples/service-update.json index 345716093d3a..219dec9ae2ea 100644 --- a/doc/notification_samples/service-update.json +++ b/doc/notification_samples/service-update.json @@ -13,7 +13,7 @@ "disabled_reason": null, "report_count": 1, "forced_down": false, - "version": 6 + "version": 7 } }, "event_type": "service.update", diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 6c467360ce94..dfd86c592247 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -270,6 +270,8 @@ "os_compute_api:servers:start": "rule:admin_or_owner", "os_compute_api:servers:stop": "rule:admin_or_owner", "os_compute_api:servers:trigger_crash_dump": "rule:admin_or_owner", + "os_compute_api:servers:migrations:discoverable": "", + "os_compute_api:servers:migrations:force_complete": "rule:admin_api", "os_compute_api:os-access-ips:discoverable": "", "os_compute_api:os-access-ips": "", "os_compute_api:os-admin-actions": "rule:admin_api", diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 967128501540..5a316257733a 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -64,7 +64,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.20 - Add attach and detach volume operations for instances in shelved and shelved_offloaded state * 2.21 - Make os-instance-actions read deleted instances - + * 2.22 - Add API to force live migration to complete """ # The minimum and maximum versions of the API supported @@ -73,7 +73,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.21" +_MAX_API_VERSION = "2.22" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/extension_info.py b/nova/api/openstack/compute/extension_info.py index 7fd395ebe03d..45e8195ea845 100644 --- a/nova/api/openstack/compute/extension_info.py +++ b/nova/api/openstack/compute/extension_info.py @@ -82,7 +82,7 @@ v21_to_v2_extension_list_mapping = { v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors', 'os-block-device-mapping-v1', 'os-consoles', 'extensions', 'image-metadata', 'ips', 'limits', - 'server-metadata' + 'server-metadata', 'server-migrations' ] # v2.1 plugins which should appear under a different name in v2 diff --git a/nova/api/openstack/compute/schemas/server_migrations.py b/nova/api/openstack/compute/schemas/server_migrations.py new file mode 100644 index 000000000000..201c543e68af --- /dev/null +++ b/nova/api/openstack/compute/schemas/server_migrations.py @@ -0,0 +1,26 @@ +# 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. + + +force_complete = { + 'type': 'object', + 'properties': { + 'force_complete': { + 'type': 'null' + } + }, + 'required': ['force_complete'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/compute/server_migrations.py b/nova/api/openstack/compute/server_migrations.py new file mode 100644 index 000000000000..caa37c1e0a7d --- /dev/null +++ b/nova/api/openstack/compute/server_migrations.py @@ -0,0 +1,78 @@ +# 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. + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack.compute.schemas import server_migrations +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api import validation +from nova import compute +from nova import exception + +ALIAS = 'servers:migrations' +authorize = extensions.os_compute_authorizer(ALIAS) + + +class ServerMigrationsController(wsgi.Controller): + """The server migrations API controller for the OpenStack API.""" + + def __init__(self): + self.compute_api = compute.API(skip_policy_check=True) + super(ServerMigrationsController, self).__init__() + + @wsgi.Controller.api_version("2.22") + @wsgi.response(202) + @extensions.expected_errors((400, 403, 404, 409)) + @wsgi.action('force_complete') + @validation.schema(server_migrations.force_complete) + def _force_complete(self, req, id, server_id, body): + context = req.environ['nova.context'] + authorize(context, action='force_complete') + + instance = common.get_instance(self.compute_api, context, server_id) + try: + self.compute_api.live_migrate_force_complete(context, instance, id) + except exception.InstanceNotFound as e: + raise exc.HTTPNotFound(explanation=e.format_message()) + except (exception.MigrationNotFoundByStatus, + exception.InvalidMigrationState, + exception.MigrationNotFoundForInstance) as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + except exception.InstanceIsLocked as e: + raise exc.HTTPConflict(explanation=e.format_message()) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state( + state_error, 'force_complete', server_id) + + +class ServerMigrations(extensions.V21APIExtensionBase): + """Server Migrations API.""" + name = "ServerMigrations" + alias = 'server-migrations' + version = 1 + + def get_resources(self): + parent = {'member_name': 'server', + 'collection_name': 'servers'} + member_actions = {'action': 'POST'} + resources = [extensions.ResourceExtension( + 'migrations', ServerMigrationsController(), + parent=parent, member_actions=member_actions)] + return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index ad35316bca24..5cd44edb59d7 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -181,5 +181,17 @@ user documentation. 2.21 ---- + The ``os-instance-actions`` API now returns information from deleted instances. + +2.22 +---- + + A new resource servers:migrations added. A new API to force live migration + to complete added:: + + POST /servers//migrations//action + { + "force_complete": null + } diff --git a/nova/compute/api.py b/nova/compute/api.py index 40fb81eb6b68..bc3cdec6d8f8 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -3287,6 +3287,35 @@ class API(base.Base): host_name, block_migration=block_migration, disk_over_commit=disk_over_commit) + @check_instance_lock + @check_instance_cell + @check_instance_state(vm_state=[vm_states.ACTIVE], + task_state=[task_states.MIGRATING]) + def live_migrate_force_complete(self, context, instance, migration_id): + """Force live migration to complete. + + :param context: Security context + :param instance: The instance that is being migrated + :param migration_id: ID of ongoing migration + + """ + LOG.debug("Going to try to force live migration to complete", + instance=instance) + + # NOTE(pkoniszewski): Get migration object to check if there is ongoing + # live migration for particular instance. Also pass migration id to + # compute to double check and avoid possible race condition. + migration = objects.Migration.get_by_id_and_instance( + context, migration_id, instance.uuid) + if migration.status != 'running': + raise exception.InvalidMigrationState(migration_id=migration_id, + instance_uuid=instance.uuid, + state=migration.status, + method='force complete') + + self.compute_rpcapi.live_migration_force_complete( + context, instance, migration.id) + @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.ERROR]) def evacuate(self, context, instance, host, on_shared_storage, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5c7b198ce84d..0d5e41355d63 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -671,7 +671,7 @@ class ComputeVirtAPI(virtapi.VirtAPI): class ComputeManager(manager.Manager): """Manages the running instances from creation to destruction.""" - target = messaging.Target(version='4.8') + target = messaging.Target(version='4.9') # How long to wait in seconds before re-issuing a shutdown # signal to an instance during power off. The overall @@ -5250,6 +5250,29 @@ class ComputeManager(manager.Manager): block_migration, migration, migrate_data) + @wrap_exception() + @wrap_instance_fault + def live_migration_force_complete(self, context, instance, migration_id): + """Force live migration to complete. + + :param context: Security context + :param instance: The instance that is being migrated + :param migration_id: ID of ongoing migration + + """ + migration = objects.Migration.get_by_id(context, migration_id) + if migration.status != 'running': + raise exception.InvalidMigrationState(migration_id=migration_id, + instance_uuid=instance.uuid, + state=migration.status, + method='force complete') + + self._notify_about_instance_usage( + context, instance, 'live.migration.force.complete.start') + self.driver.live_migration_force_complete(instance) + self._notify_about_instance_usage( + context, instance, 'live.migration.force.complete.end') + def _live_migration_cleanup_flags(self, block_migration, migrate_data): """Determine whether disks or instance path need to be cleaned up after live migration (at source on success, at destination on rollback) diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py index 67ddf03da4c1..b2435c3c44bd 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -325,6 +325,7 @@ class ComputeAPI(object): rollback_live_migration_at_destination, and pre_live_migration. * ... - Remove refresh_provider_fw_rules() + * 4.9 - Add live_migration_force_complete() ''' VERSION_ALIASES = { @@ -636,6 +637,13 @@ class ComputeAPI(object): dest=dest, block_migration=block_migration, migrate_data=migrate_data, **args) + def live_migration_force_complete(self, ctxt, instance, migration_id): + version = '4.9' + cctxt = self.client.prepare(server=_compute_host(None, instance), + version=version) + cctxt.cast(ctxt, 'live_migration_force_complete', instance=instance, + migration_id=migration_id) + def pause_instance(self, ctxt, instance): version = '4.0' cctxt = self.client.prepare(server=_compute_host(None, instance), diff --git a/nova/exception.py b/nova/exception.py index bcf45c733f7d..9ffb4965e7be 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1127,6 +1127,12 @@ class MigrationNotFoundForInstance(MigrationNotFound): "%(instance_id)s") +class InvalidMigrationState(Invalid): + msg_fmt = _("Migration %(migration_id)s state of instance " + "%(instance_uuid)s is %(state)s. Cannot %(method)s while the " + "migration is in this state.") + + class ConsoleLogOutputException(NovaException): msg_fmt = _("Console log output could not be retrieved for instance " "%(instance_id)s. Reason: %(reason)s") diff --git a/nova/objects/service.py b/nova/objects/service.py index fac74826252c..c7a5ee3ab907 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) # NOTE(danms): This is the global service version counter -SERVICE_VERSION = 6 +SERVICE_VERSION = 7 # NOTE(danms): This is our SERVICE_VERSION history. The idea is that any @@ -65,6 +65,8 @@ SERVICE_VERSION_HISTORY = ( {'compute_rpc': '4.7'}, # Version 6: Compute RPC version 4.8 {'compute_rpc': '4.8'}, + # Version 7: Add live_migration_force_complete in the compute_rpc + {'compute_rpc': '4.9'}, ) diff --git a/nova/tests/functional/api_sample_tests/api_samples/server-migrations/force_complete.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/force_complete.json.tpl new file mode 100644 index 000000000000..e2adb7b5a0ea --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/force_complete.json.tpl @@ -0,0 +1,3 @@ +{ + "force_complete": null +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/server-migrations/live-migrate-server.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/live-migrate-server.json.tpl new file mode 100644 index 000000000000..4800d4aa1110 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/live-migrate-server.json.tpl @@ -0,0 +1,7 @@ +{ + "os-migrateLive": { + "host": "%(hostname)s", + "block_migration": false, + "disk_over_commit": false + } +} diff --git a/nova/tests/functional/api_sample_tests/test_server_migrations.py b/nova/tests/functional/api_sample_tests/test_server_migrations.py new file mode 100644 index 000000000000..b2829881b71b --- /dev/null +++ b/nova/tests/functional/api_sample_tests/test_server_migrations.py @@ -0,0 +1,52 @@ +# 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 mock + +from nova.conductor import manager as conductor_manager +from nova import db +from nova import objects +from nova.tests.functional.api_sample_tests import test_servers + + +class ServerMigrationsSampleJsonTest(test_servers.ServersSampleBase): + extension_name = 'server-migrations' + scenarios = [('v2_22', {'api_major_version': 'v2.1'})] + extra_extensions_to_load = ["os-migrate-server", "os-access-ips"] + + def setUp(self): + """setUp method for server usage.""" + super(ServerMigrationsSampleJsonTest, self).setUp() + self.uuid = self._post_server() + + @mock.patch.object(conductor_manager.ComputeTaskManager, '_live_migrate') + @mock.patch.object(db, 'service_get_by_compute_host') + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + @mock.patch('nova.compute.manager.ComputeManager.' + 'live_migration_force_complete') + def test_live_migrate_force_complete(self, live_migration_pause_instance, + get_by_id_and_instance, + service_get_by_compute_host, + _live_migrate): + migration = objects.Migration() + migration.id = 1 + migration.status = 'running' + get_by_id_and_instance.return_value = migration + self._do_post('servers/%s/action' % self.uuid, 'live-migrate-server', + {'hostname': self.compute.host}) + response = self._do_post('servers/%s/migrations/%s/action' + % (self.uuid, '3'), 'force_complete', + {}, api_version='2.22') + self.assertEqual(202, response.status_code) diff --git a/nova/tests/unit/api/openstack/compute/test_server_migrations.py b/nova/tests/unit/api/openstack/compute/test_server_migrations.py new file mode 100644 index 000000000000..0d9037fbe2df --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_server_migrations.py @@ -0,0 +1,108 @@ +# 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 mock +import webob + +from nova.api.openstack.compute import server_migrations +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +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 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()) diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index 28d54fb530fb..744ca9ca42c6 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -3213,6 +3213,55 @@ class _ComputeAPIUnitTestMixIn(object): self.assertEqual(expect_statuses[instance.uuid], host_statuses[instance.uuid]) + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + def test_live_migrate_force_complete_succeeded( + self, get_by_id_and_instance): + + if self.cell_type == 'api': + # cell api has not been implemented. + return + rpcapi = self.compute_api.compute_rpcapi + + instance = self._create_instance_obj() + instance.task_state = task_states.MIGRATING + + migration = objects.Migration() + migration.id = 0 + migration.status = 'running' + get_by_id_and_instance.return_value = migration + + with mock.patch.object( + rpcapi, 'live_migration_force_complete') as lm_force_complete: + self.compute_api.live_migrate_force_complete( + self.context, instance, migration.id) + + lm_force_complete.assert_called_once_with(self.context, + instance, + 0) + + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + def test_live_migrate_force_complete_invalid_migration_state( + self, get_by_id_and_instance): + instance = self._create_instance_obj() + instance.task_state = task_states.MIGRATING + + migration = objects.Migration() + migration.id = 0 + migration.status = 'error' + get_by_id_and_instance.return_value = migration + + self.assertRaises(exception.InvalidMigrationState, + self.compute_api.live_migrate_force_complete, + self.context, instance, migration.id) + + def test_live_migrate_force_complete_invalid_vm_state(self): + instance = self._create_instance_obj() + instance.task_state = None + + self.assertRaises(exception.InstanceInvalidState, + self.compute_api.live_migrate_force_complete, + self.context, instance, '1') + class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): def setUp(self): diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 5ff8f11c3482..68cf96ffc94e 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -4356,3 +4356,50 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase): 'foo', False, {}) self.assertIsInstance(mock_lmcf.call_args_list[0][0][1], migrate_data_obj.LiveMigrateData) + + def test_live_migration_force_complete_succeeded(self): + + instance = objects.Instance(uuid=str(uuid.uuid4())) + migration = objects.Migration() + migration.status = 'running' + migration.id = 0 + + @mock.patch.object(self.compute, '_notify_about_instance_usage') + @mock.patch.object(objects.Migration, 'get_by_id', + return_value=migration) + @mock.patch.object(self.compute.driver, + 'live_migration_force_complete') + def _do_test(force_complete, get_by_id, _notify_about_instance_usage): + self.compute.live_migration_force_complete( + self.context, instance, migration.id) + + force_complete.assert_called_once_with(instance) + + _notify_usage_calls = [ + mock.call(self.context, instance, + 'live.migration.force.complete.start'), + mock.call(self.context, instance, + 'live.migration.force.complete.end') + ] + + _notify_about_instance_usage.assert_has_calls(_notify_usage_calls) + + _do_test() + + @mock.patch.object(compute_utils, 'add_instance_fault_from_exc') + def test_live_migration_pause_vm_invalid_migration_state( + self, add_instance_fault_from_exc): + + instance = objects.Instance(id=1234, uuid=str(uuid.uuid4())) + migration = objects.Migration() + migration.status = 'aborted' + migration.id = 0 + + @mock.patch.object(objects.Migration, 'get_by_id', + return_value=migration) + def _do_test(get_by_id): + self.assertRaises(exception.InvalidMigrationState, + self.compute.live_migration_force_complete, + self.context, instance, migration.id) + + _do_test() diff --git a/nova/tests/unit/compute/test_rpcapi.py b/nova/tests/unit/compute/test_rpcapi.py index 476b317c5f29..8396adef924b 100644 --- a/nova/tests/unit/compute/test_rpcapi.py +++ b/nova/tests/unit/compute/test_rpcapi.py @@ -304,6 +304,11 @@ class ComputeRpcAPITestCase(test.NoDBTestCase): migration='migration', migrate_data={}, version='4.8') + def test_live_migration_force_complete(self): + self._test_compute_api('live_migration_force_complete', 'cast', + instance=self.fake_instance_obj, + migration_id='1', version='4.9') + def test_post_live_migration_at_destination(self): self._test_compute_api('post_live_migration_at_destination', 'cast', instance=self.fake_instance_obj, diff --git a/nova/tests/unit/fake_policy.py b/nova/tests/unit/fake_policy.py index 3fee5ac2d05f..2444c2196b1a 100644 --- a/nova/tests/unit/fake_policy.py +++ b/nova/tests/unit/fake_policy.py @@ -125,6 +125,7 @@ policy_data = """ "os_compute_api:servers:start": "", "os_compute_api:servers:stop": "", "os_compute_api:servers:trigger_crash_dump": "", + "os_compute_api:servers:migrations:force_complete": "", "os_compute_api:os-access-ips": "", "compute_extension:accounts": "", "compute_extension:admin_actions:pause": "", diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index 10f8a661ee24..1754be832213 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -300,6 +300,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase): "os_compute_api:servers:detail:get_all_tenants", "os_compute_api:servers:index:get_all_tenants", "os_compute_api:servers:show:host_status", +"os_compute_api:servers:migrations:force_complete", "network:attach_external_network", "os_compute_api:os-admin-actions", "os_compute_api:os-admin-actions:reset_network", @@ -672,6 +673,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase): "os_compute_api:os-server-usage:discoverable", "os_compute_api:os-server-groups", "os_compute_api:os-server-groups:discoverable", +"os_compute_api:servers:migrations:discoverable", "os_compute_api:os-services:discoverable", "os_compute_api:server-metadata:discoverable", "os_compute_api:servers:discoverable", diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 8dd38ff26261..4dbbd8c5181b 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -13039,6 +13039,12 @@ class LibvirtConnTestCase(test.NoDBTestCase): lambda x: x, lambda x: x) + @mock.patch.object(libvirt_driver.LibvirtDriver, "pause") + def test_live_migration_force_complete(self, pause): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + drvr.live_migration_force_complete(self.test_instance) + pause.assert_called_once_with(self.test_instance) + @mock.patch('os.path.exists', return_value=True) @mock.patch('tempfile.mkstemp') @mock.patch('os.close', return_value=None) diff --git a/nova/tests/unit/virt/test_virt_drivers.py b/nova/tests/unit/virt/test_virt_drivers.py index 73bbd6bd0205..79d134735315 100644 --- a/nova/tests/unit/virt/test_virt_drivers.py +++ b/nova/tests/unit/virt/test_virt_drivers.py @@ -661,6 +661,11 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): self.connection.live_migration(self.ctxt, instance_ref, 'otherhost', lambda *a: None, lambda *a: None) + @catch_notimplementederror + def test_live_migration_force_complete(self): + instance_ref, network_info = self._get_running_instance() + self.connection.live_migration_force_complete(instance_ref) + @catch_notimplementederror def _check_available_resource_fields(self, host_status): keys = ['vcpus', 'memory_mb', 'local_gb', 'vcpus_used', diff --git a/nova/virt/driver.py b/nova/virt/driver.py index ff60a33619b2..ce376715b429 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -860,6 +860,14 @@ class ComputeDriver(object): """ raise NotImplementedError() + def live_migration_force_complete(self, instance): + """Force live migration to complete + + :param instance: Instance being live migrated + + """ + raise NotImplementedError() + def rollback_live_migration_at_destination(self, context, instance, network_info, block_device_info, diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 328ef850213b..1c0e9cfe42dc 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -468,6 +468,9 @@ class FakeDriver(driver.ComputeDriver): migrate_data) return + def live_migration_force_complete(self, instance): + return + def check_can_live_migrate_destination_cleanup(self, context, dest_check_data): return diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 88f7e07097be..52d245afdaf2 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -6411,6 +6411,12 @@ class LibvirtDriver(driver.ComputeDriver): LOG.debug("Live migration monitoring is all done", instance=instance) + def live_migration_force_complete(self, instance): + # NOTE(pkoniszewski): currently only pause during live migration is + # supported to force live migration to complete, so just try to pause + # the instance + self.pause(instance) + def _try_fetch_image(self, context, path, image_id, instance, fallback_from_host=None): try: diff --git a/releasenotes/notes/force-live-migration-be5a10cd9c8eb981.yaml b/releasenotes/notes/force-live-migration-be5a10cd9c8eb981.yaml new file mode 100644 index 000000000000..8bc0c40e7817 --- /dev/null +++ b/releasenotes/notes/force-live-migration-be5a10cd9c8eb981.yaml @@ -0,0 +1,4 @@ +--- +features: + - A new REST API to force live migration to complete has been added + in microversion 2.22. diff --git a/setup.cfg b/setup.cfg index 0974abf9c870..9a3133e1f2c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -137,6 +137,7 @@ nova.api.v21.extensions = server_diagnostics = nova.api.openstack.compute.server_diagnostics:ServerDiagnostics server_external_events = nova.api.openstack.compute.server_external_events:ServerExternalEvents server_metadata = nova.api.openstack.compute.server_metadata:ServerMetadata + server_migrations = nova.api.openstack.compute.server_migrations:ServerMigrations server_password = nova.api.openstack.compute.server_password:ServerPassword server_usage = nova.api.openstack.compute.server_usage:ServerUsage server_groups = nova.api.openstack.compute.server_groups:ServerGroups