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:
parent
cb5ad6d3c1
commit
5e7b840e48
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user