da1a588b8d
Nothing is being translated in placement so, for the sake of being clean and tidy, this patch removes the framework for translation and the import of oslo.i18n. Originally the hope was we could remove the dependency on oslo.i18n and Babel entirely to save some disk space but many other oslo-related libs depend on oslo.i18n so they are present anyway. [1] [1] http://lists.openstack.org/pipermail/openstack-discuss/2019-March/004220.html Change-Id: Ia965d028b6f7c9f04d1f29beb12f4862585631d5
464 lines
17 KiB
Python
464 lines
17 KiB
Python
# 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.
|
|
"""Inventory handlers for Placement API."""
|
|
|
|
import copy
|
|
import operator
|
|
|
|
from oslo_db import exception as db_exc
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import encodeutils
|
|
import webob
|
|
|
|
from placement.db import constants as db_const
|
|
from placement import errors
|
|
from placement import exception
|
|
from placement import microversion
|
|
from placement.objects import inventory as inv_obj
|
|
from placement.objects import resource_provider as rp_obj
|
|
from placement.policies import inventory as policies
|
|
from placement.schemas import inventory as schema
|
|
from placement import util
|
|
from placement import wsgi_wrapper
|
|
|
|
|
|
# NOTE(cdent): We keep our own representation of inventory defaults
|
|
# and output fields, separate from the versioned object to avoid
|
|
# inadvertent API changes when the object defaults are changed.
|
|
OUTPUT_INVENTORY_FIELDS = [
|
|
'total',
|
|
'reserved',
|
|
'min_unit',
|
|
'max_unit',
|
|
'step_size',
|
|
'allocation_ratio',
|
|
]
|
|
INVENTORY_DEFAULTS = {
|
|
'reserved': 0,
|
|
'min_unit': 1,
|
|
'max_unit': db_const.MAX_INT,
|
|
'step_size': 1,
|
|
'allocation_ratio': 1.0
|
|
}
|
|
|
|
|
|
def _extract_inventory(body, schema):
|
|
"""Extract and validate inventory from JSON body."""
|
|
data = util.extract_json(body, schema)
|
|
|
|
inventory_data = copy.copy(INVENTORY_DEFAULTS)
|
|
inventory_data.update(data)
|
|
|
|
return inventory_data
|
|
|
|
|
|
def _extract_inventories(body, schema):
|
|
"""Extract and validate multiple inventories from JSON body."""
|
|
data = util.extract_json(body, schema)
|
|
|
|
inventories = {}
|
|
for res_class, raw_inventory in data['inventories'].items():
|
|
inventory_data = copy.copy(INVENTORY_DEFAULTS)
|
|
inventory_data.update(raw_inventory)
|
|
inventories[res_class] = inventory_data
|
|
|
|
data['inventories'] = inventories
|
|
return data
|
|
|
|
|
|
def make_inventory_object(resource_provider, resource_class, **data):
|
|
"""Single place to catch malformed Inventories."""
|
|
# TODO(cdent): Some of the validation checks that are done here
|
|
# could be done via JSONschema (using, for example, "minimum":
|
|
# 0) for non-negative integers. It's not clear if that is
|
|
# duplication or decoupling so leaving it as this for now.
|
|
try:
|
|
inventory = inv_obj.Inventory(
|
|
resource_provider=resource_provider,
|
|
resource_class=resource_class, **data)
|
|
except (ValueError, TypeError) as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'Bad inventory %(class)s for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'class': resource_class,
|
|
'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
return inventory
|
|
|
|
|
|
def _send_inventories(req, resource_provider, inventories):
|
|
"""Send a JSON representation of a list of inventories."""
|
|
response = req.response
|
|
response.status = 200
|
|
output, last_modified = _serialize_inventories(
|
|
inventories, resource_provider.generation)
|
|
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
|
|
response.content_type = 'application/json'
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
if want_version.matches((1, 15)):
|
|
response.last_modified = last_modified
|
|
response.cache_control = 'no-cache'
|
|
return response
|
|
|
|
|
|
def _send_inventory(req, resource_provider, inventory, status=200):
|
|
"""Send a JSON representation of one single inventory."""
|
|
response = req.response
|
|
response.status = status
|
|
response.body = encodeutils.to_utf8(jsonutils.dumps(_serialize_inventory(
|
|
inventory, generation=resource_provider.generation)))
|
|
response.content_type = 'application/json'
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
if want_version.matches((1, 15)):
|
|
modified = util.pick_last_modified(None, inventory)
|
|
response.last_modified = modified
|
|
response.cache_control = 'no-cache'
|
|
return response
|
|
|
|
|
|
def _serialize_inventory(inventory, generation=None):
|
|
"""Turn a single inventory into a dictionary."""
|
|
data = {
|
|
field: getattr(inventory, field)
|
|
for field in OUTPUT_INVENTORY_FIELDS
|
|
}
|
|
if generation:
|
|
data['resource_provider_generation'] = generation
|
|
return data
|
|
|
|
|
|
def _serialize_inventories(inventories, generation):
|
|
"""Turn a list of inventories in a dict by resource class."""
|
|
inventories_by_class = {inventory.resource_class: inventory
|
|
for inventory in inventories}
|
|
inventories_dict = {}
|
|
last_modified = None
|
|
for resource_class, inventory in inventories_by_class.items():
|
|
last_modified = util.pick_last_modified(last_modified, inventory)
|
|
inventories_dict[resource_class] = _serialize_inventory(
|
|
inventory, generation=None)
|
|
return ({'resource_provider_generation': generation,
|
|
'inventories': inventories_dict}, last_modified)
|
|
|
|
|
|
def _validate_inventory_capacity(version, inventories):
|
|
"""Validate inventory capacity.
|
|
|
|
:param version: request microversion.
|
|
:param inventories: One Inventory or a list of Inventory objects to
|
|
validate capacities of.
|
|
:raises: exception.InvalidInventoryCapacityReservedCanBeTotal if request
|
|
microversion is 1.26 or higher and any inventory has capacity < 0.
|
|
:raises: exception.InvalidInventoryCapacity if request
|
|
microversion is lower than 1.26 and any inventory has capacity <= 0.
|
|
"""
|
|
if not version.matches((1, 26)):
|
|
op = operator.le
|
|
exc_class = exception.InvalidInventoryCapacity
|
|
else:
|
|
op = operator.lt
|
|
exc_class = exception.InvalidInventoryCapacityReservedCanBeTotal
|
|
if isinstance(inventories, inv_obj.Inventory):
|
|
inventories = [inventories]
|
|
for inventory in inventories:
|
|
if op(inventory.capacity, 0):
|
|
raise exc_class(
|
|
resource_class=inventory.resource_class,
|
|
resource_provider=inventory.resource_provider.uuid)
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@util.require_content('application/json')
|
|
def create_inventory(req):
|
|
"""POST to create one inventory.
|
|
|
|
On success return a 201 response, a location header pointing
|
|
to the newly created inventory and an application/json representation
|
|
of the inventory.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.CREATE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
|
context, uuid)
|
|
data = _extract_inventory(req.body, schema.POST_INVENTORY_SCHEMA)
|
|
resource_class = data.pop('resource_class')
|
|
|
|
inventory = make_inventory_object(resource_provider,
|
|
resource_class,
|
|
**data)
|
|
|
|
try:
|
|
_validate_inventory_capacity(
|
|
req.environ[microversion.MICROVERSION_ENVIRON], inventory)
|
|
resource_provider.add_inventory(inventory)
|
|
except (exception.ConcurrentUpdateDetected,
|
|
db_exc.DBDuplicateEntry) as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'Update conflict: %(error)s' % {'error': exc},
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
except (exception.InvalidInventoryCapacity,
|
|
exception.NotFound) as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'Unable to create inventory for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
|
|
response = req.response
|
|
response.location = util.inventory_url(
|
|
req.environ, resource_provider, resource_class)
|
|
return _send_inventory(req, resource_provider, inventory,
|
|
status=201)
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
def delete_inventory(req):
|
|
"""DELETE to destroy a single inventory.
|
|
|
|
If the inventory is in use or resource provider generation is out
|
|
of sync return a 409.
|
|
|
|
On success return a 204 and an empty body.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.DELETE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_class = util.wsgi_path_item(req.environ, 'resource_class')
|
|
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
|
context, uuid)
|
|
try:
|
|
resource_provider.delete_inventory(resource_class)
|
|
except (exception.ConcurrentUpdateDetected,
|
|
exception.InventoryInUse) as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'Unable to delete inventory of class %(class)s: %(error)s' %
|
|
{'class': resource_class, 'error': exc},
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
except exception.NotFound as exc:
|
|
raise webob.exc.HTTPNotFound(
|
|
'No inventory of class %(class)s found for delete: %(error)s' %
|
|
{'class': resource_class, 'error': exc})
|
|
|
|
response = req.response
|
|
response.status = 204
|
|
response.content_type = None
|
|
return response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@util.check_accept('application/json')
|
|
def get_inventories(req):
|
|
"""GET a list of inventories.
|
|
|
|
On success return a 200 with an application/json body representing
|
|
a collection of inventories.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.LIST)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
try:
|
|
rp = rp_obj.ResourceProvider.get_by_uuid(context, uuid)
|
|
except exception.NotFound as exc:
|
|
raise webob.exc.HTTPNotFound(
|
|
"No resource provider with uuid %(uuid)s found : %(error)s" %
|
|
{'uuid': uuid, 'error': exc})
|
|
|
|
inv_list = inv_obj.get_all_by_resource_provider(context, rp)
|
|
|
|
return _send_inventories(req, rp, inv_list)
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@util.check_accept('application/json')
|
|
def get_inventory(req):
|
|
"""GET one inventory.
|
|
|
|
On success return a 200 an application/json body representing one
|
|
inventory.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.SHOW)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_class = util.wsgi_path_item(req.environ, 'resource_class')
|
|
try:
|
|
rp = rp_obj.ResourceProvider.get_by_uuid(context, uuid)
|
|
except exception.NotFound as exc:
|
|
raise webob.exc.HTTPNotFound(
|
|
"No resource provider with uuid %(uuid)s found : %(error)s" %
|
|
{'uuid': uuid, 'error': exc})
|
|
|
|
inv_list = inv_obj.get_all_by_resource_provider(context, rp)
|
|
inventory = inv_obj.find(inv_list, resource_class)
|
|
|
|
if not inventory:
|
|
raise webob.exc.HTTPNotFound(
|
|
'No inventory of class %(class)s for %(rp_uuid)s' %
|
|
{'class': resource_class, 'rp_uuid': uuid})
|
|
|
|
return _send_inventory(req, rp, inventory)
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@util.require_content('application/json')
|
|
def set_inventories(req):
|
|
"""PUT to set all inventory for a resource provider.
|
|
|
|
Create, update and delete inventory as required to reset all
|
|
the inventory.
|
|
|
|
If the resource generation is out of sync, return a 409.
|
|
If an inventory to be deleted is in use, return a 409.
|
|
If any inventory to be created or updated has settings which are
|
|
invalid (for example reserved exceeds capacity), return a 400.
|
|
|
|
On success return a 200 with an application/json body representing
|
|
the inventories.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.UPDATE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
|
context, uuid)
|
|
|
|
data = _extract_inventories(req.body, schema.PUT_INVENTORY_SCHEMA)
|
|
if data['resource_provider_generation'] != resource_provider.generation:
|
|
raise webob.exc.HTTPConflict(
|
|
'resource provider generation conflict',
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
|
|
inventories = []
|
|
for res_class, inventory_data in data['inventories'].items():
|
|
inventory = make_inventory_object(
|
|
resource_provider, res_class, **inventory_data)
|
|
inventories.append(inventory)
|
|
|
|
try:
|
|
_validate_inventory_capacity(
|
|
req.environ[microversion.MICROVERSION_ENVIRON], inventories)
|
|
resource_provider.set_inventory(inventories)
|
|
except exception.ResourceClassNotFound as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'Unknown resource class in inventory for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
except exception.InventoryWithResourceClassNotFound as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'Race condition detected when setting inventory. No inventory '
|
|
'record with resource class for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
except (exception.ConcurrentUpdateDetected,
|
|
db_exc.DBDuplicateEntry) as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'update conflict: %(error)s' % {'error': exc},
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
except exception.InventoryInUse as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'update conflict: %(error)s' % {'error': exc},
|
|
comment=errors.INVENTORY_INUSE)
|
|
except exception.InvalidInventoryCapacity as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'Unable to update inventory for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
|
|
return _send_inventories(req, resource_provider, inventories)
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.5', status_code=405)
|
|
def delete_inventories(req):
|
|
"""DELETE all inventory for a resource provider.
|
|
|
|
Delete inventory as required to reset all the inventory.
|
|
If an inventory to be deleted is in use, return a 409 Conflict.
|
|
On success return a 204 No content.
|
|
Return 405 Method Not Allowed if the wanted microversion does not match.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.DELETE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
|
context, uuid)
|
|
|
|
try:
|
|
resource_provider.set_inventory([])
|
|
except exception.ConcurrentUpdateDetected:
|
|
raise webob.exc.HTTPConflict(
|
|
'Unable to delete inventory for resource provider '
|
|
'%(rp_uuid)s because the inventory was updated by '
|
|
'another process. Please retry your request.' %
|
|
{'rp_uuid': resource_provider.uuid},
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
except exception.InventoryInUse as ex:
|
|
raise webob.exc.HTTPConflict(ex.format_message(),
|
|
comment=errors.INVENTORY_INUSE)
|
|
|
|
response = req.response
|
|
response.status = 204
|
|
response.content_type = None
|
|
|
|
return response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@util.require_content('application/json')
|
|
def update_inventory(req):
|
|
"""PUT to update one inventory.
|
|
|
|
If the resource generation is out of sync, return a 409.
|
|
If the inventory has settings which are invalid (for example
|
|
reserved exceeds capacity), return a 400.
|
|
|
|
On success return a 200 with an application/json body representing
|
|
the inventory.
|
|
"""
|
|
context = req.environ['placement.context']
|
|
context.can(policies.UPDATE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
resource_class = util.wsgi_path_item(req.environ, 'resource_class')
|
|
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
|
context, uuid)
|
|
|
|
data = _extract_inventory(req.body, schema.BASE_INVENTORY_SCHEMA)
|
|
if data['resource_provider_generation'] != resource_provider.generation:
|
|
raise webob.exc.HTTPConflict(
|
|
'resource provider generation conflict',
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
|
|
inventory = make_inventory_object(resource_provider,
|
|
resource_class,
|
|
**data)
|
|
|
|
try:
|
|
_validate_inventory_capacity(
|
|
req.environ[microversion.MICROVERSION_ENVIRON], inventory)
|
|
resource_provider.update_inventory(inventory)
|
|
except (exception.ConcurrentUpdateDetected,
|
|
db_exc.DBDuplicateEntry) as exc:
|
|
raise webob.exc.HTTPConflict(
|
|
'update conflict: %(error)s' % {'error': exc},
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
except exception.InventoryWithResourceClassNotFound as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'No inventory record with resource class for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
except exception.InvalidInventoryCapacity as exc:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'Unable to update inventory for resource provider '
|
|
'%(rp_uuid)s: %(error)s' % {'rp_uuid': resource_provider.uuid,
|
|
'error': exc})
|
|
|
|
return _send_inventory(req, resource_provider, inventory)
|