[placement] POST /allocations to set allocations for >1 consumers

This provides microversion 1.13 of the placement API, giving the
ability to POST to /allocations to set (or clear) allocations for
more than one consumer uuid.

It builds on the recent work to support a dict-based JSON format
when doing a PUT to /allocations/{consumer_uuid}.

Being able to set allocations for multiple consumers in one request
helps to address race conditions when cleaning up allocations during
move operations in nova.

Clearing allocations is done by setting the 'allocations' key for a
specific consumer to an empty dict.

Updates to placement-api-ref, rest version history and a reno are
included.

Change-Id: I239f33841bb9fcd92b406f979674ae8c5f8d57e3
Implements: bp post-allocations
This commit is contained in:
Chris Dent 2017-09-01 15:16:22 +01:00
parent b60a599b5f
commit 8caf4f5148
13 changed files with 532 additions and 34 deletions

View File

@ -100,9 +100,12 @@ ROUTE_DECLARATIONS = {
'/resource_providers/{uuid}/allocations': {
'GET': allocation.list_for_resource_provider,
},
'/allocations': {
'POST': allocation.set_allocations,
},
'/allocations/{consumer_uuid}': {
'GET': allocation.list_for_consumer,
'PUT': allocation.set_allocations,
'PUT': allocation.set_allocations_for_consumer,
'DELETE': allocation.delete_allocations,
},
'/allocation_candidates': {

View File

@ -138,6 +138,24 @@ ALLOCATION_SCHEMA_V1_12 = {
}
# POST to /allocations, added in microversion 1.13, uses the
# POST_ALLOCATIONS_V1_13 schema to allow multiple allocations
# from multiple consumers in one request. It is a dict, keyed by
# consumer uuid, using the form of PUT allocations from microversion
# 1.12. In POST the allocations can be empty, so DELETABLE_ALLOCATIONS
# modifies ALLOCATION_SCHEMA_V1_12 accordingly.
DELETABLE_ALLOCATIONS = copy.deepcopy(ALLOCATION_SCHEMA_V1_12)
DELETABLE_ALLOCATIONS['properties']['allocations']['minProperties'] = 0
POST_ALLOCATIONS_V1_13 = {
"type": "object",
"minProperties": 1,
"additionalProperties": False,
"patternProperties": {
"^[0-9a-fA-F-]{36}$": DELETABLE_ALLOCATIONS
}
}
def _allocations_dict(allocations, key_fetcher, resource_provider=None,
want_version=None):
"""Turn allocations into a dict of resources keyed by key_fetcher."""
@ -277,7 +295,42 @@ def list_for_resource_provider(req):
return req.response
def _set_allocations(req, schema):
def _new_allocations(context, resource_provider_uuid, consumer_uuid,
resources, project_id, user_id):
"""Create new allocation objects for a set of resources
Returns a list of Allocation objects.
:param context: The placement context.
:param resource_provider_uuid: The uuid of the resource provider that
has the resources.
:param consumer_uuid: The uuid of the consumer of 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.
"""
allocations = []
try:
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, resource_provider_uuid)
except exception.NotFound:
raise webob.exc.HTTPBadRequest(
_("Allocation for resource provider '%(rp_uuid)s' "
"that does not exist.") %
{'rp_uuid': resource_provider_uuid})
for resource_class in resources:
allocation = rp_obj.Allocation(
resource_provider=resource_provider,
consumer_id=consumer_uuid,
resource_class=resource_class,
project_id=project_id,
user_id=user_id,
used=resources[resource_class])
allocations.append(allocation)
return allocations
def _set_allocations_for_consumer(req, schema):
context = req.environ['placement.context']
consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid')
data = util.extract_json(req.body, schema)
@ -299,25 +352,13 @@ def _set_allocations(req, schema):
# that does not exist, raise a 400.
allocation_objects = []
for resource_provider_uuid, allocation in allocation_data.items():
try:
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, resource_provider_uuid)
except exception.NotFound:
raise webob.exc.HTTPBadRequest(
_("Allocation for resource provider '%(rp_uuid)s' "
"that does not exist.") %
{'rp_uuid': resource_provider_uuid})
resources = allocation['resources']
for resource_class in resources:
allocation = rp_obj.Allocation(
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)
new_allocations = _new_allocations(context,
resource_provider_uuid,
consumer_uuid,
allocation['resources'],
data.get('project_id'),
data.get('user_id'))
allocation_objects.extend(new_allocations)
allocations = rp_obj.AllocationList(
context, objects=allocation_objects)
@ -349,22 +390,84 @@ def _set_allocations(req, schema):
@wsgi_wrapper.PlacementWsgify
@microversion.version_handler('1.0', '1.7')
@util.require_content('application/json')
def set_allocations(req):
return _set_allocations(req, ALLOCATION_SCHEMA)
def set_allocations_for_consumer(req):
return _set_allocations_for_consumer(req, ALLOCATION_SCHEMA)
@wsgi_wrapper.PlacementWsgify # noqa
@microversion.version_handler('1.8', '1.11')
@util.require_content('application/json')
def set_allocations(req):
return _set_allocations(req, ALLOCATION_SCHEMA_V1_8)
def set_allocations_for_consumer(req):
return _set_allocations_for_consumer(req, ALLOCATION_SCHEMA_V1_8)
@wsgi_wrapper.PlacementWsgify # noqa
@microversion.version_handler('1.12')
@util.require_content('application/json')
def set_allocations_for_consumer(req):
return _set_allocations_for_consumer(req, ALLOCATION_SCHEMA_V1_12)
@wsgi_wrapper.PlacementWsgify
@microversion.version_handler('1.13')
@util.require_content('application/json')
def set_allocations(req):
return _set_allocations(req, ALLOCATION_SCHEMA_V1_12)
context = req.environ['placement.context']
data = util.extract_json(req.body, POST_ALLOCATIONS_V1_13)
# Create a sequence of allocation objects to be used in an
# AllocationList.create_all() call, which will mean all the changes
# happen within a single transaction and with resource provider
# generations check all in one go.
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']
if allocations:
for resource_provider_uuid in allocations:
resources = allocations[resource_provider_uuid]['resources']
new_allocations = _new_allocations(context,
resource_provider_uuid,
consumer_uuid,
resources,
project_id,
user_id)
allocation_objects.extend(new_allocations)
else:
# The allocations are empty, which means wipe them out.
# Internal to the allocation object this is signalled by a
# used value of 0.
allocations = rp_obj.AllocationList.get_all_by_consumer_id(
context, consumer_uuid)
for allocation in allocations:
allocation.used = 0
allocation_objects.append(allocation)
allocations = rp_obj.AllocationList(
context, objects=allocation_objects)
try:
allocations.create_all()
LOG.debug("Successfully wrote allocations %s", allocations)
except exception.NotFound as exc:
raise webob.exc.HTTPBadRequest(
_("Unable to allocate inventory %(error)s") % {'error': exc})
except exception.InvalidInventory as exc:
# InvalidInventory is a parent for several exceptions that
# indicate either that Inventory is not present, or that
# capacity limits have been exceeded.
raise webob.exc.HTTPConflict(
_('Unable to allocate inventory: %(error)s') % {'error': exc})
except exception.ConcurrentUpdateDetected as exc:
raise webob.exc.HTTPConflict(
_('Inventory changed while attempting to allocate: %(error)s') %
{'error': exc})
req.response.status = 204
req.response.content_type = None
return req.response
@wsgi_wrapper.PlacementWsgify

