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

This commit is contained in:
Zuul 2017-11-28 19:01:36 +00:00 committed by Gerrit Code Review
commit 19ae741b0e
13 changed files with 532 additions and 34 deletions

@ -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': {

@ -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

@ -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
]

@ -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``.

@ -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()

@ -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

@ -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

@ -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: /

@ -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

@ -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
================

@ -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
}
}
}
}
}

@ -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

@ -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``.