def _check_capacity_exceeded(context, allocs): """Checks to see if the supplied allocation records would result in any of the inventories involved having their capacity exceeded. Raises an InvalidAllocationCapacityExceeded exception if any inventory would be exhausted by the allocation. Raises an InvalidAllocationConstraintsViolated exception if any of the `step_size`, `min_unit` or `max_unit` constraints in an inventory will be violated by any one of the allocations. If no inventories would be exceeded or violated by the allocations, the function returns a list of `ResourceProvider` objects that contain the generation at the time of the check. :param ctx: `placement.context.RequestContext` that has an oslo_db Session :param allocs: List of `Allocation` objects to check """ rc_names = set([a.resource_class for a in allocs]) # Make sure that all the rc_names are valid query = """ MATCH (rc:RESOURCE_CLASS) WHERE rc.name IN {names} RETURN rc.name AS rc_name """ result = context.tx.run(query, names=list(rc_names)) db_names = set([rec["rc_name"] for rec in result]) set_bad_names = rc_names - db_names if set_bad_names: bad_names = ",".join(set_bad_names) raise exception.ResourceClassNotFound(resource_class=bad_names) provider_uuids = set([a.resource_provider.uuid for a in allocs]) query = """ MATCH (rp:RESOURCE_PROVIDER)-[:PROVIDES]->(rc) WHERE rp.uuid IN {rp_uuids} AND labels(rc)[0] IN {labels} WITH rp, rc OPTIONAL MATCH p=(cs:CONSUMER)-[:USES]->(rc) WITH rp, rc, relationships(p)[0] AS allocs WITH rp, rc, sum(allocs.amount) AS total_usages RETURN rp, rc, labels(rc)[0] AS rc_name, total_usages """ result = context.tx.run(query, rp_uuids=list(provider_uuids), labels=list(rc_names)).data() # Create a map keyed by (rp_uuid, res_class) for the records in the DB usage_map = {} provs_with_inv = set() for record in result: map_key = (record["rp"]["uuid"], record["rc_name"]) if map_key in usage_map: raise KeyError("%s already in usage_map, bad query" % str(map_key)) usage_map[map_key] = record provs_with_inv.add(record["rp"]["uuid"]) # Ensure that all providers have existing inventory missing_provs = provider_uuids - provs_with_inv if missing_provs: class_str = ", ".join(rc_names) provider_str = ", ".join(missing_provs) raise exception.InvalidInventory(resource_class=class_str, resource_provider=provider_str) res_providers = {} rp_resource_class_sum = collections.defaultdict( lambda: collections.defaultdict(int)) for alloc in allocs: rc_name = alloc.resource_class rp_uuid = alloc.resource_provider.uuid if rp_uuid not in res_providers: res_providers[rp_uuid] = alloc.resource_provider amount_needed = alloc.used # No use checking usage if we're not asking for anything if amount_needed == 0: continue rp_resource_class_sum[rp_uuid][rc_name] += amount_needed key = (rp_uuid, rc_name) try: usage_record = usage_map[key] except KeyError: # The resource class at rc_name is not in the usage map. raise exception.InvalidInventory( resource_class=alloc.resource_class, resource_provider=rp_uuid) rc_record = usage_record["rc"] allocation_ratio = rc_record["allocation_ratio"] min_unit = rc_record["min_unit"] max_unit = rc_record["max_unit"] step_size = rc_record["step_size"] total = rc_record["total"] reserved = rc_record["reserved"] # check min_unit, max_unit, step_size if ((amount_needed < min_unit) or (amount_needed > max_unit) or (amount_needed % step_size)): LOG.warning( "Allocation for %(rc)s on resource provider %(rp)s " "violates min_unit, max_unit, or step_size. " "Requested: %(requested)s, min_unit: %(min_unit)s, " "max_unit: %(max_unit)s, step_size: %(step_size)s", { "rc": alloc.resource_class, "rp": rp_uuid, "requested": amount_needed, "min_unit": min_unit, "max_unit": max_unit, "step_size": step_size }) raise exception.InvalidAllocationConstraintsViolated( resource_class=alloc.resource_class, resource_provider=rp_uuid) # Should never be null, but just in case... used = usage_record["total_usages"] or 0 capacity = (total - reserved) * allocation_ratio if (capacity < (used + amount_needed) or capacity < (used + rp_resource_class_sum[rp_uuid][rc_name])): LOG.warning( "Over capacity for %(rc)s on resource provider %(rp)s. " "Needed: %(needed)s, Used: %(used)s, Capacity: %(cap)s", { "rc": alloc.resource_class, "rp": rp_uuid, "needed": amount_needed, "used": used, "cap": capacity }) raise exception.InvalidAllocationCapacityExceeded( resource_class=alloc.resource_class, resource_provider=rp_uuid) return res_providers
def _check_capacity_exceeded(ctx, allocs): """Checks to see if the supplied allocation records would result in any of the inventories involved having their capacity exceeded. Raises an InvalidAllocationCapacityExceeded exception if any inventory would be exhausted by the allocation. Raises an InvalidAllocationConstraintsViolated exception if any of the `step_size`, `min_unit` or `max_unit` constraints in an inventory will be violated by any one of the allocations. If no inventories would be exceeded or violated by the allocations, the function returns a list of `ResourceProvider` objects that contain the generation at the time of the check. :param ctx: `placement.context.RequestContext` that has an oslo_db Session :param allocs: List of `Allocation` objects to check """ # The SQL generated below looks like this: # SELECT # rp.id, # rp.uuid, # rp.generation, # inv.resource_class_id, # inv.total, # inv.reserved, # inv.allocation_ratio, # allocs.used # FROM resource_providers AS rp # JOIN inventories AS i1 # ON rp.id = i1.resource_provider_id # LEFT JOIN ( # SELECT resource_provider_id, resource_class_id, SUM(used) AS used # FROM allocations # WHERE resource_class_id IN ($RESOURCE_CLASSES) # AND resource_provider_id IN ($RESOURCE_PROVIDERS) # GROUP BY resource_provider_id, resource_class_id # ) AS allocs # ON inv.resource_provider_id = allocs.resource_provider_id # AND inv.resource_class_id = allocs.resource_class_id # WHERE rp.id IN ($RESOURCE_PROVIDERS) # AND inv.resource_class_id IN ($RESOURCE_CLASSES) # # We then take the results of the above and determine if any of the # inventory will have its capacity exceeded. rc_ids = set( [rc_cache.RC_CACHE.id_from_string(a.resource_class) for a in allocs]) provider_uuids = set([a.resource_provider.uuid for a in allocs]) provider_ids = set([a.resource_provider.id for a in allocs]) usage = sa.select([ _ALLOC_TBL.c.resource_provider_id, _ALLOC_TBL.c.resource_class_id, sql.func.sum(_ALLOC_TBL.c.used).label('used') ]) usage = usage.where( sa.and_(_ALLOC_TBL.c.resource_class_id.in_(rc_ids), _ALLOC_TBL.c.resource_provider_id.in_(provider_ids))) usage = usage.group_by(_ALLOC_TBL.c.resource_provider_id, _ALLOC_TBL.c.resource_class_id) usage = sa.alias(usage, name='usage') inv_join = sql.join( _RP_TBL, _INV_TBL, sql.and_(_RP_TBL.c.id == _INV_TBL.c.resource_provider_id, _INV_TBL.c.resource_class_id.in_(rc_ids))) primary_join = sql.outerjoin( inv_join, usage, sql.and_( _INV_TBL.c.resource_provider_id == usage.c.resource_provider_id, _INV_TBL.c.resource_class_id == usage.c.resource_class_id)) cols_in_output = [ _RP_TBL.c.id.label('resource_provider_id'), _RP_TBL.c.uuid, _RP_TBL.c.generation, _INV_TBL.c.resource_class_id, _INV_TBL.c.total, _INV_TBL.c.reserved, _INV_TBL.c.allocation_ratio, _INV_TBL.c.min_unit, _INV_TBL.c.max_unit, _INV_TBL.c.step_size, usage.c.used, ] sel = sa.select(cols_in_output).select_from(primary_join) sel = sel.where( sa.and_(_RP_TBL.c.id.in_(provider_ids), _INV_TBL.c.resource_class_id.in_(rc_ids))) records = ctx.session.execute(sel) # Create a map keyed by (rp_uuid, res_class) for the records in the DB usage_map = {} provs_with_inv = set() for record in records: map_key = (record['uuid'], record['resource_class_id']) if map_key in usage_map: raise KeyError("%s already in usage_map, bad query" % str(map_key)) usage_map[map_key] = record provs_with_inv.add(record["uuid"]) # Ensure that all providers have existing inventory missing_provs = provider_uuids - provs_with_inv if missing_provs: class_str = ', '.join( [rc_cache.RC_CACHE.string_from_id(rc_id) for rc_id in rc_ids]) provider_str = ', '.join(missing_provs) raise exception.InvalidInventory(resource_class=class_str, resource_provider=provider_str) res_providers = {} rp_resource_class_sum = collections.defaultdict( lambda: collections.defaultdict(int)) for alloc in allocs: rc_id = rc_cache.RC_CACHE.id_from_string(alloc.resource_class) rp_uuid = alloc.resource_provider.uuid if rp_uuid not in res_providers: res_providers[rp_uuid] = alloc.resource_provider amount_needed = alloc.used rp_resource_class_sum[rp_uuid][rc_id] += amount_needed # No use checking usage if we're not asking for anything if amount_needed == 0: continue key = (rp_uuid, rc_id) try: usage = usage_map[key] except KeyError: # The resource class at rc_id is not in the usage map. raise exception.InvalidInventory( resource_class=alloc.resource_class, resource_provider=rp_uuid) allocation_ratio = usage['allocation_ratio'] min_unit = usage['min_unit'] max_unit = usage['max_unit'] step_size = usage['step_size'] # check min_unit, max_unit, step_size if (amount_needed < min_unit or amount_needed > max_unit or amount_needed % step_size != 0): LOG.warning( "Allocation for %(rc)s on resource provider %(rp)s " "violates min_unit, max_unit, or step_size. " "Requested: %(requested)s, min_unit: %(min_unit)s, " "max_unit: %(max_unit)s, step_size: %(step_size)s", { 'rc': alloc.resource_class, 'rp': rp_uuid, 'requested': amount_needed, 'min_unit': min_unit, 'max_unit': max_unit, 'step_size': step_size }) raise exception.InvalidAllocationConstraintsViolated( resource_class=alloc.resource_class, resource_provider=rp_uuid) # usage["used"] can be returned as None used = usage['used'] or 0 capacity = (usage['total'] - usage['reserved']) * allocation_ratio if (capacity < (used + amount_needed) or capacity < (used + rp_resource_class_sum[rp_uuid][rc_id])): LOG.warning( "Over capacity for %(rc)s on resource provider %(rp)s. " "Needed: %(needed)s, Used: %(used)s, Capacity: %(cap)s", { 'rc': alloc.resource_class, 'rp': rp_uuid, 'needed': amount_needed, 'used': used, 'cap': capacity }) raise exception.InvalidAllocationCapacityExceeded( resource_class=alloc.resource_class, resource_provider=rp_uuid) return res_providers