923 lines
41 KiB
Python
923 lines
41 KiB
Python
# 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.
|
|
|
|
import collections
|
|
import copy
|
|
import itertools
|
|
|
|
import os_traits
|
|
from oslo_log import log as logging
|
|
import sqlalchemy as sa
|
|
|
|
from placement.db.sqlalchemy import models
|
|
from placement import db_api
|
|
from placement import exception
|
|
from placement.objects import research_context as res_ctx
|
|
from placement.objects import resource_provider as rp_obj
|
|
from placement.objects import trait as trait_obj
|
|
|
|
|
|
_ALLOC_TBL = models.Allocation.__table__
|
|
_INV_TBL = models.Inventory.__table__
|
|
_RP_TBL = models.ResourceProvider.__table__
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class AllocationCandidates(object):
|
|
"""The AllocationCandidates object is a collection of possible allocations
|
|
that match some request for resources, along with some summary information
|
|
about the resource providers involved in these allocation candidates.
|
|
"""
|
|
|
|
def __init__(self, allocation_requests=None, provider_summaries=None):
|
|
# A collection of allocation possibilities that can be attempted by the
|
|
# caller that would, at the time of calling, meet the requested
|
|
# resource constraints
|
|
self.allocation_requests = allocation_requests
|
|
# Information about usage and inventory that relate to any provider
|
|
# contained in any of the AllocationRequest objects in the
|
|
# allocation_requests field
|
|
self.provider_summaries = provider_summaries
|
|
|
|
@classmethod
|
|
def get_by_requests(cls, context, groups, rqparams, nested_aware=True):
|
|
"""Returns an AllocationCandidates object containing all resource
|
|
providers matching a set of supplied resource constraints, with a set
|
|
of allocation requests constructed from that list of resource
|
|
providers. If CONF.placement.randomize_allocation_candidates (on
|
|
contex.config) is True (default is False) then the order of the
|
|
allocation requests will be randomized.
|
|
|
|
:param context: placement.context.RequestContext object.
|
|
:param groups: Dict, keyed by suffix, of placement.lib.RequestGroup
|
|
:param rqparams: A RequestWideParams.
|
|
:param nested_aware: If False, we are blind to nested architecture and
|
|
can't pick resources from multiple providers even
|
|
if they come from the same tree.
|
|
:return: An instance of AllocationCandidates with allocation_requests
|
|
and provider_summaries satisfying `requests`, limited
|
|
according to `limit`.
|
|
"""
|
|
try:
|
|
alloc_reqs, provider_summaries = cls._get_by_requests(
|
|
context, groups, rqparams, nested_aware=nested_aware)
|
|
except exception.ResourceProviderNotFound:
|
|
alloc_reqs, provider_summaries = [], []
|
|
return cls(
|
|
allocation_requests=alloc_reqs,
|
|
provider_summaries=provider_summaries,
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_by_one_request(rg_ctx, rw_ctx):
|
|
"""Get allocation candidates for one RequestGroup.
|
|
|
|
Must be called from within an placement_context_manager.reader
|
|
(or writer) context.
|
|
|
|
:param rg_ctx: RequestGroupSearchContext.
|
|
:param rw_ctx: RequestWideSearchContext.
|
|
"""
|
|
if not rg_ctx.use_same_provider and (
|
|
rg_ctx.exists_sharing or rg_ctx.exists_nested):
|
|
# TODO(jaypipes): The check/callout to handle trees goes here.
|
|
# Build a dict, keyed by resource class internal ID, of lists of
|
|
# internal IDs of resource providers that share some inventory for
|
|
# each resource class requested.
|
|
# If there aren't any providers that have any of the
|
|
# required traits, just exit early...
|
|
if rg_ctx.required_trait_map:
|
|
# TODO(cdent): Now that there is also a forbidden_trait_map
|
|
# it should be possible to further optimize this attempt at
|
|
# a quick return, but we leave that to future patches for
|
|
# now.
|
|
trait_rps = res_ctx.get_provider_ids_having_any_trait(
|
|
rg_ctx.context, rg_ctx.required_trait_map)
|
|
if not trait_rps:
|
|
return set()
|
|
rp_candidates = res_ctx.get_trees_matching_all(rg_ctx, rw_ctx)
|
|
return _alloc_candidates_multiple_providers(
|
|
rg_ctx, rw_ctx, rp_candidates)
|
|
|
|
# Either we are processing a single-RP request group, or there are no
|
|
# sharing providers that (help) satisfy the request. Get a list of
|
|
# tuples of (internal provider ID, root provider ID) that have ALL
|
|
# the requested resources and more efficiently construct the
|
|
# allocation requests.
|
|
rp_tuples = res_ctx.get_provider_ids_matching(rg_ctx)
|
|
return _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples)
|
|
|
|
@classmethod
|
|
@db_api.placement_context_manager.reader
|
|
def _get_by_requests(cls, context, groups, rqparams, nested_aware=True):
|
|
rw_ctx = res_ctx.RequestWideSearchContext(
|
|
context, rqparams, nested_aware)
|
|
sharing = res_ctx.get_sharing_providers(context)
|
|
# TODO(efried): If we ran anchors_for_sharing_providers here, we could
|
|
# narrow to only sharing providers associated with our filtered trees.
|
|
# Unclear whether this would be cheaper than waiting until we've
|
|
# filtered sharing providers for other things (like resources).
|
|
|
|
seen_rcs = set()
|
|
candidates = {}
|
|
for suffix, group in groups.items():
|
|
rg_ctx = res_ctx.RequestGroupSearchContext(
|
|
context, group, rw_ctx.has_trees, sharing, suffix)
|
|
|
|
# Which resource classes are requested in more than one group?
|
|
for rc in rg_ctx.rcs:
|
|
if rc in seen_rcs:
|
|
rw_ctx.multi_group_rcs.add(rc)
|
|
else:
|
|
seen_rcs.add(rc)
|
|
|
|
alloc_reqs = cls._get_by_one_request(rg_ctx, rw_ctx)
|
|
LOG.debug("%s (suffix '%s') returned %d matches",
|
|
str(group), str(suffix), len(alloc_reqs))
|
|
if not alloc_reqs:
|
|
# Shortcut: If any one group 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 = group.use_same_provider
|
|
candidates[suffix] = alloc_reqs
|
|
|
|
# At this point, each alloc_requests 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, rw_ctx)
|
|
|
|
alloc_request_objs, summary_objs = rw_ctx.exclude_nested_providers(
|
|
alloc_request_objs, summary_objs)
|
|
|
|
return rw_ctx.limit_results(alloc_request_objs, summary_objs)
|
|
|
|
|
|
class AllocationRequest(object):
|
|
|
|
__slots__ = ('anchor_root_provider_uuid', 'use_same_provider',
|
|
'resource_requests', 'mappings')
|
|
|
|
def __init__(self, anchor_root_provider_uuid=None,
|
|
use_same_provider=None, resource_requests=None,
|
|
mappings=None):
|
|
# UUID of (the root of the tree including) the non-sharing resource
|
|
# provider associated with this AllocationRequest. Internal use only,
|
|
# not included when the object is serialized for output.
|
|
self.anchor_root_provider_uuid = anchor_root_provider_uuid
|
|
# Whether all AllocationRequestResources in this AllocationRequest are
|
|
# required to be satisfied by the same provider (based on the
|
|
# corresponding RequestGroup's use_same_provider attribute). Internal
|
|
# use only, not included when the object is serialized for output.
|
|
self.use_same_provider = use_same_provider
|
|
self.resource_requests = resource_requests or []
|
|
# mappings will be presented as a dict during output, so ensure we have
|
|
# a reasonable default here, despite mappings always being set.
|
|
self.mappings = mappings or dict()
|
|
|
|
def __repr__(self):
|
|
anchor = (self.anchor_root_provider_uuid[-8:]
|
|
if self.anchor_root_provider_uuid else '<?>')
|
|
usp = (self.use_same_provider
|
|
if self.use_same_provider is not None else '<?>')
|
|
repr_str = ('%s(anchor=...%s, same_provider=%s, '
|
|
'resource_requests=[%s])' %
|
|
(self.__class__.__name__, anchor, usp,
|
|
', '.join([str(arr) for arr in self.resource_requests])))
|
|
return repr_str
|
|
|
|
def __eq__(self, other):
|
|
return (set(self.resource_requests) == set(other.resource_requests) and
|
|
self.mappings == other.mappings)
|
|
|
|
def __hash__(self):
|
|
# We need a stable sort order on the resource requests to get an
|
|
# accurate hash. To avoid needing to update the method everytime
|
|
# the structure of an AllocationRequestResource changes, we can
|
|
# sort on the hash of each request resource.
|
|
sorted_rr = sorted(self.resource_requests, key=lambda x: hash(x))
|
|
return hash(tuple(sorted_rr))
|
|
|
|
def __copy__(self):
|
|
# This is shallow copy, so resource_requests and mappings are the
|
|
# same objects as prior to the copy.
|
|
return self.__class__(
|
|
anchor_root_provider_uuid=self.anchor_root_provider_uuid,
|
|
use_same_provider=self.use_same_provider,
|
|
resource_requests=self.resource_requests,
|
|
mappings=self.mappings
|
|
)
|
|
|
|
|
|
class AllocationRequestResource(object):
|
|
|
|
__slots__ = 'resource_provider', 'resource_class', 'amount'
|
|
|
|
def __init__(self, resource_provider=None, resource_class=None,
|
|
amount=None):
|
|
self.resource_provider = resource_provider
|
|
self.resource_class = resource_class
|
|
self.amount = amount
|
|
|
|
def __eq__(self, other):
|
|
return ((self.resource_provider.id == other.resource_provider.id) and
|
|
(self.resource_class == other.resource_class) and
|
|
(self.amount == other.amount))
|
|
|
|
def __hash__(self):
|
|
return hash((self.resource_provider.id,
|
|
self.resource_class,
|
|
self.amount))
|
|
|
|
def __copy__(self):
|
|
# This is shallow copy, so resource_provider is the same object as
|
|
# prior to the copy. resource_class is a string here, not a
|
|
# ResourceClass object
|
|
return self.__class__(
|
|
resource_provider=self.resource_provider,
|
|
resource_class=self.resource_class,
|
|
amount=self.amount)
|
|
|
|
|
|
class ProviderSummary(object):
|
|
|
|
__slots__ = 'resource_provider', 'resources', 'traits'
|
|
|
|
def __init__(self, resource_provider=None, resources=None, traits=None):
|
|
self.resource_provider = resource_provider
|
|
self.resources = resources or []
|
|
self.traits = traits or []
|
|
|
|
|
|
class ProviderSummaryResource(object):
|
|
|
|
__slots__ = 'resource_class', 'capacity', 'used', 'max_unit'
|
|
|
|
def __init__(self, resource_class=None, capacity=None, used=None,
|
|
max_unit=None):
|
|
self.resource_class = resource_class
|
|
self.capacity = capacity
|
|
self.used = used
|
|
# Internal use only; not included when the object is serialized for
|
|
# output.
|
|
self.max_unit = max_unit
|
|
|
|
|
|
def _alloc_candidates_multiple_providers(rg_ctx, rw_ctx, rp_candidates):
|
|
"""Returns a set of allocation requests for a supplied set of requested
|
|
resource amounts and tuples of (rp_id, root_id, rc_id). The supplied
|
|
resource provider trees have capacity to satisfy ALL of the resources in
|
|
the requested resources as well as ALL required traits that were requested
|
|
by the user.
|
|
|
|
This is a code path to get results for a RequestGroup with
|
|
use_same_provider=False. In this scenario, we are able to use multiple
|
|
providers within the same provider tree including sharing providers to
|
|
satisfy different resources involved in a single request group.
|
|
|
|
:param rg_ctx: RequestGroupSearchContext.
|
|
:param rw_ctx: RequestWideSearchContext
|
|
:param rp_candidates: RPCandidates object representing the providers
|
|
that satisfy the request for resources.
|
|
"""
|
|
if not rp_candidates:
|
|
return set()
|
|
|
|
# Get all the root resource provider IDs. We should include the first
|
|
# values of rp_tuples because while sharing providers are root providers,
|
|
# they have their "anchor" providers for the second value.
|
|
root_ids = rp_candidates.all_rps
|
|
|
|
# Get a dict, keyed by resource provider internal ID, of trait string names
|
|
# that provider has associated with it
|
|
prov_traits = trait_obj.get_traits_by_provider_tree(
|
|
rg_ctx.context, root_ids)
|
|
|
|
# Extend rw_ctx.summaries_by_id dict, keyed by resource provider internal
|
|
# ID, of ProviderSummary objects for all providers
|
|
_build_provider_summaries(rg_ctx.context, rw_ctx, root_ids, prov_traits)
|
|
|
|
# Get a dict, keyed by root provider internal ID, of a dict, keyed by
|
|
# resource class internal ID, of lists of AllocationRequestResource objects
|
|
tree_dict = collections.defaultdict(lambda: collections.defaultdict(list))
|
|
|
|
rc_cache = rg_ctx.context.rc_cache
|
|
for rp in rp_candidates.rps_info:
|
|
rp_summary = rw_ctx.summaries_by_id[rp.id]
|
|
tree_dict[rp.root_id][rp.rc_id].append(
|
|
AllocationRequestResource(
|
|
resource_provider=rp_summary.resource_provider,
|
|
resource_class=rc_cache.string_from_id(rp.rc_id),
|
|
amount=rg_ctx.resources[rp.rc_id]))
|
|
|
|
# Next, build up a set of allocation requests. These allocation requests
|
|
# are AllocationRequest objects, containing resource provider UUIDs,
|
|
# resource class names and amounts to consume from that resource provider
|
|
alloc_requests = set()
|
|
|
|
# Let's look into each tree
|
|
for root_id, alloc_dict in tree_dict.items():
|
|
# Get request_groups, which is a list of lists of
|
|
# AllocationRequestResource(ARR) per requested resource class(rc).
|
|
# For example, if we have the alloc_dict:
|
|
# {rc1_id: [ARR(rc1, rp1), ARR(rc1, rp2)],
|
|
# rc2_id: [ARR(rc2, rp1), ARR(rc2, rp2)],
|
|
# rc3_id: [ARR(rc3, rp1)]}
|
|
# then the request_groups would be something like
|
|
# [[ARR(rc1, rp1), ARR(rc1, rp2)],
|
|
# [ARR(rc2, rp1), ARR(rc2, rp2)],
|
|
# [ARR(rc3, rp1)]]
|
|
# , which should be ordered by the resource class id.
|
|
request_groups = [val for key, val in sorted(alloc_dict.items())]
|
|
|
|
root_summary = rw_ctx.summaries_by_id[root_id]
|
|
root_uuid = root_summary.resource_provider.uuid
|
|
root_alloc_reqs = set()
|
|
|
|
# Using itertools.product, we get all the combinations of resource
|
|
# providers in a tree.
|
|
# For example, the sample in the comment above becomes:
|
|
# [(ARR(rc1, ss1), ARR(rc2, ss1), ARR(rc3, ss1)),
|
|
# (ARR(rc1, ss1), ARR(rc2, ss2), ARR(rc3, ss1)),
|
|
# (ARR(rc1, ss2), ARR(rc2, ss1), ARR(rc3, ss1)),
|
|
# (ARR(rc1, ss2), ARR(rc2, ss2), ARR(rc3, ss1))]
|
|
for res_requests in itertools.product(*request_groups):
|
|
if not _check_traits_for_alloc_request(
|
|
res_requests, rw_ctx.summaries_by_id,
|
|
rg_ctx.required_trait_map,
|
|
rg_ctx.forbidden_trait_map):
|
|
# This combination doesn't satisfy trait constraints
|
|
continue
|
|
|
|
mappings = collections.defaultdict(set)
|
|
for rr in res_requests:
|
|
mappings[rg_ctx.suffix].add(rr.resource_provider.uuid)
|
|
alloc_req = AllocationRequest(resource_requests=list(res_requests),
|
|
anchor_root_provider_uuid=root_uuid,
|
|
mappings=mappings)
|
|
root_alloc_reqs.add(alloc_req)
|
|
alloc_requests |= root_alloc_reqs
|
|
return alloc_requests
|
|
|
|
|
|
def _alloc_candidates_single_provider(rg_ctx, rw_ctx, rp_tuples):
|
|
"""Returns a set of allocation requests for a supplied set of requested
|
|
resource amounts and resource providers. The supplied resource providers
|
|
have capacity to satisfy ALL of the resources in the requested resources
|
|
as well as ALL required traits that were requested by the user.
|
|
|
|
This is used in two circumstances:
|
|
- To get results for a RequestGroup with use_same_provider=True.
|
|
- As an optimization when no sharing providers satisfy any of the requested
|
|
resources, and nested providers are not in play.
|
|
In these scenarios, we can more efficiently build the list of
|
|
AllocationRequest and ProviderSummary objects due to not having to
|
|
determine requests across multiple providers.
|
|
|
|
:param rg_ctx: RequestGroupSearchContext
|
|
:param rw_ctx: RequestWideSearchContext
|
|
:param rp_tuples: List of two-tuples of (provider ID, root provider ID)s
|
|
for providers that matched the requested resources
|
|
"""
|
|
if not rp_tuples:
|
|
return set()
|
|
|
|
# Get all root resource provider IDs.
|
|
root_ids = set(p[1] for p in rp_tuples)
|
|
|
|
# Get a dict, keyed by resource provider internal ID, of trait string names
|
|
# that provider has associated with it
|
|
prov_traits = trait_obj.get_traits_by_provider_tree(
|
|
rg_ctx.context, root_ids)
|
|
|
|
# Extend rw_ctx.summaries_by_id dict, keyed by resource provider internal
|
|
# ID, of ProviderSummary objects for all providers
|
|
_build_provider_summaries(rg_ctx.context, rw_ctx, root_ids, prov_traits)
|
|
|
|
# Next, build up a list of allocation requests. These allocation requests
|
|
# are AllocationRequest objects, containing resource provider UUIDs,
|
|
# resource class names and amounts to consume from that resource provider
|
|
alloc_requests = []
|
|
for rp_id, root_id in rp_tuples:
|
|
rp_summary = rw_ctx.summaries_by_id[rp_id]
|
|
req_obj = _allocation_request_for_provider(
|
|
rg_ctx.context, rg_ctx.resources, rp_summary.resource_provider,
|
|
suffix=rg_ctx.suffix)
|
|
# Exclude this if its anchor (which is its root) isn't in our
|
|
# prefiltered list of anchors
|
|
if rw_ctx.in_filtered_anchors(root_id):
|
|
alloc_requests.append(req_obj)
|
|
# If this is a sharing provider, we have to include an extra
|
|
# AllocationRequest for every possible anchor.
|
|
traits = rp_summary.traits
|
|
if os_traits.MISC_SHARES_VIA_AGGREGATE in traits:
|
|
anchors = res_ctx.anchors_for_sharing_providers(
|
|
rg_ctx.context, [rp_summary.resource_provider.id])
|
|
for anchor in anchors:
|
|
# We already added self
|
|
if anchor.anchor_id == root_id:
|
|
continue
|
|
# Only include if anchor is viable
|
|
if not rw_ctx.in_filtered_anchors(anchor.anchor_id):
|
|
continue
|
|
req_obj = copy.copy(req_obj)
|
|
req_obj.anchor_root_provider_uuid = anchor.anchor_uuid
|
|
alloc_requests.append(req_obj)
|
|
return alloc_requests
|
|
|
|
|
|
def _allocation_request_for_provider(context, requested_resources, provider,
|
|
suffix):
|
|
"""Returns an AllocationRequest object containing AllocationRequestResource
|
|
objects for each resource class in the supplied requested resources dict.
|
|
|
|
:param requested_resources: dict, keyed by resource class ID, of amounts
|
|
being requested for that resource class
|
|
:param provider: ResourceProvider object representing the provider of the
|
|
resources.
|
|
:param suffix: The suffix of the RequestGroup these resources are
|
|
satisfying.
|
|
"""
|
|
resource_requests = [
|
|
AllocationRequestResource(
|
|
resource_provider=provider,
|
|
resource_class=context.rc_cache.string_from_id(rc_id),
|
|
amount=amount
|
|
) for rc_id, amount in requested_resources.items()
|
|
]
|
|
# NOTE(efried): This method only produces an AllocationRequest with its
|
|
# anchor in its own tree. If the provider is a sharing provider, the
|
|
# caller needs to identify the other anchors with which it might be
|
|
# associated.
|
|
# NOTE(tetsuro): The AllocationRequest has empty resource_requests for a
|
|
# resourceless request. Still, it has the rp uuid in the mappings field.
|
|
mappings = {suffix: set([provider.uuid])}
|
|
return AllocationRequest(
|
|
resource_requests=resource_requests,
|
|
anchor_root_provider_uuid=provider.root_provider_uuid,
|
|
mappings=mappings)
|
|
|
|
|
|
def _build_provider_summaries(context, rw_ctx, root_ids, prov_traits):
|
|
"""Given a list of dicts of usage information and a map of providers to
|
|
their associated string traits, returns a dict, keyed by resource provider
|
|
ID, of ProviderSummary objects.
|
|
|
|
Warning: This is side-effecty: It is extending the rw_ctx.summaries_by_id
|
|
dict. Nothing is returned.
|
|
|
|
:param context: placement.context.RequestContext object
|
|
:param rw_ctx: placement.research_context.RequestWideSearchContext
|
|
:param root_ids: A set of root resource provider ids
|
|
:param prov_traits: A dict, keyed by internal resource provider ID, of
|
|
string trait names associated with that provider
|
|
"""
|
|
# Filter resource providers by those we haven't seen yet.
|
|
new_roots = root_ids - set(rw_ctx.summaries_by_id)
|
|
if not new_roots:
|
|
return
|
|
|
|
# Get a dict-like usage information of resource providers in a tree where
|
|
# at least one member of the tree is contributing resources or traits to
|
|
# an allocation candidate, which has the following structure:
|
|
# {
|
|
# 'resource_provider_id': <internal resource provider ID>,
|
|
# 'resource_provider_uuid': <UUID>,
|
|
# 'resource_class_id': <internal resource class ID>,
|
|
# 'total': integer,
|
|
# 'reserved': integer,
|
|
# 'allocation_ratio': float,
|
|
# }
|
|
usages = res_ctx.get_usages_by_provider_trees(context, new_roots)
|
|
|
|
# Before we go creating provider summary objects, first grab all the
|
|
# provider information (including root, parent and UUID information) for
|
|
# the providers.
|
|
provider_ids = _provider_ids_from_root_ids(context, new_roots)
|
|
|
|
# Build up a dict, keyed by internal resource provider ID, of
|
|
# ProviderSummary objects containing one or more ProviderSummaryResource
|
|
# objects representing the resources the provider has inventory for.
|
|
for usage in usages:
|
|
rp_id = usage['resource_provider_id']
|
|
summary = rw_ctx.summaries_by_id.get(rp_id)
|
|
if not summary:
|
|
pids = provider_ids[rp_id]
|
|
parent_id = pids.parent_id
|
|
# If there is a parent, we can rely on it being in provider_ids
|
|
# because for any single provider, it also contains the full
|
|
# ancestry.
|
|
parent_uuid = provider_ids[parent_id].uuid if parent_id else None
|
|
# Update the parent_uuid_by_rp_uuid cache here. We know that we
|
|
# will visit all providers in all trees in play during
|
|
# _build_provider_summaries, so now is a good time.
|
|
rw_ctx.parent_uuid_by_rp_uuid[pids.uuid] = parent_uuid
|
|
summary = ProviderSummary(
|
|
resource_provider=rp_obj.ResourceProvider(
|
|
context, id=pids.id, uuid=pids.uuid,
|
|
root_provider_uuid=provider_ids[pids.root_id].uuid,
|
|
parent_provider_uuid=parent_uuid),
|
|
resources=[],
|
|
)
|
|
summary.traits = prov_traits[rp_id]
|
|
rw_ctx.summaries_by_id[rp_id] = summary
|
|
|
|
rc_id = usage['resource_class_id']
|
|
if rc_id is None:
|
|
# NOTE(tetsuro): This provider doesn't have any inventory itself.
|
|
# But we include this provider in summaries since another
|
|
# provider in the same tree will be in the "allocation_request".
|
|
# Let's skip the following and leave "ProviderSummary.resources"
|
|
# field empty.
|
|
continue
|
|
# NOTE(jaypipes): usage['used'] may be None due to the LEFT JOIN of
|
|
# the usages subquery, so we coerce NULL values to 0 here. It may
|
|
# also be a Decimal, as that's the type that mysql tends to return
|
|
# when func.sum is used in a query. We need an int, otherwise later
|
|
# JSON serialization will not work.
|
|
used = int(usage['used'] or 0)
|
|
allocation_ratio = usage['allocation_ratio']
|
|
cap = int((usage['total'] - usage['reserved']) * allocation_ratio)
|
|
rc_name = context.rc_cache.string_from_id(rc_id)
|
|
rpsr = ProviderSummaryResource(
|
|
resource_class=rc_name,
|
|
capacity=cap,
|
|
used=used,
|
|
max_unit=usage['max_unit'],
|
|
)
|
|
# 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_key = (rp_id, rc_name)
|
|
rw_ctx.psum_res_by_rp_rc[psum_key] = rpsr
|
|
summary.resources.append(rpsr)
|
|
|
|
|
|
def _check_traits_for_alloc_request(res_requests, summaries, required_traits,
|
|
forbidden_traits):
|
|
"""Given a list of AllocationRequestResource objects, check if that
|
|
combination can provide trait constraints. If it can, returns all
|
|
resource provider internal IDs in play, else return an empty list.
|
|
|
|
TODO(tetsuro): For optimization, we should move this logic to SQL in
|
|
res_ctx.get_trees_matching_all().
|
|
|
|
:param res_requests: a list of AllocationRequestResource objects that have
|
|
resource providers to be checked if they collectively
|
|
satisfy trait constraints in the required_traits and
|
|
forbidden_traits parameters.
|
|
:param summaries: dict, keyed by resource provider id, of ProviderSummary
|
|
objects containing usage and trait information for
|
|
resource providers involved in the overall request
|
|
:param required_traits: A map, keyed by trait string name, of required
|
|
trait internal IDs that each *allocation request's
|
|
set of providers* must *collectively* have
|
|
associated with them
|
|
:param forbidden_traits: A map, keyed by trait string name, of trait
|
|
internal IDs that a resource provider must
|
|
not have.
|
|
"""
|
|
all_prov_ids = []
|
|
all_traits = set()
|
|
for res_req in res_requests:
|
|
rp_id = res_req.resource_provider.id
|
|
rp_summary = summaries[rp_id]
|
|
rp_traits = set(rp_summary.traits)
|
|
|
|
# Check if there are forbidden_traits
|
|
conflict_traits = set(forbidden_traits) & set(rp_traits)
|
|
if conflict_traits:
|
|
LOG.debug('Excluding resource provider %s, it has '
|
|
'forbidden traits: (%s).',
|
|
rp_id, ', '.join(conflict_traits))
|
|
return []
|
|
|
|
all_prov_ids.append(rp_id)
|
|
all_traits |= rp_traits
|
|
|
|
# Check if there are missing traits
|
|
missing_traits = set(required_traits) - all_traits
|
|
if missing_traits:
|
|
LOG.debug('Excluding a set of allocation candidate %s : '
|
|
'missing traits %s are not satisfied.',
|
|
all_prov_ids, ','.join(missing_traits))
|
|
return []
|
|
|
|
return all_prov_ids
|
|
|
|
|
|
def _consolidate_allocation_requests(areqs, rw_ctx):
|
|
"""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
|
|
mappings = collections.defaultdict(set)
|
|
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 "
|
|
"`_consolidate_allocation_requests` to have the same "
|
|
"anchor!")
|
|
for arr in areq.resource_requests:
|
|
key = (arr.resource_provider.id, arr.resource_class)
|
|
if key not in arrs_by_rp_rc:
|
|
arrs_by_rp_rc[key] = rw_ctx.copy_arr_if_needed(arr)
|
|
else:
|
|
arrs_by_rp_rc[key].amount += arr.amount
|
|
for suffix, providers in areq.mappings.items():
|
|
mappings[suffix].update(providers)
|
|
return AllocationRequest(
|
|
resource_requests=list(arrs_by_rp_rc.values()),
|
|
anchor_root_provider_uuid=anchor_rp_uuid,
|
|
mappings=mappings)
|
|
|
|
|
|
# TODO(efried): Move _merge_candidates to rw_ctx?
|
|
def _merge_candidates(candidates, rw_ctx):
|
|
"""Given a dict, keyed by RequestGroup suffix, of allocation_requests,
|
|
produce a single tuple of (allocation_requests, provider_summaries) that
|
|
appropriately incorporates the elements from each.
|
|
|
|
Each alloc_reqs 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 suffix string or '', of a set of
|
|
allocation_requests to be merged.
|
|
:param rw_ctx: RequestWideSearchContext.
|
|
: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))
|
|
for suffix, areqs in candidates.items():
|
|
for areq in areqs:
|
|
anchor = areq.anchor_root_provider_uuid
|
|
areq_lists_by_anchor[anchor][suffix].append(areq)
|
|
|
|
# Create all combinations picking one AllocationRequest from each list
|
|
# for each anchor.
|
|
areqs = set()
|
|
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.
|
|
# TODO(efried): Move _satisfies_group_policy to rw_ctx?
|
|
if not _satisfies_group_policy(
|
|
areq_list, rw_ctx.group_policy, num_granular_groups):
|
|
continue
|
|
if not _satisfies_same_subtree(areq_list, rw_ctx):
|
|
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 the information telling us which RequestGroup led to
|
|
# which piece of the AllocationRequest has been lost from the outer
|
|
# layer of the data structure (the key of areq_lists_by_suffix).
|
|
# => We needed that to be present for the previous filter; we need
|
|
# it to be *absent* for the next one.
|
|
# => However, it still exists embedded in each
|
|
# AllocationRequestResource. That's needed to construct the
|
|
# mappings for the output.
|
|
areq = _consolidate_allocation_requests(areq_list, rw_ctx)
|
|
# 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 rw_ctx.exceeds_capacity(areq):
|
|
continue
|
|
areqs.add(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
|
|
# rw_ctx.summary_by_id contain all the information; we just need to filter
|
|
# it down to only the providers in trees represented by our merged list of
|
|
# allocation requests.
|
|
tree_uuids = set()
|
|
for areq in areqs:
|
|
for arr in areq.resource_requests:
|
|
tree_uuids.add(arr.resource_provider.root_provider_uuid)
|
|
psums = [
|
|
psum for psum in rw_ctx.summaries_by_id.values()
|
|
if psum.resource_provider.root_provider_uuid in tree_uuids]
|
|
|
|
LOG.debug('Merging candidates yields %d allocation requests and %d '
|
|
'provider summaries', len(areqs), len(psums))
|
|
return list(areqs), psums
|
|
|
|
|
|
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.
|
|
num_granular_groups_in_areqs = len(set().union(*(
|
|
# We can reliably use the first value of provider uuids in mappings:
|
|
# all the resource_requests are satisfied by the same provider
|
|
# by definition because use_same_provider is True.
|
|
list(areq.mappings.values())[0] for areq in areqs
|
|
if areq.use_same_provider)))
|
|
if num_granular_groups == num_granular_groups_in_areqs:
|
|
return True
|
|
LOG.debug('Excluding the following set of AllocationRequest because '
|
|
'group_policy=isolate and the number of granular groups in the '
|
|
'set (%d) does not match the number of granular groups in the '
|
|
'request (%d): %s',
|
|
num_granular_groups_in_areqs, num_granular_groups, str(areqs))
|
|
return False
|
|
|
|
|
|
def _satisfies_same_subtree(areqs, rw_ctx):
|
|
"""Applies same_subtree policy to a list of AllocationRequest.
|
|
|
|
:param areqs: A list containing one AllocationRequest for each input
|
|
RequestGroup.
|
|
:param rw_ctx: The RequestWideSearchContext for this request, from that
|
|
use the following fields:
|
|
|
|
same_subtrees: A list of sets of request group suffixes strings.
|
|
All of the resource providers satisfying the specified
|
|
request groups must be rooted at one of the resource providers
|
|
satisfying the request groups.
|
|
|
|
parent_uuid_by_rp_uuid: A dict of parent uuids keyed by rp uuids.
|
|
:return: True if areqs satisfies same_subtree policy; False otherwise.
|
|
"""
|
|
for same_subtree in rw_ctx.same_subtrees:
|
|
# Collect RP uuids that must satisfy a single same_subtree constraint.
|
|
rp_uuids = set().union(*(areq.mappings.get(suffix) for areq in areqs
|
|
for suffix in same_subtree
|
|
if areq.mappings.get(suffix)))
|
|
if not _check_same_subtree(rp_uuids, rw_ctx.parent_uuid_by_rp_uuid):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _check_same_subtree(rp_uuids, parent_uuid_by_rp_uuid):
|
|
"""Returns True if given rp uuids are all in the same subtree.
|
|
|
|
Note: The rps are in the same subtree means all the providers are
|
|
rooted at one of the providers
|
|
"""
|
|
if len(rp_uuids) == 1:
|
|
return True
|
|
# A set of uuids of common ancestors of each rp in question
|
|
common_ancestors = set.intersection(*(
|
|
_get_ancestors_by_one_uuid(rp_uuid, parent_uuid_by_rp_uuid)
|
|
for rp_uuid in rp_uuids))
|
|
# if any of the rp_uuid is in the common_ancestors set, then
|
|
# we know that, that rp_uuid is the root of the other rp_uuids
|
|
# in this same_subtree constraint.
|
|
return len(common_ancestors.intersection(rp_uuids)) != 0
|
|
|
|
|
|
def _get_ancestors_by_one_uuid(
|
|
rp_uuid, parent_uuid_by_rp_uuid, ancestors=None):
|
|
"""Returns a set of uuids of ancestors for a given rp uuid"""
|
|
if ancestors is None:
|
|
ancestors = set([rp_uuid])
|
|
parent_uuid = parent_uuid_by_rp_uuid[rp_uuid]
|
|
if parent_uuid is None:
|
|
return ancestors
|
|
ancestors.add(parent_uuid)
|
|
return _get_ancestors_by_one_uuid(
|
|
parent_uuid, parent_uuid_by_rp_uuid, ancestors=ancestors)
|
|
|
|
|
|
def _provider_ids_from_root_ids(context, root_ids):
|
|
"""Given an iterable of internal root resource provider IDs, returns a
|
|
dict, keyed by internal provider Id, of sqla objects describing those
|
|
providers under the given root providers.
|
|
|
|
:param root_ids: iterable of root provider IDs for trees to look up
|
|
:returns: dict, keyed by internal provider Id, of sqla objects with the
|
|
following attributes:
|
|
|
|
id: resource provider internal id
|
|
uuid: resource provider uuid
|
|
parent_id: internal id of the resource provider's parent
|
|
provider (None if there is no parent)
|
|
root_id: internal id of the resource providers's root provider
|
|
"""
|
|
# SELECT
|
|
# rp.id, rp.uuid, rp.parent_provider_id, rp.root_provider.id
|
|
# FROM resource_providers AS rp
|
|
# WHERE rp.root_provider_id IN ($root_ids)
|
|
me = sa.alias(_RP_TBL, name="me")
|
|
cols = [
|
|
me.c.id,
|
|
me.c.uuid,
|
|
me.c.parent_provider_id.label('parent_id'),
|
|
me.c.root_provider_id.label('root_id'),
|
|
]
|
|
sel = sa.select(cols).where(
|
|
me.c.root_provider_id.in_(sa.bindparam('root_ids', expanding=True)))
|
|
|
|
ret = {}
|
|
for r in context.session.execute(sel, {'root_ids': list(root_ids)}):
|
|
ret[r['id']] = r
|
|
return ret
|