Move project_id and user_id to Allocation object

It had been on the AllocationList object but this makes it impossible to
have an AllocationList from multiple project ids. When it was decided to
allow setting allocations from multiple different consumers, it was
decided that since we were already in the code changing things, we
should also make adjustments to allow each allocation to have its own
project and user id.

In the process, ensure that if a persisted Allocation has a project_id
and user_id associated with it, load those values when the Allocation is
loaded (either list allocations by consumer or by resource provider).

We cannot enforce (at the object level) that Allocations must have a
project_id and user_id because there is a microversion where they are
not required that we continue to support. In newer microversions the
JSON schema enforces the their requirement.

Inspecting the placement fixtures related to this change revealed a bug
with a missing user_id in one Allocation being created.

Change-Id: I3cf887bd541c187ec3baca2ae3f8c16f1754e96e
This commit is contained in:
Chris Dent 2017-09-04 01:14:53 +01:00
parent df9fbbfec9
commit e000a8f290
5 changed files with 80 additions and 64 deletions

View File

@ -236,15 +236,13 @@ def _set_allocations(req, schema):
resource_provider=resource_provider,
consumer_id=consumer_uuid,
resource_class=resource_class,
project_id=data.get('project_id'),
user_id=data.get('user_id'),
used=resources[resource_class])
allocation_objects.append(allocation)
allocations = rp_obj.AllocationList(
context,
objects=allocation_objects,
project_id=data.get('project_id'),
user_id=data.get('user_id'),
)
context, objects=allocation_objects)
try:
allocations.create_all()

View File

@ -1450,8 +1450,49 @@ class Allocation(_HasAResourceProvider):
'consumer_id': fields.UUIDField(),
'resource_class': fields.ResourceClassField(),
'used': fields.IntegerField(),
# The following two fields are allowed to be set to None to
# support Allocations that were created before the fields were
# required.
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
}
def ensure_consumer_project_user(self, conn):
"""Examines the project_id, user_id of the object along with the
supplied consumer_id and ensures that if project_id and user_id
are set that there are records in the consumers, projects, and
users table for these entities.
"""
# If project_id and user_id are not set then silently
# move on. This allows microversion <1.8 to continue to work. Since
# then the fields are required and the enforcement is at the HTTP
# API layer.
if not ('project_id' in self and
self.project_id is not None and
'user_id' in self and
self.user_id is not None):
return
# Grab the project internal ID if it exists in the projects table
pid = _ensure_project(conn, self.project_id)
# Grab the user internal ID if it exists in the users table
uid = _ensure_user(conn, self.user_id)
# Add the consumer if it doesn't already exist
sel_stmt = sa.select([_CONSUMER_TBL.c.uuid]).where(
_CONSUMER_TBL.c.uuid == self.consumer_id)
result = conn.execute(sel_stmt).fetchall()
if not result:
try:
conn.execute(_CONSUMER_TBL.insert().values(
uuid=self.consumer_id,
project_id=pid,
user_id=uid))
except db_exc.DBDuplicateEntry:
# We assume at this time that a consumer project/user can't
# change, so if we get here, we raced and should just pass
# if the consumer already exists.
pass
@db_api.api_context_manager.writer
def _delete_allocations_for_consumer(ctx, consumer_id):
@ -1707,42 +1748,8 @@ class AllocationList(base.ObjectListBase, base.NovaObject):
fields = {
'objects': fields.ListOfObjectsField('Allocation'),
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
}
def _ensure_consumer_project_user(self, conn, consumer_id):
"""Examines the project_id, user_id of the object along with the
supplied consumer_id and ensures that there are records in the
consumers, projects, and users table for these entities.
:param consumer_id: Comes from the Allocation object being processed
"""
if (self.obj_attr_is_set('project_id') and
self.project_id is not None and
self.obj_attr_is_set('user_id') and
self.user_id is not None):
# Grab the project internal ID if it exists in the projects table
pid = _ensure_project(conn, self.project_id)
# Grab the user internal ID if it exists in the users table
uid = _ensure_user(conn, self.user_id)
# Add the consumer if it doesn't already exist
sel_stmt = sa.select([_CONSUMER_TBL.c.uuid]).where(
_CONSUMER_TBL.c.uuid == consumer_id)
result = conn.execute(sel_stmt).fetchall()
if not result:
try:
conn.execute(_CONSUMER_TBL.insert().values(
uuid=consumer_id,
project_id=pid,
user_id=uid))
except db_exc.DBDuplicateEntry:
# We assume at this time that a consumer project/user can't
# change, so if we get here, we raced and should just pass
# if the consumer already exists.
pass
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@db_api.api_context_manager.writer
def _set_allocations(self, context, allocs):
@ -1779,22 +1786,29 @@ class AllocationList(base.ObjectListBase, base.NovaObject):
# objects are used at the end of the allocation transaction as a guard
# against concurrent updates.
with conn.begin():
# First delete any existing allocations for that rp/consumer combo.
consumer_id = allocs[0].consumer_id
_delete_allocations_for_consumer(context, consumer_id)
# First delete any existing allocations for this consumer. This
# must be done before checking capacity.
for alloc in allocs:
consumer_id = alloc.consumer_id
_delete_allocations_for_consumer(context, consumer_id)
# If there are any allocations with string resource class names
# that don't exist this will raise a ResourceClassNotFound
# exception.
before_gens = _check_capacity_exceeded(conn, allocs)
self._ensure_consumer_project_user(conn, consumer_id)
# Now add the allocations that were passed in.
seen_consumers = set()
for alloc in allocs:
consumer_id = alloc.consumer_id
# Only set consumer <-> project/user association if we
# haven't set it already.
if consumer_id not in seen_consumers:
alloc.ensure_consumer_project_user(conn)
seen_consumers.add(consumer_id)
rp = alloc.resource_provider
rc_id = _RC_CACHE.id_from_string(alloc.resource_class)
ins_stmt = _ALLOC_TBL.insert().values(
resource_provider_id=rp.id,
resource_class_id=rc_id,
consumer_id=alloc.consumer_id,
consumer_id=consumer_id,
used=alloc.used)
result = conn.execute(ins_stmt)
alloc.id = result.lastrowid

