Merge "Flavor extra spec and image properties validation from API"
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -12709,8 +12709,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
|
||||
@@ -13450,51 +13452,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):
|
||||
@@ -13502,9 +13504,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):
|
||||
@@ -13512,22 +13514,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)
|
||||
|
||||
@@ -13546,7 +13548,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):
|
||||
@@ -13564,7 +13566,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)
|
||||
|
||||
@@ -13580,7 +13582,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):
|
||||
@@ -13593,7 +13595,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):
|
||||
@@ -13608,7 +13610,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)
|
||||
|
||||
@@ -13623,10 +13625,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.
|
||||
Reference in New Issue
Block a user