diff --git a/api-ref/source/servers-admin-action.inc b/api-ref/source/servers-admin-action.inc index 84a86951ca46..d48d181e84de 100644 --- a/api-ref/source/servers-admin-action.inc +++ b/api-ref/source/servers-admin-action.inc @@ -155,6 +155,12 @@ Policy defaults enable only users with the administrative role to perform this operation. Cloud providers can change these permissions through the ``policy.json`` file. +Starting from REST API version 2.34 pre-live-migration checks are done +asynchronously, results of these checks are available in ``instance-actions``. +Nova responds immediately, and no pre-live-migration checks are returned. +The instance will not immediately change state to ``ERROR``, if a failure of +the live-migration checks occurs. + Normal response codes: 202 Error response codes: badRequest(400), unauthorized(401), forbidden(403) diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 3d2e02d18352..84be6f62b027 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.33", + "version": "2.34", "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 5ce4234d169e..7bef94b2d4d5 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.33", + "version": "2.34", "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 769ae17c1fab..f394c38b3394 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -83,6 +83,10 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.32 - Add tag to networks and block_device_mapping_v2 in server boot request body. * 2.33 - Add pagination support for hypervisors. + * 2.34 - Checks before live-migration are made in asynchronous way. + os-Migratelive Action does not throw badRequest in case of + pre-checks failure. Verification result is available over + instance-actions. """ # The minimum and maximum versions of the API supported @@ -91,7 +95,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.33" +_MAX_API_VERSION = "2.34" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/migrate_server.py b/nova/api/openstack/compute/migrate_server.py index 0fa47f8f2330..94e68260a3f0 100644 --- a/nova/api/openstack/compute/migrate_server.py +++ b/nova/api/openstack/compute/migrate_server.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log as logging +from oslo_utils import excutils from oslo_utils import strutils from webob import exc @@ -25,8 +27,10 @@ from nova.api import validation from nova import compute from nova import exception from nova.i18n import _ +from nova.i18n import _LE from nova.policies import migrate_server as ms_policies +LOG = logging.getLogger(__name__) ALIAS = "os-migrate-server" @@ -72,13 +76,9 @@ class MigrateServerController(wsgi.Controller): host = body["os-migrateLive"]["host"] block_migration = body["os-migrateLive"]["block_migration"] force = None - + async = api_version_request.is_supported(req, min_version='2.34') if api_version_request.is_supported(req, min_version='2.30'): - force = body["os-migrateLive"].get("force", False) - force = strutils.bool_from_string(force, strict=True) - if force is True and not host: - message = _("Can't force to a non-provided destination") - raise exc.HTTPBadRequest(explanation=message) + force = self._get_force_param_for_live_migration(body, host) if api_version_request.is_supported(req, min_version='2.25'): if block_migration == 'auto': block_migration = None @@ -97,7 +97,7 @@ class MigrateServerController(wsgi.Controller): try: instance = common.get_instance(self.compute_api, context, id) self.compute_api.live_migrate(context, instance, block_migration, - disk_over_commit, host, force) + disk_over_commit, host, force, async) except exception.InstanceUnknownCell as e: raise exc.HTTPNotFound(explanation=e.format_message()) except (exception.NoValidHost, @@ -111,13 +111,27 @@ class MigrateServerController(wsgi.Controller): exception.HypervisorUnavailable, exception.MigrationPreCheckError, exception.LiveMigrationWithOldNovaNotSupported) as ex: - raise exc.HTTPBadRequest(explanation=ex.format_message()) + if async: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unexpected exception received from " + "conductor during pre-live-migration checks " + "'%(ex)s'"), {'ex': ex}) + else: + raise exc.HTTPBadRequest(explanation=ex.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, 'os-migrateLive', id) + def _get_force_param_for_live_migration(self, body, host): + force = body["os-migrateLive"].get("force", False) + force = strutils.bool_from_string(force, strict=True) + if force is True and not host: + message = _("Can't force to a non-provided destination") + raise exc.HTTPBadRequest(explanation=message) + return force + class MigrateServer(extensions.V21APIExtensionBase): """Enable migrate and live-migrate server actions.""" diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 34608bae004f..4194ca929435 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -343,3 +343,10 @@ user documentation. API request:: GET /v2.1/{tenant_id}/os-hypervisors?marker={hypervisor_id}&limit={limit} + +2.34 +---- + + Checks in ``os-migrateLive`` before live-migration actually starts are now + made in background. ``os-migrateLive`` is not throwing `400 Bad Request` if + pre-live-migration checks fail. diff --git a/nova/tests/unit/api/openstack/compute/test_migrate_server.py b/nova/tests/unit/api/openstack/compute/test_migrate_server.py index fe5e5690f932..a21ba7858618 100644 --- a/nova/tests/unit/api/openstack/compute/test_migrate_server.py +++ b/nova/tests/unit/api/openstack/compute/test_migrate_server.py @@ -33,6 +33,7 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): _api_version = '2.1' disk_over_commit = False force = None + async = False def setUp(self): super(MigrateServerTestsV21, self).setUp() @@ -59,7 +60,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): '_migrate_live': 'live_migrate'} body_map = {'_migrate_live': self._get_migration_body(host='hostname')} args_map = {'_migrate_live': ((False, self.disk_over_commit, - 'hostname', self.force), {})} + 'hostname', self.force, self.async), + {})} self._test_actions(['_migrate', '_migrate_live'], body_map=body_map, method_translations=method_translations, args_map=args_map) @@ -69,7 +71,7 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): '_migrate_live': 'live_migrate'} body_map = {'_migrate_live': self._get_migration_body(host=None)} args_map = {'_migrate_live': ((False, self.disk_over_commit, None, - self.force), + self.force, self.async), {})} self._test_actions(['_migrate', '_migrate_live'], body_map=body_map, method_translations=method_translations, @@ -85,7 +87,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): '_migrate_live': 'live_migrate'} body_map = self._get_migration_body(host='hostname') args_map = {'_migrate_live': ((False, self.disk_over_commit, - 'hostname', self.force), {})} + 'hostname', self.force, self.async), + {})} exception_arg = {'_migrate': 'migrate', '_migrate_live': 'os-migrateLive'} self._test_actions_raise_conflict_on_invalid_state( @@ -100,7 +103,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): body_map = {'_migrate_live': self._get_migration_body(host='hostname')} args_map = {'_migrate_live': ((False, self.disk_over_commit, - 'hostname', self.force), {})} + 'hostname', self.force, self.async), + {})} self._test_actions_with_locked_instance( ['_migrate', '_migrate_live'], body_map=body_map, args_map=args_map, method_translations=method_translations) @@ -125,19 +129,13 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): instance = self._stub_instance_get() self.compute_api.live_migrate(self.context, instance, False, self.disk_over_commit, 'hostname', - self.force) + self.force, self.async) self.mox.ReplayAll() - - res = self.controller._migrate_live(self.req, instance.uuid, - body={'os-migrateLive': param}) - # NOTE: on v2.1, http status code is set as wsgi_code of API - # method instead of status_int in a response object. - if self._api_version == '2.1': - status_int = self.controller._migrate_live.wsgi_code - else: - status_int = res.status_int - self.assertEqual(202, status_int) + live_migrate_method = self.controller._migrate_live + live_migrate_method(self.req, instance.uuid, + body={'os-migrateLive': param}) + self.assertEqual(202, live_migrate_method.wsgi_code) def test_migrate_live_enabled(self): param = self._get_params(host='hostname') @@ -204,9 +202,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests): instance = self._stub_instance_get(uuid=uuid) self.compute_api.live_migrate(self.context, instance, False, self.disk_over_commit, - 'hostname', self.force + 'hostname', self.force, self.async ).AndRaise(fake_exc) - self.mox.ReplayAll() body = self._get_migration_body(host='hostname') @@ -308,8 +305,8 @@ class MigrateServerTestsV225(MigrateServerTestsV21): method_translations = {'_migrate_live': 'live_migrate'} body_map = {'_migrate_live': {'os-migrateLive': {'host': 'hostname', 'block_migration': 'auto'}}} - args_map = {'_migrate_live': ((None, None, 'hostname', self.force), - {})} + args_map = {'_migrate_live': ((None, None, 'hostname', self.force, + self.async), {})} self._test_actions(['_migrate_live'], body_map=body_map, method_translations=method_translations, args_map=args_map) @@ -329,7 +326,6 @@ class MigrateServerTestsV225(MigrateServerTestsV21): class MigrateServerTestsV230(MigrateServerTestsV225): - force = False def setUp(self): @@ -346,8 +342,8 @@ class MigrateServerTestsV230(MigrateServerTestsV225): body_map = {'_migrate_live': {'os-migrateLive': {'host': 'hostname', 'block_migration': 'auto', 'force': litteral_force}}} - args_map = {'_migrate_live': ((None, None, 'hostname', force), - {})} + args_map = {'_migrate_live': ((None, None, 'hostname', force, + self.async), {})} self._test_actions(['_migrate_live'], body_map=body_map, method_translations=method_translations, args_map=args_map) @@ -366,6 +362,75 @@ class MigrateServerTestsV230(MigrateServerTestsV225): self.req, fakes.FAKE_UUID, body=body) +class MigrateServerTestsV234(MigrateServerTestsV230): + async = True + + def setUp(self): + super(MigrateServerTestsV230, self).setUp() + self.req.api_version_request = api_version_request.APIVersionRequest( + '2.34') + + # NOTE(tdurakov): for REST API version 2.34 and above, tests below are not + # valid, as they are made in background. + def test_migrate_live_compute_service_unavailable(self): + pass + + def test_migrate_live_invalid_hypervisor_type(self): + pass + + def test_migrate_live_invalid_cpu_info(self): + pass + + def test_migrate_live_unable_to_migrate_to_self(self): + pass + + def test_migrate_live_destination_hypervisor_too_old(self): + pass + + def test_migrate_live_no_valid_host(self): + pass + + def test_migrate_live_invalid_local_storage(self): + pass + + def test_migrate_live_invalid_shared_storage(self): + pass + + def test_migrate_live_hypervisor_unavailable(self): + pass + + def test_migrate_live_instance_not_active(self): + pass + + def test_migrate_live_pre_check_error(self): + pass + + def test_migrate_live_migration_precheck_client_exception(self): + pass + + def test_migrate_live_migration_with_unexpected_error(self): + pass + + def test_migrate_live_migration_with_old_nova_not_supported(self): + pass + + def test_migrate_live_unexpected_error(self): + exc = exception.NoValidHost(reason="No valid host found") + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + instance = self._stub_instance_get() + self.compute_api.live_migrate(self.context, instance, None, + self.disk_over_commit, 'hostname', + self.force, self.async).AndRaise(exc) + + self.mox.ReplayAll() + body = {'os-migrateLive': + {'host': 'hostname', 'block_migration': 'auto'}} + + self.assertRaises(webob.exc.HTTPInternalServerError, + self.controller._migrate_live, + self.req, instance.uuid, body=body) + + class MigrateServerPolicyEnforcementV21(test.NoDBTestCase): def setUp(self): diff --git a/releasenotes/notes/async-live-migration-rest-check-675ec309a9ccc28e.yaml b/releasenotes/notes/async-live-migration-rest-check-675ec309a9ccc28e.yaml new file mode 100644 index 000000000000..4341cb910f78 --- /dev/null +++ b/releasenotes/notes/async-live-migration-rest-check-675ec309a9ccc28e.yaml @@ -0,0 +1,8 @@ +--- +features: + - Starting from REST API microversion 2.34 pre-live-migration checks are + performed asynchronously. ``instance-actions`` should be used for getting + information about the checks results. New approach allows to reduce rpc + timeouts amount, as previous workflow was fully blocking and checks before + live-migration make blocking rpc request to both source and destionation + compute node.