89bf1a232f
The exceeds_capacity check is a hot spot during get GET allocation_candidates query with multiple request groups. So logging in that function is not practical as it slows down the query and creates such amount of logs that can kill the service itself. Closes-Bug: #2126438 Signed-off-by: Balazs Gibizer <gibi@redhat.com> Change-Id: I2ac9b587a38e6c8e5b9af705c1e524c131b83214 Signed-off-by: Balazs Gibizer <gibi@redhat.com>
1391 lines
57 KiB
Python
1391 lines
57 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.
|
|
"""Utility methods for getting allocation candidates."""
|
|
import collections
|
|
import copy
|
|
|
|
import os_traits
|
|
from oslo_log import log as logging
|
|
import random
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import sql
|
|
|
|
from placement.db.sqlalchemy import models
|
|
from placement import db_api
|
|
from placement import exception
|
|
from placement.objects import rp_candidates
|
|
from placement.objects import trait as trait_obj
|
|
|
|
|
|
# TODO(tetsuro): Move these public symbols in a central place.
|
|
_ALLOC_TBL = models.Allocation.__table__
|
|
_INV_TBL = models.Inventory.__table__
|
|
_RP_TBL = models.ResourceProvider.__table__
|
|
_AGG_TBL = models.PlacementAggregate.__table__
|
|
_RP_AGG_TBL = models.ResourceProviderAggregate.__table__
|
|
_RP_TRAIT_TBL = models.ResourceProviderTrait.__table__
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
AnchorIds = collections.namedtuple(
|
|
'AnchorIds', 'rp_id rp_uuid anchor_id anchor_uuid')
|
|
|
|
|
|
class RequestGroupSearchContext(object):
|
|
"""An adapter object that represents the search for allocation candidates
|
|
for a single request group.
|
|
"""
|
|
|
|
def __init__(self, context, group, has_trees, sharing, suffix=''):
|
|
"""Initializes the object retrieving and caching matching providers
|
|
for each conditions like resource and aggregates from database.
|
|
|
|
:raises placement.exception.ResourceProviderNotFound if there is no
|
|
provider found which satisfies the request.
|
|
"""
|
|
# TODO(tetsuro): split this into smaller functions reordering
|
|
self.context = context
|
|
|
|
# The request group suffix
|
|
self.suffix = suffix
|
|
|
|
# A dict, keyed by resource class internal ID, of the amounts of that
|
|
# resource class being requested by the group.
|
|
self.resources = {}
|
|
# A set of string names of all resource classes requested by the group.
|
|
self.rcs = set()
|
|
for rc, amount in group.resources.items():
|
|
self.resources[context.rc_cache.id_from_string(rc)] = amount
|
|
self.rcs.add(rc)
|
|
|
|
# A list of lists of aggregate UUIDs that the providers matching for
|
|
# that request group must be members of
|
|
self.member_of = group.member_of
|
|
|
|
# A list of aggregate UUIDs that the providers matching for
|
|
# that request group must not be members of
|
|
self.forbidden_aggs = group.forbidden_aggs
|
|
|
|
# A set of provider ids that matches the requested positive aggregates
|
|
self.rps_in_aggs = set()
|
|
if self.member_of:
|
|
self.rps_in_aggs = provider_ids_matching_aggregates(
|
|
context, self.member_of)
|
|
if not self.rps_in_aggs:
|
|
LOG.debug('found no providers matching aggregates %s',
|
|
self.member_of)
|
|
raise exception.ResourceProviderNotFound()
|
|
|
|
# If True, this RequestGroup represents requests which must be
|
|
# satisfied by a single resource provider. If False, represents a
|
|
# request for resources in any resource provider in the same tree,
|
|
# or a sharing provider.
|
|
self.use_same_provider = group.use_same_provider
|
|
|
|
# Both required_trait_names and required_traits expresses the same
|
|
# request with the same nested list of sets structure but
|
|
# required_trait_names contains trait names while required_traits
|
|
# contains trait internal IDs
|
|
self.required_trait_names = group.required_traits
|
|
# let's map the trait names to internal IDs this is useful for DB calls
|
|
# expecting trait IDs. The structure of this field is the same as the
|
|
# required_trait_names field.
|
|
self.required_traits = []
|
|
# forbidden_traits is a dict mapping trait names to trait internal IDs
|
|
self.forbidden_traits = {}
|
|
|
|
for any_traits in group.required_traits:
|
|
self.required_traits.append(
|
|
set(trait_obj.ids_from_names(context, any_traits).values()))
|
|
if group.forbidden_traits:
|
|
self.forbidden_traits = trait_obj.ids_from_names(
|
|
context, group.forbidden_traits)
|
|
|
|
# Internal id of a root provider. If provided, this RequestGroup must
|
|
# be satisfied by resource provider(s) under the root provider.
|
|
self.tree_root_id = None
|
|
if group.in_tree:
|
|
tree_ids = provider_ids_from_uuid(context, group.in_tree)
|
|
if tree_ids is None:
|
|
LOG.debug("No provider found for in_tree%s=%s",
|
|
suffix, group.in_tree)
|
|
raise exception.ResourceProviderNotFound()
|
|
self.tree_root_id = tree_ids.root_id
|
|
LOG.debug("Group %s getting allocation candidates in the same "
|
|
"tree with the root provider %s",
|
|
self.suffix, tree_ids.root_uuid)
|
|
|
|
self._rps_with_resource = {}
|
|
for rc_id, amount in self.resources.items():
|
|
# NOTE(tetsuro): We could pass rps in requested aggregates to
|
|
# get_providers_with_resource here once we explicitly put
|
|
# aggregates to nested (non-root) providers (the aggregate
|
|
# flows down feature) rather than applying later the implicit rule
|
|
# that aggregate on root spans the whole tree
|
|
rc_name = context.rc_cache.string_from_id(rc_id)
|
|
LOG.debug('getting providers with %d %s', amount, rc_name)
|
|
provs_with_resource = get_providers_with_resource(
|
|
context, rc_id, amount, tree_root_id=self.tree_root_id)
|
|
if not provs_with_resource:
|
|
LOG.debug('found no providers with %d %s', amount, rc_name)
|
|
raise exception.ResourceProviderNotFound()
|
|
self._rps_with_resource[rc_id] = provs_with_resource
|
|
|
|
# a set of resource provider IDs that share some inventory for some
|
|
# resource class.
|
|
self._sharing_providers = sharing
|
|
|
|
# bool indicating there is some level of nesting in the environment
|
|
self.has_trees = has_trees
|
|
|
|
@property
|
|
def exists_sharing(self):
|
|
"""bool indicating there is sharing providers in the environment for
|
|
the requested resource class (if there isn't, we take faster, simpler
|
|
code paths)
|
|
"""
|
|
# NOTE: This could be refactored to see the requested resources
|
|
return bool(self._sharing_providers)
|
|
|
|
@property
|
|
def exists_nested(self):
|
|
"""bool indicating there is some level of nesting in the environment
|
|
(if there isn't, we take faster, simpler code paths)
|
|
"""
|
|
# NOTE: This could be refactored to see the requested resources
|
|
return self.has_trees
|
|
|
|
def get_rps_with_shared_capacity(self, rc_id):
|
|
sharing_in_aggs = self._sharing_providers
|
|
if self.rps_in_aggs:
|
|
sharing_in_aggs &= self.rps_in_aggs
|
|
if not sharing_in_aggs:
|
|
return set()
|
|
rps_with_resource = set(p[0] for p in self._rps_with_resource[rc_id])
|
|
return sharing_in_aggs & rps_with_resource
|
|
|
|
def get_rps_with_resource(self, rc_id):
|
|
return self._rps_with_resource.get(rc_id)
|
|
|
|
|
|
class RequestWideSearchContext(object):
|
|
"""An adapter object that represents the search for allocation candidates
|
|
for a request-wide parameters.
|
|
"""
|
|
|
|
def __init__(self, context, rqparams, nested_aware):
|
|
"""Create a RequestWideSearchContext.
|
|
|
|
:param context: placement.context.RequestContext object
|
|
:param rqparams: A RequestWideParams.
|
|
:param nested_aware: Boolean, True if we are at a microversion that
|
|
supports trees; False otherwise.
|
|
"""
|
|
self._ctx = context
|
|
self._limit = rqparams.limit
|
|
self.group_policy = rqparams.group_policy
|
|
self._nested_aware = nested_aware
|
|
self.has_trees = _has_provider_trees(context)
|
|
# This is set up by _process_anchor_* below. It remains None if no
|
|
# anchor filters were requested. Otherwise it becomes a set of internal
|
|
# IDs of root providers that conform to the requested filters.
|
|
self.anchor_root_ids = None
|
|
self._process_anchor_traits(rqparams)
|
|
self.same_subtrees = rqparams.same_subtrees
|
|
# A dict, keyed by resource provider id of ProviderSummary objects.
|
|
# Used as a cache of ProviderSummaries created in this request to
|
|
# avoid duplication.
|
|
self.summaries_by_id = {}
|
|
# A set of resource classes that were requested in more than one group
|
|
self.multi_group_rcs = set()
|
|
# A mapping of resource provider uuid to parent provider uuid, used
|
|
# when merging allocation candidates.
|
|
self.parent_uuid_by_rp_uuid = {}
|
|
# Dict mapping (resource provier uuid, resource class name) to a
|
|
# ProviderSummaryResource. Used during _exceeds_capacity in
|
|
# _merge_candidates.
|
|
self.psum_res_by_rp_rc = {}
|
|
|
|
def _process_anchor_traits(self, rqparams):
|
|
"""Set or filter self.anchor_root_ids according to anchor
|
|
required/forbidden traits.
|
|
|
|
:param rqparams: RequestWideParams.
|
|
:raises TraitNotFound: If any named trait does not exist in the
|
|
database.
|
|
:raises ResourceProviderNotFound: If anchor trait filters were
|
|
specified, but we find no matching providers.
|
|
"""
|
|
required, forbidden = (
|
|
rqparams.anchor_required_traits, rqparams.anchor_forbidden_traits)
|
|
|
|
if not (required or forbidden):
|
|
return
|
|
|
|
required_ids = set(trait_obj.ids_from_names(
|
|
self._ctx, required).values()) if required else None
|
|
forbidden_ids = set(trait_obj.ids_from_names(
|
|
self._ctx, forbidden).values()) if forbidden else None
|
|
|
|
self.anchor_root_ids = _get_roots_with_traits(
|
|
self._ctx, required_ids, forbidden_ids)
|
|
|
|
if not self.anchor_root_ids:
|
|
LOG.debug('found no providers satisfying required traits: %s and '
|
|
'forbidden traits: %s', required, forbidden)
|
|
raise exception.ResourceProviderNotFound()
|
|
|
|
def in_filtered_anchors(self, anchor_root_id):
|
|
"""Returns whether anchor_root_id is present in filtered anchors. (If
|
|
we don't have filtered anchors, that implicitly means "all possible
|
|
anchors", so we return True.)
|
|
"""
|
|
if self.anchor_root_ids is None:
|
|
# Not filtering anchors
|
|
return True
|
|
return anchor_root_id in self.anchor_root_ids
|
|
|
|
def exclude_nested_providers(
|
|
self, allocation_requests, provider_summaries):
|
|
"""Exclude allocation requests and provider summaries for old
|
|
microversions if they involve more than one provider from the same
|
|
tree.
|
|
"""
|
|
if self._nested_aware or not self.has_trees:
|
|
return allocation_requests, provider_summaries
|
|
|
|
filtered_areqs = []
|
|
all_rp_uuids = set()
|
|
for a_req in allocation_requests:
|
|
root_by_rp = {
|
|
arr.resource_provider.uuid:
|
|
arr.resource_provider.root_provider_uuid
|
|
for arr in a_req.resource_requests}
|
|
# If more than one allocation is provided by the same tree,
|
|
# we need to skip that allocation request.
|
|
if len(root_by_rp) == len(set(root_by_rp.values())):
|
|
filtered_areqs.append(a_req)
|
|
all_rp_uuids |= set(root_by_rp)
|
|
|
|
# Exclude eliminated providers from the provider summaries.
|
|
filtered_summaries = [ps for ps in provider_summaries
|
|
if ps.resource_provider.uuid in all_rp_uuids]
|
|
|
|
LOG.debug(
|
|
'Excluding nested providers yields %d allocation requests and '
|
|
'%d provider summaries', len(filtered_areqs),
|
|
len(filtered_summaries))
|
|
return filtered_areqs, filtered_summaries
|
|
|
|
def limit_results(self, alloc_request_objs, summary_objs):
|
|
# Limit the number of allocation request objects. We do this after
|
|
# creating all of them so that we can do a random slice without
|
|
# needing to mess with complex sql or add additional columns to the DB.
|
|
if self._limit and self._limit < len(alloc_request_objs):
|
|
if self._ctx.config.placement.randomize_allocation_candidates:
|
|
alloc_request_objs = random.sample(
|
|
alloc_request_objs, self._limit)
|
|
else:
|
|
alloc_request_objs = alloc_request_objs[:self._limit]
|
|
# Limit summaries to only those mentioned in the allocation reqs.
|
|
kept_summary_objs = []
|
|
alloc_req_root_uuids = set()
|
|
# Extract root resource provider uuids from the resource requests.
|
|
for aro in alloc_request_objs:
|
|
for arr in aro.resource_requests:
|
|
alloc_req_root_uuids.add(
|
|
arr.resource_provider.root_provider_uuid)
|
|
for summary in summary_objs:
|
|
rp_root_uuid = summary.resource_provider.root_provider_uuid
|
|
# Skip a summary if we are limiting and haven't selected an
|
|
# allocation request that uses the resource provider.
|
|
if rp_root_uuid not in alloc_req_root_uuids:
|
|
continue
|
|
kept_summary_objs.append(summary)
|
|
summary_objs = kept_summary_objs
|
|
LOG.debug('Limiting results yields %d allocation requests and '
|
|
'%d provider summaries', len(alloc_request_objs),
|
|
len(summary_objs))
|
|
elif self._ctx.config.placement.randomize_allocation_candidates:
|
|
random.shuffle(alloc_request_objs)
|
|
|
|
return alloc_request_objs, summary_objs
|
|
|
|
def copy_arr_if_needed(self, arr):
|
|
"""Copy or return arr, depending on the search context.
|
|
|
|
In cases with group_policy=none where multiple groups request
|
|
amounts from the same resource class, we end up using the same
|
|
AllocationRequestResource more than once when consolidating. So we
|
|
need to make a copy so we don't overwrite the one used for a
|
|
different result. But as an optimization, since this copy is not
|
|
cheap, we don't do it unless it's necessary.
|
|
|
|
:param arr: An AllocationRequestResource to be returned or copied and
|
|
returned.
|
|
:return: arr or a copy thereof.
|
|
"""
|
|
if self.group_policy != 'none':
|
|
return arr
|
|
if arr.resource_class in self.multi_group_rcs:
|
|
return copy.copy(arr)
|
|
return arr
|
|
|
|
def exceeds_capacity(self, areq):
|
|
"""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.
|
|
:return: True if areq exceeds capacity; False otherwise.
|
|
"""
|
|
# This is a hot spot, called potentially a million times during a
|
|
# GET allocation_candidates query. Please do not add logging in this
|
|
# function permanently as it will cause excessive logging.
|
|
|
|
for arr in areq.resource_requests:
|
|
key = (arr.resource_provider.id, arr.resource_class)
|
|
psum_res = self.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
|
|
|
|
@property
|
|
def config(self):
|
|
return self._ctx.config
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def provider_ids_from_uuid(context, uuid):
|
|
"""Given the UUID of a resource provider, returns a sqlalchemy object with
|
|
the internal ID, the UUID, the parent provider's internal ID, parent
|
|
provider's UUID, the root provider's internal ID and the root provider
|
|
UUID.
|
|
|
|
:returns: sqlalchemy object containing the internal IDs and UUIDs of the
|
|
provider identified by the supplied UUID
|
|
:param uuid: The UUID of the provider to look up
|
|
"""
|
|
# SELECT
|
|
# rp.id, rp.uuid,
|
|
# parent.id AS parent_id, parent.uuid AS parent_uuid,
|
|
# root.id AS root_id, root.uuid AS root_uuid
|
|
# FROM resource_providers AS rp
|
|
# INNER JOIN resource_providers AS root
|
|
# ON rp.root_provider_id = root.id
|
|
# LEFT JOIN resource_providers AS parent
|
|
# ON rp.parent_provider_id = parent.id
|
|
me = sa.alias(_RP_TBL, name="me")
|
|
parent = sa.alias(_RP_TBL, name="parent")
|
|
root = sa.alias(_RP_TBL, name="root")
|
|
cols = [
|
|
me.c.id,
|
|
me.c.uuid,
|
|
parent.c.id.label('parent_id'),
|
|
parent.c.uuid.label('parent_uuid'),
|
|
root.c.id.label('root_id'),
|
|
root.c.uuid.label('root_uuid'),
|
|
]
|
|
me_to_root = sa.join(me, root, me.c.root_provider_id == root.c.id)
|
|
me_to_parent = sa.outerjoin(
|
|
me_to_root, parent,
|
|
me.c.parent_provider_id == parent.c.id)
|
|
sel = sa.select(*cols).select_from(me_to_parent)
|
|
sel = sel.where(me.c.uuid == uuid)
|
|
res = context.session.execute(sel).fetchone()
|
|
if not res:
|
|
return None
|
|
return res
|
|
|
|
|
|
def _usage_select(rc_ids):
|
|
usage = sa.select(
|
|
_ALLOC_TBL.c.resource_provider_id,
|
|
_ALLOC_TBL.c.resource_class_id,
|
|
sql.func.sum(_ALLOC_TBL.c.used).label('used')
|
|
).where(
|
|
_ALLOC_TBL.c.resource_class_id.in_(rc_ids)
|
|
).group_by(
|
|
_ALLOC_TBL.c.resource_provider_id,
|
|
_ALLOC_TBL.c.resource_class_id,
|
|
)
|
|
return usage.subquery(name='usage')
|
|
|
|
|
|
def _capacity_check_clause(amount, usage, inv_tbl=_INV_TBL):
|
|
return sa.and_(
|
|
sql.func.coalesce(usage.c.used, 0) + amount <= (
|
|
(inv_tbl.c.total - inv_tbl.c.reserved) *
|
|
inv_tbl.c.allocation_ratio),
|
|
inv_tbl.c.min_unit <= amount,
|
|
inv_tbl.c.max_unit >= amount,
|
|
amount % inv_tbl.c.step_size == 0,
|
|
)
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_providers_with_resource(ctx, rc_id, amount, tree_root_id=None):
|
|
"""Returns a set of tuples of (provider ID, root provider ID) of providers
|
|
that satisfy the request for a single resource class.
|
|
|
|
:param ctx: Session context to use
|
|
:param rc_id: Internal ID of resource class to check inventory for
|
|
:param amount: Amount of resource being requested
|
|
:param tree_root_id: An optional root provider ID. If provided, the results
|
|
are limited to the resource providers under the given
|
|
root resource provider.
|
|
"""
|
|
# SELECT rp.id, rp.root_provider_id
|
|
# FROM resource_providers AS rp
|
|
# JOIN inventories AS inv
|
|
# ON rp.id = inv.resource_provider_id
|
|
# AND inv.resource_class_id = $RC_ID
|
|
# LEFT JOIN (
|
|
# SELECT
|
|
# allocs.resource_provider_id,
|
|
# SUM(allocs.used) AS used
|
|
# FROM allocations AS allocs
|
|
# WHERE allocs.resource_class_id = $RC_ID
|
|
# GROUP BY allocs.resource_provider_id
|
|
# ) AS usaged
|
|
# ON inv.resource_provider_id = usaged.resource_provider_id
|
|
# WHERE
|
|
# used + $AMOUNT <= ((total - reserved) * inv.allocation_ratio)
|
|
# AND inv.min_unit <= $AMOUNT
|
|
# AND inv.max_unit >= $AMOUNT
|
|
# AND $AMOUNT % inv.step_size = 0
|
|
# # If tree_root_id specified:
|
|
# AND rp.root_provider_id == $tree_root_id
|
|
rpt = sa.alias(_RP_TBL, name="rp")
|
|
inv = sa.alias(_INV_TBL, name="inv")
|
|
usage = _usage_select([rc_id])
|
|
rp_to_inv = sa.join(
|
|
rpt, inv, sa.and_(
|
|
rpt.c.id == inv.c.resource_provider_id,
|
|
inv.c.resource_class_id == rc_id))
|
|
inv_to_usage = sa.outerjoin(
|
|
rp_to_inv, usage,
|
|
inv.c.resource_provider_id == usage.c.resource_provider_id)
|
|
sel = sa.select(rpt.c.id, rpt.c.root_provider_id)
|
|
sel = sel.select_from(inv_to_usage)
|
|
where_conds = _capacity_check_clause(amount, usage, inv_tbl=inv)
|
|
if tree_root_id is not None:
|
|
where_conds = sa.and_(
|
|
rpt.c.root_provider_id == tree_root_id,
|
|
where_conds)
|
|
sel = sel.where(where_conds)
|
|
res = ctx.session.execute(sel).fetchall()
|
|
res = set((r[0], r[1]) for r in res)
|
|
return res
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_providers_with_root(ctx, allowed, forbidden):
|
|
"""Returns a set of tuples of (provider ID, root provider ID) of given
|
|
resource providers
|
|
|
|
:param ctx: Session context to use
|
|
:param allowed: resource provider ids to include
|
|
:param forbidden: resource provider ids to exclude
|
|
"""
|
|
# SELECT rp.id, rp.root_provider_id
|
|
# FROM resource_providers AS rp
|
|
# WHERE rp.id IN ($allowed)
|
|
# AND rp.id NOT IN ($forbidden)
|
|
sel = sa.select(_RP_TBL.c.id, _RP_TBL.c.root_provider_id)
|
|
sel = sel.select_from(_RP_TBL)
|
|
cond = []
|
|
if allowed:
|
|
cond.append(_RP_TBL.c.id.in_(allowed))
|
|
if forbidden:
|
|
cond.append(~_RP_TBL.c.id.in_(forbidden))
|
|
if cond:
|
|
sel = sel.where(sa.and_(*cond))
|
|
res = ctx.session.execute(sel).fetchall()
|
|
res = set((r[0], r[1]) for r in res)
|
|
return res
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_provider_ids_matching(rg_ctx):
|
|
"""Returns a list of tuples of (internal provider ID, root provider ID)
|
|
that have available inventory to satisfy all the supplied requests for
|
|
resources. If no providers match, the empty list is returned.
|
|
|
|
:note: This function is used to get results for (a) a RequestGroup with
|
|
use_same_provider=True in a granular request, or (b) a short cut
|
|
path for scenarios that do NOT involve sharing or nested providers.
|
|
Each `internal provider ID` represents a *single* provider that
|
|
can satisfy *all* of the resource/trait/aggregate criteria. This is
|
|
in contrast with get_trees_matching_all(), where each provider
|
|
might only satisfy *some* of the resources, the rest of which are
|
|
satisfied by other providers in the same tree or shared via
|
|
aggregate.
|
|
|
|
:param rg_ctx: RequestGroupSearchContext
|
|
"""
|
|
filtered_rps, forbidden_rp_ids = get_provider_ids_for_traits_and_aggs(
|
|
rg_ctx)
|
|
|
|
if filtered_rps is None:
|
|
# If no providers match the traits/aggs, we can short out
|
|
return []
|
|
|
|
# Instead of constructing a giant complex SQL statement that joins multiple
|
|
# copies of derived usage tables and inventory tables to each other, we do
|
|
# one query for each requested resource class. This allows us to log a
|
|
# rough idea of which resource class query returned no results (for
|
|
# purposes of rough debugging of a single allocation candidates request) as
|
|
# well as reduce the necessary knowledge of SQL in order to understand the
|
|
# queries being executed here.
|
|
provs_with_resource = set()
|
|
first = True
|
|
for rc_id, amount in rg_ctx.resources.items():
|
|
rc_name = rg_ctx.context.rc_cache.string_from_id(rc_id)
|
|
provs_with_resource = rg_ctx.get_rps_with_resource(rc_id)
|
|
LOG.debug("found %d providers with available %d %s",
|
|
len(provs_with_resource), amount, rc_name)
|
|
if not provs_with_resource:
|
|
return []
|
|
|
|
rc_rp_ids = set(p[0] for p in provs_with_resource)
|
|
# The branching below could be collapsed code-wise, but is in place to
|
|
# make the debug logging clearer.
|
|
if first:
|
|
first = False
|
|
if filtered_rps:
|
|
filtered_rps &= rc_rp_ids
|
|
LOG.debug("found %d providers after applying initial "
|
|
"aggregate and trait filters", len(filtered_rps))
|
|
else:
|
|
filtered_rps = rc_rp_ids
|
|
# The following condition is not necessary for the logic; just
|
|
# prevents the message from being logged unnecessarily.
|
|
if forbidden_rp_ids:
|
|
# Forbidden trait/aggregate filters only need to be applied
|
|
# a) on the first iteration; and
|
|
# b) if not already set up before the loop
|
|
# ...since any providers in the resulting set are the basis
|
|
# for intersections, and providers with forbidden traits
|
|
# are already absent from that set after we've filtered
|
|
# them once.
|
|
filtered_rps -= forbidden_rp_ids
|
|
LOG.debug("found %d providers after applying forbidden "
|
|
"traits/aggregates", len(filtered_rps))
|
|
else:
|
|
filtered_rps &= rc_rp_ids
|
|
LOG.debug("found %d providers after filtering by previous result",
|
|
len(filtered_rps))
|
|
|
|
if not filtered_rps:
|
|
return []
|
|
|
|
if not rg_ctx.resources:
|
|
# NOTE(tetsuro): This does an extra sql query that could be avoided if
|
|
# all the smaller queries in get_provider_ids_for_traits_and_aggs()
|
|
# would return the internal ID and the root ID as well for each RP.
|
|
provs_with_resource = get_providers_with_root(
|
|
rg_ctx.context, filtered_rps, forbidden_rp_ids)
|
|
|
|
# provs_with_resource will contain a superset of providers with IDs still
|
|
# in our filtered_rps set. We return the list of tuples of
|
|
# (internal provider ID, root internal provider ID)
|
|
return [rpids for rpids in provs_with_resource if rpids[0] in filtered_rps]
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_trees_matching_all(rg_ctx, rw_ctx):
|
|
"""Returns a RPCandidates object representing the providers that satisfy
|
|
the request for resources.
|
|
|
|
If traits are also required, this function only returns results where the
|
|
set of providers within a tree that satisfy the resource request
|
|
collectively have all the required traits associated with them. This means
|
|
that given the following provider tree:
|
|
|
|
cn1
|
|
|
|
|
--> pf1 (SRIOV_NET_VF:2)
|
|
|
|
|
--> pf2 (SRIOV_NET_VF:1, HW_NIC_OFFLOAD_GENEVE)
|
|
|
|
If a user requests 1 SRIOV_NET_VF resource and no required traits will
|
|
return both pf1 and pf2. However, a request for 2 SRIOV_NET_VF and required
|
|
trait of HW_NIC_OFFLOAD_GENEVE will return no results (since pf1 is the
|
|
only provider with enough inventory of SRIOV_NET_VF but it does not have
|
|
the required HW_NIC_OFFLOAD_GENEVE trait).
|
|
|
|
:note: This function is used for scenarios 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 RequestGroup.
|
|
|
|
:param rg_ctx: RequestGroupSearchContext
|
|
:param rw_ctx: RequestWideSearchContext
|
|
"""
|
|
if rg_ctx.forbidden_aggs:
|
|
rps_bad_aggs = provider_ids_matching_aggregates(
|
|
rg_ctx.context, [rg_ctx.forbidden_aggs])
|
|
|
|
# To get all trees that collectively have all required resource,
|
|
# aggregates and traits, we use `RPCandidateList` which has a list of
|
|
# three-tuples with the first element being resource provider ID, the
|
|
# second element being the root provider ID and the third being resource
|
|
# class ID.
|
|
provs_with_inv = rp_candidates.RPCandidateList()
|
|
|
|
for rc_id, amount in rg_ctx.resources.items():
|
|
rc_name = rg_ctx.context.rc_cache.string_from_id(rc_id)
|
|
|
|
provs_with_inv_rc = rp_candidates.RPCandidateList()
|
|
rc_provs_with_inv = rg_ctx.get_rps_with_resource(rc_id)
|
|
provs_with_inv_rc.add_rps(rc_provs_with_inv, rc_id)
|
|
LOG.debug("found %d providers under %d trees with available %d %s",
|
|
len(provs_with_inv_rc), len(provs_with_inv_rc.trees),
|
|
amount, rc_name)
|
|
if not provs_with_inv_rc:
|
|
# If there's no providers that have one of the resource classes,
|
|
# then we can short-circuit returning an empty RPCandidateList
|
|
return rp_candidates.RPCandidateList()
|
|
|
|
sharing_providers = rg_ctx.get_rps_with_shared_capacity(rc_id)
|
|
if sharing_providers and rg_ctx.tree_root_id is None:
|
|
# There are sharing providers for this resource class, so we
|
|
# should also get combinations of (sharing provider, anchor root)
|
|
# in addition to (non-sharing provider, anchor root) we've just
|
|
# got via get_providers_with_resource() above. We must skip this
|
|
# process if tree_root_id is provided via the ?in_tree=<rp_uuid>
|
|
# queryparam, because it restricts resources from another tree.
|
|
anchors = anchors_for_sharing_providers(
|
|
rg_ctx.context, sharing_providers)
|
|
rc_provs_with_inv = set(
|
|
(anchor.rp_id, anchor.anchor_id) for anchor in anchors)
|
|
provs_with_inv_rc.add_rps(rc_provs_with_inv, rc_id)
|
|
LOG.debug(
|
|
"considering %d sharing providers with %d %s, "
|
|
"now we've got %d provider trees",
|
|
len(sharing_providers), amount, rc_name,
|
|
len(provs_with_inv_rc.trees))
|
|
|
|
# If we have a list of viable anchor roots, filter to those
|
|
if rw_ctx.anchor_root_ids:
|
|
provs_with_inv_rc.filter_by_tree(rw_ctx.anchor_root_ids)
|
|
LOG.debug(
|
|
"found %d providers under %d trees after applying anchor root "
|
|
"filter",
|
|
len(provs_with_inv_rc.rps), len(provs_with_inv_rc.trees))
|
|
# If that left nothing, we're done
|
|
if not provs_with_inv_rc:
|
|
return rp_candidates.RPCandidateList()
|
|
|
|
if rg_ctx.member_of:
|
|
# Aggregate on root spans the whole tree, so the rp itself
|
|
# *or its root* should be in the aggregate
|
|
provs_with_inv_rc.filter_by_rp_or_tree(rg_ctx.rps_in_aggs)
|
|
LOG.debug("found %d providers under %d trees after applying "
|
|
"aggregate filter %s",
|
|
len(provs_with_inv_rc.rps), len(provs_with_inv_rc.trees),
|
|
rg_ctx.member_of)
|
|
if not provs_with_inv_rc:
|
|
# Short-circuit returning an empty RPCandidateList
|
|
return rp_candidates.RPCandidateList()
|
|
if rg_ctx.forbidden_aggs:
|
|
# Aggregate on root spans the whole tree, so the rp itself
|
|
# *and its root* should be outside the aggregate
|
|
provs_with_inv_rc.filter_by_rp_nor_tree(rps_bad_aggs)
|
|
LOG.debug("found %d providers under %d trees after applying "
|
|
"negative aggregate filter %s",
|
|
len(provs_with_inv_rc.rps), len(provs_with_inv_rc.trees),
|
|
rg_ctx.forbidden_aggs)
|
|
if not provs_with_inv_rc:
|
|
# Short-circuit returning an empty RPCandidateList
|
|
return rp_candidates.RPCandidateList()
|
|
|
|
# Adding the resource providers we've got for this resource class,
|
|
# filter provs_with_inv to have only trees with enough inventories
|
|
# for this resource class. Here "tree" includes sharing providers
|
|
# in its terminology
|
|
provs_with_inv.merge_common_trees(provs_with_inv_rc)
|
|
LOG.debug(
|
|
"found %d providers under %d trees after filtering by "
|
|
"previous result",
|
|
len(provs_with_inv.rps), len(provs_with_inv.trees))
|
|
if not provs_with_inv:
|
|
return rp_candidates.RPCandidateList()
|
|
|
|
if (not rg_ctx.required_traits and not rg_ctx.forbidden_traits) or (
|
|
rg_ctx.exists_sharing):
|
|
# If there were no traits required, there's no difference in how we
|
|
# calculate allocation requests between nested and non-nested
|
|
# environments, so just short-circuit and return. Or if sharing
|
|
# providers are in play, we check the trait constraints later
|
|
# in _alloc_candidates_multiple_providers(), so skip.
|
|
return provs_with_inv
|
|
|
|
# Return the providers where the providers have the available inventory
|
|
# capacity and that set of providers (grouped by their tree) have all
|
|
# of the required traits and none of the forbidden traits
|
|
rp_tuples_with_trait = _get_trees_with_traits(
|
|
rg_ctx.context, provs_with_inv.rps, rg_ctx.required_traits,
|
|
rg_ctx.forbidden_traits)
|
|
provs_with_inv.filter_by_rp(rp_tuples_with_trait)
|
|
LOG.debug("found %d providers under %d trees after applying "
|
|
"traits filter - required: %s, forbidden: %s",
|
|
len(provs_with_inv.rps), len(provs_with_inv.trees),
|
|
list(rg_ctx.required_trait_names),
|
|
list(rg_ctx.forbidden_traits))
|
|
|
|
return provs_with_inv
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def _get_trees_with_traits(ctx, rp_ids, required_traits, forbidden_traits):
|
|
"""Given a list of provider IDs, filter them to return a set of tuples of
|
|
(provider ID, root provider ID) of providers which belong to a tree that
|
|
can satisfy trait requirements.
|
|
|
|
This returns trees that still have the possibility to be a match according
|
|
to the required and forbidden traits. It returns every rp from
|
|
the tree that is in rp_ids, even if some of those rps are providing
|
|
forbidden traits.
|
|
This filters out a whole tree if either:
|
|
* every RPs of the tree from rp_ids having a forbidden trait (see
|
|
test_get_trees_with_traits_forbidden_1 and _2)
|
|
* there is a required trait that none of the RPs of the tree from rp_ids
|
|
provide (see test_get_trees_with_traits) or there is an RP providing
|
|
the required trait but that also provides a forbidden trait
|
|
(see test_get_trees_with_traits_forbidden_3)
|
|
|
|
The returned tree still might not be a valid tree as this function
|
|
returns a tree even if some providers need to be ignored due to forbidden
|
|
traits. So if those RPs are needed from resource perspective then the tree
|
|
will be filtered out later by
|
|
objects.allocation_candidate._check_traits_for_alloc_request
|
|
|
|
:param ctx: Session context to use
|
|
:param rp_ids: a set of resource provider IDs
|
|
:param required_traits: A list of set of trait internal IDs where the
|
|
traits in each nested set are OR'd while the items in the outer list are
|
|
AND'd together. The RPs in the tree should COLLECTIVELY fulfill this
|
|
trait request.
|
|
:param forbidden_traits: A list of trait internal IDs that a resource
|
|
provider tree must not have.
|
|
"""
|
|
# TODO(gibi): if somebody can formulate the below three SQL query to a
|
|
# single one then probably that will improve performance
|
|
|
|
# Get the root of all rps in the rp_ids as we need to return every rp from
|
|
# rp_ids that is in a matching tree but below we will filter out rps by
|
|
# traits. So we need a copy and also that copy needs to associate rps to
|
|
# trees by root_id
|
|
rpt = sa.alias(_RP_TBL, name='rpt')
|
|
sel = sa.select(rpt.c.id, rpt.c.root_provider_id).select_from(rpt)
|
|
sel = sel.where(rpt.c.id.in_(rp_ids))
|
|
res = ctx.session.execute(sel).fetchall()
|
|
original_rp_ids = {rp_id: root_id for rp_id, root_id in res}
|
|
|
|
# First filter out the rps from the rp_ids list that provide forbidden
|
|
# traits. To do that we collect those rps that provide any of the forbidden
|
|
# traits and with the outer join and the null check we filter them out
|
|
# of the result
|
|
rptt_forbidden = sa.alias(_RP_TRAIT_TBL, name="rptt_forbidden")
|
|
rp_to_trait = sa.outerjoin(
|
|
rpt, rptt_forbidden,
|
|
sa.and_(
|
|
rpt.c.id == rptt_forbidden.c.resource_provider_id,
|
|
rptt_forbidden.c.trait_id.in_(forbidden_traits)
|
|
)
|
|
)
|
|
sel = sa.select(rpt.c.id, rpt.c.root_provider_id).select_from(rp_to_trait)
|
|
sel = sel.where(
|
|
sa.and_(
|
|
rpt.c.id.in_(original_rp_ids.keys()),
|
|
rptt_forbidden.c.trait_id == sa.null()
|
|
)
|
|
)
|
|
res = ctx.session.execute(sel).fetchall()
|
|
|
|
# These are the rps that does not provide any forbidden traits
|
|
good_rp_ids = {}
|
|
for rp_id, root_id in res:
|
|
good_rp_ids[rp_id] = root_id
|
|
|
|
# shortcut if no traits required the good_rp_ids.values() contains all the
|
|
# good roots
|
|
if not required_traits:
|
|
return {
|
|
(rp_id, root_id)
|
|
for rp_id, root_id in original_rp_ids.items()
|
|
if root_id in good_rp_ids.values()
|
|
}
|
|
|
|
# now get the traits provided by the good rps per tree
|
|
rptt = sa.alias(_RP_TRAIT_TBL, name="rptt")
|
|
rp_to_trait = sa.join(
|
|
rpt, rptt, rpt.c.id == rptt.c.resource_provider_id)
|
|
sel = sa.select(
|
|
rpt.c.root_provider_id, rptt.c.trait_id
|
|
).select_from(rp_to_trait)
|
|
sel = sel.where(rpt.c.id.in_(good_rp_ids))
|
|
res = ctx.session.execute(sel).fetchall()
|
|
|
|
root_to_traits = collections.defaultdict(set)
|
|
for root_id, trait_id in res:
|
|
root_to_traits[root_id].add(trait_id)
|
|
|
|
result = set()
|
|
|
|
# filter the trees by checking if each tree provides all the
|
|
# required_traits
|
|
for root_id, provided_traits in root_to_traits.items():
|
|
# we need a match for all the items from the outer list of the
|
|
# required_traits as that describes AND relationship
|
|
if all(
|
|
# we need at least one match per nested trait set as that set
|
|
# describes OR relationship
|
|
any_traits.intersection(provided_traits)
|
|
for any_traits in required_traits
|
|
):
|
|
# This tree is matching the required traits so add result all the
|
|
# rps from the original rp_ids that belongs to this tree
|
|
result.update(
|
|
{
|
|
(rp_id, root_id)
|
|
for rp_id, original_root_id in original_rp_ids.items()
|
|
if root_id == original_root_id
|
|
}
|
|
|
|
)
|
|
return result
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def _get_roots_with_traits(ctx, required_traits, forbidden_traits):
|
|
"""Return a set of IDs of root providers (NOT trees) that can satisfy trait
|
|
requirements.
|
|
|
|
At least one of ``required_traits`` or ``forbidden_traits`` is required.
|
|
|
|
:param ctx: Session context to use
|
|
:param required_traits: A set of required trait internal IDs that each root
|
|
provider must have associated with it.
|
|
:param forbidden_traits: A set of trait internal IDs that each root
|
|
provider must not have.
|
|
:returns: A set of internal IDs of root providers that satisfy the
|
|
specified trait requirements. The empty set if no roots match.
|
|
:raises ValueError: If required_traits and forbidden_traits are both empty/
|
|
None.
|
|
"""
|
|
if not (required_traits or forbidden_traits):
|
|
raise ValueError("At least one of required_traits or forbidden_traits "
|
|
"is required.")
|
|
|
|
# The SQL we want looks like this:
|
|
#
|
|
# SELECT rp.id FROM resource_providers AS rp
|
|
rpt = sa.alias(_RP_TBL, name="rp")
|
|
sel = sa.select(rpt.c.id)
|
|
|
|
# WHERE rp.parent_provider_id IS NULL
|
|
cond = [rpt.c.parent_provider_id.is_(None)]
|
|
subq_join = None
|
|
|
|
# TODO(efried): DRY traits subquery with _get_trees_with_traits
|
|
|
|
# # Only if we have required traits...
|
|
if required_traits:
|
|
# INNER JOIN resource_provider_traits AS rptt
|
|
# ON rp.id = rptt.resource_provider_id
|
|
# AND rptt.trait_id IN ($REQUIRED_TRAIT_IDS)
|
|
rptt = sa.alias(_RP_TRAIT_TBL, name="rptt")
|
|
rpt_to_rptt = sa.join(
|
|
rpt, rptt, sa.and_(
|
|
rpt.c.id == rptt.c.resource_provider_id,
|
|
rptt.c.trait_id.in_(required_traits)))
|
|
subq_join = rpt_to_rptt
|
|
# Only get the resource providers that have ALL the required traits,
|
|
# so we need to GROUP BY the provider id and ensure that the
|
|
# COUNT(trait_id) is equal to the number of traits we are requiring
|
|
num_traits = len(required_traits)
|
|
having_cond = sa.func.count(sa.distinct(rptt.c.trait_id)) == num_traits
|
|
sel = sel.having(having_cond)
|
|
|
|
# # Only if we have forbidden_traits...
|
|
if forbidden_traits:
|
|
# LEFT JOIN resource_provider_traits AS rptt_forbid
|
|
rptt_forbid = sa.alias(_RP_TRAIT_TBL, name="rptt_forbid")
|
|
join_to = rpt
|
|
if subq_join is not None:
|
|
join_to = subq_join
|
|
rpt_to_rptt_forbid = sa.outerjoin(
|
|
# ON rp.id = rptt_forbid.resource_provider_id
|
|
# AND rptt_forbid.trait_id IN ($FORBIDDEN_TRAIT_IDS)
|
|
join_to, rptt_forbid, sa.and_(
|
|
rpt.c.id == rptt_forbid.c.resource_provider_id,
|
|
rptt_forbid.c.trait_id.in_(forbidden_traits)))
|
|
# AND rptt_forbid.resource_provider_id IS NULL
|
|
cond.append(rptt_forbid.c.resource_provider_id.is_(None))
|
|
subq_join = rpt_to_rptt_forbid
|
|
|
|
sel = sel.select_from(subq_join).where(sa.and_(*cond)).group_by(rpt.c.id)
|
|
|
|
return set(row[0] for row in ctx.session.execute(sel).fetchall())
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def provider_ids_matching_aggregates(context, member_of, rp_ids=None):
|
|
"""Given a list of lists of aggregate UUIDs, return the internal IDs of all
|
|
resource providers associated with the aggregates.
|
|
|
|
:param member_of: A list containing lists of aggregate UUIDs. Each item in
|
|
the outer list is to be AND'd together. If that item contains multiple
|
|
values, they are OR'd together.
|
|
|
|
For example, if member_of is::
|
|
|
|
[
|
|
['agg1'],
|
|
['agg2', 'agg3'],
|
|
]
|
|
|
|
we will return all the resource providers that are
|
|
associated with agg1 as well as either (agg2 or agg3)
|
|
:param rp_ids: When present, returned resource providers are limited
|
|
to only those in this value
|
|
|
|
:returns: A set of internal resource provider IDs having all required
|
|
aggregate associations
|
|
"""
|
|
# Given a request for the following:
|
|
#
|
|
# member_of = [
|
|
# [agg1],
|
|
# [agg2],
|
|
# [agg3, agg4]
|
|
# ]
|
|
#
|
|
# we need to produce the following SQL expression:
|
|
#
|
|
# SELECT
|
|
# rp.id
|
|
# FROM resource_providers AS rp
|
|
# JOIN resource_provider_aggregates AS rpa1
|
|
# ON rp.id = rpa1.resource_provider_id
|
|
# AND rpa1.aggregate_id IN ($AGG1_ID)
|
|
# JOIN resource_provider_aggregates AS rpa2
|
|
# ON rp.id = rpa2.resource_provider_id
|
|
# AND rpa2.aggregate_id IN ($AGG2_ID)
|
|
# JOIN resource_provider_aggregates AS rpa3
|
|
# ON rp.id = rpa3.resource_provider_id
|
|
# AND rpa3.aggregate_id IN ($AGG3_ID, $AGG4_ID)
|
|
# # Only if we have rp_ids...
|
|
# WHERE rp.id IN ($RP_IDs)
|
|
|
|
# First things first, get a map of all the aggregate UUID to internal
|
|
# aggregate IDs
|
|
agg_uuids = set()
|
|
for members in member_of:
|
|
for member in members:
|
|
agg_uuids.add(member)
|
|
agg_tbl = sa.alias(_AGG_TBL, name='aggs')
|
|
agg_sel = sa.select(agg_tbl.c.uuid, agg_tbl.c.id)
|
|
agg_sel = agg_sel.where(agg_tbl.c.uuid.in_(agg_uuids))
|
|
agg_uuid_map = {
|
|
r[0]: r[1] for r in context.session.execute(agg_sel).fetchall()
|
|
}
|
|
|
|
rp_tbl = sa.alias(_RP_TBL, name='rp')
|
|
join_chain = rp_tbl
|
|
|
|
for x, members in enumerate(member_of):
|
|
rpa_tbl = sa.alias(_RP_AGG_TBL, name='rpa%d' % x)
|
|
|
|
agg_ids = [agg_uuid_map[member] for member in members
|
|
if member in agg_uuid_map]
|
|
if not agg_ids:
|
|
# This member_of list contains only non-existent aggregate UUIDs
|
|
# and therefore we will always return 0 results, so short-circuit
|
|
return set()
|
|
|
|
join_cond = sa.and_(
|
|
rp_tbl.c.id == rpa_tbl.c.resource_provider_id,
|
|
rpa_tbl.c.aggregate_id.in_(agg_ids))
|
|
join_chain = sa.join(join_chain, rpa_tbl, join_cond)
|
|
sel = sa.select(rp_tbl.c.id).select_from(join_chain)
|
|
if rp_ids:
|
|
sel = sel.where(rp_tbl.c.id.in_(rp_ids))
|
|
return set(r[0] for r in context.session.execute(sel))
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def provider_ids_matching_required_traits(
|
|
context, required_traits, rp_ids=None
|
|
):
|
|
"""Given a list of set of trait internal IDs, return the internal IDs of
|
|
all resource providers that individually satisfy the requested traits.
|
|
|
|
:param context: The request context
|
|
:param required_traits: A non-empty list containing sets of trait IDs.
|
|
Each item in the outer list is to be AND'd together. If that item
|
|
contains multiple values, they are OR'd together.
|
|
|
|
For example, if required is::
|
|
|
|
[
|
|
{'trait1ID'},
|
|
{'trait2ID', 'trait3ID'},
|
|
]
|
|
|
|
we will return all the resource providers that has trait1 and either
|
|
trait2 or trait3.
|
|
:param rp_ids: When present, returned resource providers are limited
|
|
to only those in this value
|
|
|
|
:returns: A set of internal resource provider IDs having all required
|
|
traits
|
|
"""
|
|
if not required_traits:
|
|
raise ValueError('required_traits must not be empty')
|
|
|
|
# Given a request for the following:
|
|
#
|
|
# required = [
|
|
# {trait1},
|
|
# {trait2},
|
|
# {trait3, trait4}
|
|
# ]
|
|
#
|
|
# we need to produce the following SQL expression:
|
|
#
|
|
# SELECT
|
|
# rp.id
|
|
# FROM resource_providers AS rp
|
|
# JOIN resource_provider_traits AS rpt1
|
|
# ON rp.id = rpt1.resource_provider_id
|
|
# AND rpt1.trait_id IN ($TRAIT1_ID)
|
|
# JOIN resource_provider_traits AS rpt2
|
|
# ON rp.id = rpt2.resource_provider_id
|
|
# AND rpt2.trait_id IN ($TRAIT2_ID)
|
|
# JOIN resource_provider_traits AS rpt3
|
|
# ON rp.id = rpt3.resource_provider_id
|
|
# AND rpt3.trait_id IN ($TRAIT3_ID, $TRAIT4_ID)
|
|
# # Only if we have rp_ids...
|
|
# WHERE rp.id IN ($RP_IDs)
|
|
|
|
rp_tbl = sa.alias(_RP_TBL, name='rp')
|
|
join_chain = rp_tbl
|
|
|
|
for x, any_traits in enumerate(required_traits):
|
|
rpt_tbl = sa.alias(_RP_TRAIT_TBL, name='rpt%d' % x)
|
|
|
|
join_cond = sa.and_(
|
|
rp_tbl.c.id == rpt_tbl.c.resource_provider_id,
|
|
rpt_tbl.c.trait_id.in_(any_traits))
|
|
join_chain = sa.join(join_chain, rpt_tbl, join_cond)
|
|
|
|
sel = sa.select(rp_tbl.c.id).select_from(join_chain)
|
|
if rp_ids:
|
|
sel = sel.where(rp_tbl.c.id.in_(rp_ids))
|
|
return set(r[0] for r in context.session.execute(sel))
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_provider_ids_having_any_trait(ctx, traits):
|
|
"""Returns a set of resource provider internal IDs that individually
|
|
have ANY of the supplied traits.
|
|
|
|
:param ctx: Session context to use
|
|
:param traits: An iterable of trait internal IDs, at least one of which
|
|
each provider must have associated with it.
|
|
:raise ValueError: If traits is empty or None.
|
|
"""
|
|
if not traits:
|
|
raise ValueError('traits must not be empty')
|
|
|
|
rptt = sa.alias(_RP_TRAIT_TBL, name="rpt")
|
|
sel = sa.select(rptt.c.resource_provider_id)
|
|
sel = sel.where(rptt.c.trait_id.in_(traits))
|
|
sel = sel.group_by(rptt.c.resource_provider_id)
|
|
return set(r[0] for r in ctx.session.execute(sel))
|
|
|
|
|
|
def get_provider_ids_for_traits_and_aggs(rg_ctx):
|
|
"""Get internal IDs for all providers matching the specified traits/aggs.
|
|
|
|
:return: A tuple of:
|
|
filtered_rp_ids: A set of internal provider IDs matching the specified
|
|
criteria. If None, work was done and resulted in no matching
|
|
providers. This is in contrast to the empty set, which indicates
|
|
that no filtering was performed.
|
|
forbidden_rp_ids: A set of internal IDs of providers having any of the
|
|
specified forbidden_traits.
|
|
"""
|
|
filtered_rps = set()
|
|
if rg_ctx.required_traits:
|
|
trait_rps = provider_ids_matching_required_traits(
|
|
rg_ctx.context, rg_ctx.required_traits)
|
|
filtered_rps = trait_rps
|
|
LOG.debug("found %d providers after applying required traits filter "
|
|
"(%s)",
|
|
len(filtered_rps), list(rg_ctx.required_trait_names))
|
|
if not filtered_rps:
|
|
return None, []
|
|
|
|
# If 'member_of' has values, do a separate lookup to identify the
|
|
# resource providers that meet the member_of constraints.
|
|
if rg_ctx.member_of:
|
|
if filtered_rps:
|
|
filtered_rps &= rg_ctx.rps_in_aggs
|
|
else:
|
|
filtered_rps = rg_ctx.rps_in_aggs
|
|
LOG.debug("found %d providers after applying required aggregates "
|
|
"filter (%s)", len(filtered_rps), rg_ctx.member_of)
|
|
if not filtered_rps:
|
|
return None, []
|
|
|
|
forbidden_rp_ids = set()
|
|
if rg_ctx.forbidden_aggs:
|
|
rps_bad_aggs = provider_ids_matching_aggregates(
|
|
rg_ctx.context, [rg_ctx.forbidden_aggs])
|
|
forbidden_rp_ids |= rps_bad_aggs
|
|
if filtered_rps:
|
|
filtered_rps -= rps_bad_aggs
|
|
LOG.debug("found %d providers after applying forbidden aggregates "
|
|
"filter (%s)", len(filtered_rps), rg_ctx.forbidden_aggs)
|
|
if not filtered_rps:
|
|
return None, []
|
|
|
|
if rg_ctx.forbidden_traits:
|
|
rps_bad_traits = get_provider_ids_having_any_trait(
|
|
rg_ctx.context, rg_ctx.forbidden_traits.values())
|
|
forbidden_rp_ids |= rps_bad_traits
|
|
if filtered_rps:
|
|
filtered_rps -= rps_bad_traits
|
|
LOG.debug("found %d providers after applying forbidden traits "
|
|
"filter (%s)", len(filtered_rps),
|
|
list(rg_ctx.forbidden_traits))
|
|
if not filtered_rps:
|
|
return None, []
|
|
|
|
return filtered_rps, forbidden_rp_ids
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def get_sharing_providers(ctx, rp_ids=None):
|
|
"""Returns a set of resource provider IDs (internal IDs, not UUIDs)
|
|
that indicate that they share resource via an aggregate association.
|
|
|
|
Shared resource providers are marked with a standard trait called
|
|
MISC_SHARES_VIA_AGGREGATE. This indicates that the provider allows its
|
|
inventory to be consumed by other resource providers associated via an
|
|
aggregate link.
|
|
|
|
For example, assume we have two compute nodes, CN_1 and CN_2, each with
|
|
inventory of VCPU and MEMORY_MB but not DISK_GB (in other words, these are
|
|
compute nodes with no local disk). There is a resource provider called
|
|
"NFS_SHARE" that has an inventory of DISK_GB and has the
|
|
MISC_SHARES_VIA_AGGREGATE trait. Both the "CN_1" and "CN_2" compute node
|
|
resource providers and the "NFS_SHARE" resource provider are associated
|
|
with an aggregate called "AGG_1".
|
|
|
|
The scheduler needs to determine the resource providers that can fulfill a
|
|
request for 2 VCPU, 1024 MEMORY_MB and 100 DISK_GB.
|
|
|
|
Clearly, no single provider can satisfy the request for all three
|
|
resources, since neither compute node has DISK_GB inventory and the
|
|
NFS_SHARE provider has no VCPU or MEMORY_MB inventories.
|
|
|
|
However, if we consider the NFS_SHARE resource provider as providing
|
|
inventory of DISK_GB for both CN_1 and CN_2, we can include CN_1 and CN_2
|
|
as potential fits for the requested set of resources.
|
|
|
|
To facilitate that matching query, this function returns all providers that
|
|
indicate they share their inventory with providers in some aggregate.
|
|
|
|
:param rp_ids: When present, returned resource providers are limited to
|
|
only those in this value
|
|
"""
|
|
# The SQL we need to generate here looks like this:
|
|
#
|
|
# SELECT rp.id
|
|
# FROM resource_providers AS rp
|
|
# INNER JOIN resource_provider_traits AS rpt
|
|
# ON rp.id = rpt.resource_provider_id
|
|
# AND rpt.trait_id = ${"MISC_SHARES_VIA_AGGREGATE" trait id}
|
|
# WHERE rp.id IN $(RP_IDs)
|
|
|
|
sharing_trait = trait_obj.Trait.get_by_name(
|
|
ctx, os_traits.MISC_SHARES_VIA_AGGREGATE)
|
|
|
|
rp_tbl = sa.alias(_RP_TBL, name='rp')
|
|
rpt_tbl = sa.alias(_RP_TRAIT_TBL, name='rpt')
|
|
|
|
rp_to_rpt_join = sa.join(
|
|
rp_tbl, rpt_tbl,
|
|
sa.and_(rp_tbl.c.id == rpt_tbl.c.resource_provider_id,
|
|
rpt_tbl.c.trait_id == sharing_trait.id)
|
|
)
|
|
|
|
sel = sa.select(rp_tbl.c.id).select_from(rp_to_rpt_join)
|
|
if rp_ids:
|
|
sel = sel.where(rp_tbl.c.id.in_(rp_ids))
|
|
|
|
return set(r[0] for r in ctx.session.execute(sel))
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def anchors_for_sharing_providers(context, rp_ids):
|
|
"""Given a list of internal IDs of sharing providers, returns a set of
|
|
AnchorIds namedtuples, where each anchor is the unique root provider of a
|
|
tree associated with the same aggregate as the sharing provider. (These are
|
|
the providers that can "anchor" a single AllocationRequest.)
|
|
|
|
The sharing provider may or may not itself be part of a tree; in either
|
|
case, an entry for this root provider is included in the result.
|
|
|
|
If the sharing provider is not part of any aggregate, the empty list is
|
|
returned.
|
|
"""
|
|
# SELECT sps.id, sps.uuid, rps.id, rps.uuid)
|
|
# FROM resource_providers AS sps
|
|
# INNER JOIN resource_provider_aggregates AS shr_aggs
|
|
# ON sps.id = shr_aggs.resource_provider_id
|
|
# INNER JOIN resource_provider_aggregates AS shr_with_sps_aggs
|
|
# ON shr_aggs.aggregate_id = shr_with_sps_aggs.aggregate_id
|
|
# INNER JOIN resource_providers AS shr_with_sps
|
|
# ON shr_with_sps_aggs.resource_provider_id = shr_with_sps.id
|
|
# INNER JOIN resource_providers AS rps
|
|
# ON shr_with_sps.root_provider_id = rps.id
|
|
# WHERE sps.id IN $(RP_IDs)
|
|
rps = sa.alias(_RP_TBL, name='rps')
|
|
sps = sa.alias(_RP_TBL, name='sps')
|
|
shr_aggs = sa.alias(_RP_AGG_TBL, name='shr_aggs')
|
|
shr_with_sps_aggs = sa.alias(_RP_AGG_TBL, name='shr_with_sps_aggs')
|
|
shr_with_sps = sa.alias(_RP_TBL, name='shr_with_sps')
|
|
join_chain = sa.join(
|
|
sps, shr_aggs, sps.c.id == shr_aggs.c.resource_provider_id)
|
|
join_chain = sa.join(
|
|
join_chain, shr_with_sps_aggs,
|
|
shr_aggs.c.aggregate_id == shr_with_sps_aggs.c.aggregate_id)
|
|
join_chain = sa.join(
|
|
join_chain, shr_with_sps,
|
|
shr_with_sps_aggs.c.resource_provider_id == shr_with_sps.c.id)
|
|
join_chain = sa.join(
|
|
join_chain, rps, shr_with_sps.c.root_provider_id == rps.c.id)
|
|
sel = sa.select(sps.c.id, sps.c.uuid, rps.c.id, rps.c.uuid)
|
|
sel = sel.select_from(join_chain)
|
|
sel = sel.where(sps.c.id.in_(rp_ids))
|
|
return set([
|
|
AnchorIds(*res) for res in context.session.execute(sel).fetchall()])
|
|
|
|
|
|
@db_api.placement_context_manager.reader
|
|
def _has_provider_trees(ctx):
|
|
"""Simple method that returns whether provider trees (i.e. nested resource
|
|
providers) are in use in the deployment at all. This information is used to
|
|
switch code paths when attempting to retrieve allocation candidate
|
|
information. The code paths are eminently easier to execute and follow for
|
|
non-nested scenarios...
|
|
|
|
NOTE(jaypipes): The result of this function can be cached extensively.
|
|
"""
|
|
sel = sa.select(_RP_TBL.c.id)
|
|
sel = sel.where(_RP_TBL.c.parent_provider_id.isnot(None))
|
|
sel = sel.limit(1)
|
|
res = ctx.session.execute(sel).fetchall()
|
|
return len(res) > 0
|
|
|
|
|
|
def get_usages_by_provider_trees(ctx, root_ids):
|
|
"""Returns a row iterator of usage records grouped by provider ID
|
|
for all resource providers in all trees indicated in the ``root_ids``.
|
|
"""
|
|
# We build up a SQL expression that looks like this:
|
|
# SELECT
|
|
# rp.id as resource_provider_id
|
|
# , rp.uuid as resource_provider_uuid
|
|
# , inv.resource_class_id
|
|
# , inv.total
|
|
# , inv.reserved
|
|
# , inv.allocation_ratio
|
|
# , inv.max_unit
|
|
# , usage.used
|
|
# FROM resource_providers AS rp
|
|
# LEFT JOIN inventories AS inv
|
|
# ON rp.id = inv.resource_provider_id
|
|
# LEFT JOIN (
|
|
# SELECT resource_provider_id, resource_class_id, SUM(used) as used
|
|
# FROM allocations
|
|
# JOIN resource_providers
|
|
# ON allocations.resource_provider_id = resource_providers.id
|
|
# AND resource_providers.root_provider_id IN($root_ids)
|
|
# GROUP BY resource_provider_id, resource_class_id
|
|
# )
|
|
# AS usage
|
|
# ON inv.resource_provider_id = usage.resource_provider_id
|
|
# AND inv.resource_class_id = usage.resource_class_id
|
|
# WHERE rp.root_provider_id IN ($root_ids)
|
|
|
|
rpt = sa.alias(_RP_TBL, name="rp")
|
|
inv = sa.alias(_INV_TBL, name="inv")
|
|
# Build our derived table (subquery in the FROM clause) that sums used
|
|
# amounts for resource provider and resource class
|
|
derived_alloc_to_rp = sa.join(
|
|
_ALLOC_TBL, _RP_TBL,
|
|
sa.and_(_ALLOC_TBL.c.resource_provider_id == _RP_TBL.c.id,
|
|
_RP_TBL.c.root_provider_id.in_(sa.bindparam(
|
|
'root_ids', expanding=True)))
|
|
)
|
|
usage = sa.select(
|
|
_ALLOC_TBL.c.resource_provider_id,
|
|
_ALLOC_TBL.c.resource_class_id,
|
|
sql.func.sum(_ALLOC_TBL.c.used).label('used'),
|
|
).select_from(derived_alloc_to_rp).group_by(
|
|
_ALLOC_TBL.c.resource_provider_id,
|
|
_ALLOC_TBL.c.resource_class_id
|
|
).subquery(name='usage')
|
|
# Build a join between the resource providers and inventories table
|
|
rpt_inv_join = sa.outerjoin(rpt, inv,
|
|
rpt.c.id == inv.c.resource_provider_id)
|
|
# And then join to the derived table of usages
|
|
usage_join = sa.outerjoin(
|
|
rpt_inv_join,
|
|
usage,
|
|
sa.and_(
|
|
usage.c.resource_provider_id == inv.c.resource_provider_id,
|
|
usage.c.resource_class_id == inv.c.resource_class_id,
|
|
),
|
|
)
|
|
query = sa.select(
|
|
rpt.c.id.label("resource_provider_id"),
|
|
rpt.c.uuid.label("resource_provider_uuid"),
|
|
inv.c.resource_class_id,
|
|
inv.c.total,
|
|
inv.c.reserved,
|
|
inv.c.allocation_ratio,
|
|
inv.c.max_unit,
|
|
usage.c.used,
|
|
).select_from(usage_join).where(
|
|
rpt.c.root_provider_id.in_(sa.bindparam(
|
|
'root_ids', expanding=True))
|
|
)
|
|
|
|
return ctx.session.execute(query, {'root_ids': list(root_ids)}).fetchall()
|