diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py index a183f96b3770..54fdf092d8ab 100644 --- a/nova/api/openstack/placement/handler.py +++ b/nova/api/openstack/placement/handler.py @@ -28,6 +28,7 @@ import webob from oslo_log import log as logging +from nova.api.openstack.placement.handlers import allocation from nova.api.openstack.placement.handlers import inventory from nova.api.openstack.placement.handlers import resource_provider from nova.api.openstack.placement.handlers import root @@ -69,6 +70,10 @@ ROUTE_DECLARATIONS = { '/resource_providers/{uuid}/usages': { 'GET': usage.list_usages }, + '/allocations/{consumer_uuid}': { + 'PUT': allocation.set_allocations, + 'DELETE': allocation.delete_allocations, + }, } diff --git a/nova/api/openstack/placement/handlers/allocation.py b/nova/api/openstack/placement/handlers/allocation.py new file mode 100644 index 000000000000..7a538ea25c4a --- /dev/null +++ b/nova/api/openstack/placement/handlers/allocation.py @@ -0,0 +1,156 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Placement API handlers for setting and deleting allocations.""" + +import jsonschema +from oslo_serialization import jsonutils +import webob + +from nova.api.openstack.placement import util +from nova import exception +from nova import objects + + +ALLOCATION_SCHEMA = { + "type": "object", + "properties": { + "allocations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resource_provider": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": False, + "required": ["uuid"] + }, + "resources": { + "type": "object", + "patternProperties": { + "^[0-9A-Z_]+$": { + "type": "integer" + } + }, + "additionalProperties": False + } + }, + "required": [ + "resource_provider", + "resources" + ], + "additionalProperties": False + } + } + }, + "required": ["allocations"], + "additionalProperties": False +} + + +def _extract_allocations(body, schema): + """Extract allocation data from a JSON body.""" + try: + data = jsonutils.loads(body) + except ValueError as exc: + raise webob.exc.HTTPBadRequest( + 'Malformed JSON: %s' % exc, + json_formatter=util.json_error_formatter) + try: + jsonschema.validate(data, schema, + format_checker=jsonschema.FormatChecker()) + except jsonschema.ValidationError as exc: + raise webob.exc.HTTPBadRequest( + 'JSON does not validate: %s' % exc, + json_formatter=util.json_error_formatter) + return data + + +@webob.dec.wsgify +@util.require_content('application/json') +def set_allocations(req): + context = req.environ['placement.context'] + consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid') + data = _extract_allocations(req.body, ALLOCATION_SCHEMA) + allocation_data = data['allocations'] + + # If the body includes an allocation for a resource provider + # that does not exist, raise a 400. + allocation_objects = [] + for allocation in allocation_data: + resource_provider_uuid = allocation['resource_provider']['uuid'] + + try: + resource_provider = objects.ResourceProvider.get_by_uuid( + context, resource_provider_uuid) + except exception.NotFound: + raise webob.exc.HTTPBadRequest( + "Allocation for resource provider '%s' " + "that does not exist." % resource_provider_uuid, + json_formatter=util.json_error_formatter) + + resources = allocation['resources'] + for resource_class in resources: + try: + allocation = objects.Allocation( + resource_provider=resource_provider, + consumer_id=consumer_uuid, + resource_class=resource_class, + used=resources[resource_class]) + except ValueError as exc: + raise webob.exc.HTTPBadRequest( + "Allocation of class '%s' for " + "resource provider '%s' invalid: " + "%s" % (resource_class, resource_provider_uuid, exc)) + allocation_objects.append(allocation) + + allocations = objects.AllocationList(context, objects=allocation_objects) + try: + allocations.create_all() + # InvalidInventory is a parent for several exceptions that + # indicate either that Inventory is not present, or that + # capacity limits have been exceeded. + except exception.InvalidInventory as exc: + raise webob.exc.HTTPConflict( + 'Unable to allocate inventory: %s' % exc, + json_formatter=util.json_error_formatter) + except exception.ConcurrentUpdateDetected as exc: + raise webob.exc.HTTPConflict( + 'Inventory changed while attempting to allocate: %s' % exc, + json_formatter=util.json_error_formatter) + + req.response.status = 204 + req.response.content_type = None + return req.response + + +@webob.dec.wsgify +def delete_allocations(req): + context = req.environ['placement.context'] + consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid') + + allocations = objects.AllocationList.get_all_by_consumer_id( + context, consumer_uuid) + if not allocations: + raise webob.exc.HTTPNotFound( + "No allocations for consumer '%s'" % consumer_uuid, + json_formatter=util.json_error_formatter) + allocations.delete_all() + + req.response.status = 204 + req.response.content_type = None + return req.response diff --git a/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml new file mode 100644 index 000000000000..8692a2a8fbf3 --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml @@ -0,0 +1,202 @@ +# Tests of allocations API +# +# TODO(cdent): Where in the process is the consumer id being +# validated? + + +fixtures: + - APIFixture + +defaults: + request_headers: + x-auth-token: admin + accept: application/json + +tests: + +- name: get allocations no consumer is 404 + GET: /allocations + status: 404 + +- name: get allocations is 405 + GET: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + status: 405 + response_headers: + allow: /(PUT, DELETE|DELETE, PUT)/ + +- name: put an allocation no resource provider + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resources: + DISK_GB: 10 + status: 400 + +- name: create the resource provider + POST: /resource_providers + request_headers: + content-type: application/json + data: + name: $ENVIRON['RP_NAME'] + uuid: $ENVIRON['RP_UUID'] + status: 201 + +- name: put an allocation no data + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + status: 400 + +- name: put an allocation violate schema + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + cow: 10 + status: 400 + +- name: put an allocation no inventory + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 10 + status: 409 + +- name: post some inventory + POST: /resource_providers/$ENVIRON['RP_UUID']/inventories + request_headers: + content-type: application/json + data: + resource_class: DISK_GB + total: 2048 + min_unit: 10 + max_unit: 1024 + status: 201 + +- name: put an allocation + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 10 + status: 204 + +- name: put an allocation different consumer + PUT: /allocations/39715579-2167-4c63-8247-301311cc6703 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 10 + status: 204 + +- name: check usages after another 10 + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + response_json_paths: + $.usages.DISK_GB: 20 + +# NOTE(cdent): Contravening the spec, we decided that it is +# important to be able to update an existing allocation, so this +# should work but it is important to check the usage. +- name: put allocation again + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 12 + status: 204 + +- name: check usages after 12 + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + response_json_paths: + $.usages.DISK_GB: 22 + +- name: put allocation bad resource class + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + COWS: 12 + status: 400 + response_strings: + - Field value COWS is invalid + +- name: delete allocation + DELETE: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + status: 204 + +- name: delete allocation again + DELETE: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + status: 404 + +- name: delete allocation of unknown consumer id + DELETE: /allocations/da78521f-bf7e-4e6e-9901-3f79bd94d55d + status: 404 + +- name: redo an allocation + PUT: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 10 + status: 204 + +- name: add other inventory + POST: /resource_providers/$ENVIRON['RP_UUID']/inventories + request_headers: + content-type: application/json + data: + resource_class: VCPU + total: 32 + min_unit: 1 + max_unit: 8 + status: 201 + +- name: multiple allocations + PUT: /allocations/833f0885-f78c-4788-bb2b-3607b0656be7 + request_headers: + content-type: application/json + data: + allocations: + - resource_provider: + uuid: $ENVIRON['RP_UUID'] + resources: + DISK_GB: 20 + VCPU: 4 + status: 204 + +- name: check usages + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + response_json_paths: + $.resource_provider_generation: 7 + $.usages.DISK_GB: 40