scheduler: Flatten 'ResourceRequest.from_extra_specs', 'from_image_props'
The 'ResourceRequest' object sources information from three different attributes of an instance: the instance's image metadata properties, the instance's flavor, this flavor's extra specs. It's possible for a user to override resources requested via the flavor using flavor extra specs (e.g. using the 'resources:VCPU=N' extra spec), and it's possible to override traits requested via the flavor extra specs using image metadata (e.g. using the 'traits_required=foo' metadata property). This means there's an implicit hierarchy present: - Traits: image metadata > flavor extra specs - Resources: flavor extra specs > flavor Previously, we pulled information from the flavor extra specs and image metadata using two classmethods, 'from_extra_specs' and 'from_image_props', but this required a lot of glue code in between to ensure this hierarchy was maintained. Stop doing this, preferring to centralize everything in one location. This results in fewer LoC and a more grokable implementation, and will make things much easier when we start handling 'PCPU's here. Part of blueprint cpu-resources Change-Id: Ic0e6bc47b79711b38b2d4dabaeb5ae1dbaf2b18a Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
912a46c9d4
commit
7abe83f646
|
@ -54,7 +54,32 @@ class ResourceRequest(object):
|
||||||
XS_KEYPAT = re.compile(r"^(%s)([1-9][0-9]*)?:(.*)$" %
|
XS_KEYPAT = re.compile(r"^(%s)([1-9][0-9]*)?:(.*)$" %
|
||||||
'|'.join((XS_RES_PREFIX, XS_TRAIT_PREFIX)))
|
'|'.join((XS_RES_PREFIX, XS_TRAIT_PREFIX)))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, request_spec):
|
||||||
|
"""Create a new instance of ResourceRequest from a RequestSpec.
|
||||||
|
|
||||||
|
Examines the flavor, flavor extra specs, and (optional) image metadata
|
||||||
|
of the provided ``request_spec``.
|
||||||
|
|
||||||
|
For extra specs, items of the following form are examined:
|
||||||
|
|
||||||
|
- ``resources:$RESOURCE_CLASS``: $AMOUNT
|
||||||
|
- ``resources$N:$RESOURCE_CLASS``: $AMOUNT
|
||||||
|
- ``trait:$TRAIT_NAME``: "required"
|
||||||
|
- ``trait$N:$TRAIT_NAME``: "required"
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This does *not* yet handle ``member_of[$N]``.
|
||||||
|
|
||||||
|
For image metadata, traits are extracted from the ``traits_required``
|
||||||
|
property, if present.
|
||||||
|
|
||||||
|
For the flavor, ``VCPU``, ``MEMORY_MB`` and ``DISK_GB`` are calculated
|
||||||
|
from Flavor properties, though these are only used if they aren't
|
||||||
|
overridden by flavor extra specs.
|
||||||
|
|
||||||
|
:param request_spec: An instance of ``objects.RequestSpec``.
|
||||||
|
"""
|
||||||
# { ident: RequestGroup }
|
# { ident: RequestGroup }
|
||||||
self._rg_by_id = {}
|
self._rg_by_id = {}
|
||||||
self._group_policy = None
|
self._group_policy = None
|
||||||
|
@ -62,9 +87,75 @@ class ResourceRequest(object):
|
||||||
# set to None to indicate "no limit".
|
# set to None to indicate "no limit".
|
||||||
self._limit = CONF.scheduler.max_placement_results
|
self._limit = CONF.scheduler.max_placement_results
|
||||||
|
|
||||||
def __str__(self):
|
# TODO(efried): Handle member_of[$N], which will need to be reconciled
|
||||||
return ', '.join(sorted(
|
# with destination.aggregates handling in resources_from_request_spec
|
||||||
list(str(rg) for rg in list(self._rg_by_id.values()))))
|
|
||||||
|
# Parse the flavor extra specs
|
||||||
|
self._process_extra_specs(request_spec.flavor)
|
||||||
|
|
||||||
|
self.numbered_groups_from_flavor = self.get_num_of_numbered_groups()
|
||||||
|
|
||||||
|
# Now parse the (optional) image metadata
|
||||||
|
image = request_spec.image if 'image' in request_spec else None
|
||||||
|
self._process_image_meta(image)
|
||||||
|
|
||||||
|
# Finally, parse the flavor itself, though we'll only use these fields
|
||||||
|
# if they don't conflict with something already provided by the flavor
|
||||||
|
# extra specs. These are all added to the unnumbered request group.
|
||||||
|
merged_resources = self.merged_resources()
|
||||||
|
|
||||||
|
if orc.VCPU not in merged_resources:
|
||||||
|
self._add_resource(None, orc.VCPU, request_spec.vcpus)
|
||||||
|
|
||||||
|
if orc.MEMORY_MB not in merged_resources:
|
||||||
|
self._add_resource(None, orc.MEMORY_MB, request_spec.memory_mb)
|
||||||
|
|
||||||
|
if orc.DISK_GB not in merged_resources:
|
||||||
|
disk = request_spec.ephemeral_gb
|
||||||
|
disk += compute_utils.convert_mb_to_ceil_gb(request_spec.swap)
|
||||||
|
if 'is_bfv' not in request_spec or not request_spec.is_bfv:
|
||||||
|
disk += request_spec.root_gb
|
||||||
|
|
||||||
|
if disk:
|
||||||
|
self._add_resource(None, orc.DISK_GB, disk)
|
||||||
|
|
||||||
|
self.strip_zeros()
|
||||||
|
|
||||||
|
def _process_extra_specs(self, flavor):
|
||||||
|
if 'extra_specs' not in flavor:
|
||||||
|
return
|
||||||
|
|
||||||
|
for key, val in flavor.extra_specs.items():
|
||||||
|
if key == 'group_policy':
|
||||||
|
self._add_group_policy(val)
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = self.XS_KEYPAT.match(key)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 'prefix' is 'resources' or 'trait'
|
||||||
|
# 'suffix' is $N or None
|
||||||
|
# 'name' is either the resource class name or the trait name.
|
||||||
|
prefix, suffix, name = match.groups()
|
||||||
|
|
||||||
|
# Process "resources[$N]"
|
||||||
|
if prefix == self.XS_RES_PREFIX:
|
||||||
|
self._add_resource(suffix, name, val)
|
||||||
|
|
||||||
|
# Process "trait[$N]"
|
||||||
|
elif prefix == self.XS_TRAIT_PREFIX:
|
||||||
|
self._add_trait(suffix, name, val)
|
||||||
|
|
||||||
|
def _process_image_meta(self, image):
|
||||||
|
if not image or 'properties' not in image:
|
||||||
|
return
|
||||||
|
|
||||||
|
for trait in image.properties.get('traits_required', []):
|
||||||
|
# required traits from the image are always added to the
|
||||||
|
# unnumbered request group, granular request groups are not
|
||||||
|
# supported in image traits
|
||||||
|
self._add_trait(None, trait, "required")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def group_policy(self):
|
def group_policy(self):
|
||||||
|
@ -140,81 +231,6 @@ class ResourceRequest(object):
|
||||||
return
|
return
|
||||||
self._group_policy = policy
|
self._group_policy = policy
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_extra_specs(cls, extra_specs, req=None):
|
|
||||||
"""Processes resources and traits in numbered groupings in extra_specs.
|
|
||||||
|
|
||||||
Examines extra_specs for items of the following forms:
|
|
||||||
"resources:$RESOURCE_CLASS": $AMOUNT
|
|
||||||
"resources$N:$RESOURCE_CLASS": $AMOUNT
|
|
||||||
"trait:$TRAIT_NAME": "required"
|
|
||||||
"trait$N:$TRAIT_NAME": "required"
|
|
||||||
|
|
||||||
Does *not* yet handle member_of[$N].
|
|
||||||
|
|
||||||
:param extra_specs: The flavor extra_specs dict.
|
|
||||||
:param req: the ResourceRequest object to add the requirements to or
|
|
||||||
None to create a new ResourceRequest
|
|
||||||
:return: A ResourceRequest object representing the resources and
|
|
||||||
required traits in the extra_specs.
|
|
||||||
"""
|
|
||||||
# TODO(efried): Handle member_of[$N], which will need to be reconciled
|
|
||||||
# with destination.aggregates handling in resources_from_request_spec
|
|
||||||
|
|
||||||
if req is not None:
|
|
||||||
ret = req
|
|
||||||
else:
|
|
||||||
ret = cls()
|
|
||||||
|
|
||||||
for key, val in extra_specs.items():
|
|
||||||
if key == 'group_policy':
|
|
||||||
ret._add_group_policy(val)
|
|
||||||
continue
|
|
||||||
|
|
||||||
match = cls.XS_KEYPAT.match(key)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 'prefix' is 'resources' or 'trait'
|
|
||||||
# 'suffix' is $N or None
|
|
||||||
# 'name' is either the resource class name or the trait name.
|
|
||||||
prefix, suffix, name = match.groups()
|
|
||||||
|
|
||||||
# Process "resources[$N]"
|
|
||||||
if prefix == cls.XS_RES_PREFIX:
|
|
||||||
ret._add_resource(suffix, name, val)
|
|
||||||
|
|
||||||
# Process "trait[$N]"
|
|
||||||
elif prefix == cls.XS_TRAIT_PREFIX:
|
|
||||||
ret._add_trait(suffix, name, val)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_image_props(cls, image_meta_props, req=None):
|
|
||||||
"""Processes image properties and adds trait requirements to the
|
|
||||||
ResourceRequest
|
|
||||||
|
|
||||||
:param image_meta_props: The ImageMetaProps object.
|
|
||||||
:param req: the ResourceRequest object to add the requirements to or
|
|
||||||
None to create a new ResourceRequest
|
|
||||||
:return: A ResourceRequest object representing the required traits on
|
|
||||||
the image.
|
|
||||||
"""
|
|
||||||
if req is not None:
|
|
||||||
ret = req
|
|
||||||
else:
|
|
||||||
ret = cls()
|
|
||||||
|
|
||||||
if 'traits_required' in image_meta_props:
|
|
||||||
for trait in image_meta_props.traits_required:
|
|
||||||
# required traits from the image are always added to the
|
|
||||||
# unnumbered request group, granular request groups are not
|
|
||||||
# supported in image traits
|
|
||||||
ret._add_trait(None, trait, "required")
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def resource_groups(self):
|
def resource_groups(self):
|
||||||
for rg in self._rg_by_id.values():
|
for rg in self._rg_by_id.values():
|
||||||
yield rg.resources
|
yield rg.resources
|
||||||
|
@ -223,33 +239,18 @@ class ResourceRequest(object):
|
||||||
return len([ident for ident in self._rg_by_id.keys()
|
return len([ident for ident in self._rg_by_id.keys()
|
||||||
if ident is not None])
|
if ident is not None])
|
||||||
|
|
||||||
def merged_resources(self, flavor_resources=None):
|
def merged_resources(self):
|
||||||
"""Returns a merge of {resource_class: amount} for all resource groups.
|
"""Returns a merge of {resource_class: amount} for all resource groups.
|
||||||
|
|
||||||
Amounts of the same resource class from different groups are added
|
Amounts of the same resource class from different groups are added
|
||||||
together.
|
together.
|
||||||
|
|
||||||
:param flavor_resources: A flat dict of {resource_class: amount}. If
|
|
||||||
specified, the resources therein are folded
|
|
||||||
into the return dict, such that any resource
|
|
||||||
in flavor_resources is included only if that
|
|
||||||
resource class does not exist elsewhere in the
|
|
||||||
merged ResourceRequest.
|
|
||||||
:return: A dict of the form {resource_class: amount}
|
:return: A dict of the form {resource_class: amount}
|
||||||
"""
|
"""
|
||||||
ret = collections.defaultdict(lambda: 0)
|
ret = collections.defaultdict(lambda: 0)
|
||||||
for resource_dict in self.resource_groups():
|
for resource_dict in self.resource_groups():
|
||||||
for resource_class, amount in resource_dict.items():
|
for resource_class, amount in resource_dict.items():
|
||||||
ret[resource_class] += amount
|
ret[resource_class] += amount
|
||||||
if flavor_resources:
|
|
||||||
for resource_class, amount in flavor_resources.items():
|
|
||||||
# If it's in there - even if zero - ignore the one from the
|
|
||||||
# flavor.
|
|
||||||
if resource_class not in ret:
|
|
||||||
ret[resource_class] = amount
|
|
||||||
# Now strip zeros. This has to be done after the above - we can't
|
|
||||||
# use strip_zeros :(
|
|
||||||
ret = {rc: amt for rc, amt in ret.items() if amt}
|
|
||||||
return dict(ret)
|
return dict(ret)
|
||||||
|
|
||||||
def _clean_empties(self):
|
def _clean_empties(self):
|
||||||
|
@ -329,6 +330,10 @@ class ResourceRequest(object):
|
||||||
traits = traits.union(rr.required_traits)
|
traits = traits.union(rr.required_traits)
|
||||||
return traits
|
return traits
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ', '.join(sorted(
|
||||||
|
list(str(rg) for rg in list(self._rg_by_id.values()))))
|
||||||
|
|
||||||
|
|
||||||
def build_request_spec(image, instances, instance_type=None):
|
def build_request_spec(image, instances, instance_type=None):
|
||||||
"""Build a request_spec for the scheduler.
|
"""Build a request_spec for the scheduler.
|
||||||
|
@ -396,25 +401,17 @@ def resources_from_flavor(instance, flavor):
|
||||||
"""
|
"""
|
||||||
is_bfv = compute_utils.is_volume_backed_instance(instance._context,
|
is_bfv = compute_utils.is_volume_backed_instance(instance._context,
|
||||||
instance)
|
instance)
|
||||||
swap_in_gb = compute_utils.convert_mb_to_ceil_gb(flavor.swap)
|
# create a fake RequestSpec as a wrapper to the caller
|
||||||
disk = ((0 if is_bfv else flavor.root_gb) +
|
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=is_bfv)
|
||||||
swap_in_gb + flavor.ephemeral_gb)
|
|
||||||
|
|
||||||
resources = {
|
# TODO(efried): This method is currently only used from places that
|
||||||
orc.VCPU: flavor.vcpus,
|
# assume the compute node is the only resource provider. So for now, we
|
||||||
orc.MEMORY_MB: flavor.memory_mb,
|
# just merge together all the resources specified in the flavor and pass
|
||||||
orc.DISK_GB: disk,
|
# them along. This will need to be adjusted when nested and/or shared RPs
|
||||||
}
|
# are in play.
|
||||||
if "extra_specs" in flavor:
|
res_req = ResourceRequest(req_spec)
|
||||||
# TODO(efried): This method is currently only used from places that
|
|
||||||
# assume the compute node is the only resource provider. So for now,
|
|
||||||
# we just merge together all the resources specified in the flavor and
|
|
||||||
# pass them along. This will need to be adjusted when nested and/or
|
|
||||||
# shared RPs are in play.
|
|
||||||
rreq = ResourceRequest.from_extra_specs(flavor.extra_specs)
|
|
||||||
resources = rreq.merged_resources(flavor_resources=resources)
|
|
||||||
|
|
||||||
return resources
|
return res_req.merged_resources()
|
||||||
|
|
||||||
|
|
||||||
def resources_from_request_spec(ctxt, spec_obj, host_manager):
|
def resources_from_request_spec(ctxt, spec_obj, host_manager):
|
||||||
|
@ -428,63 +425,7 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager):
|
||||||
:return: A ResourceRequest object.
|
:return: A ResourceRequest object.
|
||||||
:raises NoValidHost: If the specified host/node is not found in the DB.
|
:raises NoValidHost: If the specified host/node is not found in the DB.
|
||||||
"""
|
"""
|
||||||
spec_resources = {
|
res_req = ResourceRequest(spec_obj)
|
||||||
orc.VCPU: spec_obj.vcpus,
|
|
||||||
orc.MEMORY_MB: spec_obj.memory_mb,
|
|
||||||
}
|
|
||||||
|
|
||||||
requested_disk_mb = ((1024 * spec_obj.ephemeral_gb) +
|
|
||||||
spec_obj.swap)
|
|
||||||
|
|
||||||
if 'is_bfv' not in spec_obj or not spec_obj.is_bfv:
|
|
||||||
# Either this is not a BFV instance, or we are not sure,
|
|
||||||
# so ask for root_gb allocation
|
|
||||||
requested_disk_mb += (1024 * spec_obj.root_gb)
|
|
||||||
|
|
||||||
# NOTE(sbauza): Disk request is expressed in MB but we count
|
|
||||||
# resources in GB. Since there could be a remainder of the division
|
|
||||||
# by 1024, we need to ceil the result to the next bigger Gb so we
|
|
||||||
# can be sure there would be enough disk space in the destination
|
|
||||||
# to sustain the request.
|
|
||||||
# FIXME(sbauza): All of that could be using math.ceil() but since
|
|
||||||
# we support both py2 and py3, let's fake it until we only support
|
|
||||||
# py3.
|
|
||||||
requested_disk_gb = requested_disk_mb // 1024
|
|
||||||
if requested_disk_mb % 1024 != 0:
|
|
||||||
# Let's ask for a bit more space since we count in GB
|
|
||||||
requested_disk_gb += 1
|
|
||||||
# NOTE(sbauza): Some flavors provide zero size for disk values, we need
|
|
||||||
# to avoid asking for disk usage.
|
|
||||||
if requested_disk_gb != 0:
|
|
||||||
spec_resources[orc.DISK_GB] = requested_disk_gb
|
|
||||||
|
|
||||||
# Process extra_specs
|
|
||||||
if "extra_specs" in spec_obj.flavor:
|
|
||||||
res_req = ResourceRequest.from_extra_specs(spec_obj.flavor.extra_specs)
|
|
||||||
# If any of the three standard resources above was explicitly given in
|
|
||||||
# the extra_specs - in any group - we need to replace it, or delete it
|
|
||||||
# if it was given as zero. We'll do this by grabbing a merged version
|
|
||||||
# of the ResourceRequest resources and removing matching items from the
|
|
||||||
# spec_resources.
|
|
||||||
spec_resources = {rclass: amt for rclass, amt in spec_resources.items()
|
|
||||||
if rclass not in res_req.merged_resources()}
|
|
||||||
# Now we don't need (or want) any remaining zero entries - remove them.
|
|
||||||
res_req.strip_zeros()
|
|
||||||
|
|
||||||
numbered_groups_from_flavor = res_req.get_num_of_numbered_groups()
|
|
||||||
else:
|
|
||||||
# Start with an empty one
|
|
||||||
res_req = ResourceRequest()
|
|
||||||
numbered_groups_from_flavor = 0
|
|
||||||
|
|
||||||
# Process any image properties
|
|
||||||
if 'image' in spec_obj and 'properties' in spec_obj.image:
|
|
||||||
res_req = ResourceRequest.from_image_props(spec_obj.image.properties,
|
|
||||||
req=res_req)
|
|
||||||
|
|
||||||
# Add the (remaining) items from the spec_resources to the sharing group
|
|
||||||
for rclass, amount in spec_resources.items():
|
|
||||||
res_req.get_request_group(None).resources[rclass] = amount
|
|
||||||
|
|
||||||
requested_resources = (spec_obj.requested_resources
|
requested_resources = (spec_obj.requested_resources
|
||||||
if 'requested_resources' in spec_obj and
|
if 'requested_resources' in spec_obj and
|
||||||
|
@ -574,7 +515,7 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager):
|
||||||
"group to be satisfied from a separate resource provider then "
|
"group to be satisfied from a separate resource provider then "
|
||||||
"use 'group_policy': 'isolate'.")
|
"use 'group_policy': 'isolate'.")
|
||||||
|
|
||||||
if numbered_groups_from_flavor <= 1:
|
if res_req.numbered_groups_from_flavor <= 1:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"At least one numbered request group is defined outside of "
|
"At least one numbered request group is defined outside of "
|
||||||
"the flavor (e.g. in a port that has a QoS minimum bandwidth "
|
"the flavor (e.g. in a port that has a QoS minimum bandwidth "
|
||||||
|
|
|
@ -1019,9 +1019,12 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase):
|
||||||
|
|
||||||
def test_alloc_cands_smoke(self):
|
def test_alloc_cands_smoke(self):
|
||||||
"""Simple call to get_allocation_candidates for version checking."""
|
"""Simple call to get_allocation_candidates for version checking."""
|
||||||
|
flavor = objects.Flavor(
|
||||||
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
|
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
with self._interceptor():
|
with self._interceptor():
|
||||||
self.client.get_allocation_candidates(
|
self.client.get_allocation_candidates(
|
||||||
self.context, utils.ResourceRequest())
|
self.context, utils.ResourceRequest(req_spec))
|
||||||
|
|
||||||
def _set_up_provider_tree(self):
|
def _set_up_provider_tree(self):
|
||||||
r"""Create two compute nodes in placement ("this" one, and another one)
|
r"""Create two compute nodes in placement ("this" one, and another one)
|
||||||
|
@ -1145,7 +1148,7 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase):
|
||||||
# care to check.
|
# care to check.
|
||||||
for k, expected in pdict.items():
|
for k, expected in pdict.items():
|
||||||
# For inventories, we're only validating totals
|
# For inventories, we're only validating totals
|
||||||
if k is 'inventory':
|
if k == 'inventory':
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
set(expected), set(actual_data.inventory),
|
set(expected), set(actual_data.inventory),
|
||||||
"Mismatched inventory keys for provider %s" % uuid)
|
"Mismatched inventory keys for provider %s" % uuid)
|
||||||
|
|
|
@ -2058,27 +2058,31 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||||
'allocation_requests': mock.sentinel.alloc_reqs,
|
'allocation_requests': mock.sentinel.alloc_reqs,
|
||||||
'provider_summaries': mock.sentinel.p_sums,
|
'provider_summaries': mock.sentinel.p_sums,
|
||||||
}
|
}
|
||||||
resources = scheduler_utils.ResourceRequest.from_extra_specs({
|
flavor = objects.Flavor(
|
||||||
'resources:VCPU': '1',
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||||
'resources:MEMORY_MB': '1024',
|
extra_specs={
|
||||||
'trait:HW_CPU_X86_AVX': 'required',
|
'resources:VCPU': '1',
|
||||||
'trait:CUSTOM_TRAIT1': 'required',
|
'resources:MEMORY_MB': '1024',
|
||||||
'trait:CUSTOM_TRAIT2': 'preferred',
|
'trait:HW_CPU_X86_AVX': 'required',
|
||||||
'trait:CUSTOM_TRAIT3': 'forbidden',
|
'trait:CUSTOM_TRAIT1': 'required',
|
||||||
'trait:CUSTOM_TRAIT4': 'forbidden',
|
'trait:CUSTOM_TRAIT2': 'preferred',
|
||||||
'resources1:DISK_GB': '30',
|
'trait:CUSTOM_TRAIT3': 'forbidden',
|
||||||
'trait1:STORAGE_DISK_SSD': 'required',
|
'trait:CUSTOM_TRAIT4': 'forbidden',
|
||||||
'resources2:VGPU': '2',
|
'resources1:DISK_GB': '30',
|
||||||
'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required',
|
'trait1:STORAGE_DISK_SSD': 'required',
|
||||||
'trait2:HW_GPU_API_VULKAN': 'required',
|
'resources2:VGPU': '2',
|
||||||
'resources3:SRIOV_NET_VF': '1',
|
'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required',
|
||||||
'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000',
|
'trait2:HW_GPU_API_VULKAN': 'required',
|
||||||
'group_policy': 'isolate',
|
'resources3:SRIOV_NET_VF': '1',
|
||||||
# These are ignored because misspelled, bad value, etc.
|
'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000',
|
||||||
'resources02:CUSTOM_WIDGET': '123',
|
'group_policy': 'isolate',
|
||||||
'trait:HW_NIC_OFFLOAD_LRO': 'preferred',
|
# These are ignored because misspelled, bad value, etc.
|
||||||
'group_policy3': 'none',
|
'resources02:CUSTOM_WIDGET': '123',
|
||||||
})
|
'trait:HW_NIC_OFFLOAD_LRO': 'preferred',
|
||||||
|
'group_policy3': 'none',
|
||||||
|
})
|
||||||
|
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
resources = scheduler_utils.ResourceRequest(req_spec)
|
||||||
resources.get_request_group(None).aggregates = [
|
resources.get_request_group(None).aggregates = [
|
||||||
['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
|
['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
|
||||||
expected_path = '/allocation_candidates'
|
expected_path = '/allocation_candidates'
|
||||||
|
@ -2123,12 +2127,16 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||||
'allocation_requests': mock.sentinel.alloc_reqs,
|
'allocation_requests': mock.sentinel.alloc_reqs,
|
||||||
'provider_summaries': mock.sentinel.p_sums,
|
'provider_summaries': mock.sentinel.p_sums,
|
||||||
}
|
}
|
||||||
resources = scheduler_utils.ResourceRequest.from_extra_specs({
|
flavor = objects.Flavor(
|
||||||
'resources:VCPU': '1',
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||||
'resources:MEMORY_MB': '1024',
|
extra_specs={
|
||||||
'resources1:DISK_GB': '30',
|
'resources:VCPU': '1',
|
||||||
'group_policy': 'bogus',
|
'resources:MEMORY_MB': '1024',
|
||||||
})
|
'resources1:DISK_GB': '30',
|
||||||
|
'group_policy': 'bogus',
|
||||||
|
})
|
||||||
|
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
resources = scheduler_utils.ResourceRequest(req_spec)
|
||||||
expected_path = '/allocation_candidates'
|
expected_path = '/allocation_candidates'
|
||||||
expected_query = [
|
expected_query = [
|
||||||
('limit', '42'),
|
('limit', '42'),
|
||||||
|
@ -2161,14 +2169,18 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||||
resp_mock = mock.Mock(status_code=404)
|
resp_mock = mock.Mock(status_code=404)
|
||||||
self.ks_adap_mock.get.return_value = resp_mock
|
self.ks_adap_mock.get.return_value = resp_mock
|
||||||
expected_path = '/allocation_candidates'
|
expected_path = '/allocation_candidates'
|
||||||
expected_query = {'resources': ['MEMORY_MB:1024'],
|
expected_query = {
|
||||||
'limit': ['100']}
|
'resources': ['DISK_GB:15,MEMORY_MB:1024,VCPU:1'],
|
||||||
|
'limit': ['100']
|
||||||
|
}
|
||||||
|
|
||||||
# Make sure we're also honoring the configured limit
|
# Make sure we're also honoring the configured limit
|
||||||
self.flags(max_placement_results=100, group='scheduler')
|
self.flags(max_placement_results=100, group='scheduler')
|
||||||
|
|
||||||
resources = scheduler_utils.ResourceRequest.from_extra_specs(
|
flavor = objects.Flavor(
|
||||||
{'resources:MEMORY_MB': '1024'})
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
|
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
resources = scheduler_utils.ResourceRequest(req_spec)
|
||||||
|
|
||||||
res = self.client.get_allocation_candidates(self.context, resources)
|
res = self.client.get_allocation_candidates(self.context, resources)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,19 @@ from nova.tests.unit import fake_instance
|
||||||
from nova.tests.unit.scheduler import fakes
|
from nova.tests.unit.scheduler import fakes
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResourceRequest(object):
|
||||||
|
"""A fake of ``nova.scheduler.utils.ResourceRequest``.
|
||||||
|
|
||||||
|
Allows us to assert that various properties of a real ResourceRequest
|
||||||
|
object are set as we'd like them to be.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._rg_by_id = {}
|
||||||
|
self._group_policy = None
|
||||||
|
self._limit = 1000
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class TestUtils(test.NoDBTestCase):
|
class TestUtils(test.NoDBTestCase):
|
||||||
|
|
||||||
|
@ -68,7 +81,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
root_gb=10,
|
root_gb=10,
|
||||||
ephemeral_gb=5,
|
ephemeral_gb=5,
|
||||||
swap=0)
|
swap=0)
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -89,7 +102,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
extra_specs={
|
extra_specs={
|
||||||
'trait:CUSTOM_FLAVOR_TRAIT': 'required',
|
'trait:CUSTOM_FLAVOR_TRAIT': 'required',
|
||||||
'trait:CUSTOM_IMAGE_TRAIT2': 'required'})
|
'trait:CUSTOM_IMAGE_TRAIT2': 'required'})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -116,7 +129,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
swap=0,
|
swap=0,
|
||||||
extra_specs={
|
extra_specs={
|
||||||
'trait:CUSTOM_FLAVOR_TRAIT': 'forbidden'})
|
'trait:CUSTOM_FLAVOR_TRAIT': 'forbidden'})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -136,7 +149,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
root_gb=0,
|
root_gb=0,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
swap=0)
|
swap=0)
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -153,7 +166,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
ephemeral_gb=5,
|
ephemeral_gb=5,
|
||||||
swap=0,
|
swap=0,
|
||||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -175,7 +188,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
"resources:VCPU": 99,
|
"resources:VCPU": 99,
|
||||||
"resources:MEMORY_MB": 99,
|
"resources:MEMORY_MB": 99,
|
||||||
"resources:DISK_GB": 99})
|
"resources:DISK_GB": 99})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -195,7 +208,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
extra_specs={
|
extra_specs={
|
||||||
"resources:VCPU": 0,
|
"resources:VCPU": 0,
|
||||||
"resources:DISK_GB": 0})
|
"resources:DISK_GB": 0})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -213,7 +226,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
extra_specs={
|
extra_specs={
|
||||||
"resources:VGPU": 1,
|
"resources:VGPU": 1,
|
||||||
"resources:VGPU_DISPLAY_HEAD": 1})
|
"resources:VGPU_DISPLAY_HEAD": 1})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -269,7 +282,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
# Bogus values don't make it through
|
# Bogus values don't make it through
|
||||||
'resources1:MEMORY_MB': 'bogus',
|
'resources1:MEMORY_MB': 'bogus',
|
||||||
'group_policy': 'none'})
|
'group_policy': 'none'})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._group_policy = 'none'
|
expected_resources._group_policy = 'none'
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
|
@ -327,7 +340,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
'trait:HW_CPU_X86_SSE': 'required',
|
'trait:HW_CPU_X86_SSE': 'required',
|
||||||
'trait:HW_CPU_X86_AVX': 'required',
|
'trait:HW_CPU_X86_AVX': 'required',
|
||||||
'trait:HW_CPU_X86_AVX2': 'forbidden'})
|
'trait:HW_CPU_X86_AVX2': 'forbidden'})
|
||||||
expected_resources = utils.ResourceRequest()
|
expected_resources = FakeResourceRequest()
|
||||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -394,9 +407,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
self.context, reqspec, self.mock_host_manager)
|
self.context, reqspec, self.mock_host_manager)
|
||||||
self.assertEqual([], req.get_request_group(None).aggregates)
|
self.assertEqual([], req.get_request_group(None).aggregates)
|
||||||
|
|
||||||
@mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs",
|
def test_process_extra_specs_granular_called(self):
|
||||||
return_value=utils.ResourceRequest())
|
|
||||||
def test_process_extra_specs_granular_called(self, mock_proc):
|
|
||||||
flavor = objects.Flavor(vcpus=1,
|
flavor = objects.Flavor(vcpus=1,
|
||||||
memory_mb=1024,
|
memory_mb=1024,
|
||||||
root_gb=10,
|
root_gb=10,
|
||||||
|
@ -404,21 +415,20 @@ class TestUtils(test.NoDBTestCase):
|
||||||
swap=0,
|
swap=0,
|
||||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||||
|
# just call this to make sure things don't explode
|
||||||
utils.resources_from_request_spec(
|
utils.resources_from_request_spec(
|
||||||
self.context, fake_spec, self.mock_host_manager)
|
self.context, fake_spec, self.mock_host_manager)
|
||||||
mock_proc.assert_called_once()
|
|
||||||
|
|
||||||
@mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs")
|
def test_process_extra_specs_granular_not_called(self):
|
||||||
def test_process_extra_specs_granular_not_called(self, mock_proc):
|
|
||||||
flavor = objects.Flavor(vcpus=1,
|
flavor = objects.Flavor(vcpus=1,
|
||||||
memory_mb=1024,
|
memory_mb=1024,
|
||||||
root_gb=10,
|
root_gb=10,
|
||||||
ephemeral_gb=5,
|
ephemeral_gb=5,
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||||
|
# just call this to make sure things don't explode
|
||||||
utils.resources_from_request_spec(
|
utils.resources_from_request_spec(
|
||||||
self.context, fake_spec, self.mock_host_manager)
|
self.context, fake_spec, self.mock_host_manager)
|
||||||
mock_proc.assert_not_called()
|
|
||||||
|
|
||||||
def test_process_missing_extra_specs_value(self):
|
def test_process_missing_extra_specs_value(self):
|
||||||
flavor = objects.Flavor(
|
flavor = objects.Flavor(
|
||||||
|
@ -429,6 +439,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
swap=0,
|
swap=0,
|
||||||
extra_specs={"resources:CUSTOM_TEST_CLASS": ""})
|
extra_specs={"resources:CUSTOM_TEST_CLASS": ""})
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||||
|
# just call this to make sure things don't explode
|
||||||
utils.resources_from_request_spec(
|
utils.resources_from_request_spec(
|
||||||
self.context, fake_spec, self.mock_host_manager)
|
self.context, fake_spec, self.mock_host_manager)
|
||||||
|
|
||||||
|
@ -438,7 +449,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
root_gb=15,
|
root_gb=15,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
swap=0)
|
swap=0)
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -467,7 +478,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor, force_nodes=['test'])
|
fake_spec = objects.RequestSpec(flavor=flavor, force_nodes=['test'])
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -500,7 +511,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -535,7 +546,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -575,7 +586,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(
|
fake_spec = objects.RequestSpec(
|
||||||
flavor=flavor, requested_destination=destination)
|
flavor=flavor, requested_destination=destination)
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -654,7 +665,7 @@ class TestUtils(test.NoDBTestCase):
|
||||||
swap=0)
|
swap=0)
|
||||||
fake_spec = objects.RequestSpec(
|
fake_spec = objects.RequestSpec(
|
||||||
flavor=flavor, scheduler_hints=hints)
|
flavor=flavor, scheduler_hints=hints)
|
||||||
expected = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
|
@ -735,40 +746,59 @@ class TestUtils(test.NoDBTestCase):
|
||||||
actual = utils.resources_from_flavor(instance, flavor)
|
actual = utils.resources_from_flavor(instance, flavor)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_resource_request_from_extra_specs(self):
|
def test_resource_request_init(self):
|
||||||
extra_specs = {
|
flavor = objects.Flavor(
|
||||||
'resources:VCPU': '2',
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
'resources:MEMORY_MB': '2048',
|
|
||||||
'trait:HW_CPU_X86_AVX': 'required',
|
expected = FakeResourceRequest()
|
||||||
# Key skipped because no colons
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
'nocolons': '42',
|
use_same_provider=False,
|
||||||
'trait:CUSTOM_MAGIC': 'required',
|
resources={
|
||||||
'trait:CUSTOM_BRONZE': 'forbidden',
|
'VCPU': 1,
|
||||||
# Resource skipped because invalid resource class name
|
'MEMORY_MB': 1024,
|
||||||
'resources86:CUTSOM_MISSPELLED': '86',
|
'DISK_GB': 15,
|
||||||
'resources1:SRIOV_NET_VF': '1',
|
},
|
||||||
# Resource skipped because non-int-able value
|
)
|
||||||
'resources86:CUSTOM_FOO': 'seven',
|
rs = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
# Resource skipped because negative value
|
rr = utils.ResourceRequest(rs)
|
||||||
'resources86:CUSTOM_NEGATIVE': '-7',
|
self.assertResourceRequestsEqual(expected, rr)
|
||||||
'resources1:IPV4_ADDRESS': '1',
|
|
||||||
# Trait skipped because unsupported value
|
def test_resource_request_init_with_extra_specs(self):
|
||||||
'trait86:CUSTOM_GOLD': 'preferred',
|
flavor = objects.Flavor(
|
||||||
'trait1:CUSTOM_PHYSNET_NET1': 'required',
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||||
'trait1:CUSTOM_PHYSNET_NET2': 'forbidden',
|
extra_specs={
|
||||||
'resources2:SRIOV_NET_VF': '1',
|
'resources:VCPU': '2',
|
||||||
'resources2:IPV4_ADDRESS': '2',
|
'resources:MEMORY_MB': '2048',
|
||||||
'trait2:CUSTOM_PHYSNET_NET2': 'required',
|
'trait:HW_CPU_X86_AVX': 'required',
|
||||||
'trait2:HW_NIC_ACCEL_SSL': 'required',
|
# Key skipped because no colons
|
||||||
# Groupings that don't quite match the patterns are ignored
|
'nocolons': '42',
|
||||||
'resources_5:SRIOV_NET_VF': '7',
|
'trait:CUSTOM_MAGIC': 'required',
|
||||||
'traitFoo:HW_NIC_ACCEL_SSL': 'required',
|
'trait:CUSTOM_BRONZE': 'forbidden',
|
||||||
# Solo resource, no corresponding traits
|
# Resource skipped because invalid resource class name
|
||||||
'resources3:DISK_GB': '5',
|
'resources86:CUTSOM_MISSPELLED': '86',
|
||||||
'group_policy': 'isolate',
|
'resources1:SRIOV_NET_VF': '1',
|
||||||
}
|
# Resource skipped because non-int-able value
|
||||||
# Build up a ResourceRequest from the inside to compare against.
|
'resources86:CUSTOM_FOO': 'seven',
|
||||||
expected = utils.ResourceRequest()
|
# Resource skipped because negative value
|
||||||
|
'resources86:CUSTOM_NEGATIVE': '-7',
|
||||||
|
'resources1:IPV4_ADDRESS': '1',
|
||||||
|
# Trait skipped because unsupported value
|
||||||
|
'trait86:CUSTOM_GOLD': 'preferred',
|
||||||
|
'trait1:CUSTOM_PHYSNET_NET1': 'required',
|
||||||
|
'trait1:CUSTOM_PHYSNET_NET2': 'forbidden',
|
||||||
|
'resources2:SRIOV_NET_VF': '1',
|
||||||
|
'resources2:IPV4_ADDRESS': '2',
|
||||||
|
'trait2:CUSTOM_PHYSNET_NET2': 'required',
|
||||||
|
'trait2:HW_NIC_ACCEL_SSL': 'required',
|
||||||
|
# Groupings that don't quite match the patterns are ignored
|
||||||
|
'resources_5:SRIOV_NET_VF': '7',
|
||||||
|
'traitFoo:HW_NIC_ACCEL_SSL': 'required',
|
||||||
|
# Solo resource, no corresponding traits
|
||||||
|
'resources3:DISK_GB': '5',
|
||||||
|
'group_policy': 'isolate',
|
||||||
|
})
|
||||||
|
|
||||||
|
expected = FakeResourceRequest()
|
||||||
expected._group_policy = 'isolate'
|
expected._group_policy = 'isolate'
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
|
@ -812,7 +842,8 @@ class TestUtils(test.NoDBTestCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
rr = utils.ResourceRequest.from_extra_specs(extra_specs)
|
rs = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
rr = utils.ResourceRequest(rs)
|
||||||
self.assertResourceRequestsEqual(expected, rr)
|
self.assertResourceRequestsEqual(expected, rr)
|
||||||
expected_querystring = (
|
expected_querystring = (
|
||||||
'group_policy=isolate&'
|
'group_policy=isolate&'
|
||||||
|
@ -827,89 +858,56 @@ class TestUtils(test.NoDBTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_querystring, rr.to_querystring())
|
self.assertEqual(expected_querystring, rr.to_querystring())
|
||||||
|
|
||||||
def test_resource_request_from_extra_specs_append_request(self):
|
def test_resource_request_init_with_image_props(self):
|
||||||
extra_specs = {
|
flavor = objects.Flavor(
|
||||||
'resources:VCPU': '2',
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
'resources:MEMORY_MB': '2048',
|
image = objects.ImageMeta.from_dict({
|
||||||
'trait:HW_CPU_X86_AVX': 'required',
|
'properties': {
|
||||||
}
|
'trait:CUSTOM_TRUSTED': 'required',
|
||||||
existing_req = utils.ResourceRequest()
|
},
|
||||||
existing_req._rg_by_id[None] = objects.RequestGroup(
|
'id': 'c8b1790e-a07d-4971-b137-44f2432936cd'
|
||||||
use_same_provider=False,
|
})
|
||||||
required_traits={
|
|
||||||
'CUSTOM_MAGIC',
|
expected = FakeResourceRequest()
|
||||||
}
|
|
||||||
)
|
|
||||||
# Build up a ResourceRequest from the inside to compare against.
|
|
||||||
expected = utils.ResourceRequest()
|
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'VCPU': 2,
|
'VCPU': 1,
|
||||||
'MEMORY_MB': 2048,
|
'MEMORY_MB': 1024,
|
||||||
|
'DISK_GB': 15,
|
||||||
},
|
},
|
||||||
required_traits={
|
|
||||||
# In addition to traits from extra spec, we get traits from a
|
|
||||||
# previous existing resource request
|
|
||||||
'HW_CPU_X86_AVX',
|
|
||||||
'CUSTOM_MAGIC',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.assertResourceRequestsEqual(
|
|
||||||
expected, utils.ResourceRequest.from_extra_specs(extra_specs,
|
|
||||||
req=existing_req))
|
|
||||||
|
|
||||||
def test_resource_request_from_image_props(self):
|
|
||||||
props = {'trait:CUSTOM_TRUSTED': 'required'}
|
|
||||||
image_meta_props = objects.ImageMetaProps.from_dict(props)
|
|
||||||
|
|
||||||
# Build up a ResourceRequest from the inside to compare against.
|
|
||||||
expected = utils.ResourceRequest()
|
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
|
||||||
use_same_provider=False,
|
|
||||||
required_traits={
|
required_traits={
|
||||||
'CUSTOM_TRUSTED',
|
'CUSTOM_TRUSTED',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertResourceRequestsEqual(
|
rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False)
|
||||||
expected, utils.ResourceRequest.from_image_props(image_meta_props))
|
rr = utils.ResourceRequest(rs)
|
||||||
|
self.assertResourceRequestsEqual(expected, rr)
|
||||||
|
|
||||||
def test_resource_request_from_image_props_append_request(self):
|
def test_resource_request_init_is_bfv(self):
|
||||||
props = {'trait:CUSTOM_MAGIC': 'required'}
|
flavor = objects.Flavor(
|
||||||
image_meta_props = objects.ImageMetaProps.from_dict(props)
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=1555)
|
||||||
|
|
||||||
existing_req = utils.ResourceRequest()
|
expected = FakeResourceRequest()
|
||||||
existing_req._rg_by_id[None] = objects.RequestGroup(
|
|
||||||
use_same_provider=False,
|
|
||||||
resources={
|
|
||||||
'VCPU': 2,
|
|
||||||
'MEMORY_MB': 2048,
|
|
||||||
},
|
|
||||||
required_traits={
|
|
||||||
'HW_CPU_X86_AVX',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Build up a ResourceRequest from the inside to compare against.
|
|
||||||
expected = utils.ResourceRequest()
|
|
||||||
expected._rg_by_id[None] = objects.RequestGroup(
|
expected._rg_by_id[None] = objects.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'VCPU': 2,
|
'VCPU': 1,
|
||||||
'MEMORY_MB': 2048,
|
'MEMORY_MB': 1024,
|
||||||
|
# this should only include the ephemeral and swap disk, and the
|
||||||
|
# latter should be converted from MB to GB and rounded up
|
||||||
|
'DISK_GB': 7,
|
||||||
},
|
},
|
||||||
required_traits={
|
|
||||||
# In addition to information already contained in the existing
|
|
||||||
# resource request, we add the traits from image properties
|
|
||||||
'HW_CPU_X86_AVX',
|
|
||||||
'CUSTOM_MAGIC',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
self.assertResourceRequestsEqual(
|
rs = objects.RequestSpec(flavor=flavor, is_bfv=True)
|
||||||
expected, utils.ResourceRequest.from_image_props(image_meta_props,
|
rr = utils.ResourceRequest(rs)
|
||||||
req=existing_req))
|
self.assertResourceRequestsEqual(expected, rr)
|
||||||
|
|
||||||
def test_resource_request_add_group_inserts_the_group(self):
|
def test_resource_request_add_group_inserts_the_group(self):
|
||||||
req = utils.ResourceRequest()
|
flavor = objects.Flavor(
|
||||||
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
|
rs = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
req = utils.ResourceRequest(rs)
|
||||||
rg1 = objects.RequestGroup()
|
rg1 = objects.RequestGroup()
|
||||||
req.add_request_group(rg1)
|
req.add_request_group(rg1)
|
||||||
rg2 = objects.RequestGroup()
|
rg2 = objects.RequestGroup()
|
||||||
|
@ -918,18 +916,22 @@ class TestUtils(test.NoDBTestCase):
|
||||||
self.assertIs(rg2, req.get_request_group(2))
|
self.assertIs(rg2, req.get_request_group(2))
|
||||||
|
|
||||||
def test_resource_request_add_group_inserts_the_group_implicit_group(self):
|
def test_resource_request_add_group_inserts_the_group_implicit_group(self):
|
||||||
req = utils.ResourceRequest()
|
flavor = objects.Flavor(
|
||||||
# this implicitly creates the unnumbered group
|
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||||
unnumbered_rg = req.get_request_group(None)
|
rs = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||||
|
req = utils.ResourceRequest(rs)
|
||||||
|
|
||||||
|
# this implicitly creates the specified group
|
||||||
|
unnumbered_rg = req.get_request_group(42)
|
||||||
|
|
||||||
rg1 = objects.RequestGroup()
|
rg1 = objects.RequestGroup()
|
||||||
req.add_request_group(rg1)
|
req.add_request_group(rg1)
|
||||||
rg2 = objects.RequestGroup()
|
rg2 = objects.RequestGroup()
|
||||||
req.add_request_group(rg2)
|
req.add_request_group(rg2)
|
||||||
|
|
||||||
self.assertIs(rg1, req.get_request_group(1))
|
self.assertIs(rg1, req.get_request_group(43))
|
||||||
self.assertIs(rg2, req.get_request_group(2))
|
self.assertIs(rg2, req.get_request_group(44))
|
||||||
self.assertIs(unnumbered_rg, req.get_request_group(None))
|
self.assertIs(unnumbered_rg, req.get_request_group(42))
|
||||||
|
|
||||||
def test_claim_resources_on_destination_no_source_allocations(self):
|
def test_claim_resources_on_destination_no_source_allocations(self):
|
||||||
"""Tests the negative scenario where the instance does not have
|
"""Tests the negative scenario where the instance does not have
|
||||||
|
|
Loading…
Reference in New Issue