View File

@ -52,6 +52,7 @@ VERSIONS = [
'1.12', # Add project_id and user_id to GET /allocations/{consumer_uuid}
# and PUT to /allocations/{consumer_uuid} in the same dict form
# as GET
'1.13', # Adds POST /allocations to set allocations for multiple consumers
]

View File

@ -172,3 +172,9 @@ response body. Because the `PUT` request requires `user_id` and
response. In addition, the response body for ``GET /allocation_candidates``
is updated so the allocations in the ``alocation_requests`` object work
with the new `PUT` format.
1.13 POST multiple allocations to /allocations
----------------------------------------------
Version 1.13 gives the ability to set or clear allocations for more than
one consumer uuid with a request to ``POST /allocations``.

View File

@ -80,6 +80,11 @@ class APIFixture(fixture.GabbiFixture):
os.environ['CUSTOM_RES_CLASS'] = 'CUSTOM_IRON_NFV'
os.environ['PROJECT_ID'] = uuidutils.generate_uuid()
os.environ['USER_ID'] = uuidutils.generate_uuid()
os.environ['PROJECT_ID_ALT'] = uuidutils.generate_uuid()
os.environ['USER_ID_ALT'] = uuidutils.generate_uuid()
os.environ['INSTANCE_UUID'] = uuidutils.generate_uuid()
os.environ['MIGRATION_UUID'] = uuidutils.generate_uuid()
os.environ['CONSUMER_UUID'] = uuidutils.generate_uuid()
def stop_fixture(self):
self.api_db_fixture.cleanup()

