Microversion 1.35: root_required

Microversion 1.35_ adds support for the ``root_required`` query
parameter to the ``GET /allocation_candidates`` API. It accepts a
comma-delimited list of trait names, each optionally prefixed with ``!``
to indicate a forbidden trait, in the same 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.

This is to support use cases like, "Land my VM on a host that is capable
of multi-attach," or, "Reserve my Windows-licensed hosts for special
use."

Story: #2005575
Task: #33753
Change-Id: I76cad83248920fa71da122711f1f763c4ebdb1ba
This commit is contained in:
Eric Fried 2019-06-17 10:24:16 -05:00
parent 5a6884ae12
commit b733786a0a
16 changed files with 573 additions and 32 deletions

View File

@ -42,6 +42,7 @@ Request
- in_treeN: allocation_candidates_in_tree_granular
- group_policy: allocation_candidates_group_policy
- limit: allocation_candidates_limit
- root_required: allocation_candidates_root_required
Response (microversions 1.12 - )
--------------------------------

View File

@ -173,6 +173,21 @@ allocation_candidates_member_of_granular:
It is an error to specify a ``member_ofN`` parameter without a
corresponding ``resourcesN`` parameter with the same suffix.
min_version: 1.25
allocation_candidates_root_required:
type: string
in: query
required: false
min_version: 1.35
description: |
A comma-separated list of trait requirements that the root provider of the
(non-sharing) tree must satisfy::
root_required=COMPUTE_SUPPORTS_MULTI_ATTACH,!CUSTOM_WINDOWS_LICENSED
Allocation requests in the response will be limited to those whose
(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 ``!``.
project_id: &project_id
type: string
in: query

View File

@ -384,8 +384,8 @@ resource of ``VCPU``, and not applied to the suffixed resource, ``DISK_GB``.
When you want to have ``VCPU`` from wherever and ``DISK_GB`` from ``SS1``,
the request may look like::
GET: /allocation_candidates?resources=VCPU:1
&resources1=DISK_GB:10&in_tree1=<SS1 uuid>
GET /allocation_candidates?resources=VCPU:1
&resources1=DISK_GB:10&in_tree1=<SS1 uuid>
which will stick to the first sharing provider for ``DISK_GB``.
@ -397,15 +397,101 @@ which will stick to the first sharing provider for ``DISK_GB``.
When you want to have ``VCPU`` from ``CN1`` and ``DISK_GB`` from ``SS1``,
the request may look like::
GET: /allocation_candidates?resources1=VCPU:1&in_tree1=<CN1 uuid>
&resources2=DISK_GB:10&in_tree2=<SS1 uuid>
&group_policy=isolate
GET /allocation_candidates?resources1=VCPU:1&in_tree1=<CN1 uuid>
&resources2=DISK_GB:10&in_tree2=<SS1 uuid>
&group_policy=isolate
which will return only 2 candidates.
1. ``NUMA1_1`` (``VCPU``) + ``SS1`` (``DISK_GB``)
2. ``NUMA1_2`` (``VCPU``) + ``SS1`` (``DISK_GB``)
.. _`filtering by root provider traits`:
Filtering by Root Provider Traits
=================================
When traits are associated with a particular resource, the provider tree should
be constructed such that the traits are associated with the provider possessing
the inventory of that resource. For example, trait ``HW_CPU_X86_AVX2`` is a
trait associated with the ``VCPU`` resource, so it should be placed on the
resource provider with ``VCPU`` inventory, wherever that provider is positioned
in the tree structure. (A NUMA-aware host may model ``VCPU`` inventory in a
child provider, whereas a non-NUMA-aware host may model it in the root
provider.)
On the other hand, some traits are associated not with a resource, but with the
provider itself. For example, a compute host may be capable of
``COMPUTE_VOLUME_MULTI_ATTACH``, or be associated with a
``CUSTOM_WINDOWS_LICENSE_POOL``. In this case it is recommended that the root
resource provider be used to represent the concept of the "compute host"; so
these kinds of traits should always be placed on the root resource provider.
The following environment illustrates the above concepts::
+---------------------------------+ +-------------------------------------------+
|+-------------------------------+| | +-------------------------------+ |
|| Compute Node (NON_NUMA_CN) || | | Compute Node (NUMA_CN) | |
|| VCPU: 8, || | | DISK_GB: 1000 | |
|| MEMORY_MB: 1024 || | | traits: | |
|| DISK_GB: 1000 || | | STORAGE_DISK_SSD, | |
|| traits: || | | COMPUTE_VOLUME_MULTI_ATTACH | |
|| HW_CPU_X86_AVX2, || | +-------+-------------+---------+ |
|| STORAGE_DISK_SSD, || | nested | | nested |
|| COMPUTE_VOLUME_MULTI_ATTACH, || |+-----------+-------+ +---+---------------+|
|| CUSTOM_WINDOWS_LICENSE_POOL || || NUMA1 | | NUMA2 ||
|+-------------------------------+| || VCPU: 4 | | VCPU: 4 ||
+---------------------------------+ || MEMORY_MB: 1024 | | MEMORY_MB: 1024 ||
|| | | traits: ||
|| | | HW_CPU_X86_AVX2 ||
|+-------------------+ +-------------------+|
+-------------------------------------------+
A tree modeled in this fashion can take advantage of the `root_required`_
query parameter to return only allocation candidates from trees which possess
(or do not possess) specific traits on their root provider. For example,
to return allocation candidates including ``VCPU`` with the ``HW_CPU_X86_AVX2``
instruction set from hosts capable of ``COMPUTE_VOLUME_MULTI_ATTACH``, a
request may look like::
GET /allocation_candidates
?resources1=VCPU:1,MEMORY_MB:512&required1=HW_CPU_X86_AVX2
&resources2=DISK_GB:100
&group_policy=none
&root_required=COMPUTE_VOLUME_MULTI_ATTACH
This will return results from both ``NUMA_CN`` and ``NON_NUMA_CN`` because
both have the ``COMPUTE_VOLUME_MULTI_ATTACH`` trait on the root provider; but
only ``NUMA2`` has ``HW_CPU_X86_AVX2`` so there will only be one result from
``NUMA_CN``.
1. ``NON_NUMA_CN`` (``VCPU``, ``MEMORY_MB``, ``DISK_GB``)
2. ``NUMA_CN`` (``DISK_GB``) + ``NUMA2`` (``VCPU``, ``MEMORY_MB``)
To restrict allocation candidates to only those not in your
``CUSTOM_WINDOWS_LICENSE_POOL``, a request may look like::
GET /allocation_candidates
?resources1=VCPU:1,MEMORY_MB:512
&resources2=DISK_GB:100
&group_policy=none
&root_required=!CUSTOM_WINDOWS_LICENSE_POOL
This will return results only from ``NUMA_CN`` because ``NON_NUMA_CN`` has the
forbidden ``CUSTOM_WINDOWS_LICENSE_POOL`` on the root provider.
1. ``NUMA_CN`` (``DISK_GB``) + ``NUMA1`` (``VCPU``, ``MEMORY_MB``)
2. ``NUMA_CN`` (``DISK_GB``) + ``NUMA2`` (``VCPU``, ``MEMORY_MB``)
The syntax of the ``root_required`` query parameter is identical to that of
``required[$S]``: multiple trait strings may be specified, separated by commas,
each optionally prefixed with ``!`` to indicate that it is forbidden.
.. note:: ``root_required`` may not be suffixed, and may be specified only
once, as it applies only to the root provider.
.. note:: When sharing providers are involved in the request, ``root_required``
applies only to the root of the non-sharing provider tree.
.. _`Nested Resource Providers`: https://specs.openstack.org/openstack/nova-specs/specs/queens/approved/nested-resource-providers.html
.. _`POST /resource_providers`: https://developer.openstack.org/api-ref/placement/
@ -418,3 +504,4 @@ which will return only 2 candidates.
.. _`Granular Resource Request`: https://specs.openstack.org/openstack/nova-specs/specs/rocky/implemented/granular-resource-requests.html
.. _`Filter Allocation Candidates by Provider Tree`: https://specs.openstack.org/openstack/nova-specs/specs/stein/implemented/alloc-candidates-in-tree.html
.. _`Support subtree filter`: https://review.opendev.org/#/c/595236/
.. _`root_required`: https://review.opendev.org/#/c/662191/5/doc/source/specs/train/approved/2005575-nested-magic-1.rst@304

View File

@ -46,3 +46,6 @@ PROVIDER_IN_USE = 'placement.resource_provider.inuse'
PROVIDER_CANNOT_DELETE_PARENT = (
'placement.resource_provider.cannot_delete_parent')
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'

View File

@ -256,7 +256,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, 33)):
if 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
elif want_version.matches((1, 31)):
get_schema = schema.GET_SCHEMA_1_31

