[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
This commit is contained in:
Chris Dent 2018-06-20 17:56:04 +01:00 committed by Matt Riedemann
parent fa66d9a730
commit 4d525b4ec1
21 changed files with 1062 additions and 44 deletions

View File

@ -45,3 +45,4 @@ DUPLICATE_NAME = 'placement.duplicate_name'
PROVIDER_IN_USE = 'placement.resource_provider.inuse' PROVIDER_IN_USE = 'placement.resource_provider.inuse'
PROVIDER_CANNOT_DELETE_PARENT = ( PROVIDER_CANNOT_DELETE_PARENT = (
'placement.resource_provider.cannot_delete_parent') 'placement.resource_provider.cannot_delete_parent')
RESOURCE_PROVIDER_NOT_FOUND = 'placement.resource_provider.not_found'

View File

@ -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
from nova.api.openstack.placement.handlers import allocation_candidate from nova.api.openstack.placement.handlers import allocation_candidate
from nova.api.openstack.placement.handlers import inventory 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_class
from nova.api.openstack.placement.handlers import resource_provider from nova.api.openstack.placement.handlers import resource_provider
from nova.api.openstack.placement.handlers import root from nova.api.openstack.placement.handlers import root
@ -126,6 +127,9 @@ ROUTE_DECLARATIONS = {
'/usages': { '/usages': {
'GET': usage.get_total_usages, 'GET': usage.get_total_usages,
}, },
'/reshaper': {
'POST': reshaper.reshape,
},
} }

View File

@ -17,6 +17,7 @@ import uuid
from oslo_log import log as logging from oslo_log import log as logging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
import webob import webob
@ -195,6 +196,47 @@ def create_allocation_list(context, data, consumers):
return rp_obj.AllocationList(context, objects=allocation_objects) 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 @wsgi_wrapper.PlacementWsgify
@util.check_accept('application/json') @util.check_accept('application/json')
def list_for_consumer(req): def list_for_consumer(req):
@ -313,7 +355,7 @@ def _new_allocations(context, resource_provider, consumer, resources):
return allocations return allocations
def _delete_consumers(consumers): def delete_consumers(consumers):
"""Helper function that deletes any consumer object supplied to it """Helper function that deletes any consumer object supplied to it
:param consumers: iterable of Consumer objects to delete :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) LOG.debug("Successfully wrote allocations %s", alloc_list)
except Exception: except Exception:
if created_new_consumer: if created_new_consumer:
_delete_consumers([consumer]) delete_consumers([consumer])
raise raise
try: try:
@ -466,29 +508,8 @@ def set_allocations(req):
want_schema = schema.POST_ALLOCATIONS_V1_28 want_schema = schema.POST_ALLOCATIONS_V1_28
data = util.extract_json(req.body, want_schema) data = util.extract_json(req.body, want_schema)
# First, ensure that all consumers referenced in the payload actually consumers, new_consumers_created = inspect_consumers(
# exist. And if not, create them. Keep a record of auto-created consumers context, data, want_version)
# 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
# Create a sequence of allocation objects to be used in one # Create a sequence of allocation objects to be used in one
# AllocationList.replace_all() call, which will mean all the changes # AllocationList.replace_all() call, which will mean all the changes
# happen within a single transaction and with resource provider # happen within a single transaction and with resource provider
@ -500,7 +521,7 @@ def set_allocations(req):
alloc_list.replace_all() alloc_list.replace_all()
LOG.debug("Successfully wrote allocations %s", alloc_list) LOG.debug("Successfully wrote allocations %s", alloc_list)
except Exception: except Exception:
_delete_consumers(new_consumers_created) delete_consumers(new_consumers_created)
raise raise
try: try:

View File

