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:
Stephen Finucane 2019-08-06 14:32:10 +01:00
parent 912a46c9d4
commit 7abe83f646
4 changed files with 291 additions and 333 deletions

View File

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

View File

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

View File

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

View File

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