Support same_subtree
queryparam
A new same_subtree query parameter will be accepted. The value is a comma-separated list of request group suffix strings $S. Each must exactly match a suffix on a granular group somewhere else in the request. Importantly, the identified request groups need not have a resources$S. If this is provided, at least one of the resource providers satisfying the specified request group must be an ancestor of the rest. The same_subtree query parameter can be repeated and each repeat group is treated independently. Co-Authored-By: Chris Dent <cdent@anticdent.org> Change-Id: I7fdeac24606359d37f1a7405d22c5797840e1a9e Story: 2005575 Task: 30784
This commit is contained in:
parent
7f72d1e30e
commit
8395e3f099
@ -43,6 +43,7 @@ Request
|
||||
- group_policy: allocation_candidates_group_policy
|
||||
- limit: allocation_candidates_limit
|
||||
- root_required: allocation_candidates_root_required
|
||||
- same_subtree: allocation_candidates_same_subtree
|
||||
|
||||
Response (microversions 1.12 - )
|
||||
--------------------------------
|
||||
|
@ -188,6 +188,19 @@ allocation_candidates_root_required:
|
||||
(non-sharing) tree's root provider satisfies the specified trait
|
||||
requirements. Traits which are forbidden (must **not** be present on the
|
||||
root provider) are expressed by prefixing the trait with a ``!``.
|
||||
allocation_candidates_same_subtree:
|
||||
type: string
|
||||
in: query
|
||||
required: false
|
||||
min_version: 1.36
|
||||
description: |
|
||||
A comma-separated list of request group suffix strings ($S). Each must
|
||||
exactly match a suffix on a granular group somewhere else in the request.
|
||||
Importantly, the identified request groups need not have a resources[$S].
|
||||
If this is provided, at least one of the resource providers satisfying the
|
||||
specified request group must be an ancestor of the rest.
|
||||
The ``same_subtree`` query parameter can be repeated and each repeat group
|
||||
is treated independently.
|
||||
project_id: &project_id
|
||||
type: string
|
||||
in: query
|
||||
|
@ -49,3 +49,4 @@ RESOURCE_PROVIDER_NOT_FOUND = 'placement.resource_provider.not_found'
|
||||
ILLEGAL_DUPLICATE_QUERYPARAM = 'placement.query.duplicate_key'
|
||||
# Failure of a post-schema value check
|
||||
QUERYPARAM_BAD_VALUE = 'placement.query.bad_value'
|
||||
QUERYPARAM_MISSING_VALUE = 'placement.query.missing_value'
|
||||
|
@ -251,7 +251,9 @@ def list_allocation_candidates(req):
|
||||
context.can(policies.LIST)
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
get_schema = schema.GET_SCHEMA_1_10
|
||||
if want_version.matches((1, 35)):
|
||||
if want_version.matches((1, 36)):
|
||||
get_schema = schema.GET_SCHEMA_1_36
|
||||
elif want_version.matches((1, 35)):
|
||||
get_schema = schema.GET_SCHEMA_1_35
|
||||
elif want_version.matches((1, 33)):
|
||||
get_schema = schema.GET_SCHEMA_1_33
|
||||
@ -267,8 +269,8 @@ def list_allocation_candidates(req):
|
||||
get_schema = schema.GET_SCHEMA_1_16
|
||||
util.validate_query_params(req, get_schema)
|
||||
|
||||
groups = lib.RequestGroup.dict_from_request(req)
|
||||
rqparams = lib.RequestWideParams.from_request(req)
|
||||
groups = lib.RequestGroup.dict_from_request(req, rqparams)
|
||||
|
||||
if not rqparams.group_policy:
|
||||
# group_policy is required if more than one numbered request group was
|
||||
|
@ -38,6 +38,11 @@ _QS_KEY_PATTERN_1_33 = re.compile(
|
||||
(_QS_RESOURCES, _QS_REQUIRED, _QS_MEMBER_OF, _QS_IN_TREE)),
|
||||
common.GROUP_PAT_1_33))
|
||||
|
||||
# In newer microversion we no longer check for orphaned member_of
|
||||
# and required because "providers providing no inventory to this
|
||||
# request" are now legit with `same_subtree` queryparam accompanied.
|
||||
SAME_SUBTREE_VERSION = (1, 36)
|
||||
|
||||
|
||||
def _fix_one_forbidden(traits):
|
||||
forbidden = [trait for trait in traits if trait.startswith('!')]
|
||||
@ -126,6 +131,37 @@ class RequestGroup(object):
|
||||
val)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def _check_for_one_resources(by_suffix, resourceless_suffixes):
|
||||
if len(resourceless_suffixes) == len(by_suffix):
|
||||
msg = ('There must be at least one resources or resources[$S] '
|
||||
'parameter.')
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
msg, comment=errors.QUERYPARAM_MISSING_VALUE)
|
||||
|
||||
@staticmethod
|
||||
def _check_resourceless_suffix(subtree_suffixes, resourceless_suffixes):
|
||||
bad_suffixes = [suffix for suffix in resourceless_suffixes
|
||||
if suffix not in subtree_suffixes]
|
||||
if bad_suffixes:
|
||||
msg = ("Resourceless suffixed group request should be specified "
|
||||
"in `same_subtree` query param: bad group(s) - "
|
||||
"%(suffixes)s.") % {'suffixes': bad_suffixes}
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
msg, comment=errors.QUERYPARAM_BAD_VALUE)
|
||||
|
||||
@staticmethod
|
||||
def _check_actual_suffix(subtree_suffixes, by_suffix):
|
||||
bad_suffixes = [suffix for suffix in subtree_suffixes
|
||||
if suffix not in by_suffix]
|
||||
if bad_suffixes:
|
||||
msg = ("Real suffixes should be specified in `same_subtree`: "
|
||||
"%(bad_suffixes)s not found in %(suffixes)s.") % {
|
||||
'bad_suffixes': bad_suffixes,
|
||||
'suffixes': list(by_suffix.keys())}
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
msg, comment=errors.QUERYPARAM_BAD_VALUE)
|
||||
|
||||
@staticmethod
|
||||
def _check_for_orphans(by_suffix):
|
||||
# Ensure any group with 'required' or 'member_of' also has 'resources'.
|
||||
@ -174,7 +210,7 @@ class RequestGroup(object):
|
||||
msg % ', '.join(conflicting_traits))
|
||||
|
||||
@classmethod
|
||||
def dict_from_request(cls, req):
|
||||
def dict_from_request(cls, req, rqparams):
|
||||
"""Parse suffixed resources, traits, and member_of groupings out of a
|
||||
querystring dict found in a webob Request.
|
||||
|
||||
@ -257,9 +293,11 @@ class RequestGroup(object):
|
||||
}
|
||||
|
||||
:param req: webob.Request object
|
||||
:param rqparams: RequestWideParams object
|
||||
:return: A dict, keyed by suffix, of RequestGroup instances.
|
||||
:raises `webob.exc.HTTPBadRequest` if any value is malformed, or if a
|
||||
trait list is given without corresponding resources.
|
||||
:raises `webob.exc.HTTPBadRequest` if any value is malformed, or if
|
||||
the suffix of a resourceless request is not in the
|
||||
`rqparams.same_subtrees`.
|
||||
"""
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
# Control whether we handle forbidden traits.
|
||||
@ -270,7 +308,17 @@ class RequestGroup(object):
|
||||
by_suffix = cls._parse_request_items(
|
||||
req, allow_forbidden, verbose_suffix)
|
||||
|
||||
cls._check_for_orphans(by_suffix)
|
||||
if want_version.matches(SAME_SUBTREE_VERSION):
|
||||
resourceless_suffixes = set(
|
||||
suffix for suffix, grp in by_suffix.items()
|
||||
if not grp.resources)
|
||||
subtree_suffixes = set().union(*rqparams.same_subtrees)
|
||||
cls._check_for_one_resources(by_suffix, resourceless_suffixes)
|
||||
cls._check_resourceless_suffix(
|
||||
subtree_suffixes, resourceless_suffixes)
|
||||
cls._check_actual_suffix(subtree_suffixes, by_suffix)
|
||||
else:
|
||||
cls._check_for_orphans(by_suffix)
|
||||
|
||||
# Make adjustments for forbidden traits by stripping forbidden out
|
||||
# of required.
|
||||
@ -287,7 +335,8 @@ class RequestWideParams(object):
|
||||
above).
|
||||
"""
|
||||
def __init__(self, limit=None, group_policy=None,
|
||||
anchor_required_traits=None, anchor_forbidden_traits=None):
|
||||
anchor_required_traits=None, anchor_forbidden_traits=None,
|
||||
same_subtrees=None):
|
||||
"""Create a RequestWideParams.
|
||||
|
||||
:param limit: An integer, N, representing the maximum number of
|
||||
@ -307,11 +356,18 @@ class RequestWideParams(object):
|
||||
:param anchor_forbidden_traits: Set of trait names which the anchor of
|
||||
each returned allocation candidate must NOT possess, regardless
|
||||
of any RequestGroup filters.
|
||||
:param same_subtrees: A list of sets of request group suffix strings
|
||||
where each set of strings represents the suffixes from one
|
||||
same_subtree query param. If provided, all of the resource
|
||||
providers satisfying the specified request groups must be
|
||||
rooted at one of the resource providers satisfying the request
|
||||
groups.
|
||||
"""
|
||||
self.limit = limit
|
||||
self.group_policy = group_policy
|
||||
self.anchor_required_traits = anchor_required_traits
|
||||
self.anchor_forbidden_traits = anchor_forbidden_traits
|
||||
self.same_subtrees = same_subtrees or []
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, req):
|
||||
@ -347,8 +403,22 @@ class RequestWideParams(object):
|
||||
'root_required: %s' % ', '.join(conflicts),
|
||||
comment=errors.QUERYPARAM_BAD_VALUE)
|
||||
|
||||
same_subtree = req.GET.getall('same_subtree')
|
||||
# Construct a list of sets of request group suffixes strings.
|
||||
same_subtrees = []
|
||||
if same_subtree:
|
||||
for val in same_subtree:
|
||||
suffixes = set(substr.strip() for substr in val.split(','))
|
||||
if '' in suffixes:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
'Empty string (unsuffixed group) can not be specified '
|
||||
'in `same_subtree` ',
|
||||
comment=errors.QUERYPARAM_BAD_VALUE)
|
||||
same_subtrees.append(suffixes)
|
||||
|
||||
return cls(
|
||||
limit=limit,
|
||||
group_policy=group_policy,
|
||||
anchor_required_traits=anchor_required_traits,
|
||||
anchor_forbidden_traits=anchor_forbidden_traits)
|
||||
anchor_forbidden_traits=anchor_forbidden_traits,
|
||||
same_subtrees=same_subtrees)
|
||||
|
@ -86,6 +86,8 @@ VERSIONS = [
|
||||
'1.34', # Include a mappings key in allocation requests that shows which
|
||||
# resource providers satisfied which request group suffix.
|
||||
'1.35', # Add a `root_required` queryparam on `GET /allocation_candidates`
|
||||
'1.36', # Add a `same_subtree` parameter on GET /allocation_candidates
|
||||
# and allow resourceless requests for groups in `same_subtree`.
|
||||
]
|
||||
|
||||
|
||||
|
@ -391,7 +391,7 @@ def _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples):
|
||||
for rp_id, root_id in rp_tuples:
|
||||
rp_summary = summaries[rp_id]
|
||||
req_obj = _allocation_request_for_provider(
|
||||
rg_ctx.context, rg_ctx.resources, rp_summary.resource_provider,
|
||||
rg_ctx.resources, rp_summary.resource_provider,
|
||||
suffix=rg_ctx.suffix)
|
||||
# Exclude this if its anchor (which is its root) isn't in our
|
||||
# prefiltered list of anchors
|
||||
@ -416,12 +416,10 @@ def _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples):
|
||||
return alloc_requests, list(summaries.values())
|
||||
|
||||
|
||||
def _allocation_request_for_provider(ctx, requested_resources, provider,
|
||||
suffix):
|
||||
def _allocation_request_for_provider(requested_resources, provider, suffix):
|
||||
"""Returns an AllocationRequest object containing AllocationRequestResource
|
||||
objects for each resource class in the supplied requested resources dict.
|
||||
|
||||
:param ctx: placement.context.RequestContext object
|
||||
:param requested_resources: dict, keyed by resource class ID, of amounts
|
||||
being requested for that resource class
|
||||
:param provider: ResourceProvider object representing the provider of the
|
||||
@ -440,6 +438,8 @@ def _allocation_request_for_provider(ctx, requested_resources, provider,
|
||||
# anchor in its own tree. If the provider is a sharing provider, the
|
||||
# caller needs to identify the other anchors with which it might be
|
||||
# associated.
|
||||
# NOTE(tetsuro): The AllocationRequest has empty resource_requests for a
|
||||
# resourceless request. Still, it has the rp uuid in the mappings field.
|
||||
mappings = {suffix: set([provider.uuid])}
|
||||
return AllocationRequest(
|
||||
resource_requests=resource_requests,
|
||||
@ -762,12 +762,16 @@ def _merge_candidates(candidates, rw_ctx):
|
||||
# ProviderSummaryResource. This will be used to do a final capacity
|
||||
# check/filter on each merged AllocationRequest.
|
||||
psum_res_by_rp_rc = {}
|
||||
# A dict of parent uuids keyed by rp uuids
|
||||
parent_uuid_by_rp_uuid = {}
|
||||
for suffix, (areqs, psums) in candidates.items():
|
||||
for areq in areqs:
|
||||
anchor = areq.anchor_root_provider_uuid
|
||||
areq_lists_by_anchor[anchor][suffix].append(areq)
|
||||
for psum in psums:
|
||||
all_psums.append(psum)
|
||||
parent_uuid_by_rp_uuid[psum.resource_provider.uuid] = (
|
||||
psum.resource_provider.parent_provider_uuid)
|
||||
for psum_res in psum.resources:
|
||||
key = _rp_rc_key(
|
||||
psum.resource_provider, psum_res.resource_class)
|
||||
@ -810,6 +814,9 @@ def _merge_candidates(candidates, rw_ctx):
|
||||
if not _satisfies_group_policy(
|
||||
areq_list, rw_ctx.group_policy, num_granular_groups):
|
||||
continue
|
||||
if not _satisfies_same_subtree(
|
||||
areq_list, rw_ctx.same_subtrees, parent_uuid_by_rp_uuid):
|
||||
continue
|
||||
# Now we go from this (where 'arr' is AllocationRequestResource):
|
||||
# [ areq__B(arrX, arrY, arrZ),
|
||||
# areq_1_A(arrM, arrN),
|
||||
@ -890,13 +897,12 @@ def _satisfies_group_policy(areqs, group_policy, num_granular_groups):
|
||||
# The number of unique resource providers referenced in the request groups
|
||||
# having use_same_provider=True must be equal to the number of granular
|
||||
# groups.
|
||||
num_granular_groups_in_areqs = len(set(
|
||||
# We can reliably use the first resource_request's provider: all the
|
||||
# resource_requests are satisfied by the same provider by definition
|
||||
# because use_same_provider is True.
|
||||
areq.resource_requests[0].resource_provider.uuid
|
||||
for areq in areqs
|
||||
if areq.use_same_provider))
|
||||
num_granular_groups_in_areqs = len(set().union(*(
|
||||
# We can reliably use the first value of provider uuids in mappings:
|
||||
# all the resource_requests are satisfied by the same provider
|
||||
# by definition because use_same_provider is True.
|
||||
list(areq.mappings.values())[0] for areq in areqs
|
||||
if areq.use_same_provider)))
|
||||
if num_granular_groups == num_granular_groups_in_areqs:
|
||||
return True
|
||||
LOG.debug('Excluding the following set of AllocationRequest because '
|
||||
@ -905,3 +911,57 @@ def _satisfies_group_policy(areqs, group_policy, num_granular_groups):
|
||||
'request (%d): %s',
|
||||
num_granular_groups_in_areqs, num_granular_groups, str(areqs))
|
||||
return False
|
||||
|
||||
|
||||
def _satisfies_same_subtree(
|
||||
areqs, same_subtrees, parent_uuid_by_rp_uuid):
|
||||
"""Applies same_subtree policy to a list of AllocationRequest.
|
||||
|
||||
:param areqs: A list containing one AllocationRequest for each input
|
||||
RequestGroup.
|
||||
:param same_subtrees: A list of sets of request group suffixes strings.
|
||||
If provided, all of the resource providers satisfying the specified
|
||||
request groups must be rooted at one of the resource providers
|
||||
satisfying the request groups.
|
||||
:param parent_uuid_by_rp_uuid: A dict of parent uuids keyed by rp uuids.
|
||||
:return: True if areqs satisfies same_subtree policy; False otherwise.
|
||||
"""
|
||||
for same_subtree in same_subtrees:
|
||||
# Collect RP uuids that must satisfy a single same_subtree constraint.
|
||||
rp_uuids = set().union(*(areq.mappings.get(suffix) for areq in areqs
|
||||
for suffix in same_subtree
|
||||
if areq.mappings.get(suffix)))
|
||||
if not _check_same_subtree(rp_uuids, parent_uuid_by_rp_uuid):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _check_same_subtree(rp_uuids, parent_uuid_by_rp_uuid):
|
||||
"""Returns True if given rp uuids are all in the same subtree.
|
||||
|
||||
Note: The rps are in the same subtree means all the providers are
|
||||
rooted at one of the providers
|
||||
"""
|
||||
if len(rp_uuids) == 1:
|
||||
return True
|
||||
# A set of uuids of common ancestors of each rp in question
|
||||
common_ancestors = set.intersection(*(
|
||||
_get_ancestors_by_one_uuid(rp_uuid, parent_uuid_by_rp_uuid)
|
||||
for rp_uuid in rp_uuids))
|
||||
# if any of the rp_uuid is in the common_ancestors set, then
|
||||
# we know that, that rp_uuid is the root of the other rp_uuids
|
||||
# in this same_subtree constraint.
|
||||
return len(common_ancestors.intersection(rp_uuids)) != 0
|
||||
|
||||
|
||||
def _get_ancestors_by_one_uuid(
|
||||
rp_uuid, parent_uuid_by_rp_uuid, ancestors=None):
|
||||
"""Returns a set of uuids of ancestors for a given rp uuid"""
|
||||
if ancestors is None:
|
||||
ancestors = set([rp_uuid])
|
||||
parent_uuid = parent_uuid_by_rp_uuid[rp_uuid]
|
||||
if parent_uuid is None:
|
||||
return ancestors
|
||||
ancestors.add(parent_uuid)
|
||||
return _get_ancestors_by_one_uuid(
|
||||
parent_uuid, parent_uuid_by_rp_uuid, ancestors=ancestors)
|
||||
|
@ -186,6 +186,7 @@ class RequestWideSearchContext(object):
|
||||
# IDs of root providers that conform to the requested filters.
|
||||
self.anchor_root_ids = None
|
||||
self._process_anchor_traits(rqparams)
|
||||
self.same_subtrees = rqparams.same_subtrees
|
||||
|
||||
def _process_anchor_traits(self, rqparams):
|
||||
"""Set or filter self.anchor_root_ids according to anchor
|
||||
@ -463,6 +464,33 @@ def get_providers_with_resource(ctx, rc_id, amount, tree_root_id=None):
|
||||
return res
|
||||
|
||||
|
||||
@db_api.placement_context_manager.reader
|
||||
def get_providers_with_root(ctx, allowed, forbidden):
|
||||
"""Returns a set of tuples of (provider ID, root provider ID) of given
|
||||
resource providers
|
||||
|
||||
:param ctx: Session context to use
|
||||
:param allowed: resource provider ids to include
|
||||
:param forbidden: resource provider ids to exclude
|
||||
"""
|
||||
# SELECT rp.id, rp.root_provider_id
|
||||
# FROM resource_providers AS rp
|
||||
# WHERE rp.id IN ($allowed)
|
||||
# AND rp.id NOT IN ($forbidden)
|
||||
sel = sa.select([_RP_TBL.c.id, _RP_TBL.c.root_provider_id])
|
||||
sel = sel.select_from(_RP_TBL)
|
||||
cond = []
|
||||
if allowed:
|
||||
cond.append(_RP_TBL.c.id.in_(allowed))
|
||||
if forbidden:
|
||||
cond.append(~_RP_TBL.c.id.in_(forbidden))
|
||||
if cond:
|
||||
sel = sel.where(sa.and_(*cond))
|
||||
res = ctx.session.execute(sel).fetchall()
|
||||
res = set((r[0], r[1]) for r in res)
|
||||
return res
|
||||
|
||||
|
||||
@db_api.placement_context_manager.reader
|
||||
def get_provider_ids_matching(rg_ctx):
|
||||
"""Returns a list of tuples of (internal provider ID, root provider ID)
|
||||
@ -537,6 +565,13 @@ def get_provider_ids_matching(rg_ctx):
|
||||
if not filtered_rps:
|
||||
return []
|
||||
|
||||
if not rg_ctx.resources:
|
||||
# NOTE(tetsuro): This does an extra sql query that could be avoided if
|
||||
# all the smaller queries in get_provider_ids_for_traits_and_aggs()
|
||||
# would return the internal ID and the root ID as well for each RP.
|
||||
provs_with_resource = get_providers_with_root(
|
||||
rg_ctx.context, filtered_rps, forbidden_rp_ids)
|
||||
|
||||
# provs_with_resource will contain a superset of providers with IDs still
|
||||
# in our filtered_rps set. We return the list of tuples of
|
||||
# (internal provider ID, root internal provider ID)
|
||||
|
@ -651,3 +651,17 @@ format as the ``required`` query parameter. This restricts allocation requests
|
||||
in the response to only those whose (non-sharing) tree's root resource provider
|
||||
satisfies the specified trait requirements. See
|
||||
:ref:`filtering by root provider traits` for details.
|
||||
|
||||
1.36 - Support 'same_subtree' queryparam on GET /allocation_candidates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: Train
|
||||
|
||||
Add support for the ``same_subtree`` query parameter to the ``GET
|
||||
/allocation_candidates`` API. It accepts a comma-separated list of request
|
||||
group suffix strings $S. Each must exactly match a suffix on a granular group
|
||||
somewhere else in the request. Importantly, the identified request groups need
|
||||
not have a resources$S. If this is provided, at least one of the resource
|
||||
providers satisfying the specified request group must be an ancestor of the
|
||||
rest. The ``same_subtree`` query parameter can be repeated and each repeat
|
||||
group is treated independently.
|
||||
|
@ -96,3 +96,9 @@ GET_SCHEMA_1_35 = copy.deepcopy(GET_SCHEMA_1_33)
|
||||
GET_SCHEMA_1_35["properties"]['root_required'] = {
|
||||
"type": ["string"]
|
||||
}
|
||||
|
||||
# Microversion 1.36 supports same_subtree.
|
||||
GET_SCHEMA_1_36 = copy.deepcopy(GET_SCHEMA_1_35)
|
||||
GET_SCHEMA_1_36["properties"]['same_subtree'] = {
|
||||
"type": ["string"]
|
||||
}
|
||||
|
@ -526,7 +526,7 @@ class NUMANetworkFixture(APIFixture):
|
||||
# TODO(efried): Use standard HW_NIC_ROOT trait
|
||||
tb.set_traits(nic, 'CUSTOM_HW_NIC_ROOT')
|
||||
nics.append(nic)
|
||||
os.environ['NIC%d_UUID'] = nic.uuid
|
||||
os.environ['NIC%s_UUID' % i] = nic.uuid
|
||||
# PFs for NIC1
|
||||
for i in (1, 2):
|
||||
suf = '1_%d' % i
|
||||
|
313
placement/tests/functional/gabbits/granular-same-subtree.yaml
Normal file
313
placement/tests/functional/gabbits/granular-same-subtree.yaml
Normal file
@ -0,0 +1,313 @@
|
||||
# Tests of /allocation_candidates API with same_subtree.
|
||||
|
||||
fixtures:
|
||||
- NUMANetworkFixture
|
||||
|
||||
defaults:
|
||||
request_headers:
|
||||
x-auth-token: admin
|
||||
content-type: application/json
|
||||
accept: application/json
|
||||
# version of request in which `same_subtree` is supported
|
||||
openstack-api-version: placement 1.36
|
||||
|
||||
tests:
|
||||
|
||||
- name: resourceless traits without same_subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources1: VCPU:1
|
||||
required2: COMPUTE_VOLUME_MULTI_ATTACH
|
||||
group_policy: none
|
||||
status: 400
|
||||
response_strings:
|
||||
- "Resourceless suffixed group request should be specified in `same_subtree` query param"
|
||||
response_json_paths:
|
||||
$.errors[0].title: Bad Request
|
||||
$.errors[0].code: placement.query.bad_value
|
||||
|
||||
- name: resourceless aggs without same_subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources1: VCPU:1
|
||||
member_of2: $ENVIRON['AGGA_UUID']
|
||||
group_policy: none
|
||||
status: 400
|
||||
response_strings:
|
||||
- "Resourceless suffixed group request should be specified in `same_subtree` query param"
|
||||
response_json_paths:
|
||||
$.errors[0].title: Bad Request
|
||||
$.errors[0].code: placement.query.bad_value
|
||||
|
||||
- name: resourceless without any resource
|
||||
GET: /allocation_candidates?&member_of1=$ENVIRON['AGGA_UUID']&group_policy=none
|
||||
query_parameters:
|
||||
member_of1: $ENVIRON['AGGA_UUID']
|
||||
group_policy: none
|
||||
status: 400
|
||||
response_strings:
|
||||
- 'There must be at least one resources or resources[$S] parameter.'
|
||||
response_json_paths:
|
||||
$.errors[0].title: Bad Request
|
||||
$.errors[0].code: placement.query.missing_value
|
||||
|
||||
- name: invalid same subtree missing underscores
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE: VCPU:1
|
||||
resources_ACCEL: CUSTOM_FPGA:1
|
||||
same_subtree: COMPUTE,_ACCEL
|
||||
group_policy: none
|
||||
status: 400
|
||||
response_strings:
|
||||
- "Real suffixes should be specified in `same_subtree`:"
|
||||
response_json_paths:
|
||||
$.errors[0].title: Bad Request
|
||||
$.errors[0].code: placement.query.bad_value
|
||||
|
||||
- name: invalid same subtree with empty suffix
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE: VCPU:1
|
||||
resources_ACCEL: CUSTOM_FPGA:1
|
||||
same_subtree: _COMPUTE,,_ACCEL
|
||||
group_policy: none
|
||||
status: 400
|
||||
response_strings:
|
||||
- 'Empty string (unsuffixed group) can not be specified in `same_subtree`'
|
||||
response_json_paths:
|
||||
$.errors[0].title: Bad Request
|
||||
$.errors[0].code: placement.query.bad_value
|
||||
|
||||
- name: no resourceless without same subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE: VCPU:1
|
||||
resources_ACCEL: CUSTOM_FPGA:1
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 6
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA0_UUID"]'].resources.VCPU: [1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.VCPU: [1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA0_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_0_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_1_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
|
||||
- name: no resourceless with single same subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE: VCPU:1
|
||||
resources_ACCEL: CUSTOM_FPGA:1
|
||||
same_subtree: _COMPUTE
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 6
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA0_UUID"]'].resources.VCPU: [1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.VCPU: [1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA0_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_0_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_1_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
|
||||
- name: no resourceless with same subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE: VCPU:1
|
||||
resources_ACCEL: CUSTOM_FPGA:1
|
||||
same_subtree: _COMPUTE,_ACCEL
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 3
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA0_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.VCPU: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA0_UUID"]'].resources.CUSTOM_FPGA: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_0_UUID"]'].resources.CUSTOM_FPGA: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_1_UUID"]'].resources.CUSTOM_FPGA: 1
|
||||
|
||||
- name: no resourceless with same subtree same provider
|
||||
# Ensure that "myself" is in the same subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE1: VCPU:1
|
||||
resources_COMPUTE2: MEMORY_MB:1024
|
||||
same_subtree: _COMPUTE1,_COMPUTE2
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 3
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA0_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA0_UUID"]'].resources.MEMORY_MB: 1024
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.MEMORY_MB: 1024
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.MEMORY_MB: 1024
|
||||
|
||||
- name: no resourceless with same subtree same provider isolate
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE1: VCPU:1
|
||||
resources_COMPUTE2: MEMORY_MB:1024
|
||||
same_subtree: _COMPUTE1,_COMPUTE2
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 0
|
||||
|
||||
- name: resourceful without same subtree
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources: VCPU:1
|
||||
resources_PORT1: CUSTOM_VF:4
|
||||
required_PORT1: CUSTOM_PHYSNET1
|
||||
resources_PORT2: CUSTOM_VF:4
|
||||
required_PORT2: CUSTOM_PHYSNET2
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.VCPU: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_1_UUID"]'].resources.CUSTOM_VF: 4
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_2_UUID"]'].resources.CUSTOM_VF: [4, 4]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF3_1_UUID"]'].resources.CUSTOM_VF: 4
|
||||
|
||||
- name: resourceless with same subtree 4VFs
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources: VCPU:1
|
||||
required_NIC: CUSTOM_HW_NIC_ROOT
|
||||
resources_PORT1: CUSTOM_VF:4
|
||||
required_PORT1: CUSTOM_PHYSNET1
|
||||
resources_PORT2: CUSTOM_VF:4
|
||||
required_PORT2: CUSTOM_PHYSNET2
|
||||
same_subtree: _NIC,_PORT1,_PORT2
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 1
|
||||
$.allocation_requests..allocations.`len`: 3
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_1_UUID"]'].resources.CUSTOM_VF: 4
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_2_UUID"]'].resources.CUSTOM_VF: 4
|
||||
$.allocation_requests..mappings.`len`: 4
|
||||
$.allocation_requests..mappings[''][0]: $ENVIRON["CN2_UUID"]
|
||||
$.allocation_requests..mappings['_NIC'][0]: $ENVIRON["NIC1_UUID"]
|
||||
$.allocation_requests..mappings['_PORT1'][0]: $ENVIRON["PF1_1_UUID"]
|
||||
$.allocation_requests..mappings['_PORT2'][0]: $ENVIRON["PF1_2_UUID"]
|
||||
|
||||
- name: resourceless with same subtree 2VFs
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources: VCPU:1
|
||||
required_NIC: CUSTOM_HW_NIC_ROOT
|
||||
resources_PORT1: CUSTOM_VF:2
|
||||
required_PORT1: CUSTOM_PHYSNET1
|
||||
resources_PORT2: CUSTOM_VF:2
|
||||
required_PORT2: CUSTOM_PHYSNET2
|
||||
same_subtree: _NIC,_PORT1,_PORT2
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 5
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.VCPU: [1, 1, 1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_1_UUID"]'].resources.CUSTOM_VF: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_2_UUID"]'].resources.CUSTOM_VF: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_1_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_2_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_3_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_4_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
|
||||
- name: resourceless with same subtree 2VFs isolate
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources: VCPU:1
|
||||
required_NIC: CUSTOM_HW_NIC_ROOT
|
||||
resources_PORT1: CUSTOM_VF:2
|
||||
required_PORT1: CUSTOM_PHYSNET1
|
||||
resources_PORT2: CUSTOM_VF:2
|
||||
required_PORT2: CUSTOM_PHYSNET2
|
||||
same_subtree: _NIC,_PORT1,_PORT2
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 5
|
||||
$.allocation_requests..allocations['$ENVIRON["CN2_UUID"]'].resources.VCPU: [1, 1, 1, 1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_1_UUID"]'].resources.CUSTOM_VF: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["PF1_2_UUID"]'].resources.CUSTOM_VF: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_1_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_2_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_3_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
$.allocation_requests..allocations['$ENVIRON["PF2_4_UUID"]'].resources.CUSTOM_VF: [2, 2]
|
||||
|
||||
- name: resourceless with same subtree same provider
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_PORT1: CUSTOM_VF:8
|
||||
required_PORT2: CUSTOM_PHYSNET1
|
||||
same_subtree: _PORT1,_PORT2
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 1
|
||||
$.allocation_requests..allocations.`len`: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["PF3_1_UUID"]'].resources.CUSTOM_VF: 8
|
||||
$.allocation_requests..mappings.`len`: 2
|
||||
$.allocation_requests..mappings['_PORT1'][0]: $ENVIRON["PF3_1_UUID"]
|
||||
$.allocation_requests..mappings['_PORT2'][0]: $ENVIRON["PF3_1_UUID"]
|
||||
|
||||
- name: resourceless with same subtree same provider isolate
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_PORT1: CUSTOM_VF:8
|
||||
required_PORT2: CUSTOM_PHYSNET1
|
||||
same_subtree: _PORT1,_PORT2
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 0
|
||||
|
||||
- name: multiple resourceless with same subtree same provider
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE1: VCPU:1
|
||||
required_COMPUTE2: CUSTOM_FOO
|
||||
required_COMPUTE3: HW_NUMA_ROOT
|
||||
same_subtree: _COMPUTE1,_COMPUTE2,_COMPUTE3
|
||||
group_policy: none
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 1
|
||||
$.allocation_requests..allocations.`len`: 1
|
||||
$.allocation_requests..allocations['$ENVIRON["NUMA1_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests..mappings.`len`: 3
|
||||
$.allocation_requests..mappings['_COMPUTE1'][0]: $ENVIRON["NUMA1_UUID"]
|
||||
$.allocation_requests..mappings['_COMPUTE2'][0]: $ENVIRON["NUMA1_UUID"]
|
||||
$.allocation_requests..mappings['_COMPUTE3'][0]: $ENVIRON["NUMA1_UUID"]
|
||||
|
||||
- name: multiple resourceless with same subtree same provider isolate
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
resources_COMPUTE1: VCPU:1
|
||||
required_COMPUTE2: CUSTOM_FOO
|
||||
required_COMPUTE3: HW_NUMA_ROOT
|
||||
same_subtree: _COMPUTE1,_COMPUTE2,_COMPUTE3
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 0
|
||||
|
||||
- name: resourceless with same subtree 2FPGAs
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
required_NUMA: HW_NUMA_ROOT
|
||||
resources_ACCEL1: CUSTOM_FPGA:1
|
||||
resources_ACCEL2: CUSTOM_FPGA:1
|
||||
same_subtree: _NUMA,_ACCEL1,_ACCEL2
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 2
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_0_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..allocations['$ENVIRON["FPGA1_1_UUID"]'].resources.CUSTOM_FPGA: [1, 1]
|
||||
$.allocation_requests..mappings.`len`: [3, 3]
|
||||
$.allocation_requests..mappings['_NUMA'][0]: /(?:$ENVIRON['NUMA1_UUID']|$ENVIRON['NUMA1_UUID'])/
|
||||
$.allocation_requests..mappings['_ACCEL1'][0]: /(?:$ENVIRON['FPGA1_0_UUID']|$ENVIRON['FPGA1_1_UUID'])/
|
||||
$.allocation_requests..mappings['_ACCEL2'][0]: /(?:$ENVIRON['FPGA1_0_UUID']|$ENVIRON['FPGA1_1_UUID'])/
|
||||
|
||||
- name: resourceless with same subtree 2FPGAs forbidden
|
||||
GET: /allocation_candidates
|
||||
query_parameters:
|
||||
required_NUMA: HW_NUMA_ROOT,!CUSTOM_FOO
|
||||
resources_ACCEL1: CUSTOM_FPGA:1
|
||||
resources_ACCEL2: CUSTOM_FPGA:1
|
||||
same_subtree: _NUMA,_ACCEL1,_ACCEL2
|
||||
group_policy: isolate
|
||||
response_json_paths:
|
||||
$.allocation_requests.`len`: 0
|
@ -41,13 +41,13 @@ tests:
|
||||
response_json_paths:
|
||||
$.errors[0].title: Not Acceptable
|
||||
|
||||
- name: latest microversion is 1.35
|
||||
- name: latest microversion is 1.36
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement latest
|
||||
response_headers:
|
||||
vary: /openstack-api-version/
|
||||
openstack-api-version: placement 1.35
|
||||
openstack-api-version: placement 1.36
|
||||
|
||||
- name: other accept header bad version
|
||||
GET: /
|
||||
|
@ -13,6 +13,7 @@
|
||||
import mock
|
||||
|
||||
from placement import lib as placement_lib
|
||||
from placement.objects import allocation_candidate as ac_obj
|
||||
from placement.objects import research_context as res_ctx
|
||||
from placement.tests.unit.objects import base
|
||||
|
||||
@ -55,3 +56,44 @@ class TestAllocationCandidatesNoDB(base.TestCase):
|
||||
aro, sum = rw_ctx.limit_results(aro_in, sum_in)
|
||||
self.assertEqual(aro_in[:2], aro)
|
||||
self.assertEqual(set([sum1, sum0, sum4, sum8, sum5]), set(sum))
|
||||
|
||||
def test_check_same_subtree(self):
|
||||
# Construct a tree that look like this
|
||||
#
|
||||
# 0 -+- 00 --- 000 1 -+- 10 --- 100
|
||||
# | |
|
||||
# +- 01 -+- 010 +- 11 -+- 110
|
||||
# | +- 011 | +- 111
|
||||
# +- 02 -+- 020 +- 12 -+- 120
|
||||
# +- 021 +- 121
|
||||
#
|
||||
parent_by_rp = {"0": None, "00": "0", "000": "00",
|
||||
"01": "0", "010": "01", "011": "01",
|
||||
"02": "0", "020": "02", "021": "02",
|
||||
"1": None, "10": "1", "100": "10",
|
||||
"11": "1", "110": "11", "111": "11",
|
||||
"12": "1", "120": "12", "121": "12"}
|
||||
same_subtree = [
|
||||
set(["0", "00", "01"]),
|
||||
set(["01", "010"]),
|
||||
set(["02", "020", "021"]),
|
||||
set(["02", "020", "021"]),
|
||||
set(["0", "02", "010"]),
|
||||
set(["000"])
|
||||
]
|
||||
|
||||
different_subtree = [
|
||||
set(["10", "11"]),
|
||||
set(["110", "111"]),
|
||||
set(["10", "11", "110"]),
|
||||
set(["12", "120", "100"]),
|
||||
set(["0", "1"]),
|
||||
]
|
||||
|
||||
for group in same_subtree:
|
||||
self.assertTrue(
|
||||
ac_obj._check_same_subtree(group, parent_by_rp))
|
||||
|
||||
for group in different_subtree:
|
||||
self.assertFalse(
|
||||
ac_obj._check_same_subtree(group, parent_by_rp))
|
||||
|
@ -446,7 +446,8 @@ class TestParseQsRequestGroups(testtools.TestCase):
|
||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||
microversion.min_version_string())
|
||||
req.environ['placement.microversion'] = mv_parsed
|
||||
d = pl.RequestGroup.dict_from_request(req)
|
||||
rqparam = pl.RequestWideParams.from_request(req)
|
||||
d = pl.RequestGroup.dict_from_request(req, rqparam)
|
||||
# Sort for easier testing
|
||||
return [d[suff] for suff in sorted(d)]
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
From microversion ``1.36``, a new ``same_subtree`` queryparam on
|
||||
``GET /allocation_candidates`` is supported. It accepts a comma-separated
|
||||
list of request group suffix strings ($S). Each must exactly match a suffix
|
||||
on a granular group somewhere else in the request. Importantly, the
|
||||
identified request groups need not have a resources$S. If this is provided,
|
||||
at least one of the resource providers satisfying the specified request
|
||||
group must be an ancestor of the rest. The ``same_subtree`` query parameter
|
||||
can be repeated and each repeated group is treated independently.
|
Loading…
Reference in New Issue
Block a user