diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 02d2fcdb58ea..c813152153f5 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -56,6 +56,35 @@ CONF = nova.conf.CONF LOG = logging.getLogger(__name__) +INVALID_FLAVOR_IMAGE_EXCEPTIONS = ( + exception.BadRequirementEmulatorThreadsPolicy, + exception.CPUThreadPolicyConfigurationInvalid, + exception.ImageCPUPinningForbidden, + exception.ImageCPUThreadPolicyForbidden, + exception.ImageNUMATopologyAsymmetric, + exception.ImageNUMATopologyCPUDuplicates, + exception.ImageNUMATopologyCPUOutOfRange, + exception.ImageNUMATopologyCPUsUnassigned, + exception.ImageNUMATopologyForbidden, + exception.ImageNUMATopologyIncomplete, + exception.ImageNUMATopologyMemoryOutOfRange, + exception.ImageSerialPortNumberExceedFlavorValue, + exception.ImageSerialPortNumberInvalid, + exception.ImageVCPULimitsRangeExceeded, + exception.ImageVCPUTopologyRangeExceeded, + exception.InvalidCPUAllocationPolicy, + exception.InvalidCPUThreadAllocationPolicy, + exception.InvalidEmulatorThreadsPolicy, + exception.InvalidNUMANodesNumber, + exception.InvalidRequest, + exception.MemoryPageSizeForbidden, + exception.MemoryPageSizeInvalid, + exception.PciInvalidAlias, + exception.PciRequestAliasNotDefined, + exception.RealtimeConfigurationInvalid, + exception.RealtimeMaskNotFoundOrInvalid, +) + class ServersController(wsgi.Controller): """The Server API base controller class for the OpenStack API.""" @@ -678,8 +707,7 @@ class ServersController(wsgi.Controller): except UnicodeDecodeError as error: msg = "UnicodeError: %s" % error raise exc.HTTPBadRequest(explanation=msg) - except (exception.CPUThreadPolicyConfigurationInvalid, - exception.ImageNotActive, + except (exception.ImageNotActive, exception.ImageBadRequest, exception.ImageNotAuthorized, exception.FixedIpNotFoundForAddress, @@ -687,7 +715,6 @@ class ServersController(wsgi.Controller): exception.FlavorDiskTooSmall, exception.FlavorMemoryTooSmall, exception.InvalidMetadata, - exception.InvalidRequest, exception.InvalidVolume, exception.MultiplePortsNotApplicable, exception.InvalidFixedIpAndMaxCountRequest, @@ -710,31 +737,15 @@ class ServersController(wsgi.Controller): exception.InvalidBDMSwapSize, exception.VolumeTypeNotFound, exception.AutoDiskConfigDisabledByImage, - exception.ImageCPUPinningForbidden, - exception.ImageCPUThreadPolicyForbidden, - exception.InvalidCPUAllocationPolicy, - exception.InvalidCPUThreadAllocationPolicy, - exception.ImageNUMATopologyIncomplete, - exception.ImageNUMATopologyForbidden, - exception.ImageNUMATopologyAsymmetric, - exception.ImageNUMATopologyCPUOutOfRange, - exception.ImageNUMATopologyCPUDuplicates, - exception.ImageNUMATopologyCPUsUnassigned, - exception.ImageNUMATopologyMemoryOutOfRange, - exception.InvalidNUMANodesNumber, exception.InstanceGroupNotFound, - exception.MemoryPageSizeInvalid, - exception.MemoryPageSizeForbidden, - exception.PciRequestAliasNotDefined, - exception.PciInvalidAlias, - exception.RealtimeConfigurationInvalid, - exception.RealtimeMaskNotFoundOrInvalid, exception.SnapshotNotFound, exception.UnableToAutoAllocateNetwork, exception.MultiattachNotSupportedOldMicroversion, exception.CertificateValidationFailed, exception.CreateWithPortResourceRequestOldVersion) as error: raise exc.HTTPBadRequest(explanation=error.format_message()) + except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error: + raise exc.HTTPBadRequest(explanation=error.format_message()) except (exception.PortInUse, exception.InstanceExists, exception.NetworkAmbiguous, @@ -931,8 +942,9 @@ class ServersController(wsgi.Controller): exception.CannotResizeDisk, exception.CannotResizeToSameFlavor, exception.FlavorNotFound, - exception.NoValidHost, - exception.PciRequestAliasNotDefined) as e: + exception.NoValidHost) as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + except INVALID_FLAVOR_IMAGE_EXCEPTIONS as e: raise exc.HTTPBadRequest(explanation=e.format_message()) except exception.Invalid: msg = _("Invalid instance image.") @@ -1083,13 +1095,16 @@ class ServersController(wsgi.Controller): raise exc.HTTPBadRequest(explanation=msg) except exception.QuotaError as error: raise exc.HTTPForbidden(explanation=error.format_message()) - except (exception.ImageNotActive, - exception.ImageUnacceptable, + except (exception.AutoDiskConfigDisabledByImage, + exception.CertificateValidationFailed, exception.FlavorDiskTooSmall, exception.FlavorMemoryTooSmall, + exception.ImageNotActive, + exception.ImageUnacceptable, exception.InvalidMetadata, - exception.AutoDiskConfigDisabledByImage, - exception.CertificateValidationFailed) as error: + ) as error: + raise exc.HTTPBadRequest(explanation=error.format_message()) + except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error: raise exc.HTTPBadRequest(explanation=error.format_message()) instance = self._get_server(context, req, id, is_detail=True) diff --git a/nova/compute/api.py b/nova/compute/api.py index 2e0c73dd0f45..a19f6bf79a0b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -245,6 +245,17 @@ def load_cells(): LOG.error('No cells are configured, unable to continue') +def _get_image_meta_obj(image_meta_dict): + try: + image_meta = objects.ImageMeta.from_dict(image_meta_dict) + except ValueError as e: + # there must be invalid values in the image meta properties so + # consider this an invalid request + msg = _('Invalid image metadata. Error: %s') % six.text_type(e) + raise exception.InvalidRequest(msg) + return image_meta + + @profiler.trace_cls("compute_api") class API(base.Base): """API for interacting with the compute manager.""" @@ -522,14 +533,38 @@ class API(base.Base): # reason, we rely on the DB to cast True to a String. return True if bool_val else '' - def _check_requested_image(self, context, image_id, image, - instance_type, root_bdm): + def _validate_flavor_image(self, context, image_id, image, + instance_type, root_bdm, validate_numa=True): + """Validate the flavor and image. + + This is called from the API service to ensure that the flavor + extra-specs and image properties are self-consistent and compatible + with each other. + + :param context: A context.RequestContext + :param image_id: UUID of the image + :param image: a dict representation of the image including properties, + enforces the image status is active. + :param instance_type: Flavor object + :param root_bdm: BlockDeviceMapping for root disk. Will be None for + the resize case. + :param validate_numa: Flag to indicate whether or not to validate + the NUMA-related metadata. + :raises: Many different possible exceptions. See + api.openstack.compute.servers.INVALID_FLAVOR_IMAGE_EXCEPTIONS + for the full list. + """ + if image and image['status'] != 'active': + raise exception.ImageNotActive(image_id=image_id) + self._validate_flavor_image_nostatus(context, image, instance_type, + root_bdm, validate_numa) + + @staticmethod + def _validate_flavor_image_nostatus(context, image, instance_type, + root_bdm, validate_numa=True): if not image: return - if image['status'] != 'active': - raise exception.ImageNotActive(image_id=image_id) - image_properties = image.get('properties', {}) config_drive_option = image_properties.get( 'img_config_drive', 'optional') @@ -608,6 +643,17 @@ class API(base.Base): servers_policies.ZERO_DISK_FLAVOR, fatal=False): raise exception.BootFromVolumeRequiredForZeroDiskFlavor() + image_meta = _get_image_meta_obj(image) + + # Only validate values of flavor/image so the return results of + # following 'get' functions are not used. + hardware.get_number_of_serial_ports(instance_type, image_meta) + if hardware.is_realtime_enabled(instance_type): + hardware.vcpus_realtime_topology(instance_type, image_meta) + hardware.get_cpu_topology_constraints(instance_type, image_meta) + if validate_numa: + hardware.numa_get_constraints(instance_type, image_meta) + def _get_image_defined_bdms(self, instance_type, image_meta, root_device_name): image_properties = image_meta.get('properties', {}) @@ -733,11 +779,13 @@ class API(base.Base): def _checks_for_create_and_rebuild(self, context, image_id, image, instance_type, metadata, - files_to_inject, root_bdm): + files_to_inject, root_bdm, + validate_numa=True): self._check_metadata_properties_quota(context, metadata) self._check_injected_file_quota(context, files_to_inject) - self._check_requested_image(context, image_id, image, - instance_type, root_bdm) + self._validate_flavor_image(context, image_id, image, + instance_type, root_bdm, + validate_numa=validate_numa) def _validate_and_build_base_options(self, context, instance_type, boot_meta, image_href, image_id, @@ -790,13 +838,7 @@ class API(base.Base): block_device.properties_root_device_name( boot_meta.get('properties', {}))) - try: - image_meta = objects.ImageMeta.from_dict(boot_meta) - except ValueError as e: - # there must be invalid values in the image meta properties so - # consider this an invalid request - msg = _('Invalid image metadata. Error: %s') % six.text_type(e) - raise exception.InvalidRequest(msg) + image_meta = _get_image_meta_obj(boot_meta) numa_topology = hardware.numa_get_constraints( instance_type, image_meta) @@ -1185,9 +1227,11 @@ class API(base.Base): # We can't do this check earlier because we need bdms from all sources # to have been merged in order to get the root bdm. + # Set validate_numa=False since numa validation is already done by + # _validate_and_build_base_options(). self._checks_for_create_and_rebuild(context, image_id, boot_meta, instance_type, metadata, injected_files, - block_device_mapping.root_bdm()) + block_device_mapping.root_bdm(), validate_numa=False) instance_group = self._get_requested_instance_group(context, filter_properties) @@ -3546,6 +3590,13 @@ class API(base.Base): current_instance_type, new_instance_type) + if not same_instance_type: + image = utils.get_image_from_system_metadata( + instance.system_metadata) + # Can skip root_bdm check since it will not change during resize. + self._validate_flavor_image_nostatus( + context, image, new_instance_type, root_bdm=None) + filter_properties = {'ignore_hosts': []} if not CONF.allow_resize_to_same_host: diff --git a/nova/tests/unit/api/openstack/compute/test_server_actions.py b/nova/tests/unit/api/openstack/compute/test_server_actions.py index 375b35611250..b9c9685da6a6 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_actions.py +++ b/nova/tests/unit/api/openstack/compute/test_server_actions.py @@ -545,9 +545,11 @@ class ServerActionsControllerTestV21(test.TestCase): def return_image_meta(*args, **kwargs): image_meta_table = { - '2': {'id': 2, 'status': 'active', 'container_format': 'ari'}, + '2': {'id': uuids.image_id, 'status': 'active', + 'container_format': 'ari'}, '155d900f-4e14-4e4c-a73d-069cbf4541e6': - {'id': 3, 'status': 'active', 'container_format': 'raw', + {'id': uuids.image_id, 'status': 'active', + 'container_format': 'raw', 'properties': {'kernel_id': 1, 'ramdisk_id': 2}}, } image_id = args[2] diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index 5e7ce951660b..2944c0097b1a 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -12574,8 +12574,10 @@ class DisabledInstanceTypesTestCase(BaseTestCase): self.assertRaises(exception.FlavorNotFound, self.compute_api.create, self.context, self.inst_type, None) + @mock.patch('nova.compute.api.API._validate_flavor_image_nostatus') @mock.patch('nova.objects.RequestSpec') - def test_can_resize_to_visible_instance_type(self, mock_reqspec): + def test_can_resize_to_visible_instance_type(self, mock_reqspec, + mock_validate): instance = self._create_fake_instance_obj() orig_get_flavor_by_flavor_id =\ flavors.get_flavor_by_flavor_id @@ -13315,51 +13317,51 @@ class CheckRequestedImageTestCase(test.TestCase): self.instance_type['root_gb'] = 1 def test_no_image_specified(self): - self.compute_api._check_requested_image(self.context, None, None, + self.compute_api._validate_flavor_image(self.context, None, None, self.instance_type, None) def test_image_status_must_be_active(self): - image = dict(id='123', status='foo') + image = dict(id=uuids.image_id, status='foo') self.assertRaises(exception.ImageNotActive, - self.compute_api._check_requested_image, self.context, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, None) image['status'] = 'active' - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_image_min_ram_check(self): - image = dict(id='123', status='active', min_ram='65') + image = dict(id=uuids.image_id, status='active', min_ram='65') self.assertRaises(exception.FlavorMemoryTooSmall, - self.compute_api._check_requested_image, self.context, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, None) image['min_ram'] = '64' - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_image_min_disk_check(self): - image = dict(id='123', status='active', min_disk='2') + image = dict(id=uuids.image_id, status='active', min_disk='2') self.assertRaises(exception.FlavorDiskSmallerThanMinDisk, - self.compute_api._check_requested_image, self.context, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, None) image['min_disk'] = '1' - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_image_too_large(self): - image = dict(id='123', status='active', size='1073741825') + image = dict(id=uuids.image_id, status='active', size='1073741825') self.assertRaises(exception.FlavorDiskSmallerThanImage, - self.compute_api._check_requested_image, self.context, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, None) image['size'] = '1073741824' - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_root_gb_zero_disables_size_check(self): @@ -13367,9 +13369,9 @@ class CheckRequestedImageTestCase(test.TestCase): servers_policy.ZERO_DISK_FLAVOR: servers_policy.RULE_AOO }, overwrite=False) self.instance_type['root_gb'] = 0 - image = dict(id='123', status='active', size='1073741825') + image = dict(id=uuids.image_id, status='active', size='1073741825') - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_root_gb_zero_disables_min_disk(self): @@ -13377,22 +13379,22 @@ class CheckRequestedImageTestCase(test.TestCase): servers_policy.ZERO_DISK_FLAVOR: servers_policy.RULE_AOO }, overwrite=False) self.instance_type['root_gb'] = 0 - image = dict(id='123', status='active', min_disk='2') + image = dict(id=uuids.image_id, status='active', min_disk='2') - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) def test_config_drive_option(self): - image = {'id': 1, 'status': 'active'} + image = {'id': uuids.image_id, 'status': 'active'} image['properties'] = {'img_config_drive': 'optional'} - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) image['properties'] = {'img_config_drive': 'mandatory'} - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, None) image['properties'] = {'img_config_drive': 'bar'} self.assertRaises(exception.InvalidImageConfigDrive, - self.compute_api._check_requested_image, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, None) @@ -13411,7 +13413,7 @@ class CheckRequestedImageTestCase(test.TestCase): source_type='volume', destination_type='volume', volume_id=volume_uuid, volume_size=self.instance_type.root_gb + 1) - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, root_bdm) def test_volume_blockdevicemapping_min_disk(self): @@ -13429,7 +13431,7 @@ class CheckRequestedImageTestCase(test.TestCase): volume_size=self.instance_type.root_gb) self.assertRaises(exception.VolumeSmallerThanMinDisk, - self.compute_api._check_requested_image, + self.compute_api._validate_flavor_image, self.context, image_uuid, image, self.instance_type, root_bdm) @@ -13445,7 +13447,7 @@ class CheckRequestedImageTestCase(test.TestCase): source_type='volume', destination_type='volume', volume_id=volume_uuid, volume_size=None) - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, root_bdm) def test_image_blockdevicemapping(self): @@ -13458,7 +13460,7 @@ class CheckRequestedImageTestCase(test.TestCase): root_bdm = block_device_obj.BlockDeviceMapping( source_type='image', destination_type='local', image_id=image_uuid) - self.compute_api._check_requested_image(self.context, image['id'], + self.compute_api._validate_flavor_image(self.context, image['id'], image, self.instance_type, root_bdm) def test_image_blockdevicemapping_too_big(self): @@ -13473,7 +13475,7 @@ class CheckRequestedImageTestCase(test.TestCase): source_type='image', destination_type='local', image_id=image_uuid) self.assertRaises(exception.FlavorDiskSmallerThanImage, - self.compute_api._check_requested_image, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, root_bdm) @@ -13488,10 +13490,45 @@ class CheckRequestedImageTestCase(test.TestCase): source_type='image', destination_type='local', image_id=image_uuid) self.assertRaises(exception.FlavorDiskSmallerThanMinDisk, - self.compute_api._check_requested_image, + self.compute_api._validate_flavor_image, self.context, image['id'], image, self.instance_type, root_bdm) + def test_cpu_policy(self): + image = {'id': uuids.image_id, 'status': 'active'} + for v in obj_fields.CPUAllocationPolicy.ALL: + image['properties'] = {'hw_cpu_policy': v} + self.compute_api._validate_flavor_image( + self.context, image['id'], image, self.instance_type, None) + image['properties'] = {'hw_cpu_policy': 'bar'} + self.assertRaises(exception.InvalidRequest, + self.compute_api._validate_flavor_image, + self.context, image['id'], image, self.instance_type, + None) + + def test_cpu_thread_policy(self): + image = {'id': uuids.image_id, 'status': 'active'} + image['properties'] = { + 'hw_cpu_policy': obj_fields.CPUAllocationPolicy.DEDICATED} + for v in obj_fields.CPUThreadAllocationPolicy.ALL: + image['properties']['hw_cpu_thread_policy'] = v + self.compute_api._validate_flavor_image( + self.context, image['id'], image, self.instance_type, None) + image['properties']['hw_cpu_thread_policy'] = 'bar' + self.assertRaises(exception.InvalidRequest, + self.compute_api._validate_flavor_image, + self.context, image['id'], image, self.instance_type, + None) + + image['properties'] = { + 'hw_cpu_policy': obj_fields.CPUAllocationPolicy.SHARED, + 'hw_cpu_thread_policy': + obj_fields.CPUThreadAllocationPolicy.ISOLATE} + self.assertRaises(exception.CPUThreadPolicyConfigurationInvalid, + self.compute_api._validate_flavor_image, + self.context, image['id'], image, self.instance_type, + None) + class ComputeHooksTestCase(test.BaseHookTestCase): def test_delete_instance_has_hook(self): diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index 7155deee4dd4..308effc7f89f 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -1927,6 +1927,7 @@ class _ComputeAPIUnitTestMixIn(object): self.context, fake_inst['uuid'], 'finished') mock_inst_save.assert_called_once_with(expected_task_state=[None]) + @mock.patch('nova.compute.api.API._validate_flavor_image_nostatus') @mock.patch('nova.objects.Migration') @mock.patch.object(compute_api.API, '_record_action_start') @mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user') @@ -1939,7 +1940,8 @@ class _ComputeAPIUnitTestMixIn(object): def _test_resize(self, mock_get_all_by_host, mock_get_by_instance_uuid, mock_get_flavor, mock_upsize, mock_inst_save, mock_count, mock_limit, mock_record, - mock_migration, flavor_id_passed=True, + mock_migration, mock_validate, + flavor_id_passed=True, same_host=False, allow_same_host=False, project_id=None, extra_kwargs=None, @@ -2088,6 +2090,11 @@ class _ComputeAPIUnitTestMixIn(object): mock_upsize.assert_called_once_with( test.MatchType(objects.Flavor), test.MatchType(objects.Flavor)) + image_meta = utils.get_image_from_system_metadata( + fake_inst.system_metadata) + if not same_flavor: + mock_validate.assert_called_once_with( + self.context, image_meta, new_flavor, root_bdm=None) # mock.ANY might be 'instances', 'cores', or 'ram' # depending on how the deltas dict is iterated in check_deltas mock_count.assert_called_once_with( @@ -2102,6 +2109,9 @@ class _ComputeAPIUnitTestMixIn(object): mock_inst_save.assert_called_once_with( expected_task_state=[None]) + else: + # This is a migration + mock_validate.assert_not_called() if self.cell_type == 'api' and request_spec: mock_migration.assert_called_once_with(context=self.context) @@ -2290,6 +2300,7 @@ class _ComputeAPIUnitTestMixIn(object): self.compute_api.resize, self.context, fake_inst, flavor_id='flavor-id') + @mock.patch('nova.compute.api.API._validate_flavor_image_nostatus') @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') @mock.patch('nova.compute.api.API._record_action_start') @mock.patch('nova.compute.api.API._resize_cells_support') @@ -2300,7 +2311,8 @@ class _ComputeAPIUnitTestMixIn(object): resize_instance_mock, cells_support_mock, record_mock, - get_by_inst): + get_by_inst, + validate_mock): params = dict(image_ref='') fake_inst = self._create_instance_obj(params=params) diff --git a/nova/tests/unit/compute/test_compute_cells.py b/nova/tests/unit/compute/test_compute_cells.py index 4580bab3be69..7d0cd7785970 100644 --- a/nova/tests/unit/compute/test_compute_cells.py +++ b/nova/tests/unit/compute/test_compute_cells.py @@ -645,6 +645,7 @@ class CellsConductorAPIRPCRedirect(test.NoDBTestCase): # code. self.assertTrue(sbi.called) + @mock.patch.object(compute_api.API, '_validate_flavor_image_nostatus') @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') @mock.patch.object(compute_api.API, '_record_action_start') @mock.patch.object(compute_api.API, '_resize_cells_support') @@ -654,7 +655,8 @@ class CellsConductorAPIRPCRedirect(test.NoDBTestCase): @mock.patch.object(compute_api.API, '_check_auto_disk_config') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') def test_resize_instance(self, _bdms, _check, _extract, _save, _upsize, - _cells, _record, _spec_get_by_uuid): + _cells, _record, _spec_get_by_uuid, + mock_validate): flavor = objects.Flavor(**test_flavor.fake_flavor) _extract.return_value = flavor orig_system_metadata = {} @@ -665,7 +667,6 @@ class CellsConductorAPIRPCRedirect(test.NoDBTestCase): expected_attrs=['system_metadata']) instance.flavor = flavor instance.old_flavor = instance.new_flavor = None - self.compute_api.resize(self.context, instance) self.assertTrue(self.cells_rpcapi.resize_instance.called) @@ -705,7 +706,7 @@ class CellsConductorAPIRPCRedirect(test.NoDBTestCase): launched_at=timeutils.utcnow(), image_ref=uuids.image_id, system_metadata=orig_system_metadata, expected_attrs=['system_metadata']) - get_flavor.return_value = '' + get_flavor.return_value = {} # The API request schema validates that a UUID is passed for the # imageRef parameter so we need to provide an image. image_href = uuids.image_id diff --git a/nova/utils.py b/nova/utils.py index 619480392b27..1245f80fbe66 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -945,7 +945,7 @@ def get_image_metadata_from_volume(volume): image_meta[attr] = int(val or 0) # NOTE(mriedem): Set the status to 'active' as a really old hack # from when this method was in the compute API class and is - # needed for _check_requested_image which makes sure the image + # needed for _validate_flavor_image which makes sure the image # is 'active'. For volume-backed servers, if the volume is not # available because the image backing the volume is not active, # then the compute API trying to reserve the volume should fail. diff --git a/releasenotes/notes/flavor-extra-spec-image-property-validation-7310954ba3822477.yaml b/releasenotes/notes/flavor-extra-spec-image-property-validation-7310954ba3822477.yaml new file mode 100644 index 000000000000..7e3532beec8d --- /dev/null +++ b/releasenotes/notes/flavor-extra-spec-image-property-validation-7310954ba3822477.yaml @@ -0,0 +1,17 @@ +--- +upgrade: + - | + With added validations for flavor extra-specs and image properties, the + APIs for server create, resize and rebuild will now return 400 exceptions + where they did not before due to the extra-specs or properties not being + properly formatted or being mutually incompatible. + + For all three actions we will now check both the flavor and image to + validate the CPU policy, CPU thread policy, CPU topology, memory topology, + hugepages, serial ports, realtime CPU mask, NUMA topology details, CPU + pinning, and a few other things. + + The main advantage to this is to catch invalid configurations as early + as possible so that we can return a useful error to the user rather than + fail later on much further down the stack where the operator would have + to get involved.