text: #    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
  