View File

@ -0,0 +1,288 @@
# Test that it possible to POST multiple allocations to /allocations to
# simultaneously make changes, including removing resources for a consumer if
# the allocations are empty.
fixtures:
- APIFixture
defaults:
request_headers:
x-auth-token: admin
accept: application/json
content-type: application/json
openstack-api-version: placement 1.13
tests:
- name: create compute one
POST: /resource_providers
data:
name: compute01
status: 201
- name: rp compute01
desc: provide a reference for later reuse
GET: $LOCATION
- name: create compute two
POST: /resource_providers
data:
name: compute02
status: 201
- name: rp compute02
desc: provide a reference for later reuse
GET: $LOCATION
- name: create shared disk
POST: /resource_providers
data:
name: storage01
status: 201
- name: rp storage01
desc: provide a reference for later reuse
GET: $LOCATION
- name: inventory compute01
PUT: $HISTORY['rp compute01'].$RESPONSE['links[?rel = "inventories"].href']
data:
resource_provider_generation: 0
inventories:
VCPU:
total: 16
MEMORY_MB:
total: 2048
- name: inventory compute02
PUT: $HISTORY['rp compute02'].$RESPONSE['links[?rel = "inventories"].href']
data:
resource_provider_generation: 0
inventories:
VCPU:
total: 16
MEMORY_MB:
total: 2048
- name: inventory storage01
PUT: $HISTORY['rp storage01'].$RESPONSE['links[?rel = "inventories"].href']
data:
resource_provider_generation: 0
inventories:
DISK_GB:
total: 4096
- name: confirm only POST
GET: /allocations
status: 405
response_headers:
allow: POST
- name: 404 on older 1.12 microversion post
POST: /allocations
request_headers:
openstack-api-version: placement 1.12
status: 404
- name: post allocations two consumers
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
$ENVIRON['MIGRATION_UUID']:
allocations:
$HISTORY['rp compute01'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 204
- name: confirm usages
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
response_json_paths:
$.usages.DISK_GB: 5
$.usages.VCPU: 4
$.usages.MEMORY_MB: 2048
- name: clear and set allocations
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
$ENVIRON['MIGRATION_UUID']:
allocations: {}
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 204
- name: confirm usages after clear
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
response_json_paths:
$.usages.DISK_GB: 5
$.usages.VCPU: 2
$.usages.MEMORY_MB: 1024
- name: post allocations two users
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
# We must use a fresh consumer id with the alternate project id info.
# A previously seen consumer id will be assumed to always have the same
# project and user.
$ENVIRON['CONSUMER_UUID']:
allocations:
$HISTORY['rp compute01'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
project_id: $ENVIRON['PROJECT_ID_ALT']
user_id: $ENVIRON['USER_ID_ALT']
status: 204
- name: confirm usages user a
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
response_json_paths:
$.usages.`len`: 3
$.usages.DISK_GB: 5
$.usages.VCPU: 2
$.usages.MEMORY_MB: 1024
- name: confirm usages user b
GET: /usages?project_id=$ENVIRON['PROJECT_ID_ALT']
response_json_paths:
$.usages.`len`: 2
$.usages.VCPU: 2
$.usages.MEMORY_MB: 1024
- name: fail allocations over capacity
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
$ENVIRON['CONSUMER_UUID']:
allocations:
$HISTORY['rp compute01'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 2049
VCPU: 2
project_id: $ENVIRON['PROJECT_ID_ALT']
user_id: $ENVIRON['USER_ID_ALT']
status: 409
response_strings:
- The requested amount would exceed the capacity
- name: fail allocations deep schema violate
desc: no schema yet
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
cow: moo
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 400
- name: fail allocations shallow schema violate
desc: no schema yet
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
cow: moo
status: 400
- name: fail resource provider not exist
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
# this rp does not exist
'c42def7b-498b-4442-9502-c7970b14bea4':
resources:
MEMORY_MB: 1024
VCPU: 2
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 400
response_strings:
- that does not exist
- name: fail resource class not in inventory
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
PCI_DEVICE: 1
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 409
response_strings:
- "Inventory for 'PCI_DEVICE' on"
- name: fail resource class not exist
POST: /allocations
data:
$ENVIRON['INSTANCE_UUID']:
allocations:
$HISTORY['rp compute02'].$RESPONSE['uuid']:
resources:
MEMORY_MB: 1024
VCPU: 2
CUSTOM_PONY: 1
$HISTORY['rp storage01'].$RESPONSE['uuid']:
resources:
DISK_GB: 5
project_id: $ENVIRON['PROJECT_ID']
user_id: $ENVIRON['USER_ID']
status: 400
response_strings:
- No such resource class CUSTOM_PONY

View File

@ -14,11 +14,11 @@ defaults:
tests:
- name: get allocations no consumer is 404
- name: get allocations no consumer is 405
GET: /allocations
status: 404
status: 405
response_json_paths:
$.errors[0].title: Not Found
$.errors[0].title: Method Not Allowed
- name: get allocations is empty dict
GET: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958

View File

@ -39,13 +39,13 @@ tests:
response_json_paths:
$.errors[0].title: Not Acceptable
- name: latest microversion is 1.12
- name: latest microversion is 1.13
GET: /
request_headers:
openstack-api-version: placement latest
response_headers:
vary: /OpenStack-API-Version/
openstack-api-version: placement 1.12
openstack-api-version: placement 1.13
- name: other accept header bad version
GET: /

View File

@ -74,7 +74,7 @@ class TestMicroversionIntersection(test.NoDBTestCase):
# if you add two different versions of method 'foobar' the
# number only goes up by one if no other version foobar yet
# exists. This operates as a simple sanity check.
TOTAL_VERSIONED_METHODS = 15
TOTAL_VERSIONED_METHODS = 16
def test_methods_versioned(self):
methods_data = microversion.VERSIONED_METHODS

View File

@ -7,6 +7,51 @@ and used by some consumer of that resource. They indicate the amount
of a particular resource that has been allocated to a given consumer
of that resource from a particular resource provider.
Manage allocations
==================
Create, update or delete allocations for multiple consumers in a single
request. This allows a client to atomically set or swap allocations for
multiple consumers as may be required during a migration or move type
operation.
The allocations for an individual consumer uuid mentioned in the request
can be removed by setting the `allocations` to an empty object (see the
example below).
**Available as of microversion 1.13.**
.. rest_method:: POST /allocations
Normal response codes: 204
Error response codes: badRequest(400), conflict(409)
* `409 Conflict` if there is no available inventory in any of the
resource providers for any specified resource classes or inventories
are updated by another thread while attempting the operation.
Request
-------
.. rest_parameters:: parameters.yaml
- consumer_uuid: consumer_uuid_body
- project_id: project_id_body
- user_id: user_id_body
- allocations: allocations_dict_empty
- resources: resources
Request Example
.. literalinclude:: manage-allocations-request.json
:language: javascript
Response
--------
No body content is returned after a successful request
List allocations
================

View File

@ -0,0 +1,31 @@
{
"30328d13-e299-4a93-a102-61e4ccabe474": {
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"allocations": {
"e10927c4-8bc9-465d-ac60-d2f79f7e4a00": {
"resources": {
"VCPU": 2,
"MEMORY_MB": 3
}
}
}
},
"71921e4e-1629-4c5b-bf8d-338d915d2ef3": {
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"allocations": {}
},
"48c1d40f-45d8-4947-8d46-52b4e1326df8": {
"project_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"user_id": "131d4efb-abc0-4872-9b92-8c8b9dc4320f",
"allocations": {
"e10927c4-8bc9-465d-ac60-d2f79f7e4a00": {
"resources": {
"VCPU": 4,
"MEMORY_MB": 5
}
}
}
}
}

View File

@ -1,5 +1,5 @@
# variables in path
consumer_uuid:
consumer_uuid: &consumer_uuid
type: string
in: path
required: true
@ -147,19 +147,29 @@ allocations_by_resource_provider:
required: true
description: >
A dictionary of allocations keyed by resource provider uuid.
allocations_dict:
allocations_dict: &allocations_dict
type: object
in: body
required: true
min_version: 1.12
description: >
A dictionary of resource allocations keyed by resource provider uuid.
allocations_dict_empty:
<<: *allocations_dict
description: >
A dictionary of resource allocations keyed by resource provider uuid.
If this is an empty object, allocations for this consumer will be
removed.
min_version: null
capacity:
type: integer
in: body
required: true
description: >
The amount of the resource that the provider can accommodate.
consumer_uuid_body:
<<: *consumer_uuid
in: body
inventories:
type: object
in: body

View File

@ -0,0 +1,6 @@
---
features:
- |
Microversion 1.13 of the Placement API gives the ability to set or clear
allocations for more than one consumer uuid with a request to
``POST /allocations``.