placement: Granular GET /allocation_candidates
In a new microversion, the GET /allocation_candidates API now accepts granular resource request syntax: ?resourcesN=...&requiredN=...&member_ofN=...&group_policy={isolate|none} Change-Id: I4e99974443aa513fd9f837a6057f67d744caf1b4 blueprint: granular-resource-requests
This commit is contained in:
parent
7f996eb2c4
commit
9af073384c
@ -212,7 +212,9 @@ def list_allocation_candidates(req):
|
|||||||
context = req.environ['placement.context']
|
context = req.environ['placement.context']
|
||||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||||
get_schema = schema.GET_SCHEMA_1_10
|
get_schema = schema.GET_SCHEMA_1_10
|
||||||
if want_version.matches((1, 21)):
|
if want_version.matches((1, 25)):
|
||||||
|
get_schema = schema.GET_SCHEMA_1_25
|
||||||
|
elif want_version.matches((1, 21)):
|
||||||
get_schema = schema.GET_SCHEMA_1_21
|
get_schema = schema.GET_SCHEMA_1_21
|
||||||
elif want_version.matches((1, 17)):
|
elif want_version.matches((1, 17)):
|
||||||
get_schema = schema.GET_SCHEMA_1_17
|
get_schema = schema.GET_SCHEMA_1_17
|
||||||
@ -227,9 +229,21 @@ def list_allocation_candidates(req):
|
|||||||
if limit:
|
if limit:
|
||||||
limit = int(limit[0])
|
limit = int(limit[0])
|
||||||
|
|
||||||
|
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]
|
||||||
|
else:
|
||||||
|
# group_policy is required if more than one numbered request group was
|
||||||
|
# specified.
|
||||||
|
if len([rg for rg in requests.values() if rg.use_same_provider]) > 1:
|
||||||
|
raise webob.exc.HTTPBadRequest(
|
||||||
|
_('The "group_policy" parameter is required when specifying '
|
||||||
|
'more than one "resources{N}" parameter.'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cands = rp_obj.AllocationCandidates.get_by_requests(context, requests,
|
cands = rp_obj.AllocationCandidates.get_by_requests(
|
||||||
limit)
|
context, requests, limit=limit, group_policy=group_policy)
|
||||||
except exception.ResourceClassNotFound as exc:
|
except exception.ResourceClassNotFound as exc:
|
||||||
raise webob.exc.HTTPBadRequest(
|
raise webob.exc.HTTPBadRequest(
|
||||||
_('Invalid resource class in resources parameter: %(error)s') %
|
_('Invalid resource class in resources parameter: %(error)s') %
|
||||||
|
@ -66,6 +66,8 @@ VERSIONS = [
|
|||||||
'1.23', # Add support for error codes in error response JSON
|
'1.23', # Add support for error codes in error response JSON
|
||||||
'1.24', # Support multiple ?member_of=<agg UUIDs> queryparams on
|
'1.24', # Support multiple ?member_of=<agg UUIDs> queryparams on
|
||||||
# GET /resource_providers
|
# GET /resource_providers
|
||||||
|
'1.25', # Adds support for granular resource requests via numbered
|
||||||
|
# querystring groups in GET /allocation_candidates
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -3507,7 +3507,8 @@ def _alloc_candidates_single_provider(ctx, requested_resources, rps):
|
|||||||
alloc_requests.append(req_obj)
|
alloc_requests.append(req_obj)
|
||||||
# If this is a sharing provider, we have to include an extra
|
# If this is a sharing provider, we have to include an extra
|
||||||
# AllocationRequest for every possible anchor.
|
# AllocationRequest for every possible anchor.
|
||||||
if os_traits.MISC_SHARES_VIA_AGGREGATE in rp_summary.traits:
|
traits = [trait.name for trait in rp_summary.traits]
|
||||||
|
if os_traits.MISC_SHARES_VIA_AGGREGATE in traits:
|
||||||
for anchor in _anchors_for_sharing_provider(
|
for anchor in _anchors_for_sharing_provider(
|
||||||
ctx, rp_summary.resource_provider.id):
|
ctx, rp_summary.resource_provider.id):
|
||||||
# We already added self
|
# We already added self
|
||||||
@ -3778,6 +3779,265 @@ def _trait_ids_from_names(ctx, names):
|
|||||||
return {r[0]: r[1] for r in ctx.session.execute(sel)}
|
return {r[0]: r[1] for r in ctx.session.execute(sel)}
|
||||||
|
|
||||||
|
|
||||||
|
def _rp_rc_key(rp, rc):
|
||||||
|
"""Creates hashable key unique to a provider + resource class."""
|
||||||
|
return rp.uuid, rc
|
||||||
|
|
||||||
|
|
||||||
|
def _consolidate_allocation_requests(areqs):
|
||||||
|
"""Consolidates a list of AllocationRequest into one.
|
||||||
|
|
||||||
|
:param areqs: A list containing one AllocationRequest for each input
|
||||||
|
RequestGroup. This may mean that multiple resource_requests
|
||||||
|
contain resource amounts of the same class from the same provider.
|
||||||
|
:return: A single consolidated AllocationRequest, containing no
|
||||||
|
resource_requests with duplicated (resource_provider,
|
||||||
|
resource_class).
|
||||||
|
"""
|
||||||
|
# Construct a dict, keyed by resource provider UUID + resource class, of
|
||||||
|
# AllocationRequestResource, consolidating as we go.
|
||||||
|
arrs_by_rp_rc = {}
|
||||||
|
# areqs must have at least one element. Save the anchor to populate the
|
||||||
|
# returned AllocationRequest.
|
||||||
|
anchor_rp_uuid = areqs[0].anchor_root_provider_uuid
|
||||||
|
for areq in areqs:
|
||||||
|
# Sanity check: the anchor should be the same for every areq
|
||||||
|
if anchor_rp_uuid != areq.anchor_root_provider_uuid:
|
||||||
|
# This should never happen. If it does, it's a dev bug.
|
||||||
|
raise ValueError(
|
||||||
|
_("Expected every AllocationRequest in `deflate` to have the "
|
||||||
|
"same anchor!"))
|
||||||
|
for arr in areq.resource_requests:
|
||||||
|
key = _rp_rc_key(arr.resource_provider, arr.resource_class)
|
||||||
|
if key not in arrs_by_rp_rc:
|
||||||
|
arrs_by_rp_rc[key] = copy.deepcopy(arr)
|
||||||
|
else:
|
||||||
|
arrs_by_rp_rc[key].amount += arr.amount
|
||||||
|
return AllocationRequest(
|
||||||
|
resource_requests=list(arrs_by_rp_rc.values()),
|
||||||
|
anchor_root_provider_uuid=anchor_rp_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def _satisfies_group_policy(areqs, group_policy, num_granular_groups):
|
||||||
|
"""Applies group_policy to a list of AllocationRequest.
|
||||||
|
|
||||||
|
Returns True or False, indicating whether this list of
|
||||||
|
AllocationRequest satisfies group_policy, as follows:
|
||||||
|
|
||||||
|
* "isolate": Each AllocationRequest with use_same_provider=True
|
||||||
|
is satisfied by a single resource provider. If the "isolate"
|
||||||
|
policy is in effect, each such AllocationRequest must be
|
||||||
|
satisfied by a *unique* resource provider.
|
||||||
|
* "none" or None: Always returns True.
|
||||||
|
|
||||||
|
:param areqs: A list containing one AllocationRequest for each input
|
||||||
|
RequestGroup.
|
||||||
|
:param group_policy: String indicating how RequestGroups should interact
|
||||||
|
with each other. If the value is "isolate", we will return False
|
||||||
|
if AllocationRequests that came from RequestGroups keyed by
|
||||||
|
nonempty suffixes are satisfied by the same provider.
|
||||||
|
:param num_granular_groups: The number of granular (use_same_provider=True)
|
||||||
|
RequestGroups in the request.
|
||||||
|
:return: True if areqs satisfies group_policy; False otherwise.
|
||||||
|
"""
|
||||||
|
if group_policy != 'isolate':
|
||||||
|
# group_policy="none" means no filtering
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
return num_granular_groups == 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))
|
||||||
|
|
||||||
|
|
||||||
|
def _exceeds_capacity(areq, psum_res_by_rp_rc):
|
||||||
|
"""Checks a (consolidated) AllocationRequest against the provider summaries
|
||||||
|
to ensure that it does not exceed capacity.
|
||||||
|
|
||||||
|
Exceeding capacity can mean the total amount (already used plus this
|
||||||
|
allocation) exceeds the total inventory amount; or this allocation exceeds
|
||||||
|
the max_unit in the inventory record.
|
||||||
|
|
||||||
|
:param areq: An AllocationRequest produced by the
|
||||||
|
`_consolidate_allocation_requests` method.
|
||||||
|
:param psum_res_by_rp_rc: A dict, keyed by provider + resource class via
|
||||||
|
_rp_rc_key, of ProviderSummaryResource.
|
||||||
|
:return: True if areq exceeds capacity; False otherwise.
|
||||||
|
"""
|
||||||
|
for arr in areq.resource_requests:
|
||||||
|
key = _rp_rc_key(arr.resource_provider, arr.resource_class)
|
||||||
|
psum_res = psum_res_by_rp_rc[key]
|
||||||
|
if psum_res.used + arr.amount > psum_res.capacity:
|
||||||
|
return True
|
||||||
|
if arr.amount > psum_res.max_unit:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_candidates(candidates, group_policy=None):
|
||||||
|
"""Given a dict, keyed by RequestGroup suffix, of tuples of
|
||||||
|
(allocation_requests, provider_summaries), produce a single tuple of
|
||||||
|
(allocation_requests, provider_summaries) that appropriately incorporates
|
||||||
|
the elements from each.
|
||||||
|
|
||||||
|
Each (alloc_reqs, prov_sums) in `candidates` satisfies one RequestGroup.
|
||||||
|
This method creates a list of alloc_reqs, *each* of which satisfies *all*
|
||||||
|
of the RequestGroups.
|
||||||
|
|
||||||
|
For that merged list of alloc_reqs, a corresponding provider_summaries is
|
||||||
|
produced.
|
||||||
|
|
||||||
|
:param candidates: A dict, keyed by integer suffix or '', of tuples of
|
||||||
|
(allocation_requests, provider_summaries) to be merged.
|
||||||
|
:param group_policy: String indicating how RequestGroups should interact
|
||||||
|
with each other. If the value is "isolate", we will filter out
|
||||||
|
candidates where AllocationRequests that came from RequestGroups
|
||||||
|
keyed by nonempty suffixes are satisfied by the same provider.
|
||||||
|
:return: A tuple of (allocation_requests, provider_summaries).
|
||||||
|
"""
|
||||||
|
# Build a dict, keyed by anchor root provider UUID, of dicts, keyed by
|
||||||
|
# suffix, of nonempty lists of AllocationRequest. Each inner dict must
|
||||||
|
# possess all of the suffix keys to be viable (i.e. contains at least
|
||||||
|
# one AllocationRequest per RequestGroup).
|
||||||
|
#
|
||||||
|
# areq_lists_by_anchor =
|
||||||
|
# { anchor_root_provider_uuid: {
|
||||||
|
# '': [AllocationRequest, ...], \ This dict must contain
|
||||||
|
# '1': [AllocationRequest, ...], \ exactly one nonempty list per
|
||||||
|
# ... / suffix to be viable. That
|
||||||
|
# '42': [AllocationRequest, ...], / filtering is done later.
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# }
|
||||||
|
areq_lists_by_anchor = collections.defaultdict(
|
||||||
|
lambda: collections.defaultdict(list))
|
||||||
|
# Save off all the provider summaries lists - we'll use 'em later.
|
||||||
|
all_psums = []
|
||||||
|
# Construct a dict, keyed by resource provider + resource class, of
|
||||||
|
# ProviderSummaryResource. This will be used to do a final capacity
|
||||||
|
# check/filter on each merged AllocationRequest.
|
||||||
|
psum_res_by_rp_rc = {}
|
||||||
|
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)
|
||||||
|
for psum_res in psum.resources:
|
||||||
|
key = _rp_rc_key(
|
||||||
|
psum.resource_provider, psum_res.resource_class)
|
||||||
|
psum_res_by_rp_rc[key] = psum_res
|
||||||
|
|
||||||
|
# Create all combinations picking one AllocationRequest from each list
|
||||||
|
# for each anchor.
|
||||||
|
areqs = []
|
||||||
|
all_suffixes = set(candidates)
|
||||||
|
num_granular_groups = len(all_suffixes - set(['']))
|
||||||
|
for areq_lists_by_suffix in areq_lists_by_anchor.values():
|
||||||
|
# Filter out any entries that don't have allocation requests for
|
||||||
|
# *all* suffixes (i.e. all RequestGroups)
|
||||||
|
if set(areq_lists_by_suffix) != all_suffixes:
|
||||||
|
continue
|
||||||
|
# We're using itertools.product to go from this:
|
||||||
|
# areq_lists_by_suffix = {
|
||||||
|
# '': [areq__A, areq__B, ...],
|
||||||
|
# '1': [areq_1_A, areq_1_B, ...],
|
||||||
|
# ...
|
||||||
|
# '42': [areq_42_A, areq_42_B, ...],
|
||||||
|
# }
|
||||||
|
# to this:
|
||||||
|
# [ [areq__A, areq_1_A, ..., areq_42_A], Each of these lists is one
|
||||||
|
# [areq__A, areq_1_A, ..., areq_42_B], areq_list in the loop below.
|
||||||
|
# [areq__A, areq_1_B, ..., areq_42_A], each areq_list contains one
|
||||||
|
# [areq__A, areq_1_B, ..., areq_42_B], AllocationRequest from each
|
||||||
|
# [areq__B, areq_1_A, ..., areq_42_A], RequestGroup. So taken as a
|
||||||
|
# [areq__B, areq_1_A, ..., areq_42_B], whole, each list is a viable
|
||||||
|
# [areq__B, areq_1_B, ..., areq_42_A], (preliminary) candidate to
|
||||||
|
# [areq__B, areq_1_B, ..., areq_42_B], return.
|
||||||
|
# ...,
|
||||||
|
# ]
|
||||||
|
for areq_list in itertools.product(
|
||||||
|
*list(areq_lists_by_suffix.values())):
|
||||||
|
# At this point, each AllocationRequest in areq_list is still
|
||||||
|
# marked as use_same_provider. This is necessary to filter by group
|
||||||
|
# policy, which enforces how these interact with each other.
|
||||||
|
if not _satisfies_group_policy(
|
||||||
|
areq_list, group_policy, num_granular_groups):
|
||||||
|
continue
|
||||||
|
# Now we go from this (where 'arr' is AllocationRequestResource):
|
||||||
|
# [ areq__B(arrX, arrY, arrZ),
|
||||||
|
# areq_1_A(arrM, arrN),
|
||||||
|
# ...,
|
||||||
|
# areq_42_B(arrQ)
|
||||||
|
# ]
|
||||||
|
# to this:
|
||||||
|
# areq_combined(arrX, arrY, arrZ, arrM, arrN, arrQ)
|
||||||
|
# Note that this discards the information telling us which
|
||||||
|
# RequestGroup led to which piece of the final AllocationRequest.
|
||||||
|
# We needed that to be present for the previous filter; we need it
|
||||||
|
# to be *absent* for the next one (and for the final output).
|
||||||
|
areq = _consolidate_allocation_requests(areq_list)
|
||||||
|
# Since we sourced this AllocationRequest from multiple
|
||||||
|
# *independent* queries, it's possible that the combined result
|
||||||
|
# now exceeds capacity where amounts of the same RP+RC were
|
||||||
|
# folded together. So do a final capacity check/filter.
|
||||||
|
if _exceeds_capacity(areq, psum_res_by_rp_rc):
|
||||||
|
continue
|
||||||
|
areqs.append(areq)
|
||||||
|
|
||||||
|
# It's possible we've filtered out everything. If so, short out.
|
||||||
|
if not areqs:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
# Now we have to produce provider summaries. The provider summaries in
|
||||||
|
# the candidates input contain all the information; we just need to
|
||||||
|
# filter it down to only the providers and resource classes* in our
|
||||||
|
# merged list of allocation requests.
|
||||||
|
# *With blueprint placement-return-all-resources, all resource classes
|
||||||
|
# should be included, so that condition will need to be removed either
|
||||||
|
# here or there, depending which lands first.
|
||||||
|
# To make this easier, first index all our allocation requests as a
|
||||||
|
# dict, keyed by resource provider UUID, of sets of resource class
|
||||||
|
# names.
|
||||||
|
rcs_by_rp = collections.defaultdict(set)
|
||||||
|
for areq in areqs:
|
||||||
|
for arr in areq.resource_requests:
|
||||||
|
rcs_by_rp[arr.resource_provider.uuid].add(arr.resource_class)
|
||||||
|
# Now walk the input candidates' provider summaries, building a dict,
|
||||||
|
# keyed by resource provider UUID, of ProviderSummary representing
|
||||||
|
# that provider, and including any of its resource classes found in the
|
||||||
|
# index we built from our allocation requests above*.
|
||||||
|
# *See above.
|
||||||
|
psums_by_rp = {}
|
||||||
|
for psum in all_psums:
|
||||||
|
rp_uuid = psum.resource_provider.uuid
|
||||||
|
# If everything from this provider was filtered out, don't add an
|
||||||
|
# (empty) entry for it.
|
||||||
|
if rp_uuid not in rcs_by_rp:
|
||||||
|
continue
|
||||||
|
if rp_uuid not in psums_by_rp:
|
||||||
|
psums_by_rp[rp_uuid] = ProviderSummary(
|
||||||
|
resource_provider=psum.resource_provider, resources=[],
|
||||||
|
# Should always be the same; no need to check/update below.
|
||||||
|
traits=psum.traits)
|
||||||
|
# NOTE(efried): To subsume blueprint placement-return-all-resources
|
||||||
|
# replace this loop with:
|
||||||
|
# psums_by_rp[rp_uuid].resources = psum.resources
|
||||||
|
resources = set(psums_by_rp[rp_uuid].resources)
|
||||||
|
for psumres in psum.resources:
|
||||||
|
if psumres.resource_class in rcs_by_rp[rp_uuid]:
|
||||||
|
resources.add(psumres)
|
||||||
|
psums_by_rp[rp_uuid].resources = list(resources)
|
||||||
|
|
||||||
|
return areqs, list(psums_by_rp.values())
|
||||||
|
|
||||||
|
|
||||||
@base.VersionedObjectRegistry.register_if(False)
|
@base.VersionedObjectRegistry.register_if(False)
|
||||||
class AllocationCandidates(base.VersionedObject):
|
class AllocationCandidates(base.VersionedObject):
|
||||||
"""The AllocationCandidates object is a collection of possible allocations
|
"""The AllocationCandidates object is a collection of possible allocations
|
||||||
@ -3797,7 +4057,7 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_requests(cls, context, requests, limit=None):
|
def get_by_requests(cls, context, requests, limit=None, group_policy=None):
|
||||||
"""Returns an AllocationCandidates object containing all resource
|
"""Returns an AllocationCandidates object containing all resource
|
||||||
providers matching a set of supplied resource constraints, with a set
|
providers matching a set of supplied resource constraints, with a set
|
||||||
of allocation requests constructed from that list of resource
|
of allocation requests constructed from that list of resource
|
||||||
@ -3805,7 +4065,9 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
(default is False) then the order of the allocation requests will
|
(default is False) then the order of the allocation requests will
|
||||||
be randomized.
|
be randomized.
|
||||||
|
|
||||||
:param requests: List of nova.api.openstack.placement.util.RequestGroup
|
:param context: Nova RequestContext.
|
||||||
|
:param requests: Dict, keyed by suffix, of
|
||||||
|
nova.api.openstack.placement.util.RequestGroup
|
||||||
:param limit: An integer, N, representing the maximum number of
|
:param limit: An integer, N, representing the maximum number of
|
||||||
allocation candidates to return. If
|
allocation candidates to return. If
|
||||||
CONF.placement.randomize_allocation_candidates is True
|
CONF.placement.randomize_allocation_candidates is True
|
||||||
@ -3814,12 +4076,19 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
order the database picked them, will be returned. In
|
order the database picked them, will be returned. In
|
||||||
either case if there are fewer than N total results,
|
either case if there are fewer than N total results,
|
||||||
all the results will be returned.
|
all the results will be returned.
|
||||||
|
:param group_policy: String indicating how RequestGroups with
|
||||||
|
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.
|
||||||
|
:return: An instance of AllocationCandidates with allocation_requests
|
||||||
|
and provider_summaries satisfying `requests`, limited
|
||||||
|
according to `limit`.
|
||||||
"""
|
"""
|
||||||
_ensure_rc_cache(context)
|
_ensure_rc_cache(context)
|
||||||
_ensure_trait_sync(context)
|
_ensure_trait_sync(context)
|
||||||
alloc_reqs, provider_summaries = cls._get_by_requests(context,
|
alloc_reqs, provider_summaries = cls._get_by_requests(
|
||||||
requests,
|
context, requests, limit=limit, group_policy=group_policy)
|
||||||
limit)
|
|
||||||
return cls(
|
return cls(
|
||||||
context,
|
context,
|
||||||
allocation_requests=alloc_reqs,
|
allocation_requests=alloc_reqs,
|
||||||
@ -3827,36 +4096,29 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
# TODO(efried): This is only a writer context because it accesses the
|
def _get_by_one_request(context, request):
|
||||||
# resource_providers table via ResourceProvider.get_by_uuid, which does
|
"""Get allocation candidates for one RequestGroup.
|
||||||
# data migration to populate the root_provider_uuid. Change this back to a
|
|
||||||
# reader when that migration is no longer happening.
|
|
||||||
@db_api.api_context_manager.writer
|
|
||||||
def _get_by_requests(context, requests, limit=None):
|
|
||||||
# We first get the list of "root providers" that either have the
|
|
||||||
# requested resources or are associated with the providers that
|
|
||||||
# share one or more of the requested resource(s)
|
|
||||||
# TODO(efried): Handle non-sharing groups.
|
|
||||||
# For now, this extracts just the sharing group's resources & traits.
|
|
||||||
sharing_groups = [request_group for request_group in requests
|
|
||||||
if not request_group.use_same_provider]
|
|
||||||
if len(sharing_groups) != 1 or not sharing_groups[0].resources:
|
|
||||||
raise ValueError(_("The requests parameter must contain one "
|
|
||||||
"RequestGroup with use_same_provider=False and "
|
|
||||||
"nonempty resources."))
|
|
||||||
|
|
||||||
|
Must be called from within an api_context_manager.reader (or writer)
|
||||||
|
context.
|
||||||
|
|
||||||
|
:param context: Nova RequestContext.
|
||||||
|
:param request: One nova.api.openstack.placement.util.RequestGroup
|
||||||
|
:return: A tuple of (allocation_requests, provider_summaries)
|
||||||
|
satisfying `request`.
|
||||||
|
"""
|
||||||
# Transform resource string names to internal integer IDs
|
# Transform resource string names to internal integer IDs
|
||||||
resources = {
|
resources = {
|
||||||
_RC_CACHE.id_from_string(key): value
|
_RC_CACHE.id_from_string(key): value
|
||||||
for key, value in sharing_groups[0].resources.items()
|
for key, value in request.resources.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
# maps the trait name to the trait internal ID
|
# maps the trait name to the trait internal ID
|
||||||
required_trait_map = {}
|
required_trait_map = {}
|
||||||
forbidden_trait_map = {}
|
forbidden_trait_map = {}
|
||||||
for trait_map, traits in (
|
for trait_map, traits in (
|
||||||
(required_trait_map, sharing_groups[0].required_traits),
|
(required_trait_map, request.required_traits),
|
||||||
(forbidden_trait_map, sharing_groups[0].forbidden_traits)):
|
(forbidden_trait_map, request.forbidden_traits)):
|
||||||
if traits:
|
if traits:
|
||||||
trait_map.update(_trait_ids_from_names(context, traits))
|
trait_map.update(_trait_ids_from_names(context, traits))
|
||||||
# Double-check that we found a trait ID for each requested name
|
# Double-check that we found a trait ID for each requested name
|
||||||
@ -3866,43 +4128,35 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
|
|
||||||
# Microversions prior to 1.21 will not have 'member_of' in the groups.
|
# Microversions prior to 1.21 will not have 'member_of' in the groups.
|
||||||
# This allows earlier microversions to continue to work.
|
# This allows earlier microversions to continue to work.
|
||||||
member_of = ""
|
member_of = getattr(request, "member_of", "")
|
||||||
if hasattr(sharing_groups[0], "member_of"):
|
|
||||||
member_of = sharing_groups[0].member_of
|
|
||||||
|
|
||||||
# Contains a set of resource provider IDs that share some inventory for
|
if not request.use_same_provider:
|
||||||
# each resource class requested. We do this here as an optimization. If
|
# TODO(jaypipes): The check/callout to handle trees goes here.
|
||||||
# we have no sharing providers, the SQL to find matching providers for
|
# Build a dict, keyed by resource class internal ID, of lists of
|
||||||
# the requested resources is much simpler.
|
# internal IDs of resource providers that share some inventory for
|
||||||
# TODO(jaypipes): Consider caching this for some amount of time since
|
# each resource class requested.
|
||||||
# sharing providers generally don't change often and here we aren't
|
# TODO(jaypipes): Consider caching this for some amount of time
|
||||||
# concerned with how *much* inventory/capacity the sharing provider
|
# since sharing providers generally don't change often and here we
|
||||||
# has, only that it is sharing *some* inventory of a particular
|
# aren't concerned with how *much* inventory/capacity the sharing
|
||||||
# resource class.
|
# provider has, only that it is sharing *some* inventory of a
|
||||||
|
# particular resource class.
|
||||||
sharing_providers = {
|
sharing_providers = {
|
||||||
rc_id: _get_providers_with_shared_capacity(context, rc_id, amount)
|
rc_id: _get_providers_with_shared_capacity(context, rc_id,
|
||||||
|
amount)
|
||||||
for rc_id, amount in resources.items()
|
for rc_id, amount in resources.items()
|
||||||
}
|
}
|
||||||
have_sharing = any(sharing_providers.values())
|
# We check this here as an optimization: if we have no sharing
|
||||||
if not have_sharing:
|
# providers, we fall through to the (simpler, more efficient)
|
||||||
# We know there's no sharing providers, so we can more efficiently
|
# algorithm below.
|
||||||
# get a list of resource provider IDs that have ALL the requested
|
if any(sharing_providers.values()):
|
||||||
# resources and more efficiently construct the allocation requests
|
# Okay, we have to do it the hard way: the request may be
|
||||||
# NOTE(jaypipes): When we start handling nested providers, we may
|
# satisfied by one or more sharing providers as well as (maybe)
|
||||||
# add new code paths or modify this code path to return root
|
# the non-sharing provider.
|
||||||
# provider IDs of provider trees instead of the resource provider
|
|
||||||
# IDs.
|
|
||||||
rp_ids = _get_provider_ids_matching(context, resources,
|
|
||||||
required_trait_map,
|
|
||||||
forbidden_trait_map,
|
|
||||||
member_of)
|
|
||||||
alloc_request_objs, summary_objs = (
|
|
||||||
_alloc_candidates_single_provider(context, resources, rp_ids))
|
|
||||||
else:
|
|
||||||
if required_trait_map:
|
if required_trait_map:
|
||||||
# TODO(cdent): Now that there is also a forbidden_trait_map
|
# TODO(cdent): Now that there is also a forbidden_trait_map
|
||||||
# it should be possible to further optimize this attempt at
|
# it should be possible to further optimize this attempt at
|
||||||
# a quick return, but we leave that to future patches for now.
|
# a quick return, but we leave that to future patches for
|
||||||
|
# now.
|
||||||
trait_rps = _get_provider_ids_having_any_trait(
|
trait_rps = _get_provider_ids_having_any_trait(
|
||||||
context, required_trait_map)
|
context, required_trait_map)
|
||||||
if not trait_rps:
|
if not trait_rps:
|
||||||
@ -3910,22 +4164,64 @@ class AllocationCandidates(base.VersionedObject):
|
|||||||
# required traits, just exit early...
|
# required traits, just exit early...
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
# rp_ids contains a list of resource provider IDs that EITHER have
|
# rp_ids contains a list of resource provider IDs that EITHER
|
||||||
# all the requested resources themselves OR have some resources
|
# have all the requested resources themselves OR have some
|
||||||
# and are related to a provider that is sharing some resources
|
# resources and are related to a provider that is sharing some
|
||||||
# with it. In other words, this is the list of resource provider
|
# resources with it. In other words, this is the list of
|
||||||
# IDs that are NOT sharing resources.
|
# resource provider IDs that are NOT sharing resources.
|
||||||
rps = _get_all_with_shared(context, resources, member_of)
|
rps = _get_all_with_shared(context, resources, member_of)
|
||||||
rp_ids = set([r[0] for r in rps])
|
rp_ids = set([r[0] for r in rps])
|
||||||
alloc_request_objs, summary_objs = _alloc_candidates_with_shared(
|
return _alloc_candidates_with_shared(
|
||||||
context, resources, required_trait_map, forbidden_trait_map,
|
context, resources, required_trait_map,
|
||||||
rp_ids, sharing_providers)
|
forbidden_trait_map, rp_ids, sharing_providers)
|
||||||
|
|
||||||
|
# Either we are processing a single-RP request group, or there are no
|
||||||
|
# sharing providers that (help) satisfy the request. Get a list of
|
||||||
|
# resource provider IDs that have ALL the requested resources and more
|
||||||
|
# efficiently construct the allocation requests.
|
||||||
|
# NOTE(jaypipes): When we start handling nested providers, we may
|
||||||
|
# add new code paths or modify this code path to return root
|
||||||
|
# provider IDs of provider trees instead of the resource provider
|
||||||
|
# IDs.
|
||||||
|
rp_ids = _get_provider_ids_matching(context, resources,
|
||||||
|
required_trait_map,
|
||||||
|
forbidden_trait_map, member_of)
|
||||||
|
return _alloc_candidates_single_provider(context, resources, rp_ids)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
# TODO(efried): This is only a writer context because it accesses the
|
||||||
|
# resource_providers table via ResourceProvider.get_by_uuid, which does
|
||||||
|
# data migration to populate the root_provider_uuid. Change this back to a
|
||||||
|
# reader when that migration is no longer happening.
|
||||||
|
@db_api.api_context_manager.writer
|
||||||
|
def _get_by_requests(cls, context, requests, limit=None,
|
||||||
|
group_policy=None):
|
||||||
|
candidates = {}
|
||||||
|
for suffix, request in requests.items():
|
||||||
|
alloc_reqs, summaries = cls._get_by_one_request(context, request)
|
||||||
|
if not alloc_reqs:
|
||||||
|
# Shortcut: If any one request resulted in no candidates, the
|
||||||
|
# whole operation is shot.
|
||||||
|
return [], []
|
||||||
|
# Mark each allocation request according to whether its
|
||||||
|
# corresponding RequestGroup required it to be restricted to a
|
||||||
|
# single provider. We'll need this later to evaluate group_policy.
|
||||||
|
for areq in alloc_reqs:
|
||||||
|
areq.use_same_provider = request.use_same_provider
|
||||||
|
candidates[suffix] = alloc_reqs, summaries
|
||||||
|
|
||||||
|
# At this point, each (alloc_requests, summary_obj) in `candidates` is
|
||||||
|
# independent of the others. We need to fold them together such that
|
||||||
|
# each allocation request satisfies *all* the incoming `requests`. The
|
||||||
|
# `candidates` dict is guaranteed to contain entries for all suffixes,
|
||||||
|
# or we would have short-circuited above.
|
||||||
|
alloc_request_objs, summary_objs = _merge_candidates(
|
||||||
|
candidates, group_policy=group_policy)
|
||||||
|
|
||||||
# Limit the number of allocation request objects. We do this after
|
# Limit the number of allocation request objects. We do this after
|
||||||
# creating all of them so that we can do a random slice without
|
# creating all of them so that we can do a random slice without
|
||||||
# needing to mess with the complex sql above or add additional
|
# needing to mess with the complex sql above or add additional
|
||||||
# columns to the DB.
|
# columns to the DB.
|
||||||
|
|
||||||
if limit and limit <= len(alloc_request_objs):
|
if limit and limit <= len(alloc_request_objs):
|
||||||
if CONF.placement.randomize_allocation_candidates:
|
if CONF.placement.randomize_allocation_candidates:
|
||||||
alloc_request_objs = random.sample(alloc_request_objs, limit)
|
alloc_request_objs = random.sample(alloc_request_objs, limit)
|
||||||
|
@ -292,3 +292,34 @@ the resource providers that are associated with BOTH agg1 and agg2. Issuing a
|
|||||||
request for ``GET /resource_providers?member_of=in:agg1,agg2&member_of=agg3``
|
request for ``GET /resource_providers?member_of=in:agg1,agg2&member_of=agg3``
|
||||||
means get the resource providers that are associated with agg3 and are also
|
means get the resource providers that are associated with agg3 and are also
|
||||||
associated with *any of* (agg1, agg2).
|
associated with *any of* (agg1, agg2).
|
||||||
|
|
||||||
|
1.25 Granular resource requests to ``GET /allocation_candidates``
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
|
||||||
|
``GET /allocation_candidates`` is enhanced to accept numbered groupings of
|
||||||
|
resource, required/forbidden trait, and aggregate association requests. A
|
||||||
|
``resources`` query parameter key with a positive integer suffix (e.g.
|
||||||
|
``resources42``) will be logically associated with ``required`` and/or
|
||||||
|
``member_of`` query parameter keys with the same suffix (e.g. ``required42``,
|
||||||
|
``member_of42``). The resources, required/forbidden traits, and aggregate
|
||||||
|
associations in that group will be satisfied by the same resource provider in
|
||||||
|
the response. When more than one numbered grouping is supplied, the
|
||||||
|
``group_policy`` query parameter is required to indicate how the groups should
|
||||||
|
interact. With ``group_policy=none``, separate groupings - numbered or
|
||||||
|
unnumbered - may or may not be satisfied by the same provider. With
|
||||||
|
``group_policy=isolate``, numbered groups are guaranteed to be satisfied by
|
||||||
|
*different* providers - though there may still be overlap with the unnumbered
|
||||||
|
group. In all cases, each ``allocation_request`` will be satisfied by providers
|
||||||
|
in a single non-sharing provider tree and/or sharing providers associated via
|
||||||
|
aggregate with any of the providers in that tree.
|
||||||
|
|
||||||
|
The ``required`` and ``member_of`` query parameters for a given group are
|
||||||
|
optional. That is, you may specify ``resources42=XXX`` without a corresponding
|
||||||
|
``required42=YYY`` or ``member_of42=ZZZ``. However, the reverse (specifying
|
||||||
|
``required42=YYY`` or ``member_of42=ZZZ`` without ``resources42=XXX``) will
|
||||||
|
result in an error.
|
||||||
|
|
||||||
|
The semantic of the (unnumbered) ``resources``, ``required``, and ``member_of``
|
||||||
|
query parameters is unchanged: the resources, traits, and aggregate
|
||||||
|
associations specified thereby may be satisfied by any provider in the same
|
||||||
|
non-sharing tree or associated via the specified aggregate(s).
|
||||||
|
@ -52,3 +52,27 @@ GET_SCHEMA_1_21 = copy.deepcopy(GET_SCHEMA_1_17)
|
|||||||
GET_SCHEMA_1_21['properties']['member_of'] = {
|
GET_SCHEMA_1_21['properties']['member_of'] = {
|
||||||
"type": ["string"]
|
"type": ["string"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GET_SCHEMA_1_25 = copy.deepcopy(GET_SCHEMA_1_21)
|
||||||
|
# We're going to *replace* 'resources', 'required', and 'member_of'.
|
||||||
|
del GET_SCHEMA_1_25["properties"]["resources"]
|
||||||
|
del GET_SCHEMA_1_25["required"]
|
||||||
|
del GET_SCHEMA_1_25["properties"]["required"]
|
||||||
|
del GET_SCHEMA_1_25["properties"]["member_of"]
|
||||||
|
# Pattern property key format for a numbered or un-numbered grouping
|
||||||
|
_GROUP_PAT_FMT = "^%s([1-9][0-9]*)?$"
|
||||||
|
GET_SCHEMA_1_25["patternProperties"] = {
|
||||||
|
_GROUP_PAT_FMT % "resources": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
_GROUP_PAT_FMT % "required": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
_GROUP_PAT_FMT % "member_of": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
GET_SCHEMA_1_25["properties"]["group_policy"] = {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["none", "isolate"],
|
||||||
|
}
|
||||||
|
@ -426,7 +426,8 @@ def parse_qs_request_groups(req):
|
|||||||
are only processed if ``allow_forbidden`` is True. This allows the
|
are only processed if ``allow_forbidden`` is True. This allows the
|
||||||
caller to control processing based on microversion handling.
|
caller to control processing based on microversion handling.
|
||||||
|
|
||||||
The return is a list of these RequestGroup instances.
|
The return is a dict, keyed by the numeric suffix of these RequestGroup
|
||||||
|
instances (or the empty string for the unnumbered group).
|
||||||
|
|
||||||
As an example, if qsdict represents the query string:
|
As an example, if qsdict represents the query string:
|
||||||
|
|
||||||
@ -440,7 +441,7 @@ def parse_qs_request_groups(req):
|
|||||||
|
|
||||||
...the return value will be:
|
...the return value will be:
|
||||||
|
|
||||||
[ RequestGroup(
|
{ '': RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
"VCPU": 2,
|
"VCPU": 2,
|
||||||
@ -452,11 +453,12 @@ def parse_qs_request_groups(req):
|
|||||||
"CUSTOM_STORAGE_RAID",
|
"CUSTOM_STORAGE_RAID",
|
||||||
],
|
],
|
||||||
member_of=[
|
member_of=[
|
||||||
9323b2b1-82c9-4e91-bdff-e95e808ef954,
|
[9323b2b1-82c9-4e91-bdff-e95e808ef954],
|
||||||
8592a199-7d73-4465-8df6-ab00a6243c82,
|
[8592a199-7d73-4465-8df6-ab00a6243c82,
|
||||||
|
ddbd9226-d6a6-475e-a85f-0609914dd058],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
RequestGroup(
|
'1': RequestGroup(
|
||||||
use_same_provider=True,
|
use_same_provider=True,
|
||||||
resources={
|
resources={
|
||||||
"SRIOV_NET_VF": 2,
|
"SRIOV_NET_VF": 2,
|
||||||
@ -466,7 +468,7 @@ def parse_qs_request_groups(req):
|
|||||||
"CUSTOM_SWITCH_A",
|
"CUSTOM_SWITCH_A",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
RequestGroup(
|
'2': RequestGroup(
|
||||||
use_same_provider=True,
|
use_same_provider=True,
|
||||||
resources={
|
resources={
|
||||||
"SRIOV_NET_VF": 1,
|
"SRIOV_NET_VF": 1,
|
||||||
@ -475,7 +477,7 @@ def parse_qs_request_groups(req):
|
|||||||
"CUSTOM_PHYSNET_PUBLIC",
|
"CUSTOM_PHYSNET_PUBLIC",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
}
|
||||||
|
|
||||||
:param req: webob.Request object
|
:param req: webob.Request object
|
||||||
:return: A list of RequestGroup instances.
|
:return: A list of RequestGroup instances.
|
||||||
@ -533,8 +535,18 @@ def parse_qs_request_groups(req):
|
|||||||
if orphans:
|
if orphans:
|
||||||
msg = _('All member_of parameters must be associated with '
|
msg = _('All member_of parameters must be associated with '
|
||||||
'resources. Found the following orphaned member_of '
|
'resources. Found the following orphaned member_of '
|
||||||
' values: %s')
|
'keys: %s')
|
||||||
raise webob.exc.HTTPBadRequest(msg % ', '.join(orphans))
|
raise webob.exc.HTTPBadRequest(msg % ', '.join(orphans))
|
||||||
|
# All request groups must have resources (which is almost, but not quite,
|
||||||
|
# verified by the orphan checks above).
|
||||||
|
if not all(grp.resources for grp in by_suffix.values()):
|
||||||
|
msg = _("All request groups must specify resources.")
|
||||||
|
raise webob.exc.HTTPBadRequest(msg)
|
||||||
|
# The above would still pass if there were no request groups
|
||||||
|
if not by_suffix:
|
||||||
|
msg = _("At least one request group (`resources` or `resources{N}`) "
|
||||||
|
"is required.")
|
||||||
|
raise webob.exc.HTTPBadRequest(msg)
|
||||||
|
|
||||||
# Make adjustments for forbidden traits by stripping forbidden out
|
# Make adjustments for forbidden traits by stripping forbidden out
|
||||||
# of required.
|
# of required.
|
||||||
@ -555,6 +567,4 @@ def parse_qs_request_groups(req):
|
|||||||
'following traits keys: %s')
|
'following traits keys: %s')
|
||||||
raise webob.exc.HTTPBadRequest(msg % ', '.join(conflicting_traits))
|
raise webob.exc.HTTPBadRequest(msg % ', '.join(conflicting_traits))
|
||||||
|
|
||||||
# NOTE(efried): The sorting is not necessary for the API, but it makes
|
return by_suffix
|
||||||
# testing easier.
|
|
||||||
return [by_suffix[suff] for suff in sorted(by_suffix)]
|
|
||||||
|
@ -264,9 +264,9 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
|
|
||||||
def _get_allocation_candidates(self, requests=None, limit=None):
|
def _get_allocation_candidates(self, requests=None, limit=None):
|
||||||
if requests is None:
|
if requests is None:
|
||||||
requests = [placement_lib.RequestGroup(
|
requests = {'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources)]
|
resources=self.requested_resources)}
|
||||||
return rp_obj.AllocationCandidates.get_by_requests(self.ctx, requests,
|
return rp_obj.AllocationCandidates.get_by_requests(self.ctx, requests,
|
||||||
limit)
|
limit)
|
||||||
|
|
||||||
@ -363,18 +363,11 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
|
|
||||||
self.assertEqual(expected, observed)
|
self.assertEqual(expected, observed)
|
||||||
|
|
||||||
def test_no_resources_in_first_request_group(self):
|
|
||||||
requests = [placement_lib.RequestGroup(use_same_provider=False,
|
|
||||||
resources={})]
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
rp_obj.AllocationCandidates.get_by_requests,
|
|
||||||
self.ctx, requests)
|
|
||||||
|
|
||||||
def test_unknown_traits(self):
|
def test_unknown_traits(self):
|
||||||
missing = set(['UNKNOWN_TRAIT'])
|
missing = set(['UNKNOWN_TRAIT'])
|
||||||
requests = [placement_lib.RequestGroup(
|
requests = {'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False, resources=self.requested_resources,
|
use_same_provider=False, resources=self.requested_resources,
|
||||||
required_traits=missing)]
|
required_traits=missing)}
|
||||||
self.assertRaises(exception.TraitNotFound,
|
self.assertRaises(exception.TraitNotFound,
|
||||||
rp_obj.AllocationCandidates.get_by_requests,
|
rp_obj.AllocationCandidates.get_by_requests,
|
||||||
self.ctx, requests)
|
self.ctx, requests)
|
||||||
@ -389,13 +382,13 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.add_inventory(cn1, fields.ResourceClass.MEMORY_MB, 2048)
|
tb.add_inventory(cn1, fields.ResourceClass.MEMORY_MB, 2048)
|
||||||
tb.add_inventory(cn1, fields.ResourceClass.DISK_GB, 2000)
|
tb.add_inventory(cn1, fields.ResourceClass.DISK_GB, 2000)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
fields.ResourceClass.VCPU: 1
|
fields.ResourceClass.VCPU: 1
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -476,12 +469,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# Now let's add traits into the mix. Currently, none of the compute
|
# Now let's add traits into the mix. Currently, none of the compute
|
||||||
# nodes has the AVX2 trait associated with it, so we should get 0
|
# nodes has the AVX2 trait associated with it, so we should get 0
|
||||||
# results if we required AVX2
|
# results if we required AVX2
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2])
|
required_traits=set([os_traits.HW_CPU_X86_AVX2])
|
||||||
)],
|
)},
|
||||||
)
|
)
|
||||||
self._validate_allocation_requests([], alloc_cands)
|
self._validate_allocation_requests([], alloc_cands)
|
||||||
|
|
||||||
@ -489,12 +482,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# get back just that compute node in the provider summaries
|
# get back just that compute node in the provider summaries
|
||||||
tb.set_traits(cn2, 'HW_CPU_X86_AVX2')
|
tb.set_traits(cn2, 'HW_CPU_X86_AVX2')
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2])
|
required_traits=set([os_traits.HW_CPU_X86_AVX2])
|
||||||
)],
|
)},
|
||||||
)
|
)
|
||||||
# Only cn2 should be in our allocation requests now since that's the
|
# Only cn2 should be in our allocation requests now since that's the
|
||||||
# only one with the required trait
|
# only one with the required trait
|
||||||
@ -522,12 +515,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
self._validate_provider_summary_traits(expected, alloc_cands)
|
self._validate_provider_summary_traits(expected, alloc_cands)
|
||||||
|
|
||||||
# Confirm that forbidden traits changes the results to get cn1.
|
# Confirm that forbidden traits changes the results to get cn1.
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
forbidden_traits=set([os_traits.HW_CPU_X86_AVX2])
|
forbidden_traits=set([os_traits.HW_CPU_X86_AVX2])
|
||||||
)],
|
)},
|
||||||
)
|
)
|
||||||
expected = [
|
expected = [
|
||||||
[('cn1', fields.ResourceClass.VCPU, 1),
|
[('cn1', fields.ResourceClass.VCPU, 1),
|
||||||
@ -663,12 +656,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# #1705071, this resulted in a KeyError
|
# #1705071, this resulted in a KeyError
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates(
|
alloc_cands = self._get_allocation_candidates(
|
||||||
requests=[placement_lib.RequestGroup(
|
requests={'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'DISK_GB': 10,
|
'DISK_GB': 10,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# We should only have provider summary information for the sharing
|
# We should only have provider summary information for the sharing
|
||||||
@ -693,12 +686,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# Now we're going to add a set of required traits into the request mix.
|
# Now we're going to add a set of required traits into the request mix.
|
||||||
# To start off, let's request a required trait that we know has not
|
# To start off, let's request a required trait that we know has not
|
||||||
# been associated yet with any provider, and ensure we get no results
|
# been associated yet with any provider, and ensure we get no results
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# We have not yet associated the AVX2 trait to any provider, so we
|
# We have not yet associated the AVX2 trait to any provider, so we
|
||||||
@ -713,12 +706,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
cn1.set_traits([avx2_t])
|
cn1.set_traits([avx2_t])
|
||||||
cn2.set_traits([avx2_t])
|
cn2.set_traits([avx2_t])
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# There should be 2 compute node providers and 1 shared storage
|
# There should be 2 compute node providers and 1 shared storage
|
||||||
@ -749,12 +742,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
self._validate_provider_summary_traits(expected, alloc_cands)
|
self._validate_provider_summary_traits(expected, alloc_cands)
|
||||||
|
|
||||||
# Forbid the AVX2 trait
|
# Forbid the AVX2 trait
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
forbidden_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
forbidden_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
# Should be no results as both cn1 and cn2 have the trait.
|
# Should be no results as both cn1 and cn2 have the trait.
|
||||||
expected = []
|
expected = []
|
||||||
@ -763,13 +756,13 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# Require the AVX2 trait but forbid CUSTOM_EXTRA_FASTER, which is
|
# Require the AVX2 trait but forbid CUSTOM_EXTRA_FASTER, which is
|
||||||
# added to cn2
|
# added to cn2
|
||||||
tb.set_traits(cn2, 'CUSTOM_EXTRA_FASTER')
|
tb.set_traits(cn2, 'CUSTOM_EXTRA_FASTER')
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
forbidden_traits=set(['CUSTOM_EXTRA_FASTER']),
|
forbidden_traits=set(['CUSTOM_EXTRA_FASTER']),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
expected = [
|
expected = [
|
||||||
[('cn1', fields.ResourceClass.VCPU, 1),
|
[('cn1', fields.ResourceClass.VCPU, 1),
|
||||||
@ -782,13 +775,13 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# This should result in getting only cn1.
|
# This should result in getting only cn1.
|
||||||
tb.add_inventory(cn1, fields.ResourceClass.DISK_GB, 2048,
|
tb.add_inventory(cn1, fields.ResourceClass.DISK_GB, 2048,
|
||||||
allocation_ratio=1.5)
|
allocation_ratio=1.5)
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
forbidden_traits=set(['MISC_SHARES_VIA_AGGREGATE']),
|
forbidden_traits=set(['MISC_SHARES_VIA_AGGREGATE']),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
expected = [
|
expected = [
|
||||||
[('cn1', fields.ResourceClass.VCPU, 1),
|
[('cn1', fields.ResourceClass.VCPU, 1),
|
||||||
@ -843,8 +836,8 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates(
|
alloc_cands = self._get_allocation_candidates(
|
||||||
requests=[placement_lib.RequestGroup(
|
requests={'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False, resources=requested_resources)])
|
use_same_provider=False, resources=requested_resources)})
|
||||||
|
|
||||||
# Verify the allocation requests that are returned. There should be 2
|
# Verify the allocation requests that are returned. There should be 2
|
||||||
# allocation requests, one for each compute node, containing 3
|
# allocation requests, one for each compute node, containing 3
|
||||||
@ -946,12 +939,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# Now we're going to add a set of required traits into the request mix.
|
# Now we're going to add a set of required traits into the request mix.
|
||||||
# To start off, let's request a required trait that we know has not
|
# To start off, let's request a required trait that we know has not
|
||||||
# been associated yet with any provider, and ensure we get no results
|
# been associated yet with any provider, and ensure we get no results
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# We have not yet associated the AVX2 trait to any provider, so we
|
# We have not yet associated the AVX2 trait to any provider, so we
|
||||||
@ -967,12 +960,12 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
for cn in (cn1, cn2, cn3):
|
for cn in (cn1, cn2, cn3):
|
||||||
tb.set_traits(cn, os_traits.HW_CPU_X86_AVX2)
|
tb.set_traits(cn, os_traits.HW_CPU_X86_AVX2)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates(requests=[
|
alloc_cands = self._get_allocation_candidates(requests={'':
|
||||||
placement_lib.RequestGroup(
|
placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
required_traits=set([os_traits.HW_CPU_X86_AVX2]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# There should be 3 compute node providers and 1 shared storage
|
# There should be 3 compute node providers and 1 shared storage
|
||||||
@ -1015,14 +1008,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(cn3, os_traits.HW_CPU_X86_AVX2,
|
tb.set_traits(cn3, os_traits.HW_CPU_X86_AVX2,
|
||||||
os_traits.STORAGE_DISK_SSD)
|
os_traits.STORAGE_DISK_SSD)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
|
{'':
|
||||||
placement_lib.RequestGroup(
|
placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set([
|
required_traits=set([
|
||||||
os_traits.HW_CPU_X86_AVX2, os_traits.STORAGE_DISK_SSD
|
os_traits.HW_CPU_X86_AVX2, os_traits.STORAGE_DISK_SSD
|
||||||
]),
|
]),
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# There should be only cn3 in the returned allocation candidates
|
# There should be only cn3 in the returned allocation candidates
|
||||||
@ -1103,13 +1097,13 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# The shared storage's disk is RAID
|
# The shared storage's disk is RAID
|
||||||
tb.set_traits(ss, 'MISC_SHARES_VIA_AGGREGATE', 'CUSTOM_RAID')
|
tb.set_traits(ss, 'MISC_SHARES_VIA_AGGREGATE', 'CUSTOM_RAID')
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources=self.requested_resources,
|
resources=self.requested_resources,
|
||||||
required_traits=set(['HW_CPU_X86_SSE', 'STORAGE_DISK_SSD',
|
required_traits=set(['HW_CPU_X86_SSE', 'STORAGE_DISK_SSD',
|
||||||
'CUSTOM_RAID'])
|
'CUSTOM_RAID'])
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO(efried): Bug #1724633: we'd *like* to get no candidates, because
|
# TODO(efried): Bug #1724633: we'd *like* to get no candidates, because
|
||||||
@ -1128,7 +1122,10 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
'cn': set([
|
'cn': set([
|
||||||
(fields.ResourceClass.VCPU, 24, 0),
|
(fields.ResourceClass.VCPU, 24, 0),
|
||||||
(fields.ResourceClass.MEMORY_MB, 2048, 0),
|
(fields.ResourceClass.MEMORY_MB, 2048, 0),
|
||||||
(fields.ResourceClass.DISK_GB, 1600, 0),
|
# NOTE(efried): We don't (yet) populate provider summaries with
|
||||||
|
# provider resources that aren't part of the result. With
|
||||||
|
# blueprint placement-return-all-requests, uncomment this line:
|
||||||
|
# (fields.ResourceClass.DISK_GB, 1600, 0),
|
||||||
]),
|
]),
|
||||||
'ss': set([
|
'ss': set([
|
||||||
(fields.ResourceClass.DISK_GB, 1600, 0),
|
(fields.ResourceClass.DISK_GB, 1600, 0),
|
||||||
@ -1143,15 +1140,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.add_inventory(ss1, fields.ResourceClass.SRIOV_NET_VF, 16)
|
tb.add_inventory(ss1, fields.ResourceClass.SRIOV_NET_VF, 16)
|
||||||
tb.add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)
|
tb.add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'IPV4_ADDRESS': 2,
|
'IPV4_ADDRESS': 2,
|
||||||
'SRIOV_NET_VF': 1,
|
'SRIOV_NET_VF': 1,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -1179,14 +1176,14 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
||||||
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'IPV4_ADDRESS': 2,
|
'IPV4_ADDRESS': 2,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -1215,15 +1212,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
||||||
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'IPV4_ADDRESS': 2,
|
'IPV4_ADDRESS': 2,
|
||||||
'SRIOV_NET_VF': 1,
|
'SRIOV_NET_VF': 1,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -1255,15 +1252,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
tb.set_traits(ss2, "MISC_SHARES_VIA_AGGREGATE")
|
||||||
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
tb.add_inventory(ss2, fields.ResourceClass.DISK_GB, 1600)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates(requests=[
|
alloc_cands = self._get_allocation_candidates(requests={
|
||||||
placement_lib.RequestGroup(
|
'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'IPV4_ADDRESS': 2,
|
'IPV4_ADDRESS': 2,
|
||||||
'SRIOV_NET_VF': 1,
|
'SRIOV_NET_VF': 1,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# We expect two candidates: one that gets all the resources from ss1;
|
# We expect two candidates: one that gets all the resources from ss1;
|
||||||
@ -1311,14 +1308,14 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(ss1, "MISC_SHARES_VIA_AGGREGATE")
|
tb.set_traits(ss1, "MISC_SHARES_VIA_AGGREGATE")
|
||||||
tb.add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)
|
tb.add_inventory(ss1, fields.ResourceClass.DISK_GB, 1600)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'VCPU': 2,
|
'VCPU': 2,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
expected = [
|
expected = [
|
||||||
[('cn1', fields.ResourceClass.VCPU, 2),
|
[('cn1', fields.ResourceClass.VCPU, 2),
|
||||||
@ -1370,15 +1367,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.set_traits(ss3, "MISC_SHARES_VIA_AGGREGATE")
|
tb.set_traits(ss3, "MISC_SHARES_VIA_AGGREGATE")
|
||||||
tb.add_inventory(ss3, fields.ResourceClass.IPV4_ADDRESS, 24)
|
tb.add_inventory(ss3, fields.ResourceClass.IPV4_ADDRESS, 24)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
'VCPU': 2,
|
'VCPU': 2,
|
||||||
'DISK_GB': 1500,
|
'DISK_GB': 1500,
|
||||||
'IPV4_ADDRESS': 2,
|
'IPV4_ADDRESS': 2,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -1613,15 +1610,15 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
tb.add_inventory(pf1, fields.ResourceClass.SRIOV_NET_VF, 8)
|
tb.add_inventory(pf1, fields.ResourceClass.SRIOV_NET_VF, 8)
|
||||||
tb.set_traits(pf1, os_traits.HW_NIC_OFFLOAD_GENEVE)
|
tb.set_traits(pf1, os_traits.HW_NIC_OFFLOAD_GENEVE)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
fields.ResourceClass.VCPU: 2,
|
fields.ResourceClass.VCPU: 2,
|
||||||
fields.ResourceClass.MEMORY_MB: 256,
|
fields.ResourceClass.MEMORY_MB: 256,
|
||||||
fields.ResourceClass.SRIOV_NET_VF: 1,
|
fields.ResourceClass.SRIOV_NET_VF: 1,
|
||||||
}
|
}
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO(jaypipes): This should be the following once nested providers
|
# TODO(jaypipes): This should be the following once nested providers
|
||||||
@ -1671,8 +1668,8 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# Now add required traits to the mix and verify we still get the same
|
# Now add required traits to the mix and verify we still get the same
|
||||||
# result (since we haven't yet consumed the second physical function's
|
# result (since we haven't yet consumed the second physical function's
|
||||||
# inventory of SRIOV_NET_VF.
|
# inventory of SRIOV_NET_VF.
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
placement_lib.RequestGroup(
|
{'': placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
fields.ResourceClass.VCPU: 2,
|
fields.ResourceClass.VCPU: 2,
|
||||||
@ -1680,7 +1677,7 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
fields.ResourceClass.SRIOV_NET_VF: 1,
|
fields.ResourceClass.SRIOV_NET_VF: 1,
|
||||||
},
|
},
|
||||||
required_traits=[os_traits.HW_NIC_OFFLOAD_GENEVE],
|
required_traits=[os_traits.HW_NIC_OFFLOAD_GENEVE],
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO(jaypipes): This should be the following once nested providers
|
# TODO(jaypipes): This should be the following once nested providers
|
||||||
@ -1730,7 +1727,8 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
# function with the required trait no longer has any inventory.
|
# function with the required trait no longer has any inventory.
|
||||||
tb.allocate_from_provider(pf1, fields.ResourceClass.SRIOV_NET_VF, 8)
|
tb.allocate_from_provider(pf1, fields.ResourceClass.SRIOV_NET_VF, 8)
|
||||||
|
|
||||||
alloc_cands = self._get_allocation_candidates([
|
alloc_cands = self._get_allocation_candidates(
|
||||||
|
{'':
|
||||||
placement_lib.RequestGroup(
|
placement_lib.RequestGroup(
|
||||||
use_same_provider=False,
|
use_same_provider=False,
|
||||||
resources={
|
resources={
|
||||||
@ -1739,7 +1737,7 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
|
|||||||
fields.ResourceClass.SRIOV_NET_VF: 1,
|
fields.ResourceClass.SRIOV_NET_VF: 1,
|
||||||
},
|
},
|
||||||
required_traits=[os_traits.HW_NIC_OFFLOAD_GENEVE],
|
required_traits=[os_traits.HW_NIC_OFFLOAD_GENEVE],
|
||||||
)]
|
)}
|
||||||
)
|
)
|
||||||
|
|
||||||
self._validate_allocation_requests([], alloc_cands)
|
self._validate_allocation_requests([], alloc_cands)
|
||||||
|
@ -17,11 +17,13 @@ from oslo_middleware import cors
|
|||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from nova.api.openstack.placement import deploy
|
from nova.api.openstack.placement import deploy
|
||||||
|
from nova.api.openstack.placement import exception
|
||||||
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
from nova.api.openstack.placement.objects import resource_provider as rp_obj
|
||||||
from nova import conf
|
from nova import conf
|
||||||
from nova import config
|
from nova import config
|
||||||
from nova import context
|
from nova import context
|
||||||
from nova.tests import fixtures
|
from nova.tests import fixtures
|
||||||
|
from nova.tests import uuidsentinel as uuids
|
||||||
|
|
||||||
|
|
||||||
CONF = conf.CONF
|
CONF = conf.CONF
|
||||||
@ -390,3 +392,125 @@ class CORSFixture(APIFixture):
|
|||||||
# wants to load the CORS middleware, it will not.
|
# wants to load the CORS middleware, it will not.
|
||||||
self.conf.set_override('allowed_origin', 'http://valid.example.com',
|
self.conf.set_override('allowed_origin', 'http://valid.example.com',
|
||||||
group='cors')
|
group='cors')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(efried): Common with test_allocation_candidates
|
||||||
|
def _add_inventory(rp, rc, total, **kwargs):
|
||||||
|
kwargs.setdefault('max_unit', total)
|
||||||
|
inv = rp_obj.Inventory(rp._context, resource_provider=rp,
|
||||||
|
resource_class=rc, total=total, **kwargs)
|
||||||
|
inv.obj_set_defaults()
|
||||||
|
rp.add_inventory(inv)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(efried): Common with test_allocation_candidates
|
||||||
|
def _set_traits(rp, *traits):
|
||||||
|
tlist = []
|
||||||
|
for tname in traits:
|
||||||
|
try:
|
||||||
|
trait = rp_obj.Trait.get_by_name(rp._context, tname)
|
||||||
|
except exception.TraitNotFound:
|
||||||
|
trait = rp_obj.Trait(rp._context, name=tname)
|
||||||
|
trait.create()
|
||||||
|
tlist.append(trait)
|
||||||
|
rp.set_traits(rp_obj.TraitList(objects=tlist))
|
||||||
|
|
||||||
|
|
||||||
|
class GranularFixture(APIFixture):
|
||||||
|
"""An APIFixture that sets up the following provider environment for
|
||||||
|
testing granular resource requests.
|
||||||
|
|
||||||
|
+========================++========================++========================+
|
||||||
|
|cn_left ||cn_middle ||cn_right |
|
||||||
|
|VCPU: 8 ||VCPU: 8 ||VCPU: 8 |
|
||||||
|
|MEMORY_MB: 4096 ||MEMORY_MB: 4096 ||MEMORY_MB: 4096 |
|
||||||
|
|DISK_GB: 500 ||SRIOV_NET_VF: 8 ||DISK_GB: 500 |
|
||||||
|
|VGPU: 8 ||CUSTOM_NET_MBPS: 4000 ||VGPU: 8 |
|
||||||
|
|SRIOV_NET_VF: 8 ||traits: HW_CPU_X86_AVX, || - max_unit: 2 |
|
||||||
|
|CUSTOM_NET_MBPS: 4000 || HW_CPU_X86_AVX2,||traits: HW_CPU_X86_MMX, |
|
||||||
|
|traits: HW_CPU_X86_AVX, || HW_CPU_X86_SSE, || HW_GPU_API_DXVA,|
|
||||||
|
| HW_CPU_X86_AVX2,|| HW_NIC_ACCEL_TLS|| CUSTOM_DISK_SSD,|
|
||||||
|
| HW_GPU_API_DXVA,|+=+=====+================++==+========+============+
|
||||||
|
| HW_NIC_DCB_PFC, | : : : : a
|
||||||
|
| CUSTOM_FOO +..+ +--------------------+ : g
|
||||||
|
+========================+ : a : : g
|
||||||
|
: g : : C
|
||||||
|
+========================+ : g : +===============+======+
|
||||||
|
|shr_disk_1 | : A : |shr_net |
|
||||||
|
|DISK_GB: 1000 +..+ : |SRIOV_NET_VF: 16 |
|
||||||
|
|traits: CUSTOM_DISK_SSD,| : : a |CUSTOM_NET_MBPS: 40000|
|
||||||
|
| MISC_SHARES_VIA_AGG...| : : g |traits: MISC_SHARES...|
|
||||||
|
+========================+ : : g +======================+
|
||||||
|
+=======================+ : : B
|
||||||
|
|shr_disk_2 +...+ :
|
||||||
|
|DISK_GB: 1000 | :
|
||||||
|
|traits: MISC_SHARES... +.........+
|
||||||
|
+=======================+
|
||||||
|
"""
|
||||||
|
def _create_provider(self, name, *aggs, **kwargs):
|
||||||
|
# TODO(efried): Common with test_allocation_candidates.ProviderDBBase
|
||||||
|
parent = kwargs.get('parent')
|
||||||
|
rp = rp_obj.ResourceProvider(self.ctx, name=name,
|
||||||
|
uuid=getattr(uuids, name))
|
||||||
|
if parent:
|
||||||
|
rp.parent_provider_uuid = parent
|
||||||
|
rp.create()
|
||||||
|
if aggs:
|
||||||
|
rp.set_aggregates(aggs)
|
||||||
|
return rp
|
||||||
|
|
||||||
|
def start_fixture(self):
|
||||||
|
super(GranularFixture, self).start_fixture()
|
||||||
|
self.ctx = context.get_admin_context()
|
||||||
|
|
||||||
|
rp_obj.ResourceClass(context=self.ctx, name='CUSTOM_NET_MBPS').create()
|
||||||
|
|
||||||
|
os.environ['AGGA'] = uuids.aggA
|
||||||
|
os.environ['AGGB'] = uuids.aggB
|
||||||
|
os.environ['AGGC'] = uuids.aggC
|
||||||
|
|
||||||
|
cn_left = self._create_provider('cn_left', uuids.aggA)
|
||||||
|
os.environ['CN_LEFT'] = cn_left.uuid
|
||||||
|
_add_inventory(cn_left, 'VCPU', 8)
|
||||||
|
_add_inventory(cn_left, 'MEMORY_MB', 4096)
|
||||||
|
_add_inventory(cn_left, 'DISK_GB', 500)
|
||||||
|
_add_inventory(cn_left, 'VGPU', 8)
|
||||||
|
_add_inventory(cn_left, 'SRIOV_NET_VF', 8)
|
||||||
|
_add_inventory(cn_left, 'CUSTOM_NET_MBPS', 4000)
|
||||||
|
_set_traits(cn_left, 'HW_CPU_X86_AVX', 'HW_CPU_X86_AVX2',
|
||||||
|
'HW_GPU_API_DXVA', 'HW_NIC_DCB_PFC', 'CUSTOM_FOO')
|
||||||
|
|
||||||
|
cn_middle = self._create_provider('cn_middle', uuids.aggA, uuids.aggB)
|
||||||
|
os.environ['CN_MIDDLE'] = cn_middle.uuid
|
||||||
|
_add_inventory(cn_middle, 'VCPU', 8)
|
||||||
|
_add_inventory(cn_middle, 'MEMORY_MB', 4096)
|
||||||
|
_add_inventory(cn_middle, 'SRIOV_NET_VF', 8)
|
||||||
|
_add_inventory(cn_middle, 'CUSTOM_NET_MBPS', 4000)
|
||||||
|
_set_traits(cn_middle, 'HW_CPU_X86_AVX', 'HW_CPU_X86_AVX2',
|
||||||
|
'HW_CPU_X86_SSE', 'HW_NIC_ACCEL_TLS')
|
||||||
|
|
||||||
|
cn_right = self._create_provider('cn_right', uuids.aggB, uuids.aggC)
|
||||||
|
os.environ['CN_RIGHT'] = cn_right.uuid
|
||||||
|
_add_inventory(cn_right, 'VCPU', 8)
|
||||||
|
_add_inventory(cn_right, 'MEMORY_MB', 4096)
|
||||||
|
_add_inventory(cn_right, 'DISK_GB', 500)
|
||||||
|
_add_inventory(cn_right, 'VGPU', 8, max_unit=2)
|
||||||
|
_set_traits(cn_right, 'HW_CPU_X86_MMX', 'HW_GPU_API_DXVA',
|
||||||
|
'CUSTOM_DISK_SSD')
|
||||||
|
|
||||||
|
shr_disk_1 = self._create_provider('shr_disk_1', uuids.aggA)
|
||||||
|
os.environ['SHR_DISK_1'] = shr_disk_1.uuid
|
||||||
|
_add_inventory(shr_disk_1, 'DISK_GB', 1000)
|
||||||
|
_set_traits(shr_disk_1, 'MISC_SHARES_VIA_AGGREGATE', 'CUSTOM_DISK_SSD')
|
||||||
|
|
||||||
|
shr_disk_2 = self._create_provider(
|
||||||
|
'shr_disk_2', uuids.aggA, uuids.aggB)
|
||||||
|
os.environ['SHR_DISK_2'] = shr_disk_2.uuid
|
||||||
|
_add_inventory(shr_disk_2, 'DISK_GB', 1000)
|
||||||
|
_set_traits(shr_disk_2, 'MISC_SHARES_VIA_AGGREGATE')
|
||||||
|
|
||||||
|
shr_net = self._create_provider('shr_net', uuids.aggC)
|
||||||
|
os.environ['SHR_NET'] = shr_net.uuid
|
||||||
|
_add_inventory(shr_net, 'SRIOV_NET_VF', 16)
|
||||||
|
_add_inventory(shr_net, 'CUSTOM_NET_MBPS', 40000)
|
||||||
|
_set_traits(shr_net, 'MISC_SHARES_VIA_AGGREGATE')
|
||||||
|
@ -0,0 +1,471 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# Tests for granular resource requests
|
||||||
|
|
||||||
|
fixtures:
|
||||||
|
# See the layout diagram in this fixture's docstring in ../fixtures.py
|
||||||
|
- GranularFixture
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
request_headers:
|
||||||
|
x-auth-token: admin
|
||||||
|
content-type: application/json
|
||||||
|
accept: application/json
|
||||||
|
openstack-api-version: placement 1.25
|
||||||
|
|
||||||
|
tests:
|
||||||
|
|
||||||
|
- name: different groups hit with group_policy=none
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: MEMORY_MB:1024
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.allocation_requests.`len`: 3
|
||||||
|
$.provider_summaries.`len`: 3
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
MEMORY_MB: 1024
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_MIDDLE']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
MEMORY_MB: 1024
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
MEMORY_MB: 1024
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources:
|
||||||
|
VCPU:
|
||||||
|
capacity: 8
|
||||||
|
used: 0
|
||||||
|
MEMORY_MB:
|
||||||
|
capacity: 4096
|
||||||
|
used: 0
|
||||||
|
$.provider_summaries["$ENVIRON['CN_MIDDLE']"].resources:
|
||||||
|
VCPU:
|
||||||
|
capacity: 8
|
||||||
|
used: 0
|
||||||
|
MEMORY_MB:
|
||||||
|
capacity: 4096
|
||||||
|
used: 0
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources:
|
||||||
|
VCPU:
|
||||||
|
capacity: 8
|
||||||
|
used: 0
|
||||||
|
MEMORY_MB:
|
||||||
|
capacity: 4096
|
||||||
|
used: 0
|
||||||
|
|
||||||
|
- name: different groups miss with group_policy=isolate
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: MEMORY_MB:1024
|
||||||
|
group_policy: isolate
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# We asked for VCPU and MEMORY_MB to be satisfied by *different*
|
||||||
|
# providers, because they're in separate numbered request groups and
|
||||||
|
# group_policy=isolate. Since there are no sharing providers of these
|
||||||
|
# resources, we get no results.
|
||||||
|
$.allocation_requests.`len`: 0
|
||||||
|
$.provider_summaries.`len`: 0
|
||||||
|
|
||||||
|
- name: resources combine
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources: VCPU:3,MEMORY_MB:512
|
||||||
|
resources1: VCPU:1,MEMORY_MB:1024
|
||||||
|
resources2: VCPU:2
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.allocation_requests.`len`: 3
|
||||||
|
$.provider_summaries.`len`: 3
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources:
|
||||||
|
VCPU: 6
|
||||||
|
MEMORY_MB: 1536
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_MIDDLE']"].resources:
|
||||||
|
VCPU: 6
|
||||||
|
MEMORY_MB: 1536
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources:
|
||||||
|
VCPU: 6
|
||||||
|
MEMORY_MB: 1536
|
||||||
|
|
||||||
|
- name: group policy not required with only one numbered group
|
||||||
|
GET: /allocation_candidates?resources=VCPU:1&resources1=MEMORY_MB:2048
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.allocation_requests.`len`: 3
|
||||||
|
$.provider_summaries.`len`: 3
|
||||||
|
|
||||||
|
- name: disk sharing isolated
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1,MEMORY_MB:1024
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
group_policy: isolate
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# Here we've asked for VCPU and MEMORY_MB to be satisfied by the same
|
||||||
|
# provider - all three of our non-sharing providers can do that - and
|
||||||
|
# the DISK_GB to be satisfied by a *different* provider than the VCPU and
|
||||||
|
# MEMORY_MB. So we'll get all permutations where cn_* provide VCPU and
|
||||||
|
# MEMORY_MB and shr_disk_* provide the DISK_GB; but *no* results where
|
||||||
|
# DISK_GB is provided by the cn_*s themselves.
|
||||||
|
$.allocation_requests.`len`: 5
|
||||||
|
$.provider_summaries.`len`: 5
|
||||||
|
|
||||||
|
- name: disk sharing non-isolated
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1,MEMORY_MB:1024
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.allocation_requests.`len`: 7
|
||||||
|
$.provider_summaries.`len`: 5
|
||||||
|
|
||||||
|
- name: isolated ssd
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1,MEMORY_MB:1024
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
required2: CUSTOM_DISK_SSD
|
||||||
|
group_policy: isolate
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# We get candidates [cn_left + shr_disk_1] and [cn_middle + shr_disk_1]
|
||||||
|
# We don't get [cn_right + shr_disk_1] because they're not associated via aggregate.
|
||||||
|
# We don't get [cn_left/middle + shr_disk_2] because shr_disk_2 doesn't have the SSD trait
|
||||||
|
# We don't get [cn_left] or [cn_right] even though they have SSD disk because we asked to isolate
|
||||||
|
$.allocation_requests.`len`: 2
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
MEMORY_MB: 1024
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_MIDDLE']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
MEMORY_MB: 1024
|
||||||
|
# shr_disk_1 satisfies the disk for both allocation requests
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_1']"].resources[DISK_GB]: [100, 100]
|
||||||
|
$.provider_summaries.`len`: 3
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources:
|
||||||
|
VCPU:
|
||||||
|
capacity: 8
|
||||||
|
used: 0
|
||||||
|
MEMORY_MB:
|
||||||
|
capacity: 4096
|
||||||
|
used: 0
|
||||||
|
$.provider_summaries["$ENVIRON['CN_MIDDLE']"].resources:
|
||||||
|
VCPU:
|
||||||
|
capacity: 8
|
||||||
|
used: 0
|
||||||
|
MEMORY_MB:
|
||||||
|
capacity: 4096
|
||||||
|
used: 0
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_1']"].resources:
|
||||||
|
DISK_GB:
|
||||||
|
capacity: 1000
|
||||||
|
used: 0
|
||||||
|
|
||||||
|
- name: no isolation, forbid ssd
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
required2: "!CUSTOM_DISK_SSD"
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# The permutations we *don't* get are:
|
||||||
|
# cn_right by itself because it has SSD
|
||||||
|
# - anything involving shr_disk_1 because it has SSD
|
||||||
|
$.allocation_requests.`len`: 4
|
||||||
|
# We get two allocation requests involving cn_left - one where it
|
||||||
|
# satisfies the disk itself and one where shr_disk_2 provides it
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VCPU]: [1, 1]
|
||||||
|
# We get one for [cn_middle + shr_disk_2] - it doesn't have disk to provide for itself
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_MIDDLE']"].resources[VCPU]: 1
|
||||||
|
# We get one for [cn_right + shr_disk_2] - cn_right can't provide its own
|
||||||
|
# disk due to the forbidden SSD trait.
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VCPU]: 1
|
||||||
|
# shr_disk_2 satisfies the disk for three out of the four allocation
|
||||||
|
# requests (all except the one where cn_left provides for itself)
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB]: [100, 100, 100]
|
||||||
|
# Validate that we got the correct four providers in the summaries
|
||||||
|
$.provider_summaries.`len`: 4
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_MIDDLE']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB][capacity]: 1000
|
||||||
|
|
||||||
|
- name: member_of filters
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
member_of2: $ENVIRON['AGGC']
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
$.allocation_requests.`len`: 1
|
||||||
|
$.allocation_requests[0].allocations["$ENVIRON['CN_RIGHT']"].resources:
|
||||||
|
VCPU: 1
|
||||||
|
DISK_GB: 100
|
||||||
|
$.provider_summaries.`len`: 1
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[DISK_GB][capacity]: 500
|
||||||
|
|
||||||
|
- name: required, forbidden, member_of in
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
required1: "!HW_CPU_X86_SSE"
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
required2: CUSTOM_DISK_SSD
|
||||||
|
member_of2: in:$ENVIRON['AGGA'],$ENVIRON['AGGC']
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# cn_middle won't appear (forbidden SSE trait)
|
||||||
|
# shr_disk_2 won't appear (required SSD trait is absent)
|
||||||
|
# [cn_left] won't be in the results (required SSD trait is absent)
|
||||||
|
# So we'll get:
|
||||||
|
# [cn_left, shr_disk_1]
|
||||||
|
# [cn_right]
|
||||||
|
$.allocation_requests.`len`: 2
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_1']"].resources[DISK_GB]: 100
|
||||||
|
$.provider_summaries.`len`: 3
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[DISK_GB][capacity]: 500
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_1']"].resources[DISK_GB][capacity]: 1000
|
||||||
|
|
||||||
|
- name: multiple member_of
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: DISK_GB:100
|
||||||
|
member_of2: $ENVIRON['AGGA']
|
||||||
|
member_of2: in:$ENVIRON['AGGB'],$ENVIRON['AGGC']
|
||||||
|
group_policy: isolate
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# The member_of2 specifications say that the DISK_GB resource must come
|
||||||
|
# from a provider that's in aggA and also in (aggB and/or aggC). Only
|
||||||
|
# shr_disk_2 qualifies; so we'll get results anchored at cn_middle and
|
||||||
|
# cn_right. But note that we'll also get a result anchored at cn_left:
|
||||||
|
# it doesn't meet the member_of criteria, but it doesn't need to, since
|
||||||
|
# it's not providing the DISK_GB resource.
|
||||||
|
$.allocation_requests.`len`: 3
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_MIDDLE']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB]: [100, 100, 100]
|
||||||
|
$.provider_summaries.`len`: 4
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_MIDDLE']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB][capacity]: 1000
|
||||||
|
|
||||||
|
- name: multiple disks, multiple networks
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources1: VCPU:1
|
||||||
|
resources2: VGPU:1
|
||||||
|
required2: HW_GPU_API_DXVA
|
||||||
|
resources3: MEMORY_MB:1024
|
||||||
|
resources4: DISK_GB:100
|
||||||
|
required4: CUSTOM_DISK_SSD
|
||||||
|
resources5: DISK_GB:50
|
||||||
|
required5: "!CUSTOM_DISK_SSD"
|
||||||
|
resources6: SRIOV_NET_VF:1,CUSTOM_NET_MBPS:1000
|
||||||
|
resources7: SRIOV_NET_VF:2,CUSTOM_NET_MBPS:2000
|
||||||
|
group_policy: none
|
||||||
|
# Breaking it down:
|
||||||
|
# => These could come from cn_left, cn_middle, or cn_right
|
||||||
|
# ?resources1=VCPU:1
|
||||||
|
# &resources3=MEMORY_MB:1024
|
||||||
|
# => But this limits us to cn_left and cn_right
|
||||||
|
# &resources2=VGPU:1&required2=HW_GPU_API_DXVA
|
||||||
|
# => Since we're not isolating, this SSD can come from cn_right or shr_disk_1
|
||||||
|
# &resources4=DISK_GB:100&required4=CUSTOM_DISK_SSD
|
||||||
|
# => This non-SSD can come from cn_left or shr_disk_2
|
||||||
|
# &resources5=DISK_GB:50&required5=!CUSTOM_DISK_SSD
|
||||||
|
# => These VFs and bandwidth can come from cn_left or shr_net. Since cn_left
|
||||||
|
# can't be an anchor for shr_net, these will always combine.
|
||||||
|
# &resources6=SRIOV_NET_VF:1,CUSTOM_NET_MBPS:1000
|
||||||
|
# &resources7=SRIOV_NET_VF:2,CUSTOM_NET_MBPS:2000
|
||||||
|
# => If we didn't do this, the separated VCPU/MEMORY_MB/VGPU resources would
|
||||||
|
# cause us to get no results
|
||||||
|
# &group_policy=none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# We have two permutations involving cn_left.
|
||||||
|
# - One where the non-SSD is satisfied by cn_left itself
|
||||||
|
# [cn_left(VCPU:1, MEMORY_MB:1024, VGPU:1, DISK_GB:50, SRIOV_NET_VF:3, CUSTOM_NET_MBPS:3000),
|
||||||
|
# shr_disk_1(DISK_GB:100)]
|
||||||
|
# - And one where the non-SSD is satisfied by shr_disk_2
|
||||||
|
# [cn_left(VCPU:1, MEMORY_MB:1024, VGPU:1, SRIOV_NET_VF:3, CUSTOM_NET_MBPS:3000),
|
||||||
|
# shr_disk_1(DISK_GB:100),
|
||||||
|
# shr_disk_2(DISK_GB: 50)]
|
||||||
|
# There's only one result involving cn_right.
|
||||||
|
# - We must satisfy the SSD from cn_right and the non-SSD from shr_disk_2
|
||||||
|
# - We must satisfy the network stuff from shr_net
|
||||||
|
# [cn_right(VCPU:1, MEMORY_MB:1024, VGPU:1, DISK_GB:100),
|
||||||
|
# shr_disk_2(DISK_GB:50),
|
||||||
|
# shr_net(SRIOV_NET_VF:3, CUSTOM_NET_MBPS:3000)]
|
||||||
|
$.allocation_requests.`len`: 3
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VCPU]: [1, 1]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[MEMORY_MB]: [1024, 1024]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VGPU]: [1, 1]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[SRIOV_NET_VF]: [3, 3]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[CUSTOM_NET_MBPS]: [3000, 3000]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[DISK_GB]: 50
|
||||||
|
# These come from the cn_left results
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_1']"].resources[DISK_GB]: [100, 100]
|
||||||
|
# One of these comes from the second cn_left result, the other from the cn_right result
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB]: [50, 50]
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VCPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[MEMORY_MB]: 1024
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VGPU]: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[DISK_GB]: 100
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_NET']"].resources[SRIOV_NET_VF]: 3
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_NET']"].resources[CUSTOM_NET_MBPS]: 3000
|
||||||
|
# Just make sure we got the correct four providers in the summaries
|
||||||
|
$.provider_summaries.`len`: 5
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_1']"].resources[DISK_GB][capacity]: 1000
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_DISK_2']"].resources[DISK_GB][capacity]: 1000
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_NET']"].resources[SRIOV_NET_VF][capacity]: 16
|
||||||
|
|
||||||
|
- name: combining request groups exceeds capacity
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources: VCPU:2,MEMORY_MB:2048,SRIOV_NET_VF:1,CUSTOM_NET_MBPS:2000
|
||||||
|
resources1: SRIOV_NET_VF:1,CUSTOM_NET_MBPS:3000
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# CUSTOM_NET_MBPS of 2000 + 3000 = 5000 is too much for cn_left, but
|
||||||
|
# shr_net can accomodate it.
|
||||||
|
$.allocation_requests.`len`: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[VCPU]: 2
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_RIGHT']"].resources[MEMORY_MB]: 2048
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_NET']"].resources[SRIOV_NET_VF]: 2
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['SHR_NET']"].resources[CUSTOM_NET_MBPS]: 5000
|
||||||
|
$.provider_summaries.`len`: 2
|
||||||
|
$.provider_summaries["$ENVIRON['CN_RIGHT']"].resources[VCPU][capacity]: 8
|
||||||
|
$.provider_summaries["$ENVIRON['SHR_NET']"].resources[CUSTOM_NET_MBPS][capacity]: 40000
|
||||||
|
|
||||||
|
- name: combining request groups exceeds max_unit
|
||||||
|
GET: /allocation_candidates
|
||||||
|
query_parameters:
|
||||||
|
resources: VGPU:1
|
||||||
|
resources1: VGPU:1
|
||||||
|
resources2: VGPU:1
|
||||||
|
group_policy: none
|
||||||
|
status: 200
|
||||||
|
response_json_paths:
|
||||||
|
# VGPU of 1 + 1 + 1 = 3 exceeds max_unit on cn_right, but cn_left can handle it.
|
||||||
|
$.allocation_requests.`len`: 1
|
||||||
|
$.allocation_requests..allocations["$ENVIRON['CN_LEFT']"].resources[VGPU]: 3
|
||||||
|
$.provider_summaries.`len`: 1
|
||||||
|
$.provider_summaries["$ENVIRON['CN_LEFT']"].resources[VGPU][capacity]: 8
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Error scenarios
|
||||||
|
#################
|
||||||
|
- name: numbered resources bad microversion
|
||||||
|
GET: /allocation_candidates?resources=MEMORY_MB:1024&resources1=VCPU:1
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.24
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'resources1' was unexpected"
|
||||||
|
|
||||||
|
- name: numbered traits bad microversion
|
||||||
|
GET: /allocation_candidates?resources=MEMORY_MB:1024&required2=HW_CPU_X86_AVX2
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.24
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'required2' was unexpected"
|
||||||
|
|
||||||
|
- name: numbered member_of bad microversion
|
||||||
|
GET: /allocation_candidates?resources=MEMORY_MB:1024&member_of3=$ENVIRON['AGGB']
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.24
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'member_of3' was unexpected"
|
||||||
|
|
||||||
|
- name: group_policy bad microversion
|
||||||
|
GET: /allocation_candidates?resources=VCPU:1&group_policy=isolate
|
||||||
|
request_headers:
|
||||||
|
openstack-api-version: placement 1.24
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'group_policy' was unexpected"
|
||||||
|
|
||||||
|
- name: bogus numbering
|
||||||
|
GET: /allocation_candidates?resources01=VCPU:1
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'resources01' does not match any of the regexes"
|
||||||
|
|
||||||
|
- name: bogus suffix
|
||||||
|
GET: /allocation_candidates?resources1a=VCPU:1
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'resources1a' does not match any of the regexes"
|
||||||
|
|
||||||
|
- name: invalid group_policy value
|
||||||
|
GET: /allocation_candidates?resources=VCPU:1&group_policy=bogus
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- Invalid query string parameters
|
||||||
|
- "'bogus' is not one of ['none', 'isolate']"
|
||||||
|
|
||||||
|
- name: group_policy required when more than one numbered group
|
||||||
|
GET: /allocation_candidates?resources1=VCPU:1&resources2=VCPU:1
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- The \"group_policy\" parameter is required when specifying more than one \"resources{N}\" parameter.
|
||||||
|
|
||||||
|
- name: orphaned traits keys
|
||||||
|
GET: /allocation_candidates?required=FOO&required1=BAR
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- 'Found the following orphaned traits keys: required, required1'
|
||||||
|
|
||||||
|
- name: orphaned member_of keys
|
||||||
|
GET: /allocation_candidates?member_of=$ENVIRON['AGGA']&member_of3=$ENVIRON['AGGC']
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- 'Found the following orphaned member_of keys: member_of, member_of3'
|
||||||
|
|
||||||
|
- name: at least one request group required
|
||||||
|
GET: /allocation_candidates?group_policy=isolate
|
||||||
|
status: 400
|
||||||
|
response_strings:
|
||||||
|
- At least one request group (`resources` or `resources{N}`) is required.
|
@ -39,13 +39,13 @@ tests:
|
|||||||
response_json_paths:
|
response_json_paths:
|
||||||
$.errors[0].title: Not Acceptable
|
$.errors[0].title: Not Acceptable
|
||||||
|
|
||||||
- name: latest microversion is 1.24
|
- name: latest microversion is 1.25
|
||||||
GET: /
|
GET: /
|
||||||
request_headers:
|
request_headers:
|
||||||
openstack-api-version: placement latest
|
openstack-api-version: placement latest
|
||||||
response_headers:
|
response_headers:
|
||||||
vary: /openstack-api-version/
|
vary: /openstack-api-version/
|
||||||
openstack-api-version: placement 1.24
|
openstack-api-version: placement 1.25
|
||||||
|
|
||||||
- name: other accept header bad version
|
- name: other accept header bad version
|
||||||
GET: /
|
GET: /
|
||||||
|
@ -440,15 +440,18 @@ class TestParseQsRequestGroups(test.NoDBTestCase):
|
|||||||
mv_parsed.min_version = microversion_parse.parse_version_string(
|
mv_parsed.min_version = microversion_parse.parse_version_string(
|
||||||
microversion.min_version_string())
|
microversion.min_version_string())
|
||||||
req.environ['placement.microversion'] = mv_parsed
|
req.environ['placement.microversion'] = mv_parsed
|
||||||
return util.parse_qs_request_groups(req)
|
d = util.parse_qs_request_groups(req)
|
||||||
|
# Sort for easier testing
|
||||||
|
return [d[suff] for suff in sorted(d)]
|
||||||
|
|
||||||
def assertRequestGroupsEqual(self, expected, observed):
|
def assertRequestGroupsEqual(self, expected, observed):
|
||||||
self.assertEqual(len(expected), len(observed))
|
self.assertEqual(len(expected), len(observed))
|
||||||
for exp, obs in zip(expected, observed):
|
for exp, obs in zip(expected, observed):
|
||||||
self.assertEqual(vars(exp), vars(obs))
|
self.assertEqual(vars(exp), vars(obs))
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty_raises(self):
|
||||||
self.assertRequestGroupsEqual([], self.do_parse(''))
|
# TODO(efried): Check the specific error code
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest, self.do_parse, '')
|
||||||
|
|
||||||
def test_unnumbered_only(self):
|
def test_unnumbered_only(self):
|
||||||
"""Unnumbered resources & traits - no numbered groupings."""
|
"""Unnumbered resources & traits - no numbered groupings."""
|
||||||
|
@ -29,10 +29,14 @@ Request
|
|||||||
|
|
||||||
.. rest_parameters:: parameters.yaml
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
- resources: resources_query_required
|
- resources: resources_query_ac
|
||||||
- limit: allocation_candidates_limit
|
- required: required_traits_unnumbered
|
||||||
- required: allocation_candidates_required
|
|
||||||
- member_of: member_of_1_21
|
- member_of: member_of_1_21
|
||||||
|
- resourcesN: resources_query_granular
|
||||||
|
- requiredN: required_traits_granular
|
||||||
|
- member_ofN: member_of_granular
|
||||||
|
- group_policy: allocation_candidates_group_policy
|
||||||
|
- limit: allocation_candidates_limit
|
||||||
|
|
||||||
Response (microversions 1.12 - )
|
Response (microversions 1.12 - )
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
@ -42,6 +42,19 @@ trait_name:
|
|||||||
The name of a trait.
|
The name of a trait.
|
||||||
|
|
||||||
# variables in query
|
# variables in query
|
||||||
|
allocation_candidates_group_policy:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
min_version: 1.25
|
||||||
|
description: >
|
||||||
|
When more than one ``resourcesN`` query parameter is supplied,
|
||||||
|
``group_policy`` is required to indicate how the groups should interact.
|
||||||
|
With ``group_policy=none``, separate groupings - numbered or unnumbered -
|
||||||
|
may or may not be satisfied by the same provider. With
|
||||||
|
``group_policy=isolate``, numbered groups are guaranteed to be satisfied by
|
||||||
|
*different* providers - though there may still be overlap with the
|
||||||
|
unnumbered group.
|
||||||
allocation_candidates_limit:
|
allocation_candidates_limit:
|
||||||
type: integer
|
type: integer
|
||||||
in: query
|
in: query
|
||||||
@ -50,18 +63,6 @@ allocation_candidates_limit:
|
|||||||
description: >
|
description: >
|
||||||
A positive integer used to limit the maximum number of allocation
|
A positive integer used to limit the maximum number of allocation
|
||||||
candidates returned in the response.
|
candidates returned in the response.
|
||||||
allocation_candidates_required:
|
|
||||||
type: string
|
|
||||||
in: query
|
|
||||||
required: false
|
|
||||||
min_version: 1.17
|
|
||||||
description: >
|
|
||||||
Accepts a list of comma-separated traits. Allocation requests in the
|
|
||||||
response will be for resource providers that have capacity for all
|
|
||||||
requested resources and the set of those resource providers will
|
|
||||||
*collectively* contain all of the required traits. **Starting from
|
|
||||||
microversion 1.22** traits which are forbidden from any resource provider
|
|
||||||
may be expressed by prefixing a trait with a ``!``.
|
|
||||||
member_of: &member_of
|
member_of: &member_of
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
@ -87,12 +88,70 @@ member_of: &member_of
|
|||||||
member_of_1_21:
|
member_of_1_21:
|
||||||
<<: *member_of
|
<<: *member_of
|
||||||
min_version: 1.21
|
min_version: 1.21
|
||||||
|
member_of_granular:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: >
|
||||||
|
A string representing an aggregate uuid; or the prefix ``in:`` followed by
|
||||||
|
a comma-separated list of strings representing aggregate uuids. The
|
||||||
|
returned resource providers must be associated with at least one of the
|
||||||
|
aggregates identified by uuid. The parameter key is ``member_ofN``, where
|
||||||
|
``N`` represents a positive integer suffix corresponding with a
|
||||||
|
``resourcesN`` parameter. The value format is the same as for the
|
||||||
|
(unnumbered) ``member_of`` parameter; but all of the resources and traits
|
||||||
|
specified in a numbered grouping will always be satisfied by the same
|
||||||
|
resource provider. Separate groupings - numbered or unnumbered - may or may
|
||||||
|
not be satisfied by the same provider, depending on the value of the
|
||||||
|
``group_policy`` parameter.
|
||||||
|
|
||||||
|
It is an error to specify a ``member_ofN`` parameter without a
|
||||||
|
corresponding ``resourcesN`` parameter with the same suffix.
|
||||||
|
min_version: 1.25
|
||||||
project_id: &project_id
|
project_id: &project_id
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
description: >
|
description: >
|
||||||
The uuid of a project.
|
The uuid of a project.
|
||||||
|
required_traits_granular:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
A comma-separated list of traits that a provider must have, or (if prefixed
|
||||||
|
with a ``!``) **not** have::
|
||||||
|
|
||||||
|
required42=HW_CPU_X86_AVX,HW_CPU_X86_SSE,!HW_CPU_X86_AVX2
|
||||||
|
|
||||||
|
The parameter key is ``requiredN``, where ``N`` represents a
|
||||||
|
positive integer suffix corresponding with a ``resourcesN`` parameter.
|
||||||
|
The value format is the same as for the (unnumbered) ``required``
|
||||||
|
parameter; but all of the resources and traits specified in a numbered
|
||||||
|
grouping will always be satisfied by the same resource provider. Separate
|
||||||
|
groupings - numbered or unnumbered - may or may not be satisfied by the
|
||||||
|
same provider, depending on the value of the ``group_policy`` parameter.
|
||||||
|
|
||||||
|
It is an error to specify a ``requiredN`` parameter without a corresponding
|
||||||
|
``resourcesN`` parameter with the same suffix.
|
||||||
|
min_version: 1.25
|
||||||
|
required_traits_unnumbered:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
min_version: 1.17
|
||||||
|
description: |
|
||||||
|
A comma-separated list of traits that a provider must have::
|
||||||
|
|
||||||
|
required=HW_CPU_X86_AVX,HW_CPU_X86_SSE
|
||||||
|
|
||||||
|
Allocation requests in the response will be for resource providers that
|
||||||
|
have capacity for all requested resources and the set of those resource
|
||||||
|
providers will *collectively* contain all of the required traits. These
|
||||||
|
traits may be satisfied by any provider in the same non-sharing tree or
|
||||||
|
associated via aggregate. **Starting from microversion 1.22** traits which
|
||||||
|
are forbidden from any resource provider may be expressed by prefixing a
|
||||||
|
trait with a ``!``.
|
||||||
resource_provider_name_query:
|
resource_provider_name_query:
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
@ -121,7 +180,7 @@ resource_provider_uuid_query:
|
|||||||
<<: *resource_provider_uuid_path
|
<<: *resource_provider_uuid_path
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
resources_query:
|
resources_query_1_4:
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
@ -132,16 +191,38 @@ resources_query:
|
|||||||
|
|
||||||
resources=VCPU:4,DISK_GB:64,MEMORY_MB:2048
|
resources=VCPU:4,DISK_GB:64,MEMORY_MB:2048
|
||||||
min_version: 1.4
|
min_version: 1.4
|
||||||
resources_query_required:
|
resources_query_ac:
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: false
|
||||||
description: |
|
description: |
|
||||||
A comma-separated list of strings indicating an amount of
|
A comma-separated list of strings indicating an amount of
|
||||||
resource of a specified class that providers in each allocation request
|
resource of a specified class that providers in each allocation request
|
||||||
must *collectively* have the capacity and availability to serve::
|
must *collectively* have the capacity and availability to serve::
|
||||||
|
|
||||||
resources=VCPU:4,DISK_GB:64,MEMORY_MB:2048
|
resources=VCPU:4,DISK_GB:64,MEMORY_MB:2048
|
||||||
|
|
||||||
|
These resources may be satisfied by any provider in the same non-sharing
|
||||||
|
tree or associated via aggregate.
|
||||||
|
resources_query_granular:
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
A comma-separated list of strings indicating an amount of
|
||||||
|
resource of a specified class that a provider must have the
|
||||||
|
capacity to serve::
|
||||||
|
|
||||||
|
resources42=VCPU:4,DISK_GB:64,MEMORY_MB:2048
|
||||||
|
|
||||||
|
The parameter key is ``resourcesN``, where ``N`` represents a unique
|
||||||
|
positive integer suffix. The value format is the same as for the
|
||||||
|
(unnumbered) ``resources`` parameter, but the resources specified in a
|
||||||
|
``resourcesN`` parameter will always be satisfied by a single provider.
|
||||||
|
Separate groupings - numbered or unnumbered - may or may not be satisfied
|
||||||
|
by the same provider depending on the value of the ``group_policy``
|
||||||
|
parameter.
|
||||||
|
min_version: 1.25
|
||||||
trait_associated:
|
trait_associated:
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
|
@ -33,7 +33,7 @@ of all filters are merged with a boolean `AND`.
|
|||||||
- name: resource_provider_name_query
|
- name: resource_provider_name_query
|
||||||
- uuid: resource_provider_uuid_query
|
- uuid: resource_provider_uuid_query
|
||||||
- member_of: member_of
|
- member_of: member_of
|
||||||
- resources: resources_query
|
- resources: resources_query_1_4
|
||||||
- in_tree: resource_provider_tree_query
|
- in_tree: resource_provider_tree_query
|
||||||
- required: resource_provider_required_query
|
- required: resource_provider_required_query
|
||||||
|
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
In version 1.25 of the Placement API, ``GET /allocation_candidates`` is
|
||||||
|
enhanced to accept numbered groupings of resource, required/forbidden
|
||||||
|
trait, and aggregate association requests. A ``resources`` query parameter
|
||||||
|
key with a positive integer suffix (e.g. ``resources42``) will be logically
|
||||||
|
associated with ``required`` and/or ``member_of`` query parameter keys with
|
||||||
|
the same suffix (e.g. ``required42``, ``member_of42``). The resources,
|
||||||
|
required/forbidden traits, and aggregate associations in that group will be
|
||||||
|
satisfied by the same resource provider in the response. When more than one
|
||||||
|
numbered grouping is supplied, the ``group_policy`` query parameter is
|
||||||
|
required to indicate how the groups should interact. With
|
||||||
|
``group_policy=none``, separate groupings - numbered or unnumbered - may or
|
||||||
|
may not be satisfied by the same provider. With ``group_policy=isolate``,
|
||||||
|
numbered groups are guaranteed to be satisfied by *different* providers -
|
||||||
|
though there may still be overlap with the unnumbered group. In all cases,
|
||||||
|
each ``allocation_request`` will be satisfied by providers in a single
|
||||||
|
non-sharing provider tree and/or sharing providers associated via aggregate
|
||||||
|
with any of the providers in that tree.
|
||||||
|
|
||||||
|
The ``required`` and ``member_of`` query parameters for a given group are
|
||||||
|
optional. That is, you may specify ``resources42=XXX`` without a
|
||||||
|
corresponding ``required42=YYY`` or ``member_of42=ZZZ``. However, the
|
||||||
|
reverse (specifying ``required42=YYY`` or ``member_of42=ZZZ`` without
|
||||||
|
``resources42=XXX``) will result in an error.
|
||||||
|
|
||||||
|
The semantic of the (unnumbered) ``resources``, ``required``, and
|
||||||
|
``member_of`` query parameters is unchanged: the resources, traits, and
|
||||||
|
aggregate associations specified thereby may be satisfied by any provider
|
||||||
|
in the same non-sharing tree or associated via the specified aggregate(s).
|
Loading…
Reference in New Issue
Block a user