From 4d525b4ec1afbcfb93c93a19ccdc452af2ad0ebc Mon Sep 17 00:00:00 2001 From: Chris Dent Date: Wed, 20 Jun 2018 17:56:04 +0100 Subject: [PATCH] [placement] Add /reshaper handler for POST /reshaper provides a way to atomically modify some allocations and inventory in a single transaction, allowing operations like migrating some inventory from a parent provider to a new child. A fair amount of code is reused from handler/inventory.py, some refactoring is in order before things get too far with that. In handler/allocation.py some code is extracted to its own methods so it can be reused from reshaper.py. This is done as microversion 1.30. A suite of gabbi tests is provided which attempt to cover various failures including schema violations, generation conflicts, and data conflicts. api-ref, release notes and rest history are updated Change-Id: I5b33ac3572bc3789878174ffc86ca42ae8035cfa Partially-Implements: blueprint reshape-provider-tree --- nova/api/openstack/placement/errors.py | 1 + nova/api/openstack/placement/handler.py | 4 + .../placement/handlers/allocation.py | 73 ++- .../openstack/placement/handlers/inventory.py | 16 +- .../openstack/placement/handlers/reshaper.py | 127 ++++ nova/api/openstack/placement/microversion.py | 2 + .../openstack/placement/policies/__init__.py | 4 +- .../openstack/placement/policies/reshaper.py | 38 ++ .../placement/rest_api_version_history.rst | 12 + .../openstack/placement/schemas/reshaper.py | 47 ++ .../openstack/placement/fixtures/gabbits.py | 1 - .../placement/gabbits/microversion.yaml | 4 +- .../placement/gabbits/reshaper-policy.yaml | 20 + .../openstack/placement/gabbits/reshaper.yaml | 599 ++++++++++++++++++ .../openstack/placement/test_microversion.py | 2 +- placement-api-ref/source/allocations.inc | 6 +- placement-api-ref/source/index.rst | 1 + placement-api-ref/source/parameters.yaml | 26 +- placement-api-ref/source/reshaper.inc | 44 ++ .../samples/reshaper/post-reshaper-1.30.json | 67 ++ .../placement-reshaper-6f3ef70c3a550d09.yaml | 12 + 21 files changed, 1062 insertions(+), 44 deletions(-) create mode 100644 nova/api/openstack/placement/handlers/reshaper.py create mode 100644 nova/api/openstack/placement/policies/reshaper.py create mode 100644 nova/api/openstack/placement/schemas/reshaper.py create mode 100644 nova/tests/functional/api/openstack/placement/gabbits/reshaper-policy.yaml create mode 100644 nova/tests/functional/api/openstack/placement/gabbits/reshaper.yaml create mode 100644 placement-api-ref/source/reshaper.inc create mode 100644 placement-api-ref/source/samples/reshaper/post-reshaper-1.30.json create mode 100644 releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml diff --git a/nova/api/openstack/placement/errors.py b/nova/api/openstack/placement/errors.py index b22cb38f1cd8..15e4fbc4cddf 100644 --- a/nova/api/openstack/placement/errors.py +++ b/nova/api/openstack/placement/errors.py @@ -45,3 +45,4 @@ DUPLICATE_NAME = 'placement.duplicate_name' PROVIDER_IN_USE = 'placement.resource_provider.inuse' PROVIDER_CANNOT_DELETE_PARENT = ( 'placement.resource_provider.cannot_delete_parent') +RESOURCE_PROVIDER_NOT_FOUND = 'placement.resource_provider.not_found' diff --git a/nova/api/openstack/placement/handler.py b/nova/api/openstack/placement/handler.py index 62b40280d2b6..c714c464c5f2 100644 --- a/nova/api/openstack/placement/handler.py +++ b/nova/api/openstack/placement/handler.py @@ -33,6 +33,7 @@ from nova.api.openstack.placement.handlers import aggregate from nova.api.openstack.placement.handlers import allocation from nova.api.openstack.placement.handlers import allocation_candidate from nova.api.openstack.placement.handlers import inventory +from nova.api.openstack.placement.handlers import reshaper from nova.api.openstack.placement.handlers import resource_class from nova.api.openstack.placement.handlers import resource_provider from nova.api.openstack.placement.handlers import root @@ -126,6 +127,9 @@ ROUTE_DECLARATIONS = { '/usages': { 'GET': usage.get_total_usages, }, + '/reshaper': { + 'POST': reshaper.reshape, + }, } diff --git a/nova/api/openstack/placement/handlers/allocation.py b/nova/api/openstack/placement/handlers/allocation.py index 8063cc9b3749..9b2f5d8f3ae1 100644 --- a/nova/api/openstack/placement/handlers/allocation.py +++ b/nova/api/openstack/placement/handlers/allocation.py @@ -17,6 +17,7 @@ import uuid from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils +from oslo_utils import excutils from oslo_utils import timeutils from oslo_utils import uuidutils import webob @@ -195,6 +196,47 @@ def create_allocation_list(context, data, consumers): return rp_obj.AllocationList(context, objects=allocation_objects) +def inspect_consumers(context, data, want_version): + """Look at consumer data in allocations and create consumers as needed. + + Keep a record of the consumers that are created in case they need + to be removed later. + + If an exception is raised by ensure_consumer, commonly HTTPConflict but + also anything else, the newly created consumers will be deleted and the + exception reraised to the caller. + + :param context: The placement context. + :param data: A dictionary of multiple allocations by consumer uuid. + :param want_version: the microversion matcher. + :return: A tuple of a dict of all consumer objects (by consumer uuid) + and a list of those consumer objects which are new. + """ + # 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 replace_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. + with excutils.save_and_reraise_exception(): + delete_consumers(new_consumers_created) + return consumers, new_consumers_created + + @wsgi_wrapper.PlacementWsgify @util.check_accept('application/json') def list_for_consumer(req): @@ -313,7 +355,7 @@ def _new_allocations(context, resource_provider, consumer, resources): return allocations -def _delete_consumers(consumers): +def delete_consumers(consumers): """Helper function that deletes any consumer object supplied to it :param consumers: iterable of Consumer objects to delete @@ -399,7 +441,7 @@ def _set_allocations_for_consumer(req, schema): LOG.debug("Successfully wrote allocations %s", alloc_list) except Exception: if created_new_consumer: - _delete_consumers([consumer]) + delete_consumers([consumer]) raise try: @@ -466,29 +508,8 @@ 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 replace_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 - + consumers, new_consumers_created = inspect_consumers( + context, data, want_version) # Create a sequence of allocation objects to be used in one # AllocationList.replace_all() call, which will mean all the changes # happen within a single transaction and with resource provider @@ -500,7 +521,7 @@ def set_allocations(req): alloc_list.replace_all() LOG.debug("Successfully wrote allocations %s", alloc_list) except Exception: - _delete_consumers(new_consumers_created) + delete_consumers(new_consumers_created) raise try: diff --git a/nova/api/openstack/placement/handlers/inventory.py b/nova/api/openstack/placement/handlers/inventory.py index 46135761568f..019ada01aa30 100644 --- a/nova/api/openstack/placement/handlers/inventory.py +++ b/nova/api/openstack/placement/handlers/inventory.py @@ -75,7 +75,7 @@ def _extract_inventories(body, schema): return data -def _make_inventory_object(resource_provider, resource_class, **data): +def make_inventory_object(resource_provider, resource_class, **data): """Single place to catch malformed Inventories.""" # TODO(cdent): Some of the validation checks that are done here # could be done via JSONschema (using, for example, "minimum": @@ -191,9 +191,9 @@ def create_inventory(req): data = _extract_inventory(req.body, schema.POST_INVENTORY_SCHEMA) resource_class = data.pop('resource_class') - inventory = _make_inventory_object(resource_provider, - resource_class, - **data) + inventory = make_inventory_object(resource_provider, + resource_class, + **data) try: _validate_inventory_capacity( @@ -336,7 +336,7 @@ def set_inventories(req): inv_list = [] for res_class, inventory_data in data['inventories'].items(): - inventory = _make_inventory_object( + inventory = make_inventory_object( resource_provider, res_class, **inventory_data) inv_list.append(inventory) inventories = rp_obj.InventoryList(objects=inv_list) @@ -440,9 +440,9 @@ def update_inventory(req): _('resource provider generation conflict'), comment=errors.CONCURRENT_UPDATE) - inventory = _make_inventory_object(resource_provider, - resource_class, - **data) + inventory = make_inventory_object(resource_provider, + resource_class, + **data) try: _validate_inventory_capacity( diff --git a/nova/api/openstack/placement/handlers/reshaper.py b/nova/api/openstack/placement/handlers/reshaper.py new file mode 100644 index 000000000000..18e202430ebe --- /dev/null +++ b/nova/api/openstack/placement/handlers/reshaper.py @@ -0,0 +1,127 @@ +# 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 handler for the reshaper. + +The reshaper provides for atomically migrating resource provider inventories +and associated allocations when some of the inventory moves from one resource +provider to another, such as when a class of inventory moves from a parent +provider to a new child provider. +""" + +import copy + +from oslo_utils import excutils +import webob + +from nova.api.openstack.placement import errors +from nova.api.openstack.placement import exception +# TODO(cdent): That we are doing this suggests that there's stuff to be +# extracted from the handler to a shared module. +from nova.api.openstack.placement.handlers import allocation +from nova.api.openstack.placement.handlers import inventory +from nova.api.openstack.placement import microversion +from nova.api.openstack.placement.objects import resource_provider as rp_obj +from nova.api.openstack.placement.policies import reshaper as policies +from nova.api.openstack.placement.schemas import reshaper as schema +from nova.api.openstack.placement import util +from nova.api.openstack.placement import wsgi_wrapper +# TODO(cdent): placement needs its own version of this +from nova.i18n import _ + + +@wsgi_wrapper.PlacementWsgify +@microversion.version_handler('1.30') +@util.require_content('application/json') +def reshape(req): + context = req.environ['placement.context'] + want_version = req.environ[microversion.MICROVERSION_ENVIRON] + context.can(policies.RESHAPE) + data = util.extract_json(req.body, schema.POST_RESHAPER_SCHEMA) + inventories = data['inventories'] + allocations = data['allocations'] + # We're going to create several InventoryList, by rp uuid. + inventory_by_rp = {} + + # TODO(cdent): this has overlaps with inventory:set_inventories + # and is a mess of bad names and lack of method extraction. + for rp_uuid, inventory_data in inventories.items(): + try: + resource_provider = rp_obj.ResourceProvider.get_by_uuid( + context, rp_uuid) + except exception.NotFound as exc: + raise webob.exc.HTTPBadRequest( + _('Resource provider %(rp_uuid)s in inventories not found: ' + '%(error)s') % {'rp_uuid': rp_uuid, 'error': exc}, + comment=errors.RESOURCE_PROVIDER_NOT_FOUND) + + # Do an early generation check. + generation = inventory_data['resource_provider_generation'] + if generation != resource_provider.generation: + raise webob.exc.HTTPConflict( + _('resource provider generation conflict: ' + 'actual: %(actual)s, given: %(given)s') % + {'actual': resource_provider.generation, + 'given': generation}, + comment=errors.CONCURRENT_UPDATE) + + inv_list = [] + for res_class, raw_inventory in inventory_data['inventories'].items(): + inv_data = copy.copy(inventory.INVENTORY_DEFAULTS) + inv_data.update(raw_inventory) + inv_obj = inventory.make_inventory_object( + resource_provider, res_class, **inv_data) + inv_list.append(inv_obj) + inventory_by_rp[rp_uuid] = rp_obj.InventoryList(objects=inv_list) + + # Make the consumer objects associated with the allocations. + consumers, new_consumers_created = allocation.inspect_consumers( + context, allocations, want_version) + + # Nest exception handling so that any exception results in new consumer + # objects being deleted, then reraise for translating to HTTP exceptions. + try: + try: + # When these allocations are created they get resource provider + # objects which are different instances (usually with the same + # data) from those loaded above when creating inventory objects. + # The reshape method below is responsible for ensuring that the + # resource providers and their generations do not conflict. + allocation_objects = allocation.create_allocation_list( + context, allocations, consumers) + + rp_obj.reshape(context, inventory_by_rp, allocation_objects) + except Exception: + with excutils.save_and_reraise_exception(): + allocation.delete_consumers(new_consumers_created) + # Generation conflict is a (rare) possibility in a few different + # places in reshape(). + except exception.ConcurrentUpdateDetected as exc: + raise webob.exc.HTTPConflict( + _('update conflict: %(error)s') % {'error': exc}, + comment=errors.CONCURRENT_UPDATE) + # A NotFound here means a resource class that does not exist was named + except exception.NotFound as exc: + raise webob.exc.HTTPBadRequest( + _('malformed reshaper data: %(error)s') % {'error': exc}) + # Distinguish inventory in use (has allocations on it)... + except exception.InventoryInUse as exc: + raise webob.exc.HTTPConflict( + _('update conflict: %(error)s') % {'error': exc}, + comment=errors.INVENTORY_INUSE) + # ...from allocations which won't fit for a variety of reasons. + except exception.InvalidInventory as exc: + raise webob.exc.HTTPConflict( + _('Unable to allocate inventory: %(error)s') % {'error': exc}) + + req.response.status = 204 + req.response.content_type = None + return req.response diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index ec88d86068e8..c832a1a4ed14 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -75,6 +75,8 @@ VERSIONS = [ # the resource class is not in the requested resources. '1.28', # Add support for consumer generation '1.29', # Support nested providers in GET /allocation_candidates API. + '1.30', # Add POST /reshaper for atomically migrating resource provider + # inventories and allocations. ] diff --git a/nova/api/openstack/placement/policies/__init__.py b/nova/api/openstack/placement/policies/__init__.py index cd65514d39a3..be0496d23b5d 100644 --- a/nova/api/openstack/placement/policies/__init__.py +++ b/nova/api/openstack/placement/policies/__init__.py @@ -17,6 +17,7 @@ from nova.api.openstack.placement.policies import allocation from nova.api.openstack.placement.policies import allocation_candidate from nova.api.openstack.placement.policies import base from nova.api.openstack.placement.policies import inventory +from nova.api.openstack.placement.policies import reshaper from nova.api.openstack.placement.policies import resource_class from nova.api.openstack.placement.policies import resource_provider from nova.api.openstack.placement.policies import trait @@ -33,5 +34,6 @@ def list_rules(): usage.list_rules(), trait.list_rules(), allocation.list_rules(), - allocation_candidate.list_rules() + allocation_candidate.list_rules(), + reshaper.list_rules(), ) diff --git a/nova/api/openstack/placement/policies/reshaper.py b/nova/api/openstack/placement/policies/reshaper.py new file mode 100644 index 000000000000..a6615ac48752 --- /dev/null +++ b/nova/api/openstack/placement/policies/reshaper.py @@ -0,0 +1,38 @@ +# 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. + + +from oslo_policy import policy + +from nova.api.openstack.placement.policies import base + + +PREFIX = 'placement:reshaper:%s' +RESHAPE = PREFIX % 'reshape' + +rules = [ + policy.DocumentedRuleDefault( + RESHAPE, + base.RULE_ADMIN_API, + "Reshape Inventory and Allocations.", + [ + { + 'method': 'POST', + 'path': '/reshaper' + } + ], + scope_types=['system']), +] + + +def list_rules(): + return rules diff --git a/nova/api/openstack/placement/rest_api_version_history.rst b/nova/api/openstack/placement/rest_api_version_history.rst index 47c6c8f31e6b..10bd180195d6 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -504,3 +504,15 @@ provider trees are present, ``allocation_requests`` in the response of multiple resource providers in the same tree. 2) ``root_provider_uuid`` and ``parent_provider_uuid`` are added to ``provider_summaries`` in the response of ``GET /allocation_candidates``. + +1.30 Provide a /reshaper resource +--------------------------------- + +Add support for a ``POST /reshaper`` resource that provides for atomically +migrating resource provider inventories and associated allocations when some of +the inventory moves from one resource provider to another, such as when a class +of inventory moves from a parent provider to a new child provider. + +.. note:: This is a special operation that should only be used in rare cases + of resource provider topology changing when inventory is in use. + Only use this if you are really sure of what you are doing. diff --git a/nova/api/openstack/placement/schemas/reshaper.py b/nova/api/openstack/placement/schemas/reshaper.py new file mode 100644 index 000000000000..1658d925153e --- /dev/null +++ b/nova/api/openstack/placement/schemas/reshaper.py @@ -0,0 +1,47 @@ +# 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. +"""Reshaper schema for Placement API.""" + +import copy + +from nova.api.openstack.placement.schemas import allocation +from nova.api.openstack.placement.schemas import common +from nova.api.openstack.placement.schemas import inventory + + +ALLOCATIONS = copy.deepcopy(allocation.POST_ALLOCATIONS_V1_28) +# In the reshaper we need to allow allocations to be an empty dict +# because it may be the case that there simply are no allocations +# (now) for any of the inventory being moved. +ALLOCATIONS['minProperties'] = 0 +POST_RESHAPER_SCHEMA = { + "type": "object", + "properties": { + "inventories": { + "type": "object", + "patternProperties": { + # resource provider uuid + common.UUID_PATTERN: inventory.PUT_INVENTORY_SCHEMA, + }, + # We expect at least one inventories, otherwise there is no reason + # to call the reshaper. + "minProperties": 1, + "additionalProperties": False, + }, + "allocations": ALLOCATIONS, + }, + "required": [ + "inventories", + "allocations", + ], + "additionalProperties": False, +} diff --git a/nova/tests/functional/api/openstack/placement/fixtures/gabbits.py b/nova/tests/functional/api/openstack/placement/fixtures/gabbits.py index 12698a8e5de7..1a26fe6893c1 100644 --- a/nova/tests/functional/api/openstack/placement/fixtures/gabbits.py +++ b/nova/tests/functional/api/openstack/placement/fixtures/gabbits.py @@ -174,7 +174,6 @@ class AllocationFixture(APIFixture): # Create a second consumer for the VCPU allocations consumer2 = tb.ensure_consumer(self.context, user, project) tb.set_allocation(self.context, rp, consumer2, {'VCPU': 6}) - # This consumer is referenced from the gabbits os.environ['CONSUMER_ID'] = consumer2.uuid # Create a consumer object for a different user diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index 146eb55a7480..fa73a3979392 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml @@ -41,13 +41,13 @@ tests: response_json_paths: $.errors[0].title: Not Acceptable -- name: latest microversion is 1.29 +- name: latest microversion is 1.30 GET: / request_headers: openstack-api-version: placement latest response_headers: vary: /openstack-api-version/ - openstack-api-version: placement 1.29 + openstack-api-version: placement 1.30 - name: other accept header bad version GET: / diff --git a/nova/tests/functional/api/openstack/placement/gabbits/reshaper-policy.yaml b/nova/tests/functional/api/openstack/placement/gabbits/reshaper-policy.yaml new file mode 100644 index 000000000000..8e1674626146 --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/reshaper-policy.yaml @@ -0,0 +1,20 @@ +# This tests POSTs to /reshaper using a non-admin user with an open policy +# configuration. The response is a 400 because of bad content, meaning we got +# past policy enforcement. If policy was being enforced we'd get a 403. +fixtures: + - OpenPolicyFixture + +defaults: + request_headers: + x-auth-token: user + accept: application/json + content-type: application/json + openstack-api-version: placement latest + +tests: + +- name: attempt reshape + POST: /reshaper + data: + bad: content + status: 400 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/reshaper.yaml b/nova/tests/functional/api/openstack/placement/gabbits/reshaper.yaml new file mode 100644 index 000000000000..e5752043ff6c --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/reshaper.yaml @@ -0,0 +1,599 @@ +# /reshaper provides a way to atomically move inventory and allocations from +# one resource provider to another, often from a root provider to a new child. + +fixtures: + - AllocationFixture + +defaults: + request_headers: + x-auth-token: admin + accept: application/json + content-type: application/json + openstack-api-version: placement 1.30 + +tests: + +- name: reshaper is POST only + GET: /reshaper + status: 405 + response_headers: + allow: POST + +- name: reshaper requires admin not user + POST: /reshaper + request_headers: + x-auth-token: user + status: 403 + +- name: reshaper not there old + POST: /reshaper + request_headers: + openstack-api-version: placement 1.29 + status: 404 + +- name: very invalid 400 + POST: /reshaper + status: 400 + data: + cows: moo + response_strings: + - JSON does not validate + +- name: missing allocations + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 1 + status: 400 + +# There are existing allocations on RP_UUID (created by the AllocationFixture). +# As the code is currently we cannot null out those allocations from reshaper +# because the allocations identify nothing (replace_all() is a no op). +- name: empty allocations inv in use + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + VCPU: + total: 1 + allocations: {} + status: 409 + response_json_paths: + $.errors[0].code: placement.inventory.inuse + +# Again, with the existing allocations on RP_UUID being held by CONSUMER_ID, +# not INSTANCE_ID, when we try to allocate here, we don't have room. This +# is a correctly invalid operation as to be actually reshaping here, we +# would be needing to move the CONSUMER_ID allocations in this call (and +# setting the inventory to something that could accomodate them). +- name: with allocations + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + VCPU: + total: 1 + allocations: + $ENVIRON['INSTANCE_UUID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + VCPU: 1 + consumer_generation: null + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + status: 409 + response_strings: + - Unable to allocate inventory + +- name: bad rp gen + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 4 + inventories: + VCPU: + total: 1 + allocations: {} + status: 409 + response_strings: + - resource provider generation conflict + - 'actual: 5, given: 4' + +- name: bad consumer gen + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + VCPU: + total: 1 + allocations: + $ENVIRON['INSTANCE_UUID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + VCPU: 1 + # The correct generation here is null, because INSTANCE_UUID + # represents a new consumer at this point. + consumer_generation: 99 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + status: 409 + response_strings: + - consumer generation conflict + +- name: create a child provider + POST: /resource_providers + data: + uuid: $ENVIRON['ALT_RP_UUID'] + name: $ENVIRON['ALT_RP_NAME'] + parent_provider_uuid: $ENVIRON['RP_UUID'] + +# This and subsequent error checking tests are modelled on the successful +# test which is at the end of this file. Using the same data, with minor +# adjustments, so that the cause of failure is clear. + +- name: move to bad child 400 + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + # this was 600 originally but we reset it to 1200 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 1200 + # This resource provider does not exist. + '39bafc00-3fff-444d-b87a-2ead3f866e05': + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 400 + response_json_paths: + $.errors[0].code: placement.resource_provider.not_found + +- name: poorly formed inventory 400 + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + # this was 600 originally but we reset it to 1200 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 1200 + bad_field: moo + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 400 + response_strings: + - JSON does not validate + - "'bad_field' was unexpected" + +- name: poorly formed allocation 400 + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + # this was 600 originally but we reset it to 1200 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 1200 + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + # This bad field will cause a failure in the schema. + bad_field: moo + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 400 + response_strings: + - JSON does not validate + - "'bad_field' was unexpected" + +- name: target resource class not found + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + # not a real inventory, but valid form + DISK_OF_STEEL: + total: 2048 + step_size: 10 + min_unit: 10 + # this was 600 originally but we reset it to 1200 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 1200 + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 400 + response_strings: + - No such resource class DISK_OF_STEEL + +- name: move bad allocation 409 + desc: max unit on disk gb inventory violated + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + max_unit: 600 + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + # Violates max unit + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 409 + response_strings: + - Unable to allocate inventory + +# This is a successful reshape using information as it was established above +# or in the AllocationFixture. A non-obvious fact of this test is that it +# confirms that resource provider and consumer generations are rolled back +# when failures occur, as in the tests above. +- name: move vcpu inventory and allocations to child + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 5 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + # this was 600 originally but we reset it to 1200 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 1200 + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 0 + inventories: + VCPU: + total: 10 + # this was 4 originally but we reset it to 8 + # here because the fixture allocated twice for + # the same consumer and we can't do that from + # the api. + max_unit: 8 + # these consumer generations are all 1 because they have + # previously allocated + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['CONSUMER_ID']: + allocations: + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 1 + $ENVIRON['ALT_CONSUMER_ID']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + $ENVIRON['ALT_RP_UUID']: + resources: + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: 1 + status: 204 + +- name: get usages on parent after move + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + response_json_paths: + $.usages: + DISK_GB: 1020 + $.resource_provider_generation: 8 + +- name: get usages on child after move + GET: /resource_providers/$ENVIRON['ALT_RP_UUID']/usages + response_json_paths: + $.usages: + VCPU: 9 + $.resource_provider_generation: 3 + +# Now move some of the inventory back to the original provider, and put all +# the allocations under two new consumers. This is an artificial test to +# exercise new consumer creation. +- name: consolidate inventory and allocations + # TODO(efried): bug https://bugs.launchpad.net/nova/+bug/1783130 + xfail: true + POST: /reshaper + data: + inventories: + $ENVIRON['RP_UUID']: + resource_provider_generation: 8 + inventories: + DISK_GB: + total: 2048 + step_size: 10 + min_unit: 10 + max_unit: 1200 + VCPU: + total: 10 + max_unit: 8 + $ENVIRON['ALT_RP_UUID']: + resource_provider_generation: 3 + # bug https://bugs.launchpad.net/nova/+bug/1783130 + # means that this will cause an IndexError. + inventories: {} + allocations: + $ENVIRON['CONSUMER_0']: + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 1000 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: 2 + '7bd2e864-0415-445c-8fc2-328520ef7642': + allocations: + $ENVIRON['RP_UUID']: + resources: + VCPU: 8 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['USER_ID'] + consumer_generation: null + '2dfa608c-cecb-4fe0-a1bb-950015fa731f': + allocations: + $ENVIRON['RP_UUID']: + resources: + DISK_GB: 20 + VCPU: 1 + project_id: $ENVIRON['PROJECT_ID'] + user_id: $ENVIRON['ALT_USER_ID'] + consumer_generation: null + status: 204 + +- name: get usages on parent after move back + # TODO(efried): bug https://bugs.launchpad.net/nova/+bug/1783130 + # Fails because 'consolidate inventory and allocations' above fails. + xfail: true + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + response_json_paths: + $.usages: + VCPU: 9 + DISK_GB: 1040 + $.resource_provider_generation: 11 + +- name: get usages on child after move back + # TODO(efried): bug https://bugs.launchpad.net/nova/+bug/1783130 + # Fails because 'consolidate inventory and allocations' above fails. + xfail: true + GET: /resource_providers/$ENVIRON['ALT_RP_UUID']/usages + response_json_paths: + $.usages: {} + $.resource_provider_generation: 5 diff --git a/nova/tests/unit/api/openstack/placement/test_microversion.py b/nova/tests/unit/api/openstack/placement/test_microversion.py index da5c08c538df..25e53a7f7c38 100644 --- a/nova/tests/unit/api/openstack/placement/test_microversion.py +++ b/nova/tests/unit/api/openstack/placement/test_microversion.py @@ -88,7 +88,7 @@ class TestMicroversionIntersection(testtools.TestCase): # 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 = 19 + TOTAL_VERSIONED_METHODS = 20 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 7a79862f44b5..f4b63e7ca919 100644 --- a/placement-api-ref/source/allocations.inc +++ b/placement-api-ref/source/allocations.inc @@ -37,7 +37,7 @@ Request .. rest_parameters:: parameters.yaml - consumer_uuid: consumer_uuid_body - - consumer_generation: consumer_generation + - consumer_generation: consumer_generation_min - project_id: project_id_body - user_id: user_id_body - allocations: allocations_dict_empty @@ -89,7 +89,7 @@ Response - allocations: allocations_by_resource_provider - generation: resource_provider_generation - resources: resources - - consumer_generation: consumer_generation + - consumer_generation: consumer_generation_min - project_id: project_id_body_1_12 - user_id: user_id_body_1_12 @@ -131,7 +131,7 @@ Request (microversions 1.12 - ) - consumer_uuid: consumer_uuid - allocations: allocations_dict - resources: resources - - consumer_generation: consumer_generation + - consumer_generation: consumer_generation_min - project_id: project_id_body - user_id: user_id_body - generation: resource_provider_generation_optional diff --git a/placement-api-ref/source/index.rst b/placement-api-ref/source/index.rst index d09315b8effd..c72c796210de 100644 --- a/placement-api-ref/source/index.rst +++ b/placement-api-ref/source/index.rst @@ -30,3 +30,4 @@ header for APIs sending data payloads in the request body (i.e. ``PUT`` and .. include:: usages.inc .. include:: resource_provider_usages.inc .. include:: allocation_candidates.inc +.. include:: reshaper.inc diff --git a/placement-api-ref/source/parameters.yaml b/placement-api-ref/source/parameters.yaml index 6ee63cd93f1c..9c76f4b1012b 100644 --- a/placement-api-ref/source/parameters.yaml +++ b/placement-api-ref/source/parameters.yaml @@ -315,14 +315,16 @@ capacity: required: true description: > The amount of the resource that the provider can accommodate. -consumer_generation: +consumer_generation: &consumer_generation type: integer in: body required: true - min_version: 1.28 description: > The generation of the consumer. Should be set to ``null`` when indicating that the caller expects the consumer does not yet exist. +consumer_generation_min: + <<: *consumer_generation + min_version: 1.28 consumer_uuid_body: <<: *consumer_uuid in: body @@ -392,6 +394,26 @@ reserved_opt: Up to microversion 1.25, this value has to be less than the value of ``total``. Starting from microversion 1.26, this value has to be less than or equal to the value of ``total``. +reshaper_allocations: + type: object + in: body + required: true + description: > + A dictionary of multiple allocations, keyed by consumer uuid. Each + collection of allocations describes the full set of allocations for + each consumer. Each consumer allocations dict is itself a dictionary + of resource allocations keyed by resource provider uuid. An empty + dictionary indicates no change in existing allocations, whereas an empty + ``allocations`` dictionary **within** a consumer dictionary indicates that + all allocations for that consumer should be deleted. +reshaper_inventories: + type: object + in: body + required: true + description: > + A dictionary of multiple inventories, keyed by resource provider uuid. Each + inventory describes the desired full inventory for each resource provider. + An empty dictionary causes the inventory for that provider to be deleted. resource_class: <<: *resource_class_path in: body diff --git a/placement-api-ref/source/reshaper.inc b/placement-api-ref/source/reshaper.inc new file mode 100644 index 000000000000..0f9fa507e51d --- /dev/null +++ b/placement-api-ref/source/reshaper.inc @@ -0,0 +1,44 @@ +======== +Reshaper +======== + +.. note:: Reshaper requests are available starting from version 1.30. + +Reshaper +======== + +Atomically migrate resource provider inventories and associated allocations. +This is used when some of the inventory needs to move from one resource +provider to another, such as when a class of inventory moves from a parent +provider to a new child provider. + +.. note:: This is a special operation that should only be used in rare cases + of resource provider topology changing when inventory is in use. + Only use this if you are really sure of what you are doing. + +.. rest_method:: POST /reshaper + +Normal Response Codes: 204 + +Error Response Codes: badRequest(400), conflict(409) + +Request +------- + +.. rest_parameters:: parameters.yaml + + - inventories: reshaper_inventories + - inventories.{resource_provider_uuid}.resource_provider_generation: resource_provider_generation + - inventories.{resource_provider_uuid}.inventories: inventories + - allocations: reshaper_allocations + - allocations.{consumer_uuid}.allocations: allocations_dict_empty + - allocations.{consumer_uuid}.allocations.{resource_provider_uuid}.resources: resources + - allocations.{consumer_uuid}.project_id: project_id_body + - allocations.{consumer_uuid}.user_id: user_id_body + - allocations.{consumer_uuid}.consumer_generation: consumer_generation + +Request Example +--------------- + +.. literalinclude:: ./samples/reshaper/post-reshaper-1.30.json + :language: javascript diff --git a/placement-api-ref/source/samples/reshaper/post-reshaper-1.30.json b/placement-api-ref/source/samples/reshaper/post-reshaper-1.30.json new file mode 100644 index 000000000000..baeebe173844 --- /dev/null +++ b/placement-api-ref/source/samples/reshaper/post-reshaper-1.30.json @@ -0,0 +1,67 @@ +{ + "allocations": { + "9ae60315-80c2-48a0-a168-ca4f27c307e1": { + "allocations": { + "a7466641-cd72-499b-b6c9-c208eacecb3d": { + "resources": { + "DISK_GB": 1000 + } + } + }, + "project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f", + "user_id": "cc8a0fe0-2b7c-4392-ae51-747bc73cf473", + "consumer_generation": 1 + }, + "4a6444e5-10d6-43f6-9a0b-8acce9309ac9": { + "allocations": { + "c4ddddbb-01ee-4814-85c9-f57a962c22ba": { + "resources": { + "VCPU": 1 + } + }, + "a7466641-cd72-499b-b6c9-c208eacecb3d": { + "resources": { + "DISK_GB": 20 + } + } + }, + "project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f", + "user_id": "406e1095-71cb-47b9-9b3c-aedb7f663f5a", + "consumer_generation": 1 + }, + "e10e7ca0-2ac5-4c98-bad9-51c95b1930ed": { + "allocations": { + "c4ddddbb-01ee-4814-85c9-f57a962c22ba": { + "resources": { + "VCPU": 8 + } + } + }, + "project_id": "2f0c4ffc-4c4d-407a-b334-56297b871b7f", + "user_id": "cc8a0fe0-2b7c-4392-ae51-747bc73cf473", + "consumer_generation": 1 + } + }, + "inventories": { + "c4ddddbb-01ee-4814-85c9-f57a962c22ba": { + "inventories": { + "VCPU": { + "max_unit": 8, + "total": 10 + } + }, + "resource_provider_generation": null + }, + "a7466641-cd72-499b-b6c9-c208eacecb3d": { + "inventories": { + "DISK_GB": { + "min_unit": 10, + "total": 2048, + "max_unit": 1200, + "step_size": 10 + } + }, + "resource_provider_generation": 5 + } + } +} diff --git a/releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml b/releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml new file mode 100644 index 000000000000..5f94266bb05b --- /dev/null +++ b/releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Microversion 1.30 of the placement API adds support for a + ``POST /reshaper`` resource that provides for atomically migrating resource + provider inventories and associated allocations when some of the inventory + moves from one resource provider to another, such as when a class of + inventory moves from a parent provider to a new child provider. + + .. note:: This is a special operation that should only be used in rare + cases of resource provider topology changing when inventory is in + use. Only use this if you are really sure of what you are doing.