View File

@ -18,6 +18,7 @@ import re
import webob
from placement import errors
from placement import microversion
from placement.schemas import common
from placement import util
@ -38,6 +39,14 @@ _QS_KEY_PATTERN_1_33 = re.compile(
common.GROUP_PAT_1_33))
def _fix_one_forbidden(traits):
forbidden = [trait for trait in traits if trait.startswith('!')]
required = traits - set(forbidden)
forbidden = set(trait.lstrip('!') for trait in forbidden)
conflicts = forbidden & required
return required, forbidden, conflicts
class RequestGroup(object):
def __init__(self, use_same_provider=True, resources=None,
required_traits=None, forbidden_traits=None, member_of=None,
@ -151,12 +160,8 @@ class RequestGroup(object):
def _fix_forbidden(by_suffix):
conflicting_traits = []
for suff, group in by_suffix.items():
forbidden = [trait for trait in group.required_traits
if trait.startswith('!')]
group.required_traits = group.required_traits - set(forbidden)
group.forbidden_traits = set([trait.lstrip('!') for trait in
forbidden])
conflicts = group.forbidden_traits & group.required_traits
group.required_traits, group.forbidden_traits, conflicts = (
_fix_one_forbidden(group.required_traits))
if conflicts:
conflicting_traits.append('required%s: (%s)'
% (suff, ', '.join(conflicts)))
@ -164,6 +169,7 @@ class RequestGroup(object):
msg = (
'Conflicting required and forbidden traits found in the '
'following traits keys: %s')
# TODO(efried): comment=errors.QUERYPARAM_BAD_VALUE
raise webob.exc.HTTPBadRequest(
msg % ', '.join(conflicting_traits))
@ -280,7 +286,8 @@ class RequestWideParams(object):
This is in contrast with individual request groups (list of RequestGroup
above).
"""
def __init__(self, limit=None, group_policy=None):
def __init__(self, limit=None, group_policy=None,
anchor_required_traits=None, anchor_forbidden_traits=None):
"""Create a RequestWideParams.
:param limit: An integer, N, representing the maximum number of
@ -294,23 +301,54 @@ class RequestWideParams(object):
use_same_provider=True should interact with each other. If the
value is "isolate", we will filter out allocation requests
where any such RequestGroups are satisfied by the same RP.
:param anchor_required_traits: Set of trait names which the anchor of
each returned allocation candidate must possess, regardless of
any RequestGroup filters.
:param anchor_forbidden_traits: Set of trait names which the anchor of
each returned allocation candidate must NOT possess, regardless
of any RequestGroup filters.
"""
self.limit = limit
self.group_policy = group_policy
self.anchor_required_traits = anchor_required_traits
self.anchor_forbidden_traits = anchor_forbidden_traits
@classmethod
def from_request(cls, req):
# TODO(efried): Make it an error to specify limit more than once -
# maybe when we make group_policy optional.
limit = req.GET.getall('limit')
# JSONschema has already confirmed that limit has the form
# of an integer.
if limit:
limit = int(limit[0])
# TODO(efried): Make it an error to specify group_policy more than once
# - maybe when we make it optional.
group_policy = req.GET.getall('group_policy') or None
# Schema ensures we get either "none" or "isolate"
if group_policy:
group_policy = group_policy[0]
anchor_required_traits = None
anchor_forbidden_traits = None
root_required = req.GET.getall('root_required')
if root_required:
if len(root_required) > 1:
raise webob.exc.HTTPBadRequest(
"Query parameter 'root_required' may be specified only "
"once.", comment=errors.ILLEGAL_DUPLICATE_QUERYPARAM)
anchor_required_traits, anchor_forbidden_traits, conflicts = (
_fix_one_forbidden(util.normalize_traits_qs_param(
root_required[0], allow_forbidden=True)))
if conflicts:
raise webob.exc.HTTPBadRequest(
'Conflicting required and forbidden traits found in '
'root_required: %s' % ', '.join(conflicts),
comment=errors.QUERYPARAM_BAD_VALUE)
return cls(
limit=limit,
group_policy=group_policy)
group_policy=group_policy,
anchor_required_traits=anchor_required_traits,
anchor_forbidden_traits=anchor_forbidden_traits)

View File

@ -85,6 +85,7 @@ VERSIONS = [
# [A-Za-z0-9_-]{1,64}.
'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`
]

View File

@ -72,21 +72,25 @@ class AllocationCandidates(object):
and provider_summaries satisfying `requests`, limited
according to `limit`.
"""
alloc_reqs, provider_summaries = cls._get_by_requests(
context, groups, rqparams, nested_aware=nested_aware)
try:
alloc_reqs, provider_summaries = cls._get_by_requests(
context, groups, rqparams, nested_aware=nested_aware)
except exception.ResourceProviderNotFound:
alloc_reqs, provider_summaries = [], []
return cls(
allocation_requests=alloc_reqs,
provider_summaries=provider_summaries,
)
@staticmethod
def _get_by_one_request(rg_ctx):
def _get_by_one_request(rg_ctx, rw_ctx):
"""Get allocation candidates for one RequestGroup.
Must be called from within an placement_context_manager.reader
(or writer) context.
:param rg_ctx: RequestGroupSearchContext.
:param rw_ctx: RequestWideSearchContext.
"""
if not rg_ctx.use_same_provider and (
rg_ctx.exists_sharing or rg_ctx.exists_nested):
@ -105,7 +109,7 @@ class AllocationCandidates(object):
rg_ctx.context, rg_ctx.required_trait_map)
if not trait_rps:
return [], []
rp_candidates = res_ctx.get_trees_matching_all(rg_ctx)
rp_candidates = res_ctx.get_trees_matching_all(rg_ctx, rw_ctx)
return _alloc_candidates_multiple_providers(rg_ctx, rp_candidates)
# Either we are processing a single-RP request group, or there are no
@ -114,7 +118,7 @@ class AllocationCandidates(object):
# the requested resources and more efficiently construct the
# allocation requests.
rp_tuples = res_ctx.get_provider_ids_matching(rg_ctx)
return _alloc_candidates_single_provider(rg_ctx, rp_tuples)
return _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples)
@classmethod
@db_api.placement_context_manager.reader
@ -122,16 +126,17 @@ class AllocationCandidates(object):
rw_ctx = res_ctx.RequestWideSearchContext(
context, rqparams, nested_aware)
sharing = res_ctx.get_sharing_providers(context)
# TODO(efried): If we ran anchors_for_sharing_providers here, we could
# narrow to only sharing providers associated with our filtered trees.
# Unclear whether this would be cheaper than waiting until we've
# filtered sharing providers for other things (like resources).
candidates = {}
for suffix, group in groups.items():
try:
rg_ctx = res_ctx.RequestGroupSearchContext(
context, group, rw_ctx.has_trees, sharing, suffix)
except exception.ResourceProviderNotFound:
return [], []
rg_ctx = res_ctx.RequestGroupSearchContext(
context, group, rw_ctx.has_trees, sharing, suffix)
alloc_reqs, summaries = cls._get_by_one_request(rg_ctx)
alloc_reqs, summaries = cls._get_by_one_request(rg_ctx, rw_ctx)
LOG.debug("%s (suffix '%s') returned %d matches",
str(group), str(suffix), len(alloc_reqs))
if not alloc_reqs:
@ -337,7 +342,7 @@ def _alloc_candidates_multiple_providers(rg_ctx, rp_candidates):
return list(alloc_requests), list(summaries.values())
def _alloc_candidates_single_provider(rg_ctx, rp_tuples):
def _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples):
"""Returns a tuple of (allocation requests, provider summaries) for a
supplied set of requested resource amounts and resource providers. The
supplied resource providers have capacity to satisfy ALL of the resources
@ -353,6 +358,7 @@ def _alloc_candidates_single_provider(rg_ctx, rp_tuples):
determine requests across multiple providers.
:param rg_ctx: RequestGroupSearchContext
:param rw_ctx: RequestWideSearchContext
:param rp_tuples: List of two-tuples of (provider ID, root provider ID)s
for providers that matched the requested resources
"""
@ -383,7 +389,10 @@ def _alloc_candidates_single_provider(rg_ctx, rp_tuples):
req_obj = _allocation_request_for_provider(
rg_ctx.context, rg_ctx.resources, rp_summary.resource_provider,
suffix=rg_ctx.suffix)
alloc_requests.append(req_obj)
# Exclude this if its anchor (which is its root) isn't in our
# prefiltered list of anchors
if rw_ctx.in_filtered_anchors(root_id):
alloc_requests.append(req_obj)
# If this is a sharing provider, we have to include an extra
# AllocationRequest for every possible anchor.
traits = rp_summary.traits
@ -394,6 +403,9 @@ def _alloc_candidates_single_provider(rg_ctx, rp_tuples):
# We already added self
if anchor.anchor_id == root_id:
continue
# Only include if anchor is viable
if not rw_ctx.in_filtered_anchors(anchor.anchor_id):
continue
req_obj = copy.copy(req_obj)
req_obj.anchor_root_provider_uuid = anchor.anchor_uuid
alloc_requests.append(req_obj)

