Chris Dent da1a588b8d Remove use of oslo.i18n and translation
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
2019-03-25 16:59:21 +00:00

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)