@ -75,7 +75,7 @@ def _extract_inventories(body, schema):
return data 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.""" """Single place to catch malformed Inventories."""
# TODO(cdent): Some of the validation checks that are done here # TODO(cdent): Some of the validation checks that are done here
# could be done via JSONschema (using, for example, "minimum": # 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) data = _extract_inventory(req.body, schema.POST_INVENTORY_SCHEMA)
resource_class = data.pop('resource_class') resource_class = data.pop('resource_class')
inventory = _make_inventory_object(resource_provider, inventory = make_inventory_object(resource_provider,
resource_class, resource_class,
**data) **data)
try: try:
_validate_inventory_capacity( _validate_inventory_capacity(
@ -336,7 +336,7 @@ def set_inventories(req):
inv_list = [] inv_list = []
for res_class, inventory_data in data['inventories'].items(): for res_class, inventory_data in data['inventories'].items():
inventory = _make_inventory_object( inventory = make_inventory_object(
resource_provider, res_class, **inventory_data) resource_provider, res_class, **inventory_data)
inv_list.append(inventory) inv_list.append(inventory)
inventories = rp_obj.InventoryList(objects=inv_list) inventories = rp_obj.InventoryList(objects=inv_list)
@ -440,9 +440,9 @@ def update_inventory(req):
_('resource provider generation conflict'), _('resource provider generation conflict'),
comment=errors.CONCURRENT_UPDATE) comment=errors.CONCURRENT_UPDATE)
inventory = _make_inventory_object(resource_provider, inventory = make_inventory_object(resource_provider,
resource_class, resource_class,
**data) **data)
try: try:
_validate_inventory_capacity( _validate_inventory_capacity(

View File

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

View File

@ -75,6 +75,8 @@ VERSIONS = [
# the resource class is not in the requested resources. # the resource class is not in the requested resources.
'1.28', # Add support for consumer generation '1.28', # Add support for consumer generation
'1.29', # Support nested providers in GET /allocation_candidates API. '1.29', # Support nested providers in GET /allocation_candidates API.
'1.30', # Add POST /reshaper for atomically migrating resource provider
# inventories and allocations.
] ]

View File

@ -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 allocation_candidate
from nova.api.openstack.placement.policies import base from nova.api.openstack.placement.policies import base
from nova.api.openstack.placement.policies import inventory 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_class
from nova.api.openstack.placement.policies import resource_provider from nova.api.openstack.placement.policies import resource_provider
from nova.api.openstack.placement.policies import trait from nova.api.openstack.placement.policies import trait
@ -33,5 +34,6 @@ def list_rules():
usage.list_rules(), usage.list_rules(),
trait.list_rules(), trait.list_rules(),
allocation.list_rules(), allocation.list_rules(),
allocation_candidate.list_rules() allocation_candidate.list_rules(),
reshaper.list_rules(),
) )

View File

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

View File

@ -504,3 +504,15 @@ provider trees are present, ``allocation_requests`` in the response of
multiple resource providers in the same tree. multiple resource providers in the same tree.
2) ``root_provider_uuid`` and ``parent_provider_uuid`` are added to 2) ``root_provider_uuid`` and ``parent_provider_uuid`` are added to
``provider_summaries`` in the response of ``GET /allocation_candidates``. ``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.

View File

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

View File

@ -174,7 +174,6 @@ class AllocationFixture(APIFixture):
# Create a second consumer for the VCPU allocations # Create a second consumer for the VCPU allocations
consumer2 = tb.ensure_consumer(self.context, user, project) consumer2 = tb.ensure_consumer(self.context, user, project)
tb.set_allocation(self.context, rp, consumer2, {'VCPU': 6}) tb.set_allocation(self.context, rp, consumer2, {'VCPU': 6})
# This consumer is referenced from the gabbits
os.environ['CONSUMER_ID'] = consumer2.uuid os.environ['CONSUMER_ID'] = consumer2.uuid
# Create a consumer object for a different user # Create a consumer object for a different user

View File

@ -41,13 +41,13 @@ tests:
response_json_paths: response_json_paths:
$.errors[0].title: Not Acceptable $.errors[0].title: Not Acceptable
- name: latest microversion is 1.29 - name: latest microversion is 1.30
GET: / GET: /
request_headers: request_headers:
openstack-api-version: placement latest openstack-api-version: placement latest
response_headers: response_headers:
vary: /openstack-api-version/ vary: /openstack-api-version/
openstack-api-version: placement 1.29 openstack-api-version: placement 1.30
- name: other accept header bad version - name: other accept header bad version
GET: / GET: /

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ Request
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- consumer_uuid: consumer_uuid_body - consumer_uuid: consumer_uuid_body
- consumer_generation: consumer_generation - consumer_generation: consumer_generation_min
- project_id: project_id_body - project_id: project_id_body
- user_id: user_id_body - user_id: user_id_body
- allocations: allocations_dict_empty - allocations: allocations_dict_empty
@ -89,7 +89,7 @@ Response
- allocations: allocations_by_resource_provider - allocations: allocations_by_resource_provider
- generation: resource_provider_generation - generation: resource_provider_generation
- resources: resources - resources: resources
- consumer_generation: consumer_generation - consumer_generation: consumer_generation_min
- project_id: project_id_body_1_12 - project_id: project_id_body_1_12
- user_id: user_id_body_1_12 - user_id: user_id_body_1_12
@ -131,7 +131,7 @@ Request (microversions 1.12 - )
- consumer_uuid: consumer_uuid - consumer_uuid: consumer_uuid
- allocations: allocations_dict - allocations: allocations_dict
- resources: resources - resources: resources
- consumer_generation: consumer_generation - consumer_generation: consumer_generation_min
- project_id: project_id_body - project_id: project_id_body
- user_id: user_id_body - user_id: user_id_body
- generation: resource_provider_generation_optional - generation: resource_provider_generation_optional

View File

@ -30,3 +30,4 @@ header for APIs sending data payloads in the request body (i.e. ``PUT`` and
.. include:: usages.inc .. include:: usages.inc
.. include:: resource_provider_usages.inc .. include:: resource_provider_usages.inc
.. include:: allocation_candidates.inc .. include:: allocation_candidates.inc
.. include:: reshaper.inc

View File

@ -315,14 +315,16 @@ capacity:
required: true required: true
description: > description: >
The amount of the resource that the provider can accommodate. The amount of the resource that the provider can accommodate.
consumer_generation: consumer_generation: &consumer_generation
type: integer type: integer
in: body in: body
required: true required: true
min_version: 1.28
description: > description: >
The generation of the consumer. Should be set to ``null`` when indicating The generation of the consumer. Should be set to ``null`` when indicating
that the caller expects the consumer does not yet exist. that the caller expects the consumer does not yet exist.
consumer_generation_min:
<<: *consumer_generation
min_version: 1.28
consumer_uuid_body: consumer_uuid_body:
<<: *consumer_uuid <<: *consumer_uuid
in: body in: body
@ -392,6 +394,26 @@ reserved_opt:
Up to microversion 1.25, this value has to be less than the value of 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 ``total``. Starting from microversion 1.26, this value has to be less
than or equal to the value of ``total``. 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:
<<: *resource_class_path <<: *resource_class_path
in: body in: body

View File

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

View File

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

View File

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