View File

@ -178,6 +178,48 @@ class RequestWideSearchContext(object):
self.group_policy = rqparams.group_policy
self._nested_aware = nested_aware
self.has_trees = _has_provider_trees(context)
# This is set up by _process_anchor_* below. It remains None if no
# anchor filters were requested. Otherwise it becomes a set of internal
# IDs of root providers that conform to the requested filters.
self.anchor_root_ids = None
self._process_anchor_traits(rqparams)
def _process_anchor_traits(self, rqparams):
"""Set or filter self.anchor_root_ids according to anchor
required/forbidden traits.
:param rqparams: RequestWideParams.
:raises TraitNotFound: If any named trait does not exist in the
database.
:raises ResourceProviderNotFound: If anchor trait filters were
specified, but we find no matching providers.
"""
required, forbidden = (
rqparams.anchor_required_traits, rqparams.anchor_forbidden_traits)
if not (required or forbidden):
return
required = set(trait_obj.ids_from_names(
self._ctx, required).values()) if required else None
forbidden = set(trait_obj.ids_from_names(
self._ctx, forbidden).values()) if forbidden else None
self.anchor_root_ids = _get_roots_with_traits(
self._ctx, required, forbidden)
if not self.anchor_root_ids:
raise exception.ResourceProviderNotFound()
def in_filtered_anchors(self, anchor_root_id):
"""Returns whether anchor_root_id is present in filtered anchors. (If
we don't have filtered anchors, that implicitly means "all possible
anchors", so we return True.)
"""
if self.anchor_root_ids is None:
# Not filtering anchors
return True
return anchor_root_id in self.anchor_root_ids
def exclude_nested_providers(
self, allocation_requests, provider_summaries):
@ -503,7 +545,7 @@ def get_provider_ids_matching(rg_ctx):
@db_api.placement_context_manager.reader
def get_trees_matching_all(rg_ctx):
def get_trees_matching_all(rg_ctx, rw_ctx):
"""Returns a RPCandidates object representing the providers that satisfy
the request for resources.
@ -530,6 +572,7 @@ def get_trees_matching_all(rg_ctx):
providers to satisfy different resources involved in a single RequestGroup.
:param rg_ctx: RequestGroupSearchContext
:param rw_ctx: RequestWideSearchContext
"""
if rg_ctx.forbidden_aggs:
rps_bad_aggs = provider_ids_matching_aggregates(
@ -575,6 +618,17 @@ def get_trees_matching_all(rg_ctx):
len(sharing_providers), amount, rc_name,
len(provs_with_inv_rc.trees))
# If we have a list of viable anchor roots, filter to those
if rw_ctx.anchor_root_ids:
provs_with_inv_rc.filter_by_tree(rw_ctx.anchor_root_ids)
LOG.debug(
"found %d providers under %d trees after applying anchor root "
"filter",
len(provs_with_inv_rc.rps), len(provs_with_inv_rc.trees))
# If that left nothing, we're done
if not provs_with_inv_rc:
return rp_candidates.RPCandidateList()
if rg_ctx.member_of:
# Aggregate on root spans the whole tree, so the rp itself
# *or its root* should be in the aggregate

