Flavor extra spec and image properties validation from API

Validate the combination of the flavor extra-specs and image properties
as early as possible once they're both known (since you can specify
mutually-incompatible changes in the two places). If validation failed
then synchronously return error to user. We need to do this anywhere
the flavor or image changes, so basically instance creation, rebuild,
and resize.

- Rename _check_requested_image() to _validate_flavor_image() and add
  a call from the resize code path.  (It's already called for create
  and rebuild.)
- In _validate_flavor_image() add new checks to validate numa related
  options from flavor and image including CPU policy, CPU thread
  policy, CPU topology, memory topology, hugepages, CPU pinning,
  serial ports, realtime mask, etc.
- Added new 400 exceptions in Server API correspondent to added
  validations.

blueprint: flavor-extra-spec-image-property-validation
Change-Id: I06fad233006c7bab14749a51ffa226c3801f951b
Signed-off-by: Jack Ding <jack.ding@windriver.com>
Signed-off-by: Chris Friesen <chris.friesen@windriver.com>
This commit is contained in:
Chris Friesen 2019-03-04 11:49:18 -06:00
parent cb5ad6d3c1
commit 5e7b840e48
8 changed files with 214 additions and 79 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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]

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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.