diff --git a/lower-constraints.txt b/lower-constraints.txt index 85fc28770d51..42d97ad81483 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -66,7 +66,7 @@ numpy==1.14.2 openstacksdk==0.35.0 os-brick==2.6.1 os-client-config==1.29.0 -os-resource-classes==0.1.0 +os-resource-classes==0.4.0 os-service-types==1.7.0 os-traits==0.16.0 os-vif==1.14.0 diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 0e556d5bda38..86eab27d77d4 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -57,6 +57,7 @@ LOG = logging.getLogger(__name__) INVALID_FLAVOR_IMAGE_EXCEPTIONS = ( exception.BadRequirementEmulatorThreadsPolicy, exception.CPUThreadPolicyConfigurationInvalid, + exception.FlavorImageConflict, exception.ImageCPUPinningForbidden, exception.ImageCPUThreadPolicyForbidden, exception.ImageNUMATopologyAsymmetric, diff --git a/nova/compute/api.py b/nova/compute/api.py index 7bffd9878e44..6b1e42e2ba62 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -676,6 +676,8 @@ class API(base.Base): """ image_meta = _get_image_meta_obj(image) + API._validate_flavor_image_mem_encryption(instance_type, image_meta) + # validate PMU extra spec and image metadata flavor_pmu = instance_type.extra_specs.get('hw:pmu') image_pmu = image_meta.properties.get('hw_pmu') @@ -694,6 +696,19 @@ class API(base.Base): if validate_pci: pci_request.get_pci_requests_from_flavor(instance_type) + @staticmethod + def _validate_flavor_image_mem_encryption(instance_type, image): + """Validate that the flavor and image don't make contradictory + requests regarding memory encryption. + + :param instance_type: Flavor object + :param image: an ImageMeta object + :raises: nova.exception.FlavorImageConflict + """ + # This library function will raise the exception for us if + # necessary; if not, we can ignore the result returned. + hardware.get_mem_encryption_constraint(instance_type, image) + def _get_image_defined_bdms(self, instance_type, image_meta, root_device_name): image_properties = image_meta.get('properties', {}) diff --git a/nova/exception.py b/nova/exception.py index 4d0db83f6b1c..3c1de5d56b03 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2450,6 +2450,11 @@ class ReshapeNeeded(NovaException): "moved.") +class FlavorImageConflict(NovaException): + msg_fmt = _("Conflicting values for %(setting)s found in the flavor " + "(%(flavor_val)s) and the image (%(image_val)s).") + + class HealPortAllocationException(NovaException): msg_fmt = _("Healing port allocation failed.") diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 5263ad5f09d8..f81812f1fdc7 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -173,12 +173,15 @@ class ImageMetaProps(base.NovaObject): # Version 1.21: Added 'hw_time_hpet' field # Version 1.22: Added 'gop', 'virtio' and 'none' to hw_video_model field # Version 1.23: Added 'hw_pmu' field - VERSION = '1.23' + # Version 1.24: Added 'hw_mem_encryption' field + VERSION = '1.24' def obj_make_compatible(self, primitive, target_version): super(ImageMetaProps, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 24): + primitive.pop('hw_mem_encryption', None) if target_version < (1, 23): primitive.pop('hw_pmu', None) # NOTE(sean-k-mooney): unlike other nova object we version this object @@ -312,6 +315,10 @@ class ImageMetaProps(base.NovaObject): # form string 'hw_machine_type': fields.StringField(), + # boolean indicating that the guest needs to be booted with + # encrypted memory + 'hw_mem_encryption': fields.FlexibleBooleanField(), + # One of the magic strings 'small', 'any', 'large' # or an explicit page size in KB (eg 4, 2048, ...) 'hw_mem_page_size': fields.StringField(), diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index a319d5ccb63c..9ed4878289de 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -35,6 +35,7 @@ from nova.objects import base as obj_base from nova.objects import instance as obj_instance from nova import rpc from nova.scheduler.filters import utils as filters_utils +import nova.virt.hardware as hw LOG = logging.getLogger(__name__) @@ -90,13 +91,15 @@ class ResourceRequest(object): # TODO(efried): Handle member_of[$N], which will need to be reconciled # with destination.aggregates handling in resources_from_request_spec + image = (request_spec.image if 'image' in request_spec + else objects.ImageMeta(properties=objects.ImageMetaProps())) + # Parse the flavor extra specs self._process_extra_specs(request_spec.flavor) self.numbered_groups_from_flavor = self.get_num_of_numbered_groups() # Now parse the (optional) image metadata - image = request_spec.image if 'image' in request_spec else None self._process_image_meta(image) # Finally, parse the flavor itself, though we'll only use these fields @@ -119,6 +122,8 @@ class ResourceRequest(object): if disk: self._add_resource(None, orc.DISK_GB, disk) + self._translate_memory_encryption(request_spec.flavor, image) + self.strip_zeros() def _process_extra_specs(self, flavor): @@ -157,6 +162,23 @@ class ResourceRequest(object): # supported in image traits self._add_trait(None, trait, "required") + def _translate_memory_encryption(self, flavor, image): + """When the hw:mem_encryption extra spec or the hw_mem_encryption + image property are requested, translate into a request for + resources:MEM_ENCRYPTION_CONTEXT=1 which requires a slot on a + host which can support encryption of the guest memory. + """ + # NOTE(aspiers): In theory this could raise FlavorImageConflict, + # but we already check it in the API layer, so that should never + # happen. + if not hw.get_mem_encryption_constraint(flavor, image): + # No memory encryption required, so no further action required. + return + + self._add_resource(None, orc.MEM_ENCRYPTION_CONTEXT, 1) + LOG.debug("Added %s=1 to requested resources", + orc.MEM_ENCRYPTION_CONTEXT) + @property def group_policy(self): return self._group_policy diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 26fb7e233dfe..54b999579c2f 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -6131,6 +6131,15 @@ class ServersControllerCreateTest(test.TestCase): self.controller.create, self.req, body=self.body) + @mock.patch('nova.virt.hardware.get_mem_encryption_constraint', + side_effect=exception.FlavorImageConflict( + message="fake conflict reason")) + def test_create_instance_raise_flavor_image_conflict( + self, mock_conflict): + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, body=self.body) + @mock.patch('nova.virt.hardware.numa_get_constraints', side_effect=exception.ImageCPUPinningForbidden()) def test_create_instance_raise_image_cpu_pinning_forbidden( diff --git a/nova/tests/unit/image/fake.py b/nova/tests/unit/image/fake.py index 529967fda49a..aa6cabee8793 100644 --- a/nova/tests/unit/image/fake.py +++ b/nova/tests/unit/image/fake.py @@ -24,6 +24,7 @@ from oslo_utils import uuidutils import nova.conf from nova import exception +from nova import objects from nova.objects import fields as obj_fields from nova.tests import fixtures as nova_fixtures @@ -323,3 +324,28 @@ def stub_out_image_service(test): test.useFixture(nova_fixtures.ConfPatcher( group="glance", api_servers=['http://localhost:9292'])) return image_service + + +def fake_image_obj(default_image_meta=None, default_image_props=None, + variable_image_props=None): + """Helper for constructing a test ImageMeta object with attributes and + properties coming from a combination of (probably hard-coded) + values within a test, and (optionally) variable values from the + test's caller, if the test is actually a helper written to be + reusable and run multiple times with different parameters from + different "wrapper" tests. + """ + image_meta_props = default_image_props or {} + if variable_image_props: + image_meta_props.update(variable_image_props) + + test_image_meta = default_image_meta or {"disk_format": "raw"} + if 'name' not in test_image_meta: + # NOTE(aspiers): the name is specified here in case it's needed + # by the logging in nova.virt.hardware.get_mem_encryption_constraint() + test_image_meta['name'] = 'fake_image' + test_image_meta.update({ + 'properties': image_meta_props, + }) + + return objects.ImageMeta.from_dict(test_image_meta) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index f2032db0d043..465942a8e4b3 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1069,7 +1069,7 @@ object_data = { 'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', - 'ImageMetaProps': '1.23-ed659d0bb5dfb3b2c2c717850c732abc', + 'ImageMetaProps': '1.24-f92fa09d54185499da98f5430524964e', 'Instance': '2.6-5fefbcb483703c85e4d328b887c8af33', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceActionEvent': '1.3-c749e1b3589e7117c81cb2aa6ac438d5', diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index 64f003a9bb87..76ddf629d33f 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -12,6 +12,7 @@ import ddt import mock +import os_resource_classes as orc from oslo_utils.fixture import uuidsentinel as uuids import six @@ -38,11 +39,9 @@ class FakeResourceRequest(object): self._limit = 1000 -@ddt.ddt -class TestUtils(test.NoDBTestCase): - +class TestUtilsBase(test.NoDBTestCase): def setUp(self): - super(TestUtils, self).setUp() + super(TestUtilsBase, self).setUp() self.context = nova_context.get_admin_context() self.mock_host_manager = mock.Mock() @@ -55,6 +54,9 @@ class TestUtils(test.NoDBTestCase): for ident in ex_by_id: self.assertEqual(vars(ex_by_id[ident]), vars(ob_by_id[ident])) + +@ddt.ddt +class TestUtils(TestUtilsBase): @staticmethod def _get_image_with_traits(): image_prop = { @@ -67,8 +69,9 @@ class TestUtils(test.NoDBTestCase): image = objects.ImageMeta.from_dict(image_prop) return image - def _test_resources_from_request_spec(self, expected, flavor, - image=objects.ImageMeta()): + def _test_resources_from_request_spec(self, expected, flavor, image=None): + if image is None: + image = objects.ImageMeta(properties=objects.ImageMetaProps()) fake_spec = objects.RequestSpec(flavor=flavor, image=image) resources = utils.resources_from_request_spec( self.context, fake_spec, self.mock_host_manager) @@ -1161,6 +1164,190 @@ class TestUtils(test.NoDBTestCase): ) +class TestEncryptedMemoryTranslation(TestUtilsBase): + flavor_name = 'm1.test' + image_name = 'cirros' + + def _get_request_spec(self, extra_specs, image): + flavor = objects.Flavor(name=self.flavor_name, + vcpus=1, + memory_mb=1024, + root_gb=10, + ephemeral_gb=5, + swap=0, + extra_specs=extra_specs) + + # NOTE(aspiers): RequestSpec.flavor is not nullable, but + # RequestSpec.image is. + reqspec = objects.RequestSpec(flavor=flavor) + + if image: + reqspec.image = image + + return reqspec + + def _get_resource_request(self, extra_specs, image): + reqspec = self._get_request_spec(extra_specs, image) + return utils.ResourceRequest(reqspec) + + def _get_expected_resource_request(self, mem_encryption_context): + expected_resources = { + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, + } + if mem_encryption_context: + expected_resources[orc.MEM_ENCRYPTION_CONTEXT] = 1 + + expected = FakeResourceRequest() + expected._rg_by_id[None] = objects.RequestGroup( + use_same_provider=False, + resources=expected_resources) + return expected + + def _test_encrypted_memory_support_not_required(self, extra_specs, + image=None): + resreq = self._get_resource_request(extra_specs, image) + expected = self._get_expected_resource_request(False) + + self.assertResourceRequestsEqual(expected, resreq) + + def test_encrypted_memory_support_empty_extra_specs(self): + self._test_encrypted_memory_support_not_required(extra_specs={}) + + def test_encrypted_memory_support_false_extra_spec(self): + for extra_spec in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + extra_specs={'hw:mem_encryption': extra_spec}) + + def test_encrypted_memory_support_empty_image_props(self): + self._test_encrypted_memory_support_not_required( + extra_specs={}, + image=objects.ImageMeta(properties=objects.ImageMetaProps())) + + def test_encrypted_memory_support_false_image_prop(self): + for image_prop in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + extra_specs={}, + image=objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop)) + ) + + def test_encrypted_memory_support_both_false(self): + for extra_spec in ('0', 'false', 'False'): + for image_prop in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + extra_specs={'hw:mem_encryption': extra_spec}, + image=objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop)) + ) + + def _test_encrypted_memory_support_conflict(self, extra_spec, + image_prop_in, + image_prop_out): + # NOTE(aspiers): hw_mem_encryption image property is a + # FlexibleBooleanField, so the result should always be coerced + # to a boolean. + self.assertIsInstance(image_prop_out, bool) + + image = objects.ImageMeta( + name=self.image_name, + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop_in) + ) + + reqspec = self._get_request_spec( + extra_specs={'hw:mem_encryption': extra_spec}, + image=image) + + # Sanity check that our test request spec has an extra_specs + # dict, which is needed in order for there to be a conflict. + self.assertIn('flavor', reqspec) + self.assertIn('extra_specs', reqspec.flavor) + + error = ( + "Flavor %(flavor_name)s has hw:mem_encryption extra spec " + "explicitly set to %(flavor_val)s, conflicting with " + "image %(image_name)s which has hw_mem_encryption property " + "explicitly set to %(image_val)s" + ) + exc = self.assertRaises( + exception.FlavorImageConflict, + utils.ResourceRequest, reqspec + ) + error_data = { + 'flavor_name': self.flavor_name, + 'flavor_val': extra_spec, + 'image_name': self.image_name, + 'image_val': image_prop_out, + } + self.assertEqual(error % error_data, str(exc)) + + def test_encrypted_memory_support_conflict1(self): + for extra_spec in ('0', 'false', 'False'): + for image_prop_in in ('1', 'true', 'True'): + self._test_encrypted_memory_support_conflict( + extra_spec, image_prop_in, True + ) + + def test_encrypted_memory_support_conflict2(self): + for extra_spec in ('1', 'true', 'True'): + for image_prop_in in ('0', 'false', 'False'): + self._test_encrypted_memory_support_conflict( + extra_spec, image_prop_in, False + ) + + @mock.patch.object(utils, 'LOG') + def _test_encrypted_memory_support_required(self, requesters, extra_specs, + mock_log, image=None): + resreq = self._get_resource_request(extra_specs, image) + expected = self._get_expected_resource_request(True) + + self.assertResourceRequestsEqual(expected, resreq) + mock_log.debug.assert_has_calls([ + mock.call('Added %s=1 to requested resources', + orc.MEM_ENCRYPTION_CONTEXT) + ]) + + def test_encrypted_memory_support_extra_spec(self): + for extra_spec in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + 'hw:mem_encryption extra spec', + {'hw:mem_encryption': extra_spec}, + image=objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_firmware_type='uefi')) + ) + + def test_encrypted_memory_support_image_prop(self): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + 'hw_mem_encryption image property', + {}, + image=objects.ImageMeta( + name=self.image_name, + properties=objects.ImageMetaProps( + hw_firmware_type='uefi', + hw_mem_encryption=image_prop)) + ) + + def test_encrypted_memory_support_both_required(self): + for extra_spec in ('1', 'true', 'True'): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + 'hw:mem_encryption extra spec and ' + 'hw_mem_encryption image property', + {'hw:mem_encryption': extra_spec}, + image=objects.ImageMeta( + name=self.image_name, + properties=objects.ImageMetaProps( + hw_firmware_type='uefi', + hw_mem_encryption=image_prop)) + ) + + class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): """These test cases assert what happens when the group policy is missing from the flavor but more than one numbered request group is requested from @@ -1190,6 +1377,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): "required": ["CUSTOM_PHYSNET_3", "CUSTOM_VNIC_TYPE_DIRECT"] }) + self.image = objects.ImageMeta(properties=objects.ImageMetaProps()) def test_one_group_from_flavor_dont_warn(self): flavor = objects.Flavor( @@ -1198,7 +1386,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): 'resources1:CUSTOM_BAR': '2', }) request_spec = objects.RequestSpec( - flavor=flavor, image=objects.ImageMeta(), requested_resources=[]) + flavor=flavor, image=self.image, requested_resources=[]) rr = utils.resources_from_request_spec( self.context, request_spec, host_manager=mock.Mock()) @@ -1220,7 +1408,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, extra_specs={}) request_spec = objects.RequestSpec( - flavor=flavor, image=objects.ImageMeta(), + flavor=flavor, image=self.image, requested_resources=[self.port_group1]) rr = utils.resources_from_request_spec( @@ -1246,7 +1434,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): 'resources2:CUSTOM_FOO': '1' }) request_spec = objects.RequestSpec( - flavor=flavor, image=objects.ImageMeta(), requested_resources=[]) + flavor=flavor, image=self.image, requested_resources=[]) rr = utils.resources_from_request_spec( self.context, request_spec, host_manager=mock.Mock()) @@ -1270,7 +1458,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): 'resources1:CUSTOM_BAR': '2', }) request_spec = objects.RequestSpec( - flavor=flavor, image=objects.ImageMeta(), + flavor=flavor, image=self.image, requested_resources=[self.port_group1]) rr = utils.resources_from_request_spec( @@ -1293,7 +1481,7 @@ class TestResourcesFromRequestGroupDefaultPolicy(test.NoDBTestCase): vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, extra_specs={}) request_spec = objects.RequestSpec( - flavor=flavor, image=objects.ImageMeta(), + flavor=flavor, image=self.image, requested_resources=[self.port_group1, self.port_group2]) rr = utils.resources_from_request_spec( diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index d240307435fe..ee0a6ddcc3fb 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -23,6 +23,7 @@ from nova.objects import fields from nova.pci import stats from nova import test from nova.tests.unit import fake_pci_device_pools as fake_pci +from nova.tests.unit.image.fake import fake_image_obj from nova.virt import hardware as hw @@ -3430,3 +3431,195 @@ class NetworkRequestSupportTestCase(test.NoDBTestCase): supports = hw._numa_cells_support_network_metadata( self.host, self.host.cells, network_metadata) self.assertTrue(supports) + + +class MemEncryptionNotRequiredTestCase(test.NoDBTestCase): + def _test_encrypted_memory_support_not_required(self, flavor=None, + image_meta=None): + if flavor is None: + flavor = objects.Flavor(extra_specs={}) + + if image_meta is None: + image_meta = objects.ImageMeta(properties=objects.ImageMetaProps()) + + self.assertFalse(hw.get_mem_encryption_constraint(flavor, image_meta)) + + def test_requirement_disabled(self): + self._test_encrypted_memory_support_not_required() + + def test_require_false_extra_spec(self): + for extra_spec in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + objects.Flavor(extra_specs={'hw:mem_encryption': extra_spec}) + ) + + def test_require_false_image_prop(self): + for image_prop in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + image_meta=objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop)) + ) + + def test_require_both_false(self): + for extra_spec in ('0', 'false', 'False'): + for image_prop in ('0', 'false', 'False'): + self._test_encrypted_memory_support_not_required( + objects.Flavor( + extra_specs={'hw:mem_encryption': extra_spec}), + objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop)) + ) + + +class MemEncryptionFlavorImageConflictTestCase(test.NoDBTestCase): + def _test_encrypted_memory_support_conflict(self, extra_spec, + image_prop_in, image_prop_out): + # NOTE(aspiers): hw_mem_encryption image property is a + # FlexibleBooleanField, so the result should always be coerced + # to a boolean. + self.assertIsInstance(image_prop_out, bool) + + flavor_name = 'm1.faketiny' + image_name = 'fakecirros' + flavor = objects.Flavor( + name=flavor_name, + extra_specs={'hw:mem_encryption': extra_spec} + ) + image_meta = objects.ImageMeta( + name=image_name, + properties=objects.ImageMetaProps( + hw_mem_encryption=image_prop_in) + ) + + error = ( + "Flavor %(flavor_name)s has hw:mem_encryption extra spec " + "explicitly set to %(flavor_val)s, conflicting with " + "image %(image_name)s which has hw_mem_encryption property " + "explicitly set to %(image_val)s" + ) + exc = self.assertRaises( + exception.FlavorImageConflict, + hw.get_mem_encryption_constraint, + flavor, image_meta + ) + error_data = { + 'flavor_name': flavor_name, + 'flavor_val': extra_spec, + 'image_name': image_name, + 'image_val': image_prop_out, + } + self.assertEqual(error % error_data, str(exc)) + + def test_require_encrypted_memory_support_conflict1(self): + for extra_spec in ('0', 'false', 'False'): + for image_prop_in in ('1', 'true', 'True'): + self._test_encrypted_memory_support_conflict( + extra_spec, image_prop_in, True + ) + + def test_require_encrypted_memory_support_conflict2(self): + for extra_spec in ('1', 'true', 'True'): + for image_prop_in in ('0', 'false', 'False'): + self._test_encrypted_memory_support_conflict( + extra_spec, image_prop_in, False + ) + + +class MemEncryptionRequestedWithoutUEFITestCase(test.NoDBTestCase): + flavor_name = 'm1.faketiny' + image_name = 'fakecirros' + + def _test_encrypted_memory_support_no_uefi(self, extra_spec, image_prop, + requesters): + extra_specs = {} + if extra_spec: + extra_specs['hw:mem_encryption'] = extra_spec + flavor = objects.Flavor(name=self.flavor_name, extra_specs=extra_specs) + image_meta = fake_image_obj( + {'name': self.image_name}, {'hw_firmware_type': 'bios'}, + {'hw_mem_encryption': True} if image_prop else {}) + error = ( + "Memory encryption requested by %(requesters)s but image " + "%(image_name)s doesn't have 'hw_firmware_type' property " + "set to 'uefi'" + ) + exc = self.assertRaises( + exception.FlavorImageConflict, + hw.get_mem_encryption_constraint, + flavor, image_meta + ) + error_data = {'requesters': requesters, + 'image_name': self.image_name} + self.assertEqual(error % error_data, str(exc)) + + def test_flavor_requires_encrypted_memory_support_no_uefi(self): + for extra_spec in ('1', 'true', 'True'): + self._test_encrypted_memory_support_no_uefi( + extra_spec, None, + "hw:mem_encryption extra spec in %s flavor" % self.flavor_name) + + def test_image_requires_encrypted_memory_support_no_uefi(self): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_no_uefi( + None, image_prop, + "hw_mem_encryption property of image %s" % self.image_name) + + def test_flavor_image_require_encrypted_memory_support_no_uefi(self): + for extra_spec in ('1', 'true', 'True'): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_no_uefi( + extra_spec, image_prop, + "hw:mem_encryption extra spec in %s flavor and " + "hw_mem_encryption property of image %s" + % (self.flavor_name, self.image_name)) + + +class MemEncryptionRequiredTestCase(test.NoDBTestCase): + flavor_name = "m1.faketiny" + image_name = 'fakecirros' + + @mock.patch.object(hw, 'LOG') + def _test_encrypted_memory_support_required(self, extra_specs, + image_props, + requesters, mock_log): + flavor = objects.Flavor(name=self.flavor_name, extra_specs=extra_specs) + image_meta = objects.ImageMeta(name=self.image_name, + properties=image_props) + + self.assertTrue(hw.get_mem_encryption_constraint(flavor, image_meta)) + mock_log.debug.assert_has_calls([ + mock.call("Memory encryption requested by %s", requesters) + ]) + + def test_require_encrypted_memory_support_extra_spec(self): + for extra_spec in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + {'hw:mem_encryption': extra_spec}, + objects.ImageMetaProps(hw_firmware_type='uefi'), + "hw:mem_encryption extra spec in %s flavor" % self.flavor_name + ) + + def test_require_encrypted_memory_support_image_prop(self): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + {}, + objects.ImageMetaProps( + hw_mem_encryption=image_prop, + hw_firmware_type='uefi'), + "hw_mem_encryption property of image %s" % self.image_name + ) + + def test_require_encrypted_memory_support_both_required(self): + for extra_spec in ('1', 'true', 'True'): + for image_prop in ('1', 'true', 'True'): + self._test_encrypted_memory_support_required( + {'hw:mem_encryption': extra_spec}, + objects.ImageMetaProps( + hw_mem_encryption=image_prop, + hw_firmware_type='uefi'), + "hw:mem_encryption extra spec in %s flavor and " + "hw_mem_encryption property of image %s" % + (self.flavor_name, self.image_name) + ) diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 2315c7f2590c..2dbb26d2b7a1 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1137,6 +1137,96 @@ def _get_flavor_image_meta(key, flavor, image_meta, default=None): return flavor_policy, image_policy +def get_mem_encryption_constraint(flavor, image_meta): + """Return a boolean indicating whether encryption of guest memory was + requested, either via the hw:mem_encryption extra spec or the + hw_mem_encryption image property (or both). + + Also watch out for contradictory requests between the flavor and + image regarding memory encryption, and raise an exception where + encountered. These conflicts can arise in two different ways: + + 1) the flavor requests memory encryption but the image + explicitly requests *not* to have memory encryption, or + vice-versa + + 2) the flavor and/or image request memory encryption, but the + image is missing hw_firmware_type=uefi + + :param instance_type: Flavor object + :param image: an ImageMeta object + :raises: nova.exception.FlavorImageConflict + :returns: boolean indicating whether encryption of guest memory + was requested + """ + + flavor_mem_enc_str, image_mem_enc = _get_flavor_image_meta( + 'mem_encryption', flavor, image_meta) + + flavor_mem_enc = None + if flavor_mem_enc_str is not None: + flavor_mem_enc = strutils.bool_from_string(flavor_mem_enc_str) + + # Image property is a FlexibleBooleanField, so coercion to a + # boolean is handled automatically + + if not flavor_mem_enc and not image_mem_enc: + return False + + _check_for_mem_encryption_requirement_conflicts( + flavor_mem_enc_str, flavor_mem_enc, image_mem_enc, flavor, image_meta) + + # If we get this far, either the extra spec or image property explicitly + # specified a requirement regarding memory encryption, and if both did, + # they are asking for the same thing. + requesters = [] + if flavor_mem_enc: + requesters.append("hw:mem_encryption extra spec in %s flavor" % + flavor.name) + if image_mem_enc: + requesters.append("hw_mem_encryption property of image %s" % + image_meta.name) + + _check_mem_encryption_uses_uefi_image(requesters, image_meta) + + LOG.debug("Memory encryption requested by %s", " and ".join(requesters)) + return True + + +def _check_for_mem_encryption_requirement_conflicts( + flavor_mem_enc_str, flavor_mem_enc, image_mem_enc, flavor, image_meta): + # Check for conflicts between explicit requirements regarding + # memory encryption. + if (flavor_mem_enc is not None and image_mem_enc is not None and + flavor_mem_enc != image_mem_enc): + emsg = _( + "Flavor %(flavor_name)s has hw:mem_encryption extra spec " + "explicitly set to %(flavor_val)s, conflicting with " + "image %(image_name)s which has hw_mem_encryption property " + "explicitly set to %(image_val)s" + ) + data = { + 'flavor_name': flavor.name, + 'flavor_val': flavor_mem_enc_str, + 'image_name': image_meta.name, + 'image_val': image_mem_enc, + } + raise exception.FlavorImageConflict(emsg % data) + + +def _check_mem_encryption_uses_uefi_image(requesters, image_meta): + if image_meta.properties.hw_firmware_type == 'uefi': + return + + emsg = _( + "Memory encryption requested by %(requesters)s but image " + "%(image_name)s doesn't have 'hw_firmware_type' property set to 'uefi'" + ) + data = {'requesters': " and ".join(requesters), + 'image_name': image_meta.name} + raise exception.FlavorImageConflict(emsg % data) + + def _get_numa_pagesize_constraint(flavor, image_meta): """Return the requested memory page size diff --git a/requirements.txt b/requirements.txt index 9cede6465321..2849da625298 100644 --- a/requirements.txt +++ b/requirements.txt @@ -55,7 +55,7 @@ oslo.middleware>=3.31.0 # Apache-2.0 psutil>=3.2.2 # BSD oslo.versionedobjects>=1.35.0 # Apache-2.0 os-brick>=2.6.1 # Apache-2.0 -os-resource-classes>=0.1.0 # Apache-2.0 +os-resource-classes>=0.4.0 # Apache-2.0 os-traits>=0.16.0 # Apache-2.0 os-vif>=1.14.0 # Apache-2.0 os-win>=3.0.0 # Apache-2.0