[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:
parent
fa66d9a730
commit
4d525b4ec1
@ -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'
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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(
|
||||||
|
127
nova/api/openstack/placement/handlers/reshaper.py
Normal file
127
nova/api/openstack/placement/handlers/reshaper.py
Normal 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
|
@ -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.
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
38
nova/api/openstack/placement/policies/reshaper.py
Normal file
38
nova/api/openstack/placement/policies/reshaper.py
Normal 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
|
@ -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.
|
||||||
|
47
nova/api/openstack/placement/schemas/reshaper.py
Normal file
47
nova/api/openstack/placement/schemas/reshaper.py
Normal 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,
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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: /
|
||||||
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
44
placement-api-ref/source/reshaper.inc
Normal file
44
placement-api-ref/source/reshaper.inc
Normal 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
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml
Normal file
12
releasenotes/notes/placement-reshaper-6f3ef70c3a550d09.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user