View File

@ -638,3 +638,16 @@ those resource providers that satisfy the identified request group. For
convenience, this mapping can be included in the request payload for
``POST /allocations``, ``PUT /allocations/{consumer_uuid}``, and
``POST /reshaper``, but it will be ignored.
1.35 - Support 'root_required' queryparam on GET /allocation_candidates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: Train
Add support for the ``root_required`` query parameter to the ``GET
/allocation_candidates`` API. It accepts a comma-delimited list of trait names,
each optionally prefixed with ``!`` to indicate a forbidden trait, in the same
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.

View File

@ -90,3 +90,9 @@ _GROUP_PAT_FMT_1_33 = "^%s(" + common.GROUP_PAT_1_33 + ")?$"
GET_SCHEMA_1_33["patternProperties"] = {
_GROUP_PAT_FMT_1_33 % group_type: {"type": "string"}
for group_type in ('resources', 'required', 'member_of', 'in_tree')}
# Microversion 1.35 supports root_required.
GET_SCHEMA_1_35 = copy.deepcopy(GET_SCHEMA_1_33)
GET_SCHEMA_1_35["properties"]['root_required'] = {
"type": ["string"]
}

View File

@ -424,7 +424,9 @@ class ProviderTreeDBHelperTestCase(tb.PlacementDbBaseTestCase):
except Exception:
pass
rg_ctx = _req_group_search_context(self.ctx, **kwargs)
results = res_ctx.get_trees_matching_all(rg_ctx)
rw_ctx = res_ctx.RequestWideSearchContext(
self.ctx, placement_lib.RequestWideParams(), True)
results = res_ctx.get_trees_matching_all(rg_ctx, rw_ctx)
tree_ids = self._get_rp_ids_matching_names(expected_trees)
rp_ids = self._get_rp_ids_matching_names(expected_rps)
@ -907,7 +909,8 @@ class ProviderTreeDBHelperTestCase(tb.PlacementDbBaseTestCase):
expected=(3,))
# Required & forbidden overlap. No results because it is impossible for
# one provider to both have and not have a trait.
# one provider to both have and not have a trait. (Unreachable in real
# life due to conflict check in the handler.)
do_test(required=[avx2_t, ssd_t], forbidden=[ssd_t, geneve_t])