View File

@ -119,8 +119,6 @@ class AllocationFixture(APIFixture):
rp.create()
# Create some DISK_GB inventory and allocations.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
inventory = rp_obj.Inventory(
self.context, resource_provider=rp,
@ -132,23 +130,23 @@ class AllocationFixture(APIFixture):
self.context, resource_provider=rp,
resource_class='DISK_GB',
consumer_id=consumer_id,
project_id=project_id,
user_id=user_id,
used=500)
alloc2 = rp_obj.Allocation(
self.context, resource_provider=rp,
resource_class='DISK_GB',
consumer_id=consumer_id,
project_id=project_id,
user_id=user_id,
used=500)
alloc_list = rp_obj.AllocationList(
self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=user_id,
objects=[alloc1, alloc2]
)
alloc_list.create_all()
# Create some VCPU inventory and allocations.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
inventory = rp_obj.Inventory(
self.context, resource_provider=rp,
@ -160,38 +158,40 @@ class AllocationFixture(APIFixture):
self.context, resource_provider=rp,
resource_class='VCPU',
consumer_id=consumer_id,
project_id=project_id,
user_id=user_id,
used=2)
alloc2 = rp_obj.Allocation(
self.context, resource_provider=rp,
resource_class='VCPU',
consumer_id=consumer_id,
project_id=project_id,
user_id=user_id,
used=4)
alloc_list = rp_obj.AllocationList(
self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=user_id)
objects=[alloc1, alloc2])
alloc_list.create_all()
# Create a couple of allocations for a different user.
# Each set of allocations must have the same consumer_id because only
# the first allocation is used for the project/user association.
consumer_id = uuidutils.generate_uuid()
alloc1 = rp_obj.Allocation(
self.context, resource_provider=rp,
resource_class='DISK_GB',
consumer_id=consumer_id,
project_id=project_id,
user_id=alt_user_id,
used=20)
alloc2 = rp_obj.Allocation(
self.context, resource_provider=rp,
resource_class='VCPU',
consumer_id=consumer_id,
project_id=project_id,
user_id=alt_user_id,
used=1)
alloc_list = rp_obj.AllocationList(
self.context,
objects=[alloc1, alloc2],
project_id=project_id,
user_id=alt_user_id)
objects=[alloc1, alloc2])
alloc_list.create_all()
# The ALT_RP_XXX variables are for a resource provider that has

View File

@ -1248,16 +1248,18 @@ class TestAllocationListCreateDelete(ResourceProviderBaseCase):
allocation1 = rp_obj.Allocation(resource_provider=rp,
consumer_id=consumer_uuid,
resource_class=rp_class,
project_id=self.ctx.project_id,
user_id=self.ctx.user_id,
used=100)
allocation2 = rp_obj.Allocation(resource_provider=rp,
consumer_id=consumer_uuid,
resource_class=rp_class,
project_id=self.ctx.project_id,
user_id=self.ctx.user_id,
used=200)
allocation_list = rp_obj.AllocationList(
self.ctx,
objects=[allocation1, allocation2],
project_id=self.ctx.project_id,
user_id=self.ctx.user_id,
)
allocation_list.create_all()
@ -1290,12 +1292,12 @@ class TestAllocationListCreateDelete(ResourceProviderBaseCase):
allocation3 = rp_obj.Allocation(resource_provider=rp,
consumer_id=other_consumer_uuid,
resource_class=rp_class,
project_id=self.ctx.project_id,
user_id=uuidsentinel.other_user,
used=200)
allocation_list = rp_obj.AllocationList(
self.ctx,
objects=[allocation3],
project_id=self.ctx.project_id,
user_id=uuidsentinel.other_user,
)
allocation_list.create_all()

View File

@ -59,6 +59,8 @@ _ALLOCATION_DB = {
'resource_class_id': _RESOURCE_CLASS_ID,
'consumer_id': uuids.fake_instance,
'used': 8,
'user_id': None,
'project_id': None,
}