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]*)?:(.*)$" %
|
||||
'|'.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 }
|
||||
self._rg_by_id = {}
|
||||
self._group_policy = None
|
||||
@ -62,9 +87,75 @@ class ResourceRequest(object):
|
||||
# set to None to indicate "no limit".
|
||||
self._limit = CONF.scheduler.max_placement_results
|
||||
|
||||
def __str__(self):
|
||||
return ', '.join(sorted(
|
||||
list(str(rg) for rg in list(self._rg_by_id.values()))))
|
||||
# TODO(efried): Handle member_of[$N], which will need to be reconciled
|
||||
# with destination.aggregates handling in resources_from_request_spec
|
||||
|
||||
# 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
|
||||
def group_policy(self):
|
||||
@ -140,81 +231,6 @@ class ResourceRequest(object):
|
||||
return
|
||||
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):
|
||||
for rg in self._rg_by_id.values():
|
||||
yield rg.resources
|
||||
@ -223,33 +239,18 @@ class ResourceRequest(object):
|
||||
return len([ident for ident in self._rg_by_id.keys()
|
||||
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.
|
||||
|
||||
Amounts of the same resource class from different groups are added
|
||||
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}
|
||||
"""
|
||||
ret = collections.defaultdict(lambda: 0)
|
||||
for resource_dict in self.resource_groups():
|
||||
for resource_class, amount in resource_dict.items():
|
||||
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)
|
||||
|
||||
def _clean_empties(self):
|
||||
@ -329,6 +330,10 @@ class ResourceRequest(object):
|
||||
traits = traits.union(rr.required_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):
|
||||
"""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,
|
||||
instance)
|
||||
swap_in_gb = compute_utils.convert_mb_to_ceil_gb(flavor.swap)
|
||||
disk = ((0 if is_bfv else flavor.root_gb) +
|
||||
swap_in_gb + flavor.ephemeral_gb)
|
||||
# create a fake RequestSpec as a wrapper to the caller
|
||||
req_spec = objects.RequestSpec(flavor=flavor, is_bfv=is_bfv)
|
||||
|
||||
resources = {
|
||||
orc.VCPU: flavor.vcpus,
|
||||
orc.MEMORY_MB: flavor.memory_mb,
|
||||
orc.DISK_GB: disk,
|
||||
}
|
||||
if "extra_specs" in flavor:
|
||||
# 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)
|
||||
# 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.
|
||||
res_req = ResourceRequest(req_spec)
|
||||
|
||||
return resources
|
||||
return res_req.merged_resources()
|
||||
|
||||
|
||||
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.
|
||||
:raises NoValidHost: If the specified host/node is not found in the DB.
|
||||
"""
|
||||
spec_resources = {
|
||||
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
|
||||
res_req = ResourceRequest(spec_obj)
|
||||
|
||||
requested_resources = (spec_obj.requested_resources
|
||||
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 "
|
||||
"use 'group_policy': 'isolate'.")
|
||||
|
||||
if numbered_groups_from_flavor <= 1:
|
||||
if res_req.numbered_groups_from_flavor <= 1:
|
||||
LOG.info(
|
||||
"At least one numbered request group is defined outside of "
|
||||
"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):
|
||||
"""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():
|
||||
self.client.get_allocation_candidates(
|
||||
self.context, utils.ResourceRequest())
|
||||
self.context, utils.ResourceRequest(req_spec))
|
||||
|
||||
def _set_up_provider_tree(self):
|
||||
r"""Create two compute nodes in placement ("this" one, and another one)
|
||||
@ -1145,7 +1148,7 @@ class SchedulerReportClientTests(SchedulerReportClientTestBase):
|
||||
# care to check.
|
||||
for k, expected in pdict.items():
|
||||
# For inventories, we're only validating totals
|
||||
if k is 'inventory':
|
||||
if k == 'inventory':
|
||||
self.assertEqual(
|
||||
set(expected), set(actual_data.inventory),
|
||||
"Mismatched inventory keys for provider %s" % uuid)
|
||||
|
@ -2058,27 +2058,31 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||
'allocation_requests': mock.sentinel.alloc_reqs,
|
||||
'provider_summaries': mock.sentinel.p_sums,
|
||||
}
|
||||
resources = scheduler_utils.ResourceRequest.from_extra_specs({
|
||||
'resources:VCPU': '1',
|
||||
'resources:MEMORY_MB': '1024',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
'trait:CUSTOM_TRAIT1': 'required',
|
||||
'trait:CUSTOM_TRAIT2': 'preferred',
|
||||
'trait:CUSTOM_TRAIT3': 'forbidden',
|
||||
'trait:CUSTOM_TRAIT4': 'forbidden',
|
||||
'resources1:DISK_GB': '30',
|
||||
'trait1:STORAGE_DISK_SSD': 'required',
|
||||
'resources2:VGPU': '2',
|
||||
'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required',
|
||||
'trait2:HW_GPU_API_VULKAN': 'required',
|
||||
'resources3:SRIOV_NET_VF': '1',
|
||||
'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000',
|
||||
'group_policy': 'isolate',
|
||||
# These are ignored because misspelled, bad value, etc.
|
||||
'resources02:CUSTOM_WIDGET': '123',
|
||||
'trait:HW_NIC_OFFLOAD_LRO': 'preferred',
|
||||
'group_policy3': 'none',
|
||||
})
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={
|
||||
'resources:VCPU': '1',
|
||||
'resources:MEMORY_MB': '1024',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
'trait:CUSTOM_TRAIT1': 'required',
|
||||
'trait:CUSTOM_TRAIT2': 'preferred',
|
||||
'trait:CUSTOM_TRAIT3': 'forbidden',
|
||||
'trait:CUSTOM_TRAIT4': 'forbidden',
|
||||
'resources1:DISK_GB': '30',
|
||||
'trait1:STORAGE_DISK_SSD': 'required',
|
||||
'resources2:VGPU': '2',
|
||||
'trait2:HW_GPU_RESOLUTION_W2560H1600': 'required',
|
||||
'trait2:HW_GPU_API_VULKAN': 'required',
|
||||
'resources3:SRIOV_NET_VF': '1',
|
||||
'resources3:CUSTOM_NET_EGRESS_BYTES_SEC': '125000',
|
||||
'group_policy': 'isolate',
|
||||
# These are ignored because misspelled, bad value, etc.
|
||||
'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 = [
|
||||
['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
|
||||
expected_path = '/allocation_candidates'
|
||||
@ -2123,12 +2127,16 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||
'allocation_requests': mock.sentinel.alloc_reqs,
|
||||
'provider_summaries': mock.sentinel.p_sums,
|
||||
}
|
||||
resources = scheduler_utils.ResourceRequest.from_extra_specs({
|
||||
'resources:VCPU': '1',
|
||||
'resources:MEMORY_MB': '1024',
|
||||
'resources1:DISK_GB': '30',
|
||||
'group_policy': 'bogus',
|
||||
})
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={
|
||||
'resources:VCPU': '1',
|
||||
'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_query = [
|
||||
('limit', '42'),
|
||||
@ -2161,14 +2169,18 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
||||
resp_mock = mock.Mock(status_code=404)
|
||||
self.ks_adap_mock.get.return_value = resp_mock
|
||||
expected_path = '/allocation_candidates'
|
||||
expected_query = {'resources': ['MEMORY_MB:1024'],
|
||||
'limit': ['100']}
|
||||
expected_query = {
|
||||
'resources': ['DISK_GB:15,MEMORY_MB:1024,VCPU:1'],
|
||||
'limit': ['100']
|
||||
}
|
||||
|
||||
# Make sure we're also honoring the configured limit
|
||||
self.flags(max_placement_results=100, group='scheduler')
|
||||
|
||||
resources = scheduler_utils.ResourceRequest.from_extra_specs(
|
||||
{'resources:MEMORY_MB': '1024'})
|
||||
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)
|
||||
resources = scheduler_utils.ResourceRequest(req_spec)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
class TestUtils(test.NoDBTestCase):
|
||||
|
||||
@ -68,7 +81,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0)
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -89,7 +102,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
extra_specs={
|
||||
'trait:CUSTOM_FLAVOR_TRAIT': 'required',
|
||||
'trait:CUSTOM_IMAGE_TRAIT2': 'required'})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -116,7 +129,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
swap=0,
|
||||
extra_specs={
|
||||
'trait:CUSTOM_FLAVOR_TRAIT': 'forbidden'})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -136,7 +149,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
root_gb=0,
|
||||
ephemeral_gb=0,
|
||||
swap=0)
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -153,7 +166,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -175,7 +188,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
"resources:VCPU": 99,
|
||||
"resources:MEMORY_MB": 99,
|
||||
"resources:DISK_GB": 99})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -195,7 +208,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
extra_specs={
|
||||
"resources:VCPU": 0,
|
||||
"resources:DISK_GB": 0})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -213,7 +226,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
extra_specs={
|
||||
"resources:VGPU": 1,
|
||||
"resources:VGPU_DISPLAY_HEAD": 1})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -269,7 +282,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
# Bogus values don't make it through
|
||||
'resources1:MEMORY_MB': 'bogus',
|
||||
'group_policy': 'none'})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._group_policy = 'none'
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
@ -327,7 +340,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
'trait:HW_CPU_X86_SSE': 'required',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
'trait:HW_CPU_X86_AVX2': 'forbidden'})
|
||||
expected_resources = utils.ResourceRequest()
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -394,9 +407,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
self.context, reqspec, self.mock_host_manager)
|
||||
self.assertEqual([], req.get_request_group(None).aggregates)
|
||||
|
||||
@mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs",
|
||||
return_value=utils.ResourceRequest())
|
||||
def test_process_extra_specs_granular_called(self, mock_proc):
|
||||
def test_process_extra_specs_granular_called(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
@ -404,21 +415,20 @@ class TestUtils(test.NoDBTestCase):
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
# just call this to make sure things don't explode
|
||||
utils.resources_from_request_spec(
|
||||
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, mock_proc):
|
||||
def test_process_extra_specs_granular_not_called(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
# just call this to make sure things don't explode
|
||||
utils.resources_from_request_spec(
|
||||
self.context, fake_spec, self.mock_host_manager)
|
||||
mock_proc.assert_not_called()
|
||||
|
||||
def test_process_missing_extra_specs_value(self):
|
||||
flavor = objects.Flavor(
|
||||
@ -429,6 +439,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": ""})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
# just call this to make sure things don't explode
|
||||
utils.resources_from_request_spec(
|
||||
self.context, fake_spec, self.mock_host_manager)
|
||||
|
||||
@ -438,7 +449,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
root_gb=15,
|
||||
ephemeral_gb=0,
|
||||
swap=0)
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -467,7 +478,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
ephemeral_gb=0,
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(flavor=flavor, force_nodes=['test'])
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -500,7 +511,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
ephemeral_gb=0,
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -535,7 +546,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
ephemeral_gb=0,
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(flavor=flavor, force_hosts=['test'])
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -575,7 +586,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(
|
||||
flavor=flavor, requested_destination=destination)
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -654,7 +665,7 @@ class TestUtils(test.NoDBTestCase):
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(
|
||||
flavor=flavor, scheduler_hints=hints)
|
||||
expected = utils.ResourceRequest()
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
@ -735,40 +746,59 @@ class TestUtils(test.NoDBTestCase):
|
||||
actual = utils.resources_from_flavor(instance, flavor)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_resource_request_from_extra_specs(self):
|
||||
extra_specs = {
|
||||
'resources:VCPU': '2',
|
||||
'resources:MEMORY_MB': '2048',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
# Key skipped because no colons
|
||||
'nocolons': '42',
|
||||
'trait:CUSTOM_MAGIC': 'required',
|
||||
'trait:CUSTOM_BRONZE': 'forbidden',
|
||||
# Resource skipped because invalid resource class name
|
||||
'resources86:CUTSOM_MISSPELLED': '86',
|
||||
'resources1:SRIOV_NET_VF': '1',
|
||||
# Resource skipped because non-int-able value
|
||||
'resources86:CUSTOM_FOO': 'seven',
|
||||
# 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',
|
||||
}
|
||||
# Build up a ResourceRequest from the inside to compare against.
|
||||
expected = utils.ResourceRequest()
|
||||
def test_resource_request_init(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
'VCPU': 1,
|
||||
'MEMORY_MB': 1024,
|
||||
'DISK_GB': 15,
|
||||
},
|
||||
)
|
||||
rs = objects.RequestSpec(flavor=flavor, is_bfv=False)
|
||||
rr = utils.ResourceRequest(rs)
|
||||
self.assertResourceRequestsEqual(expected, rr)
|
||||
|
||||
def test_resource_request_init_with_extra_specs(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={
|
||||
'resources:VCPU': '2',
|
||||
'resources:MEMORY_MB': '2048',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
# Key skipped because no colons
|
||||
'nocolons': '42',
|
||||
'trait:CUSTOM_MAGIC': 'required',
|
||||
'trait:CUSTOM_BRONZE': 'forbidden',
|
||||
# Resource skipped because invalid resource class name
|
||||
'resources86:CUTSOM_MISSPELLED': '86',
|
||||
'resources1:SRIOV_NET_VF': '1',
|
||||
# Resource skipped because non-int-able value
|
||||
'resources86:CUSTOM_FOO': 'seven',
|
||||
# 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._rg_by_id[None] = objects.RequestGroup(
|
||||
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)
|
||||
expected_querystring = (
|
||||
'group_policy=isolate&'
|
||||
@ -827,89 +858,56 @@ class TestUtils(test.NoDBTestCase):
|
||||
)
|
||||
self.assertEqual(expected_querystring, rr.to_querystring())
|
||||
|
||||
def test_resource_request_from_extra_specs_append_request(self):
|
||||
extra_specs = {
|
||||
'resources:VCPU': '2',
|
||||
'resources:MEMORY_MB': '2048',
|
||||
'trait:HW_CPU_X86_AVX': 'required',
|
||||
}
|
||||
existing_req = utils.ResourceRequest()
|
||||
existing_req._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
required_traits={
|
||||
'CUSTOM_MAGIC',
|
||||
}
|
||||
)
|
||||
# Build up a ResourceRequest from the inside to compare against.
|
||||
expected = utils.ResourceRequest()
|
||||
def test_resource_request_init_with_image_props(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)
|
||||
image = objects.ImageMeta.from_dict({
|
||||
'properties': {
|
||||
'trait:CUSTOM_TRUSTED': 'required',
|
||||
},
|
||||
'id': 'c8b1790e-a07d-4971-b137-44f2432936cd'
|
||||
})
|
||||
|
||||
expected = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
'VCPU': 2,
|
||||
'MEMORY_MB': 2048,
|
||||
'VCPU': 1,
|
||||
'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={
|
||||
'CUSTOM_TRUSTED',
|
||||
}
|
||||
)
|
||||
self.assertResourceRequestsEqual(
|
||||
expected, utils.ResourceRequest.from_image_props(image_meta_props))
|
||||
rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False)
|
||||
rr = utils.ResourceRequest(rs)
|
||||
self.assertResourceRequestsEqual(expected, rr)
|
||||
|
||||
def test_resource_request_from_image_props_append_request(self):
|
||||
props = {'trait:CUSTOM_MAGIC': 'required'}
|
||||
image_meta_props = objects.ImageMetaProps.from_dict(props)
|
||||
def test_resource_request_init_is_bfv(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=1555)
|
||||
|
||||
existing_req = utils.ResourceRequest()
|
||||
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 = FakeResourceRequest()
|
||||
expected._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
'VCPU': 2,
|
||||
'MEMORY_MB': 2048,
|
||||
'VCPU': 1,
|
||||
'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(
|
||||
expected, utils.ResourceRequest.from_image_props(image_meta_props,
|
||||
req=existing_req))
|
||||
rs = objects.RequestSpec(flavor=flavor, is_bfv=True)
|
||||
rr = utils.ResourceRequest(rs)
|
||||
self.assertResourceRequestsEqual(expected, rr)
|
||||
|
||||
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()
|
||||
req.add_request_group(rg1)
|
||||
rg2 = objects.RequestGroup()
|
||||
@ -918,18 +916,22 @@ class TestUtils(test.NoDBTestCase):
|
||||
self.assertIs(rg2, req.get_request_group(2))
|
||||
|
||||
def test_resource_request_add_group_inserts_the_group_implicit_group(self):
|
||||
req = utils.ResourceRequest()
|
||||
# this implicitly creates the unnumbered group
|
||||
unnumbered_rg = req.get_request_group(None)
|
||||
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)
|
||||
|
||||
# this implicitly creates the specified group
|
||||
unnumbered_rg = req.get_request_group(42)
|
||||
|
||||
rg1 = objects.RequestGroup()
|
||||
req.add_request_group(rg1)
|
||||
rg2 = objects.RequestGroup()
|
||||
req.add_request_group(rg2)
|
||||
|
||||
self.assertIs(rg1, req.get_request_group(1))
|
||||
self.assertIs(rg2, req.get_request_group(2))
|
||||
self.assertIs(unnumbered_rg, req.get_request_group(None))
|
||||
self.assertIs(rg1, req.get_request_group(43))
|
||||
self.assertIs(rg2, req.get_request_group(44))
|
||||
self.assertIs(unnumbered_rg, req.get_request_group(42))
|
||||
|
||||
def test_claim_resources_on_destination_no_source_allocations(self):
|
||||
"""Tests the negative scenario where the instance does not have
|
||||
|
Loading…
x
Reference in New Issue
Block a user