From 84db8b3f3d202a234221ed265ec00a7cf32999c9 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Wed, 4 Sep 2019 19:27:15 +0100 Subject: [PATCH] Reject live migration and suspend on SEV guests As per the spec[0], live migration and suspend are not (yet) supported for SEV guests, so reject them at the API level with an HTTP 409 (Conflict). [0] http://specs.openstack.org/openstack/nova-specs/specs/train/approved/amd-sev-libvirt-support.html#limitations blueprint: amd-sev-libvirt-support Change-Id: I69b6e153324a3e5680e096cd714e5d4dd05bae34 --- nova/api/openstack/compute/migrate_server.py | 2 ++ nova/api/openstack/compute/suspend_server.py | 3 ++- nova/compute/api.py | 20 +++++++++++++++++++ nova/exception.py | 6 ++++++ .../openstack/compute/test_migrate_server.py | 14 +++++++++++++ .../openstack/compute/test_suspend_server.py | 14 +++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/compute/migrate_server.py b/nova/api/openstack/compute/migrate_server.py index 17c1975fa1b0..6d75bdc4ac65 100644 --- a/nova/api/openstack/compute/migrate_server.py +++ b/nova/api/openstack/compute/migrate_server.py @@ -171,6 +171,8 @@ class MigrateServerController(wsgi.Controller): "'%(ex)s'", {'ex': ex}) else: raise exc.HTTPBadRequest(explanation=ex.format_message()) + except exception.OperationNotSupportedForSEV as e: + raise exc.HTTPConflict(explanation=e.format_message()) except exception.InstanceIsLocked as e: raise exc.HTTPConflict(explanation=e.format_message()) except exception.ComputeHostNotFound as e: diff --git a/nova/api/openstack/compute/suspend_server.py b/nova/api/openstack/compute/suspend_server.py index dd576684b74e..fcee538c7b03 100644 --- a/nova/api/openstack/compute/suspend_server.py +++ b/nova/api/openstack/compute/suspend_server.py @@ -38,7 +38,8 @@ class SuspendServerController(wsgi.Controller): target={'user_id': server.user_id, 'project_id': server.project_id}) self.compute_api.suspend(context, server) - except exception.InstanceIsLocked as e: + except (exception.OperationNotSupportedForSEV, + 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, diff --git a/nova/compute/api.py b/nova/compute/api.py index db5287e10094..06b9f67dd778 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -205,6 +205,24 @@ def check_instance_lock(function): return inner +def reject_sev_instances(operation): + """Decorator. Raise OperationNotSupportedForSEV if instance has SEV + enabled. + """ + + def outer(f): + @six.wraps(f) + def inner(self, context, instance, *args, **kw): + if hardware.get_mem_encryption_constraint(instance.flavor, + instance.image_meta): + raise exception.OperationNotSupportedForSEV( + instance_uuid=instance.uuid, + operation=operation) + return f(self, context, instance, *args, **kw) + return inner + return outer + + def _diff_dict(orig, new): """Return a dict describing how to change orig to new. The keys correspond to values that have changed; the value will be a list @@ -3785,6 +3803,7 @@ class API(base.Base): return self.compute_rpcapi.get_instance_diagnostics(context, instance=instance) + @reject_sev_instances(instance_actions.SUSPEND) @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE]) def suspend(self, context, instance): @@ -4448,6 +4467,7 @@ class API(base.Base): diff=diff) return _metadata + @reject_sev_instances(instance_actions.LIVE_MIGRATION) @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED]) def live_migrate(self, context, instance, block_migration, diff --git a/nova/exception.py b/nova/exception.py index 90b75e44bdd6..e8a8e253a0f0 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -533,6 +533,12 @@ class UnableToMigrateToSelf(Invalid): "to current host (%(host)s).") +class OperationNotSupportedForSEV(NovaException): + msg_fmt = _("Operation '%(operation)s' not supported for SEV-enabled " + "instance (%(instance_uuid)s).") + code = 409 + + class InvalidHypervisorType(Invalid): msg_fmt = _("The supplied hypervisor type of is invalid.") 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 dfc4638fde00..2bd91c6385ab 100644 --- a/nova/tests/unit/api/openstack/compute/test_migrate_server.py +++ b/nova/tests/unit/api/openstack/compute/test_migrate_server.py @@ -609,6 +609,20 @@ class MigrateServerTestsV268(MigrateServerTestsV256): method_translations=method_translations, args_map=args_map) + @mock.patch('nova.virt.hardware.get_mem_encryption_constraint', + new=mock.Mock(return_value=True)) + @mock.patch.object(objects.instance.Instance, 'image_meta') + def test_live_migrate_sev_rejected(self, mock_image): + instance = self._stub_instance_get() + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}} + ex = self.assertRaises(webob.exc.HTTPConflict, + self.controller._migrate_live, + self.req, fakes.FAKE_UUID, body=body) + self.assertIn("Operation 'live-migration' not supported for " + "SEV-enabled instance (%s)" % instance.uuid, + six.text_type(ex)) + def test_live_migrate_with_forced_host(self): body = {'os-migrateLive': {'host': 'hostname', 'block_migration': 'auto', diff --git a/nova/tests/unit/api/openstack/compute/test_suspend_server.py b/nova/tests/unit/api/openstack/compute/test_suspend_server.py index 74b3840614d5..d7ee0d15612d 100644 --- a/nova/tests/unit/api/openstack/compute/test_suspend_server.py +++ b/nova/tests/unit/api/openstack/compute/test_suspend_server.py @@ -13,10 +13,13 @@ # under the License. import mock +import six +import webob from nova.api.openstack.compute import suspend_server as \ suspend_server_v21 from nova import exception +from nova import objects from nova import test from nova.tests.unit.api.openstack.compute import admin_only_action_common from nova.tests.unit.api.openstack import fakes @@ -39,6 +42,17 @@ class SuspendServerTestsV21(admin_only_action_common.CommonTests): def test_suspend_resume(self): self._test_actions(['_suspend', '_resume']) + @mock.patch('nova.virt.hardware.get_mem_encryption_constraint', + new=mock.Mock(return_value=True)) + @mock.patch.object(objects.instance.Instance, 'image_meta') + def test_suspend_sev_rejected(self, mock_image): + instance = self._stub_instance_get() + ex = self.assertRaises(webob.exc.HTTPConflict, + self.controller._suspend, + self.req, fakes.FAKE_UUID, body={}) + self.assertIn("Operation 'suspend' not supported for SEV-enabled " + "instance (%s)" % instance.uuid, six.text_type(ex)) + def test_suspend_resume_with_non_existed_instance(self): self._test_actions_with_non_existed_instance(['_suspend', '_resume'])