diff --git a/api-ref/source/server-migrations.inc b/api-ref/source/server-migrations.inc index e1467983d3c5..3f0011b28cd0 100644 --- a/api-ref/source/server-migrations.inc +++ b/api-ref/source/server-migrations.inc @@ -184,6 +184,9 @@ Abort an in-progress live migration. .. note:: Microversion 2.24 or greater is required for this API. +.. note:: With microversion 2.65 or greater, you can abort live migrations + also in ``queued`` and ``preparing`` status. + .. note:: Not all compute back ends support aborting an in-progress live migration. @@ -198,7 +201,9 @@ The server OS-EXT-STS:task_state value must be ``migrating``. If the server is locked, you must have administrator privileges to force the completion of the server migration. -The migration status must be ``running``. +For microversions from 2.24 to 2.64 the migration status must be ``running``, +for microversion 2.65 and greater, the migration status can also be ``queued`` +and ``preparing``. **Asynchronous Postconditions** diff --git a/doc/api_samples/server-migrations/v2.65/live-migrate-server.json b/doc/api_samples/server-migrations/v2.65/live-migrate-server.json new file mode 100644 index 000000000000..c2f5bf6c989f --- /dev/null +++ b/doc/api_samples/server-migrations/v2.65/live-migrate-server.json @@ -0,0 +1,6 @@ +{ + "os-migrateLive": { + "host": null, + "block_migration": "auto" + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index c362dafd504e..7e2434ee3325 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.64", + "version": "2.65", "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 7e4cc0aab0c7..8fb2d2430cf4 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.64", + "version": "2.65", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 586395904ce2..058945ae5b15 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -156,6 +156,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: and ``rules`` (optional) fields are added in response body of GET, POST /os-server-groups APIs and GET /os-server-groups/{group_id} API. + * 2.65 - Add support for abort live migrations in ``queued`` and + ``preparing`` status. """ # The minimum and maximum versions of the API supported @@ -164,7 +166,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.64" +_MAX_API_VERSION = "2.65" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 607dd55f740a..78228a84113a 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -835,4 +835,10 @@ in server group APIs: ``/os-server-groups/{server_group_id}`` API. * The ``policies`` and ``metadata`` fields have been removed from the response body of POST, GET ``/os-server-groups`` API and GET - ``/os-server-groups/{server_group_id}`` API. \ No newline at end of file + ``/os-server-groups/{server_group_id}`` API. + +2.65 +---- + +Add support for abort live migrations in ``queued`` and ``preparing`` status +for API ``DELETE /servers/{server_id}/migrations/{migration_id}``. diff --git a/nova/api/openstack/compute/server_migrations.py b/nova/api/openstack/compute/server_migrations.py index 91e5ed58b823..3e3097123043 100644 --- a/nova/api/openstack/compute/server_migrations.py +++ b/nova/api/openstack/compute/server_migrations.py @@ -146,9 +146,13 @@ class ServerMigrationsController(wsgi.Controller): context = req.environ['nova.context'] context.can(sm_policies.POLICY_ROOT % 'delete') + support_abort_in_queue = api_version_request.is_supported(req, '2.65') + instance = common.get_instance(self.compute_api, context, server_id) try: - self.compute_api.live_migrate_abort(context, instance, id) + self.compute_api.live_migrate_abort( + context, instance, id, + support_abort_in_queue=support_abort_in_queue) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state( state_error, "abort live migration", server_id) @@ -156,3 +160,5 @@ class ServerMigrationsController(wsgi.Controller): raise exc.HTTPNotFound(explanation=e.format_message()) except exception.InvalidMigrationState as e: raise exc.HTTPBadRequest(explanation=e.format_message()) + except exception.AbortQueuedLiveMigrationNotYetSupported as e: + raise exc.HTTPConflict(explanation=e.format_message()) diff --git a/nova/compute/api.py b/nova/compute/api.py index a14e16062677..7e0972cfc064 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -106,6 +106,7 @@ BFV_RESERVE_MIN_COMPUTE_VERSION = 17 CINDER_V3_ATTACH_MIN_COMPUTE_VERSION = 24 MIN_COMPUTE_MULTIATTACH = 27 MIN_COMPUTE_TRUSTED_CERTS = 31 +MIN_COMPUTE_ABORT_QUEUED_LIVE_MIGRATION = 34 # FIXME(danms): Keep a global cache of the cells we find the # first time we look. This needs to be refreshed on a timer or @@ -4411,12 +4412,15 @@ class API(base.Base): @check_instance_lock @check_instance_cell @check_instance_state(task_state=[task_states.MIGRATING]) - def live_migrate_abort(self, context, instance, migration_id): + def live_migrate_abort(self, context, instance, migration_id, + support_abort_in_queue=False): """Abort an in-progress live migration. :param context: Security context :param instance: The instance that is being migrated :param migration_id: ID of in-progress live migration + :param support_abort_in_queue: Flag indicating whether we can support + abort migrations in "queued" or "preparing" status. """ migration = objects.Migration.get_by_id_and_instance(context, @@ -4424,7 +4428,30 @@ class API(base.Base): LOG.debug("Going to cancel live migration %s", migration.id, instance=instance) - if migration.status != 'running': + # If the microversion does not support abort migration in queue, + # we are only be able to abort migrations with `running` status; + # if it is supported, we are able to also abort migrations in + # `queued` and `preparing` status. + allowed_states = ['running'] + queued_states = ['queued', 'preparing'] + if support_abort_in_queue: + # The user requested a microversion that supports aborting a queued + # or preparing live migration. But we need to check that the + # compute service hosting the instance is new enough to support + # aborting a queued/preparing live migration, so we check the + # service version here. + # TODO(Kevin_Zheng): This service version check can be removed in + # Stein (at the earliest) when the API only supports Rocky or + # newer computes. + if migration.status in queued_states: + service = objects.Service.get_by_compute_host( + context, instance.host) + if service.version < MIN_COMPUTE_ABORT_QUEUED_LIVE_MIGRATION: + raise exception.AbortQueuedLiveMigrationNotYetSupported( + migration_id=migration_id, status=migration.status) + allowed_states.extend(queued_states) + + if migration.status not in allowed_states: raise exception.InvalidMigrationState(migration_id=migration_id, instance_uuid=instance.uuid, state=migration.status, diff --git a/nova/exception.py b/nova/exception.py index b01e73abfad1..6b8a6d52cb06 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1172,6 +1172,12 @@ class InvalidMigrationState(Invalid): "migration is in this state.") +class AbortQueuedLiveMigrationNotYetSupported(NovaException): + msg_fmt = _("Aborting live migration %(migration_id)s with status " + "%(status)s is not yet supported for this instance.") + code = 409 + + class ConsoleLogOutputException(NovaException): msg_fmt = _("Console log output could not be retrieved for instance " "%(instance_id)s. Reason: %(reason)s") diff --git a/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.65/live-migrate-server.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.65/live-migrate-server.json.tpl new file mode 100644 index 000000000000..c2f5bf6c989f --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.65/live-migrate-server.json.tpl @@ -0,0 +1,6 @@ +{ + "os-migrateLive": { + "host": null, + "block_migration": "auto" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_server_migrations.py b/nova/tests/functional/api_sample_tests/test_server_migrations.py index 4b9966fdb558..890a1c5db6d5 100644 --- a/nova/tests/functional/api_sample_tests/test_server_migrations.py +++ b/nova/tests/functional/api_sample_tests/test_server_migrations.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from concurrent import futures import datetime import mock @@ -226,3 +227,21 @@ class ServerMigrationsSamplesJsonTestV2_59( self.fake_migrations[1][ 'uuid'] = '22341d4b-346a-40d0-83c6-5f4f6892b650' super(ServerMigrationsSamplesJsonTestV2_59, self).setUp() + + +class ServerMigrationsSampleJsonTestV2_65(ServerMigrationsSampleJsonTestV2_24): + ADMIN_API = True + microversion = '2.65' + scenarios = [('v2_65', {'api_major_version': 'v2.1'})] + + @mock.patch.object(conductor_manager.ComputeTaskManager, '_live_migrate') + def test_live_migrate_abort_migration_queued(self, _live_migrate): + self.migration.status = 'queued' + self.migration.save() + self._do_post('servers/%s/action' % self.uuid, 'live-migrate-server', + {'hostname': self.compute.host}) + self.compute._waiting_live_migrations[self.uuid] = ( + self.migration, futures.Future()) + uri = 'servers/%s/migrations/%s' % (self.uuid, self.migration.id) + response = self._do_delete(uri) + 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 index 4a335eca6823..11bd9900e1ad 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_migrations.py +++ b/nova/tests/unit/api/openstack/compute/test_server_migrations.py @@ -17,6 +17,7 @@ import copy import datetime import mock +import six import webob from nova.api.openstack.compute import server_migrations @@ -273,7 +274,8 @@ class ServerMigrationsTestsV224(ServerMigrationsTestsV21): self.controller.delete(self.req, 'server-id', 'migration-id') mock_abort.assert_called_once_with(self.context, mock_get(), - 'migration-id') + 'migration-id', + support_abort_in_queue=False) _do_test() def _test_cancel_live_migration_failed(self, fake_exc, expected_exc): @@ -318,6 +320,39 @@ class ServerMigrationsTestsV224(ServerMigrationsTestsV21): 'migration-id') +class ServerMigrationsTestsV265(ServerMigrationsTestsV224): + wsgi_api_version = '2.65' + + 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', 1) + mock_abort.assert_called_once_with(self.context, + mock_get.return_value, 1, + support_abort_in_queue=True) + _do_test() + + def test_cancel_live_migration_in_queue_not_yet_available(self): + exc = exception.AbortQueuedLiveMigrationNotYetSupported( + migration_id=1, status='queued') + + @mock.patch.object(self.compute_api, 'live_migrate_abort', + side_effect=exc) + @mock.patch.object(self.compute_api, 'get') + def _do_test(mock_get, mock_abort): + error = self.assertRaises(webob.exc.HTTPConflict, + self.controller.delete, + self.req, 'server-id', 1) + self.assertIn("Aborting live migration 1 with status queued is " + "not yet supported for this instance.", + six.text_type(error)) + mock_abort.assert_called_once_with(self.context, + mock_get.return_value, 1, + support_abort_in_queue=True) + _do_test() + + class ServerMigrationsPolicyEnforcementV21(test.NoDBTestCase): wsgi_api_version = '2.22' diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index 3848ed838c71..41d98fb15a16 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -5364,6 +5364,65 @@ class _ComputeAPIUnitTestMixIn(object): instance_actions.LIVE_MIGRATION_CANCEL) mock_lm_abort.called_once_with(self.context, instance, migration.id) + @mock.patch('nova.compute.api.API._record_action_start') + @mock.patch.object(compute_rpcapi.ComputeAPI, 'live_migration_abort') + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + @mock.patch.object(objects.Service, 'get_by_compute_host') + def test_live_migrate_abort_in_queue_succeeded(self, + mock_get_service, + mock_get_migration, + mock_lm_abort, + mock_rec_action): + service_obj = objects.Service() + service_obj.version = ( + compute_api.MIN_COMPUTE_ABORT_QUEUED_LIVE_MIGRATION) + mock_get_service.return_value = service_obj + instance = self._create_instance_obj() + instance.task_state = task_states.MIGRATING + for migration_status in ('queued', 'preparing'): + migration = self._get_migration( + 21, migration_status, 'live-migration') + mock_get_migration.return_value = migration + self.compute_api.live_migrate_abort(self.context, + instance, + migration.id, + support_abort_in_queue=True) + mock_rec_action.assert_called_once_with( + self.context, instance, instance_actions.LIVE_MIGRATION_CANCEL) + mock_lm_abort.called_once_with(self.context, instance, migration) + mock_get_migration.reset_mock() + mock_rec_action.reset_mock() + mock_lm_abort.reset_mock() + + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + def test_live_migration_abort_in_queue_old_microversion_fails( + self, mock_get_migration): + instance = self._create_instance_obj() + instance.task_state = task_states.MIGRATING + migration = self._get_migration(21, 'queued', 'live-migration') + mock_get_migration.return_value = migration + self.assertRaises(exception.InvalidMigrationState, + self.compute_api.live_migrate_abort, self.context, + instance, migration.id, + support_abort_in_queue=False) + + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') + @mock.patch.object(objects.Service, 'get_by_compute_host') + def test_live_migration_abort_in_queue_old_compute_conflict( + self, mock_get_service, mock_get_migration): + service_obj = objects.Service() + service_obj.version = ( + compute_api.MIN_COMPUTE_ABORT_QUEUED_LIVE_MIGRATION - 1) + mock_get_service.return_value = service_obj + instance = self._create_instance_obj() + instance.task_state = task_states.MIGRATING + migration = self._get_migration(21, 'queued', 'live-migration') + mock_get_migration.return_value = migration + self.assertRaises(exception.AbortQueuedLiveMigrationNotYetSupported, + self.compute_api.live_migrate_abort, self.context, + instance, migration.id, + support_abort_in_queue=True) + @mock.patch.object(objects.Migration, 'get_by_id_and_instance') def test_live_migration_abort_wrong_migration_status(self, mock_get_migration): diff --git a/releasenotes/notes/abort-live-migration-in-queue-0c917f415d6dac5a.yaml b/releasenotes/notes/abort-live-migration-in-queue-0c917f415d6dac5a.yaml new file mode 100644 index 000000000000..fd6f2aa83823 --- /dev/null +++ b/releasenotes/notes/abort-live-migration-in-queue-0c917f415d6dac5a.yaml @@ -0,0 +1,5 @@ +--- +features: + - The support to abort live migrations with ``queued`` and ``preparing`` + status using ``DELETE /servers/{server_id}/migrations/{migration_id}`` + API has been added in microversion 2.65. \ No newline at end of file