View File

@ -417,6 +417,7 @@ class NUMANetworkFixture(APIFixture):
>>....(from ss1)........| min_unit: 1024 |
| step_size: 128 |
| DISK_GB: 1000 |
| traits: FOO |
| agg: [aggA] |
+---------+----------+
|
@ -515,6 +516,7 @@ class NUMANetworkFixture(APIFixture):
tb.add_inventory(
cn2, orc.MEMORY_MB, 2048, min_unit=1024, step_size=128)
tb.add_inventory(cn2, orc.DISK_GB, 1000)
tb.set_traits(cn2, 'CUSTOM_FOO')
os.environ['CN2_UUID'] = cn2.uuid
nics = []

View File

@ -0,0 +1,292 @@
# Tests of allocation candidates API with root_required
fixtures:
- NUMANetworkFixture
defaults:
request_headers:
x-auth-token: admin
accept: application/json
openstack-api-version: placement 1.35
tests:
- name: root_required before microversion
GET: /allocation_candidates?resources=VCPU:1&root_required=HW_CPU_X86_AVX2
request_headers:
openstack-api-version: placement 1.34
status: 400
response_strings:
- Invalid query string parameters
- "'root_required' does not match any of the regexes"
- name: conflicting required and forbidden
GET: /allocation_candidates?resources=VCPU:1&root_required=HW_CPU_X86_AVX2,HW_CPU_X86_SSE,!HW_CPU_X86_AVX2
status: 400
response_strings:
- "Conflicting required and forbidden traits found in root_required: HW_CPU_X86_AVX2"
response_json_paths:
errors[0].code: placement.query.bad_value
- name: nonexistent required
GET: /allocation_candidates?resources=VCPU:1&root_required=CUSTOM_NO_EXIST,HW_CPU_X86_SSE,!HW_CPU_X86_AVX
status: 400
response_strings:
- "No such trait(s): CUSTOM_NO_EXIST"
- name: nonexistent forbidden
GET: /allocation_candidates?resources=VCPU:1&root_required=!CUSTOM_NO_EXIST,HW_CPU_X86_SSE,!HW_CPU_X86_AVX
status: 400
response_strings:
- "No such trait(s): CUSTOM_NO_EXIST"
- name: multiple root_required is an error
GET: /allocation_candidates?resources=VCPU:1&root_required=MISC_SHARES_VIA_AGGREGATE&root_required=!HW_NUMA_ROOT
status: 400
response_strings:
- Query parameter 'root_required' may be specified only once.
response_json_paths:
errors[0].code: placement.query.duplicate_key
- name: no hits for a required trait that is on children in one tree and absent from the other
GET: /allocation_candidates?resources=VCPU:1&root_required=HW_NUMA_ROOT
status: 200
response_json_paths:
# No root has HW_NUMA_ROOT
$.allocation_requests.`len`: 0
- name: required trait on a sharing root
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=MISC_SHARES_VIA_AGGREGATE
status: 200
response_json_paths:
# MISC_SHARES is on the sharing root, but not on any of the anchor roots
$.allocation_requests.`len`: 0
- name: root_required trait on children
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=HW_NUMA_ROOT
status: 200
response_json_paths:
# HW_NUMA_ROOT is on child providers, not on any root
$.allocation_requests.`len`: 0
- name: required trait not on any provider
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=HW_CPU_X86_AVX2
status: 200
response_json_paths:
# HW_CPU_X86_AVX2 isn't anywhere in the env.
$.allocation_requests.`len`: 0
- name: limit to multiattach-capable unsuffixed no sharing
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024&root_required=COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# We only get results from cn1 because only it has MULTI_ATTACH
# We get candidates where VCPU and MEMORY_MB are provided by the same or
# alternate NUMA roots.
$.allocation_requests.`len`: 4
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.MEMORY_MB: [1024, 1024]
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.MEMORY_MB: [1024, 1024]
- name: limit to multiattach-capable separate granular no isolate no sharing
GET: /allocation_candidates?resources1=VCPU:1&resources2=MEMORY_MB:1024&group_policy=none&root_required=COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# Same as above
$.allocation_requests.`len`: 4
# Prove we didn't break provider summaries
$.provider_summaries["$ENVIRON['NUMA0_UUID']"].resources[VCPU][capacity]: 4
$.provider_summaries["$ENVIRON['NUMA1_UUID']"].resources[MEMORY_MB][capacity]: 2048
- name: limit to multiattach-capable separate granular isolate no sharing
GET: /allocation_candidates?resources1=VCPU:1&resources2=MEMORY_MB:1024&group_policy=isolate&root_required=COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# Now we (perhaps unrealistically) only get candidates where VCPU and
# MEMORY_MB are on alternate NUMA roots.
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.MEMORY_MB: 1024
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.MEMORY_MB: 1024
- name: limit to multiattach-capable unsuffixed sharing
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# We only get results from cn1 because only it has MULTI_ATTACH
# We get candidates where VCPU and MEMORY_MB are provided by the same or
# alternate NUMA roots. DISK_GB is always provided by the sharing provider.
$.allocation_requests.`len`: 4
$.provider_summaries["$ENVIRON['NUMA0_UUID']"].traits:
- HW_NUMA_ROOT
$.provider_summaries["$ENVIRON['NUMA1_UUID']"].traits:
- HW_NUMA_ROOT
- CUSTOM_FOO
- name: limit to multiattach-capable granular sharing
GET: /allocation_candidates?resources1=VCPU:1,MEMORY_MB:1024&resources2=DISK_GB:100&&group_policy=none&root_required=COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# We only get results from cn1 because only it has MULTI_ATTACH
# We only get candidates where VCPU and MEMORY_MB are provided by the same
# NUMA root, because requested in the same suffixed group. DISK_GB is
# always provided by the sharing provider.
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.MEMORY_MB: 1024
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.MEMORY_MB: 1024
- name: trait exists on root and child in separate trees case 1 unsuffixed required
GET: /allocation_candidates?resources=VCPU:1,DISK_GB:100&required=CUSTOM_FOO
status: 200
response_json_paths:
# We get a candidates from cn2 and cn2+ss1 because cn2 has all the
# resources and the trait.
# We get a candidate from numa1+ss1 because (even in the unsuffixed group)
# regular `required` is tied to the resource in that group.
$.allocation_requests.`len`: 3
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: [100, 100]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
- name: trait exists on root and child in separate trees case 2 unsuffixed root_required
GET: /allocation_candidates?resources=VCPU:1,DISK_GB:100&root_required=CUSTOM_FOO
status: 200
response_json_paths:
# We only get candidates from cn2 and cn2+ss1 because only cn2 has FOO on
# the root
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: 100
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
- name: trait exists on root and child in separate trees case 3 suffixed required
GET: /allocation_candidates?resources1=VCPU:1&required1=CUSTOM_FOO&resources2=DISK_GB:100&group_policy=none
status: 200
response_json_paths:
# We get a candidates from cn2 because has all the resources and the trait;
# and from cn2+ss1 because group_policy=none and the required trait is on
# the group with the VCPU.
# We get a candidate from numa1+ss1 because the required trait is on the
# group with the VCPU.
$.allocation_requests.`len`: 3
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: [100, 100]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
- name: trait exists on root and child in separate trees case 4 suffixed root_required
GET: /allocation_candidates?resources1=VCPU:1&resources2=DISK_GB:100&group_policy=none&root_required=CUSTOM_FOO
status: 200
response_json_paths:
# We only get candidates from cn2 and cn2+ss1 because only cn2 has FOO on
# the root
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: 100
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
- name: no filtering for a forbidden trait that is on children in one tree and absent from the other
GET: /allocation_candidates?resources=VCPU:1&root_required=!HW_NUMA_ROOT
status: 200
response_json_paths:
# No root has HW_NUMA_ROOT, so we hit all providers of VCPU
$.allocation_requests.`len`: 3
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: 1
- name: forbidden trait on a sharing root
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=!MISC_SHARES_VIA_AGGREGATE
status: 200
response_json_paths:
# This does not filter out candidates including the sharing provider, of
# which there are five (four from the combinations of VCPU+MEMORY_MB on cn1
# because non-isolated; one using VCPU+MEMORY_MB from cn2). The sixth is
# where cn2 provides all the resources.
$.allocation_requests.`len`: 6
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: [100, 100, 100, 100, 100]
- name: combine required with irrelevant forbidden
# This time the irrelevant forbidden is on a child provider
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=CUSTOM_FOO,!HW_NUMA_ROOT
status: 200
response_json_paths:
# This is as above, but filtered to the candidates involving cn2, which has
# CUSTOM_FOO on the root.
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.MEMORY_MB: [1024, 1024]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: 100
- name: redundant required and forbidden
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=CUSTOM_FOO,!COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# Same result as above. The forbidden multi-attach and the required foo are
# both doing the same thing.
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: [1, 1]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.MEMORY_MB: [1024, 1024]
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.DISK_GB: 100
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: 100
- name: forbiddens cancel each other
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100&root_required=!CUSTOM_FOO,!COMPUTE_VOLUME_MULTI_ATTACH
status: 200
response_json_paths:
# !foo gets rid of cn2; !multi-attach gets rid of cn1.
$.allocation_requests.`len`: 0
- name: isolate foo granular sharing
GET: /allocation_candidates?resources1=VCPU:1,MEMORY_MB:1024&resources2=DISK_GB:100&&group_policy=none&root_required=!CUSTOM_FOO
status: 200
response_json_paths:
# We only get results from cn1 because cn2 has the forbidden foo trait.
# We only get candidates where VCPU and MEMORY_MB are provided by the same
# NUMA root, because requested in the same suffixed group. DISK_GB is
# always provided by the sharing provider.
$.allocation_requests.`len`: 2
$.allocation_requests..allocations["$ENVIRON['NUMA0_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
$.allocation_requests..allocations["$ENVIRON['SS1_UUID']"].resources.DISK_GB: [100, 100]
- name: unsuffixed required and root_required same trait
GET: /allocation_candidates?resources=VCPU:1&required=CUSTOM_FOO&root_required=CUSTOM_FOO
status: 200
response_json_paths:
# required=FOO would have limited us to getting VCPU from numa1 and cn2
# BUT root_required=FOO should further restrict us to just cn2
$.allocation_requests.`len`: 1
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: 1
- name: granular required and root_required same trait
GET: /allocation_candidates?resources1=VCPU:1&required1=CUSTOM_FOO&root_required=CUSTOM_FOO
status: 200
response_json_paths:
# same as above
$.allocation_requests.`len`: 1
$.allocation_requests..allocations["$ENVIRON['CN2_UUID']"].resources.VCPU: 1
- name: required positive and root_required negative same trait
GET: /allocation_candidates?resources1=VCPU:1&required1=CUSTOM_FOO&root_required=!CUSTOM_FOO
status: 200
response_json_paths:
# Both numa1 and cn2 match required1=FOO, but since we're forbidding FOO on
# the root, we should only get numa1
$.allocation_requests.`len`: 1
$.allocation_requests..allocations["$ENVIRON['NUMA1_UUID']"].resources.VCPU: 1
- name: required negative and root_required positive same trait
GET: /allocation_candidates?resources1=VCPU:1&required1=!CUSTOM_FOO&root_required=CUSTOM_FOO
status: 200
response_json_paths:
# The only provider of VCPU that doesn't have FOO is numa0. But numa0 is on
# cn1, which doesn't have the required FOO on the root.
$.allocation_requests.`len`: 0

View File

@ -41,13 +41,13 @@ tests:
response_json_paths:
$.errors[0].title: Not Acceptable
- name: latest microversion is 1.34
- name: latest microversion is 1.35
GET: /
request_headers:
openstack-api-version: placement latest
response_headers:
vary: /openstack-api-version/
openstack-api-version: placement 1.34
openstack-api-version: placement 1.35
- name: other accept header bad version
GET: /

View File

@ -0,0 +1,12 @@
---
features:
- |
Microversion 1.35_ adds support for the ``root_required`` query parameter
to the ``GET /allocation_candidates`` API. It accepts a comma-delimited
list of trait names, each optionally prefixed with ``!`` to indicate a
forbidden trait, in the same 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.
.. _1.35: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html#support-root_required-queryparam-on-get-allocation_candidates