diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py index a54360ded556..38ed64c51310 100644 --- a/nova/api/openstack/placement/handler.py +++ b/nova/api/openstack/placement/handler.py @@ -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': { diff --git a/nova/api/openstack/placement/handlers/allocation.py b/nova/api/openstack/placement/handlers/allocation.py index 1504ff415597..cb5a7c528b72 100644 --- a/nova/api/openstack/placement/handlers/allocation.py +++ b/nova/api/openstack/placement/handlers/allocation.py @@ -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 diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index 410aab3bbe5d..210acca4027f 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -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 ] diff --git a/nova/api/openstack/placement/rest_api_version_history.rst b/nova/api/openstack/placement/rest_api_version_history.rst index ef2c61c6f7d4..b4a2c1827267 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -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``. diff --git a/nova/tests/functional/api/openstack/placement/fixtures.py b/nova/tests/functional/api/openstack/placement/fixtures.py index cbd3518b1cf9..f419cd4f8d98 100644 --- a/nova/tests/functional/api/openstack/placement/fixtures.py +++ b/nova/tests/functional/api/openstack/placement/fixtures.py @@ -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() diff --git a/nova/tests/functional/api/openstack/placement/gabbits/allocations-post.yaml b/nova/tests/functional/api/openstack/placement/gabbits/allocations-post.yaml new file mode 100644 index 000000000000..e1a7fdaf8732 --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/allocations-post.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml index bf9b64833202..403e9bbf7187 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index 6255f6833ccb..ccbdcbb8c9c6 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml @@ -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: / diff --git a/nova/tests/unit/api/openstack/placement/test_microversion.py b/nova/tests/unit/api/openstack/placement/test_microversion.py index fa103eb9257d..e5e38cd9885d 100644 --- a/nova/tests/unit/api/openstack/placement/test_microversion.py +++ b/nova/tests/unit/api/openstack/placement/test_microversion.py @@ -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 diff --git a/placement-api-ref/source/allocations.inc b/placement-api-ref/source/allocations.inc index e85f65d22ae2..8e3a0f9169b1 100644 --- a/placement-api-ref/source/allocations.inc +++ b/placement-api-ref/source/allocations.inc @@ -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 ================ diff --git a/placement-api-ref/source/manage-allocations-request.json b/placement-api-ref/source/manage-allocations-request.json new file mode 100644 index 000000000000..8bdcb2151d5d --- /dev/null +++ b/placement-api-ref/source/manage-allocations-request.json @@ -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 + } + } + } + } +} diff --git a/placement-api-ref/source/parameters.yaml b/placement-api-ref/source/parameters.yaml index e59f3a1cbcf8..5c000a701d8e 100644 --- a/placement-api-ref/source/parameters.yaml +++ b/placement-api-ref/source/parameters.yaml @@ -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 diff --git a/releasenotes/notes/post-allocations-427581fa41671820.yaml b/releasenotes/notes/post-allocations-427581fa41671820.yaml new file mode 100644 index 000000000000..80a9115d4b6c --- /dev/null +++ b/releasenotes/notes/post-allocations-427581fa41671820.yaml @@ -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``.