Merge "placement: delete auto-created consumers on fail"
This commit is contained in:
@@ -155,22 +155,21 @@ def _serialize_allocations_for_resource_provider(allocations,
|
||||
# but having it in this file seems wrong, however, since it uses
|
||||
# _new_allocations it's being left here for now. We need a place for shared
|
||||
# handler code, but util.py is already too big and too diverse.
|
||||
def create_allocation_list(context, data, want_version):
|
||||
def create_allocation_list(context, data, consumers):
|
||||
"""Create an AllocationList based on provided data.
|
||||
|
||||
:param context: The placement context.
|
||||
:param data: A dictionary of multiple allocations by consumer uuid.
|
||||
:param want_version: The desired microversion, which controls how
|
||||
consumer generations are handled.
|
||||
:param consumers: A dictionary, keyed by consumer UUID, of Consumer objects
|
||||
:return: An AllocationList.
|
||||
:raises: `webob.exc.HTTPBadRequest` if a resource provider included in the
|
||||
allocations does not exist.
|
||||
"""
|
||||
allocation_objects = []
|
||||
|
||||
for consumer_uuid in data:
|
||||
project_id = data[consumer_uuid]['project_id']
|
||||
user_id = data[consumer_uuid]['user_id']
|
||||
allocations = data[consumer_uuid]['allocations']
|
||||
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
||||
consumer = consumers[consumer_uuid]
|
||||
if allocations:
|
||||
rp_objs = _resource_providers_by_uuid(context, allocations.keys())
|
||||
for resource_provider_uuid in allocations:
|
||||
@@ -178,12 +177,8 @@ def create_allocation_list(context, data, want_version):
|
||||
resources = allocations[resource_provider_uuid]['resources']
|
||||
new_allocations = _new_allocations(context,
|
||||
resource_provider,
|
||||
consumer_uuid,
|
||||
resources,
|
||||
project_id,
|
||||
user_id,
|
||||
consumer_generation,
|
||||
want_version)
|
||||
consumer,
|
||||
resources)
|
||||
allocation_objects.extend(new_allocations)
|
||||
else:
|
||||
# The allocations are empty, which means wipe them out.
|
||||
@@ -295,28 +290,17 @@ def _resource_providers_by_uuid(ctx, rp_uuids):
|
||||
return res
|
||||
|
||||
|
||||
def _new_allocations(context, resource_provider, consumer_uuid,
|
||||
resources, project_id, user_id, consumer_generation,
|
||||
want_version):
|
||||
def _new_allocations(context, resource_provider, consumer, resources):
|
||||
"""Create new allocation objects for a set of resources
|
||||
|
||||
Returns a list of Allocation objects.
|
||||
Returns a list of Allocation objects
|
||||
|
||||
:param context: The placement context.
|
||||
:param resource_provider: The resource provider that has the resources.
|
||||
:param consumer_uuid: The uuid of the consumer of the resources.
|
||||
:param consumer: The Consumer object consuming the resources.
|
||||
:param resources: A dict of resource classes and values.
|
||||
:param project_id: The project consuming the resources.
|
||||
:param user_id: The user consuming the resources.
|
||||
:param consumer_generation: The generation supplied by the user when
|
||||
PUT/POST'ing allocations. May be None if
|
||||
the microversion is <1.28
|
||||
:param want_version: The microversion object from the context.
|
||||
"""
|
||||
allocations = []
|
||||
consumer = util.ensure_consumer(
|
||||
context, consumer_uuid, project_id, user_id, consumer_generation,
|
||||
want_version)
|
||||
for resource_class in resources:
|
||||
allocation = rp_obj.Allocation(
|
||||
resource_provider=resource_provider,
|
||||
@@ -327,6 +311,21 @@ def _new_allocations(context, resource_provider, consumer_uuid,
|
||||
return allocations
|
||||
|
||||
|
||||
def _delete_consumers(consumers):
|
||||
"""Helper function that deletes any consumer object supplied to it
|
||||
|
||||
:param consumers: iterable of Consumer objects to delete
|
||||
"""
|
||||
for consumer in consumers:
|
||||
try:
|
||||
consumer.delete()
|
||||
LOG.debug("Deleted auto-created consumer with consumer UUID "
|
||||
"%s after failed allocation", consumer.uuid)
|
||||
except Exception as err:
|
||||
LOG.warning("Got an exception when deleting auto-created "
|
||||
"consumer with UUID %s: %s", consumer.uuid, err)
|
||||
|
||||
|
||||
def _set_allocations_for_consumer(req, schema):
|
||||
context = req.environ['placement.context']
|
||||
context.can(policies.ALLOC_UPDATE)
|
||||
@@ -346,9 +345,12 @@ def _set_allocations_for_consumer(req, schema):
|
||||
}
|
||||
allocation_data = allocations_dict
|
||||
|
||||
# If the body includes an allocation for a resource provider
|
||||
# that does not exist, raise a 400.
|
||||
allocation_objects = []
|
||||
# Consumer object saved in case we need to delete the auto-created consumer
|
||||
# record
|
||||
consumer = None
|
||||
# Whether we created a new consumer record
|
||||
created_new_consumer = False
|
||||
if not allocation_data:
|
||||
# The allocations are empty, which means wipe them out. Internal
|
||||
# to the allocation object this is signalled by a used value of 0.
|
||||
@@ -366,25 +368,35 @@ def _set_allocations_for_consumer(req, schema):
|
||||
allocation.used = 0
|
||||
allocation_objects.append(allocation)
|
||||
else:
|
||||
# If the body includes an allocation for a resource provider
|
||||
# that does not exist, raise a 400.
|
||||
rp_objs = _resource_providers_by_uuid(context, allocation_data.keys())
|
||||
consumer, created_new_consumer = util.ensure_consumer(
|
||||
context, consumer_uuid, data.get('project_id'),
|
||||
data.get('user_id'), data.get('consumer_generation'),
|
||||
want_version)
|
||||
for resource_provider_uuid, allocation in allocation_data.items():
|
||||
resource_provider = rp_objs[resource_provider_uuid]
|
||||
new_allocations = _new_allocations(context,
|
||||
resource_provider,
|
||||
consumer_uuid,
|
||||
allocation['resources'],
|
||||
data.get('project_id'),
|
||||
data.get('user_id'),
|
||||
data.get('consumer_generation'),
|
||||
want_version)
|
||||
consumer,
|
||||
allocation['resources'])
|
||||
allocation_objects.extend(new_allocations)
|
||||
|
||||
allocations = rp_obj.AllocationList(
|
||||
context, objects=allocation_objects)
|
||||
|
||||
def _create_allocations(alloc_list):
|
||||
try:
|
||||
alloc_list.create_all()
|
||||
LOG.debug("Successfully wrote allocations %s", alloc_list)
|
||||
except Exception:
|
||||
if created_new_consumer:
|
||||
_delete_consumers([consumer])
|
||||
raise
|
||||
|
||||
try:
|
||||
allocations.create_all()
|
||||
LOG.debug("Successfully wrote allocations %s", allocations)
|
||||
_create_allocations(allocations)
|
||||
# InvalidInventory is a parent for several exceptions that
|
||||
# indicate either that Inventory is not present, or that
|
||||
# capacity limits have been exceeded.
|
||||
@@ -447,15 +459,45 @@ def set_allocations(req):
|
||||
want_schema = schema.POST_ALLOCATIONS_V1_28
|
||||
data = util.extract_json(req.body, want_schema)
|
||||
|
||||
# First, ensure that all consumers referenced in the payload actually
|
||||
# exist. And if not, create them. Keep a record of auto-created consumers
|
||||
# so we can clean them up if the end allocation create_all() fails.
|
||||
consumers = {} # dict of Consumer objects, keyed by consumer UUID
|
||||
new_consumers_created = []
|
||||
for consumer_uuid in data:
|
||||
project_id = data[consumer_uuid]['project_id']
|
||||
user_id = data[consumer_uuid]['user_id']
|
||||
consumer_generation = data[consumer_uuid].get('consumer_generation')
|
||||
try:
|
||||
consumer, new_consumer_created = util.ensure_consumer(
|
||||
context, consumer_uuid, project_id, user_id,
|
||||
consumer_generation, want_version)
|
||||
if new_consumer_created:
|
||||
new_consumers_created.append(consumer)
|
||||
consumers[consumer_uuid] = consumer
|
||||
except Exception:
|
||||
# If any errors (for instance, a consumer generation conflict)
|
||||
# occur when ensuring consumer records above, make sure we delete
|
||||
# any auto-created consumers.
|
||||
_delete_consumers(new_consumers_created)
|
||||
raise
|
||||
|
||||
# Create a sequence of allocation objects to be used in one
|
||||
# AllocationList.create_all() call, which will mean all the changes
|
||||
# happen within a single transaction and with resource provider
|
||||
# and consumer generations (if applicable) check all in one go.
|
||||
allocations = create_allocation_list(context, data, want_version)
|
||||
allocations = create_allocation_list(context, data, consumers)
|
||||
|
||||
def _create_allocations(alloc_list):
|
||||
try:
|
||||
alloc_list.create_all()
|
||||
LOG.debug("Successfully wrote allocations %s", alloc_list)
|
||||
except Exception:
|
||||
_delete_consumers(new_consumers_created)
|
||||
raise
|
||||
|
||||
try:
|
||||
allocations.create_all()
|
||||
LOG.debug("Successfully wrote allocations %s", allocations)
|
||||
_create_allocations(allocations)
|
||||
except exception.NotFound as exc:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_("Unable to allocate inventory %(error)s") % {'error': exc})
|
||||
|
||||
@@ -130,6 +130,17 @@ def _increment_consumer_generation(ctx, consumer):
|
||||
return new_generation
|
||||
|
||||
|
||||
@db_api.placement_context_manager.writer
|
||||
def _delete_consumer(ctx, consumer):
|
||||
"""Deletes the supplied consumer.
|
||||
|
||||
:param ctx: `nova.context.RequestContext` that contains an oslo_db Session
|
||||
:param consumer: `Consumer` whose generation should be updated.
|
||||
"""
|
||||
del_stmt = CONSUMER_TBL.delete().where(CONSUMER_TBL.c.id == consumer.id)
|
||||
ctx.session.execute(del_stmt)
|
||||
|
||||
|
||||
@base.VersionedObjectRegistry.register_if(False)
|
||||
class Consumer(base.VersionedObject, base.TimestampedObject):
|
||||
|
||||
@@ -193,3 +204,6 @@ class Consumer(base.VersionedObject, base.TimestampedObject):
|
||||
consumer)
|
||||
"""
|
||||
self.generation = _increment_consumer_generation(self._context, self)
|
||||
|
||||
def delete(self):
|
||||
_delete_consumer(self._context, self)
|
||||
|
||||
@@ -582,7 +582,9 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||
"""Ensures there are records in the consumers, projects and users table for
|
||||
the supplied external identifiers.
|
||||
|
||||
Returns a populated Consumer object containing Project and User sub-objects
|
||||
Returns a tuple containing the populated Consumer object containing Project
|
||||
and User sub-objects and a boolean indicating whether a new Consumer object
|
||||
was created (as opposed to an existing consumer record retrieved)
|
||||
|
||||
:param ctx: The request context.
|
||||
:param consumer_uuid: The uuid of the consumer of the resources.
|
||||
@@ -594,6 +596,7 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||
:raises webob.exc.HTTPConflict if consumer generation is required and there
|
||||
was a mismatch
|
||||
"""
|
||||
created_new_consumer = False
|
||||
requires_consumer_generation = want_version.matches((1, 28))
|
||||
if project_id is None:
|
||||
project_id = CONF.placement.incomplete_consumer_project_id
|
||||
@@ -647,7 +650,8 @@ def ensure_consumer(ctx, consumer_uuid, project_id, user_id,
|
||||
consumer = consumer_obj.Consumer(
|
||||
ctx, uuid=consumer_uuid, project=proj, user=user)
|
||||
consumer.create()
|
||||
created_new_consumer = True
|
||||
except exception.ConsumerExists:
|
||||
# No worries, another thread created this user already
|
||||
consumer = consumer_obj.Consumer.get_by_uuid(ctx, consumer_uuid)
|
||||
return consumer
|
||||
return consumer, created_new_consumer
|
||||
|
||||
@@ -231,10 +231,7 @@ tests:
|
||||
$.allocation_requests[0].allocations['$ENVIRON["RP_UUID"]'].resources.VCPU: 1
|
||||
$.allocation_requests[0].allocations['8aa83304-4b6d-4a23-b954-06d8b36b206a'].resources.DISK_GB: 200
|
||||
|
||||
# This fails because of bug 1778576
|
||||
# https://bugs.launchpad.net/nova/+bug/1778576
|
||||
- name: put that allocation to new consumer
|
||||
xfail: True
|
||||
PUT: /allocations/55555555-5555-5555-5555-555555555555
|
||||
data:
|
||||
allocations: $RESPONSE['$.allocation_requests[0].allocations']
|
||||
|
||||
@@ -55,8 +55,10 @@ tests:
|
||||
$:
|
||||
allocations: {}
|
||||
|
||||
- name: retry allocations new consumer, wrong gen
|
||||
desc: consumer generation is wrong below to make test pass
|
||||
# The failure to allocate above should have deleted the auto-created consumer,
|
||||
# so when we retry the allocation here, we should be able to use the
|
||||
# appropriate null generation to indicate this is a new consumer
|
||||
- name: retry allocations new consumer, still null gen
|
||||
PUT: /allocations/88888888-8888-8888-8888-888888888888
|
||||
data:
|
||||
allocations:
|
||||
@@ -65,8 +67,5 @@ tests:
|
||||
VCPU: 1
|
||||
project_id: $ENVIRON['PROJECT_ID']
|
||||
user_id: $ENVIRON['USER_ID']
|
||||
# The API suggests we should have null here
|
||||
# consumer_generation: null
|
||||
# but the error response says 0, so use it to make test pass
|
||||
consumer_generation: 0
|
||||
consumer_generation: null
|
||||
status: 204
|
||||
|
||||
@@ -108,6 +108,31 @@ tests:
|
||||
user_id: $ENVIRON['USER_ID']
|
||||
status: 204
|
||||
|
||||
- name: get allocations for instance consumer
|
||||
GET: /allocations/$ENVIRON['INSTANCE_UUID']
|
||||
request_headers:
|
||||
# We want to inspect the consumer generations...
|
||||
openstack-api-version: placement 1.28
|
||||
response_json_paths:
|
||||
$.allocations["$HISTORY['rp compute02'].$RESPONSE['uuid']"].resources[MEMORY_MB]: 1024
|
||||
$.allocations["$HISTORY['rp compute02'].$RESPONSE['uuid']"].resources[VCPU]: 2
|
||||
$.allocations["$HISTORY['rp storage01'].$RESPONSE['uuid']"].resources[DISK_GB]: 5
|
||||
$.consumer_generation: 1
|
||||
$.project_id: $ENVIRON['PROJECT_ID']
|
||||
$.user_id: $ENVIRON['USER_ID']
|
||||
|
||||
- name: get allocations for migration consumer
|
||||
GET: /allocations/$ENVIRON['MIGRATION_UUID']
|
||||
request_headers:
|
||||
# We want to inspect the consumer generations...
|
||||
openstack-api-version: placement 1.28
|
||||
response_json_paths:
|
||||
$.allocations["$HISTORY['rp compute01'].$RESPONSE['uuid']"].resources[MEMORY_MB]: 1024
|
||||
$.allocations["$HISTORY['rp compute01'].$RESPONSE['uuid']"].resources[VCPU]: 2
|
||||
$.consumer_generation: 1
|
||||
$.project_id: $ENVIRON['PROJECT_ID']
|
||||
$.user_id: $ENVIRON['USER_ID']
|
||||
|
||||
- name: confirm usages
|
||||
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
|
||||
response_json_paths:
|
||||
|
||||
Reference in New Issue
Block a user