placement: change resource class to a StringField
This patch modifies the fields.ResourceClass field type from an EnumField to a StringField. This keeps the over-the-wire format of the field backwards-compatible while allowing us to add non-standardized, custom resource classes to the new resource_classes database table. We change all locations of fields.ResourceClass.index() and fields.ResourceClass.from_index() to use the ResourceClassCache object added in the previous patch in this series and add some check logic in Inventory and Allocation object's obj_make_compatible() methods to ensure backversioned objects requesting or sending with a resource class string different than the set of strings in the ResourceClass field at the time of this patch raises a ValueError. Change-Id: I2c1d4ae277ba25791c426e1c638dca1b1cb207a4 blueprint: custom-resource-classes
This commit is contained in:
parent
0e854d91ac
commit
5661f879b0
@ -240,18 +240,11 @@ def set_allocations(req):
|
||||
|
||||
resources = allocation['resources']
|
||||
for resource_class in resources:
|
||||
try:
|
||||
allocation = objects.Allocation(
|
||||
resource_provider=resource_provider,
|
||||
consumer_id=consumer_uuid,
|
||||
resource_class=resource_class,
|
||||
used=resources[resource_class])
|
||||
except ValueError as exc:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_("Allocation of class '%(class)s' for "
|
||||
"resource provider '%(rp_uuid)s' invalid: %(error)s") %
|
||||
{'class': resource_class, 'rp_uuid':
|
||||
resource_provider_uuid, 'error': exc})
|
||||
allocation = objects.Allocation(
|
||||
resource_provider=resource_provider,
|
||||
consumer_id=consumer_uuid,
|
||||
resource_class=resource_class,
|
||||
used=resources[resource_class])
|
||||
allocation_objects.append(allocation)
|
||||
|
||||
allocations = objects.AllocationList(context, objects=allocation_objects)
|
||||
@ -262,6 +255,11 @@ def set_allocations(req):
|
||||
# InvalidInventory is a parent for several exceptions that
|
||||
# indicate either that Inventory is not present, or that
|
||||
# capacity limits have been exceeded.
|
||||
except exception.NotFound as exc:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
_("Unable to allocate inventory for resource provider "
|
||||
"%(rp_uuid)s: %(error)s") %
|
||||
{'rp_uuid': resource_provider_uuid, 'error': exc})
|
||||
except exception.InvalidInventory as exc:
|
||||
LOG.exception(_LE("Bad inventory"))
|
||||
raise webob.exc.HTTPConflict(
|
||||
|
@ -215,7 +215,8 @@ def create_inventory(req):
|
||||
raise webob.exc.HTTPConflict(
|
||||
_('Update conflict: %(error)s') % {'error': exc},
|
||||
json_formatter=util.json_error_formatter)
|
||||
except exception.InvalidInventoryCapacity as exc:
|
||||
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,
|
||||
|
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
import sqlalchemy as sa
|
||||
|
||||
from nova.db.sqlalchemy import api as db_api
|
||||
@ -19,6 +20,23 @@ from nova.objects import fields
|
||||
_RC_TBL = models.ResourceClass.__table__
|
||||
|
||||
|
||||
def raise_if_custom_resource_class_pre_v1_1(rc):
|
||||
"""Raises ValueError if the supplied resource class identifier is
|
||||
*not* in the set of standard resource classes as of Inventory/Allocation
|
||||
object version 1.1
|
||||
|
||||
param rc: Integer or string identifier for a resource class
|
||||
"""
|
||||
if isinstance(rc, six.string_types):
|
||||
if rc not in fields.ResourceClass.V1_0:
|
||||
raise ValueError
|
||||
else:
|
||||
try:
|
||||
fields.ResourceClass.V1_0[rc]
|
||||
except IndexError:
|
||||
raise ValueError
|
||||
|
||||
|
||||
@db_api.api_context_manager.reader
|
||||
def _refresh_from_db(ctx, cache):
|
||||
"""Grabs all custom resource classes from the DB table and populates the
|
||||
|
@ -255,7 +255,7 @@ class OSType(BaseNovaEnum):
|
||||
return super(OSType, self).coerce(obj, attr, value)
|
||||
|
||||
|
||||
class ResourceClass(BaseNovaEnum):
|
||||
class ResourceClass(StringField):
|
||||
"""Classes of resources provided to consumers."""
|
||||
|
||||
VCPU = 'VCPU'
|
||||
@ -274,15 +274,11 @@ class ResourceClass(BaseNovaEnum):
|
||||
ALL = (VCPU, MEMORY_MB, DISK_GB, PCI_DEVICE, SRIOV_NET_VF, NUMA_SOCKET,
|
||||
NUMA_CORE, NUMA_THREAD, NUMA_MEMORY_MB, IPV4_ADDRESS)
|
||||
|
||||
@classmethod
|
||||
def index(cls, value):
|
||||
"""Return an index into the Enum given a value."""
|
||||
return cls.ALL.index(value)
|
||||
|
||||
@classmethod
|
||||
def from_index(cls, index):
|
||||
"""Return the Enum value at a given index."""
|
||||
return cls.ALL[index]
|
||||
# This is the set of standard resource classes that existed before
|
||||
# we opened up for custom resource classes in version 1.1 of various
|
||||
# objects in nova/objects/resource_provider.py
|
||||
V1_0 = (VCPU, MEMORY_MB, DISK_GB, PCI_DEVICE, SRIOV_NET_VF, NUMA_SOCKET,
|
||||
NUMA_CORE, NUMA_THREAD, NUMA_MEMORY_MB, IPV4_ADDRESS)
|
||||
|
||||
|
||||
class RNGModel(BaseNovaEnum):
|
||||
@ -822,17 +818,9 @@ class OSTypeField(BaseEnumField):
|
||||
AUTO_TYPE = OSType()
|
||||
|
||||
|
||||
class ResourceClassField(BaseEnumField):
|
||||
class ResourceClassField(AutoTypedField):
|
||||
AUTO_TYPE = ResourceClass()
|
||||
|
||||
def index(self, value):
|
||||
"""Return an index into the Enum given a value."""
|
||||
return self._type.index(value)
|
||||
|
||||
def from_index(self, index):
|
||||
"""Return the Enum value at a given index."""
|
||||
return self._type.from_index(index)
|
||||
|
||||
|
||||
class RNGModelField(BaseEnumField):
|
||||
AUTO_TYPE = RNGModel()
|
||||
|
@ -11,6 +11,7 @@
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import versionutils
|
||||
import six
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import func
|
||||
@ -19,6 +20,7 @@ from sqlalchemy import sql
|
||||
|
||||
from nova.db.sqlalchemy import api as db_api
|
||||
from nova.db.sqlalchemy import api_models as models
|
||||
from nova.db.sqlalchemy import resource_class_cache as rc_cache
|
||||
from nova import exception
|
||||
from nova.i18n import _LW
|
||||
from nova import objects
|
||||
@ -28,10 +30,25 @@ from nova.objects import fields
|
||||
_ALLOC_TBL = models.Allocation.__table__
|
||||
_INV_TBL = models.Inventory.__table__
|
||||
_RP_TBL = models.ResourceProvider.__table__
|
||||
_RC_CACHE = None
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@db_api.api_context_manager.reader
|
||||
def _ensure_rc_cache(ctx):
|
||||
"""Ensures that a singleton resource class cache has been created in the
|
||||
module's scope.
|
||||
|
||||
:param ctx: `nova.context.RequestContext` that may be used to grab a DB
|
||||
connection.
|
||||
"""
|
||||
global _RC_CACHE
|
||||
if _RC_CACHE is not None:
|
||||
return
|
||||
_RC_CACHE = rc_cache.ResourceClassCache(ctx)
|
||||
|
||||
|
||||
def _get_current_inventory_resources(conn, rp):
|
||||
"""Returns a set() containing the resource class IDs for all resources
|
||||
currently having an inventory record for the supplied resource provider.
|
||||
@ -64,8 +81,8 @@ def _delete_inventory_from_provider(conn, rp, to_delete):
|
||||
).group_by(_ALLOC_TBL.c.resource_class_id)
|
||||
allocations = conn.execute(allocation_query).fetchall()
|
||||
if allocations:
|
||||
resource_classes = ', '.join([fields.ResourceClass.from_index(
|
||||
allocation.resource_class) for allocation in allocations])
|
||||
resource_classes = ', '.join([_RC_CACHE.string_from_id(alloc[0])
|
||||
for alloc in allocations])
|
||||
raise exception.InventoryInUse(resource_classes=resource_classes,
|
||||
resource_provider=rp.uuid)
|
||||
|
||||
@ -85,15 +102,16 @@ def _add_inventory_to_provider(conn, rp, inv_list, to_add):
|
||||
:param to_add: set() containing resource class IDs to search inv_list for
|
||||
adding to resource provider.
|
||||
"""
|
||||
for res_class in to_add:
|
||||
inv_record = inv_list.find(res_class)
|
||||
for rc_id in to_add:
|
||||
rc_str = _RC_CACHE.string_from_id(rc_id)
|
||||
inv_record = inv_list.find(rc_str)
|
||||
if inv_record.capacity <= 0:
|
||||
raise exception.InvalidInventoryCapacity(
|
||||
resource_class=fields.ResourceClass.from_index(res_class),
|
||||
resource_class=rc_str,
|
||||
resource_provider=rp.uuid)
|
||||
ins_stmt = _INV_TBL.insert().values(
|
||||
resource_provider_id=rp.id,
|
||||
resource_class_id=res_class,
|
||||
resource_class_id=rc_id,
|
||||
total=inv_record.total,
|
||||
reserved=inv_record.reserved,
|
||||
min_unit=inv_record.min_unit,
|
||||
@ -115,24 +133,24 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update):
|
||||
capacity after this inventory update.
|
||||
"""
|
||||
exceeded = []
|
||||
for res_class in to_update:
|
||||
inv_record = inv_list.find(res_class)
|
||||
for rc_id in to_update:
|
||||
rc_str = _RC_CACHE.string_from_id(rc_id)
|
||||
inv_record = inv_list.find(rc_str)
|
||||
if inv_record.capacity <= 0:
|
||||
raise exception.InvalidInventoryCapacity(
|
||||
resource_class=fields.ResourceClass.from_index(res_class),
|
||||
resource_class=rc_str,
|
||||
resource_provider=rp.uuid)
|
||||
allocation_query = sa.select(
|
||||
[func.sum(_ALLOC_TBL.c.used).label('usage')]).\
|
||||
where(sa.and_(
|
||||
_ALLOC_TBL.c.resource_provider_id == rp.id,
|
||||
_ALLOC_TBL.c.resource_class_id == res_class))
|
||||
_ALLOC_TBL.c.resource_class_id == rc_id))
|
||||
allocations = conn.execute(allocation_query).first()
|
||||
if allocations and allocations['usage'] > inv_record.capacity:
|
||||
exceeded.append((rp.uuid,
|
||||
fields.ResourceClass.from_index(res_class)))
|
||||
exceeded.append((rp.uuid, rc_str))
|
||||
upd_stmt = _INV_TBL.update().where(sa.and_(
|
||||
_INV_TBL.c.resource_provider_id == rp.id,
|
||||
_INV_TBL.c.resource_class_id == res_class)).values(
|
||||
_INV_TBL.c.resource_class_id == rc_id)).values(
|
||||
total=inv_record.total,
|
||||
reserved=inv_record.reserved,
|
||||
min_unit=inv_record.min_unit,
|
||||
@ -142,8 +160,7 @@ def _update_inventory_for_provider(conn, rp, inv_list, to_update):
|
||||
res = conn.execute(upd_stmt)
|
||||
if not res.rowcount:
|
||||
raise exception.NotFound(
|
||||
'No inventory of class %s found for update'
|
||||
% fields.ResourceClass.from_index(res_class))
|
||||
'No inventory of class %s found for update' % rc_str)
|
||||
return exceeded
|
||||
|
||||
|
||||
@ -175,38 +192,47 @@ def _increment_provider_generation(conn, rp):
|
||||
@db_api.api_context_manager.writer
|
||||
def _add_inventory(context, rp, inventory):
|
||||
"""Add one Inventory that wasn't already on the provider."""
|
||||
resource_class_id = fields.ResourceClass.index(inventory.resource_class)
|
||||
_ensure_rc_cache(context)
|
||||
rc_id = _RC_CACHE.id_from_string(inventory.resource_class)
|
||||
if rc_id is None:
|
||||
raise exception.NotFound("No such resource class '%s'" %
|
||||
inventory.resource_class)
|
||||
inv_list = InventoryList(objects=[inventory])
|
||||
conn = context.session.connection()
|
||||
with conn.begin():
|
||||
_add_inventory_to_provider(
|
||||
conn, rp, inv_list, set([resource_class_id]))
|
||||
conn, rp, inv_list, set([rc_id]))
|
||||
rp.generation = _increment_provider_generation(conn, rp)
|
||||
|
||||
|
||||
@db_api.api_context_manager.writer
|
||||
def _update_inventory(context, rp, inventory):
|
||||
"""Update an inventory already on the provider."""
|
||||
resource_class_id = fields.ResourceClass.index(inventory.resource_class)
|
||||
_ensure_rc_cache(context)
|
||||
rc_id = _RC_CACHE.id_from_string(inventory.resource_class)
|
||||
if rc_id is None:
|
||||
raise exception.NotFound("No such resource class '%s'" %
|
||||
inventory.resource_class)
|
||||
inv_list = InventoryList(objects=[inventory])
|
||||
conn = context.session.connection()
|
||||
with conn.begin():
|
||||
exceeded = _update_inventory_for_provider(
|
||||
conn, rp, inv_list, set([resource_class_id]))
|
||||
conn, rp, inv_list, set([rc_id]))
|
||||
rp.generation = _increment_provider_generation(conn, rp)
|
||||
return exceeded
|
||||
|
||||
|
||||
@db_api.api_context_manager.writer
|
||||
def _delete_inventory(context, rp, resource_class_id):
|
||||
"""Delete up to one Inventory of the given resource_class id."""
|
||||
|
||||
def _delete_inventory(context, rp, resource_class):
|
||||
"""Delete up to one Inventory of the given resource_class string."""
|
||||
_ensure_rc_cache(context)
|
||||
conn = context.session.connection()
|
||||
rc_id = _RC_CACHE.id_from_string(resource_class)
|
||||
with conn.begin():
|
||||
if not _delete_inventory_from_provider(conn, rp, [resource_class_id]):
|
||||
if not _delete_inventory_from_provider(conn, rp, [rc_id]):
|
||||
raise exception.NotFound(
|
||||
'No inventory of class %s found for delete'
|
||||
% fields.ResourceClass.from_index(resource_class_id))
|
||||
% resource_class)
|
||||
rp.generation = _increment_provider_generation(conn, rp)
|
||||
|
||||
|
||||
@ -226,11 +252,11 @@ def _set_inventory(context, rp, inv_list):
|
||||
in between the time when this object was originally read
|
||||
and the call to set the inventory.
|
||||
"""
|
||||
|
||||
_ensure_rc_cache(context)
|
||||
conn = context.session.connection()
|
||||
|
||||
existing_resources = _get_current_inventory_resources(conn, rp)
|
||||
these_resources = set([fields.ResourceClass.index(r.resource_class)
|
||||
these_resources = set([_RC_CACHE.id_from_string(r.resource_class)
|
||||
for r in inv_list.objects])
|
||||
|
||||
# Determine which resources we should be adding, deleting and/or
|
||||
@ -323,8 +349,7 @@ class ResourceProvider(base.NovaObject):
|
||||
@base.remotable
|
||||
def delete_inventory(self, resource_class):
|
||||
"""Delete Inventory of provided resource_class."""
|
||||
resource_class_id = fields.ResourceClass.index(resource_class)
|
||||
_delete_inventory(self._context, self, resource_class_id)
|
||||
_delete_inventory(self._context, self, resource_class)
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
@ -449,25 +474,24 @@ class _HasAResourceProvider(base.NovaObject):
|
||||
action='create',
|
||||
reason='resource_provider required')
|
||||
try:
|
||||
resource_class = updates.pop('resource_class')
|
||||
rc_str = updates.pop('resource_class')
|
||||
except KeyError:
|
||||
raise exception.ObjectActionError(
|
||||
action='create',
|
||||
reason='resource_class required')
|
||||
updates['resource_class_id'] = fields.ResourceClass.index(
|
||||
resource_class)
|
||||
updates['resource_class_id'] = _RC_CACHE.id_from_string(rc_str)
|
||||
return updates
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(context, target, source):
|
||||
_ensure_rc_cache(context)
|
||||
for field in target.fields:
|
||||
if field not in ('resource_provider', 'resource_class'):
|
||||
setattr(target, field, source[field])
|
||||
|
||||
if 'resource_class' not in target:
|
||||
target.resource_class = (
|
||||
target.fields['resource_class'].from_index(
|
||||
source['resource_class_id']))
|
||||
rc_str = _RC_CACHE.string_from_id(source['resource_class_id'])
|
||||
target.resource_class = rc_str
|
||||
if ('resource_provider' not in target and
|
||||
'resource_provider' in source):
|
||||
target.resource_provider = ResourceProvider()
|
||||
@ -500,7 +524,8 @@ def _update_inventory_in_db(context, id_, updates):
|
||||
@base.NovaObjectRegistry.register
|
||||
class Inventory(_HasAResourceProvider):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Changed resource_class to allow custom strings
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(read_only=True),
|
||||
@ -514,6 +539,13 @@ class Inventory(_HasAResourceProvider):
|
||||
'allocation_ratio': fields.NonNegativeFloatField(default=1.0),
|
||||
}
|
||||
|
||||
def obj_make_compatible(self, primitive, target_version):
|
||||
super(Inventory, self).obj_make_compatible(primitive, target_version)
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 1) and 'resource_class' in primitive:
|
||||
rc = primitive['resource_class']
|
||||
rc_cache.raise_if_custom_resource_class_pre_v1_1(rc)
|
||||
|
||||
@property
|
||||
def capacity(self):
|
||||
"""Inventory capacity, adjusted by allocation_ratio."""
|
||||
@ -524,6 +556,7 @@ class Inventory(_HasAResourceProvider):
|
||||
if 'id' in self:
|
||||
raise exception.ObjectActionError(action='create',
|
||||
reason='already created')
|
||||
_ensure_rc_cache(self._context)
|
||||
updates = self._make_db(self.obj_get_changes())
|
||||
db_inventory = self._create_in_db(self._context, updates)
|
||||
self._from_db_object(self._context, self, db_inventory)
|
||||
@ -533,6 +566,7 @@ class Inventory(_HasAResourceProvider):
|
||||
if 'id' not in self:
|
||||
raise exception.ObjectActionError(action='save',
|
||||
reason='not created')
|
||||
_ensure_rc_cache(self._context)
|
||||
updates = self.obj_get_changes()
|
||||
updates.pop('id', None)
|
||||
self._update_in_db(self._context, self.id, updates)
|
||||
@ -564,15 +598,11 @@ class InventoryList(base.ObjectListBase, base.NovaObject):
|
||||
looks up the resource class identifier from the
|
||||
string.
|
||||
"""
|
||||
if isinstance(res_class, six.string_types):
|
||||
try:
|
||||
res_class = fields.ResourceClass.index(res_class)
|
||||
except ValueError:
|
||||
raise exception.NotFound("No such resource class '%s'" %
|
||||
res_class)
|
||||
if not isinstance(res_class, six.string_types):
|
||||
raise ValueError
|
||||
|
||||
for inv_rec in self.objects:
|
||||
if fields.ResourceClass.index(inv_rec.resource_class) == res_class:
|
||||
if inv_rec.resource_class == res_class:
|
||||
return inv_rec
|
||||
|
||||
@staticmethod
|
||||
@ -594,7 +624,8 @@ class InventoryList(base.ObjectListBase, base.NovaObject):
|
||||
@base.NovaObjectRegistry.register
|
||||
class Allocation(_HasAResourceProvider):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Changed resource_class to allow custom strings
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
@ -604,6 +635,13 @@ class Allocation(_HasAResourceProvider):
|
||||
'used': fields.IntegerField(),
|
||||
}
|
||||
|
||||
def obj_make_compatible(self, primitive, target_version):
|
||||
super(Allocation, self).obj_make_compatible(primitive, target_version)
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 1) and 'resource_class' in primitive:
|
||||
rc = primitive['resource_class']
|
||||
rc_cache.raise_if_custom_resource_class_pre_v1_1(rc)
|
||||
|
||||
@staticmethod
|
||||
@db_api.api_context_manager.writer
|
||||
def _create_in_db(context, updates):
|
||||
@ -628,6 +666,7 @@ class Allocation(_HasAResourceProvider):
|
||||
if 'id' in self:
|
||||
raise exception.ObjectActionError(action='create',
|
||||
reason='already created')
|
||||
_ensure_rc_cache(self._context)
|
||||
updates = self._make_db(self.obj_get_changes())
|
||||
db_allocation = self._create_in_db(self._context, updates)
|
||||
self._from_db_object(self._context, self, db_allocation)
|
||||
@ -689,7 +728,7 @@ def _check_capacity_exceeded(conn, allocs):
|
||||
#
|
||||
# We then take the results of the above and determine if any of the
|
||||
# inventory will have its capacity exceeded.
|
||||
res_classes = set([fields.ResourceClass.index(a.resource_class)
|
||||
rc_ids = set([_RC_CACHE.id_from_string(a.resource_class)
|
||||
for a in allocs])
|
||||
provider_uuids = set([a.resource_provider.uuid for a in allocs])
|
||||
|
||||
@ -697,14 +736,14 @@ def _check_capacity_exceeded(conn, allocs):
|
||||
_ALLOC_TBL.c.consumer_id,
|
||||
_ALLOC_TBL.c.resource_class_id,
|
||||
sql.func.sum(_ALLOC_TBL.c.used).label('used')])
|
||||
usage = usage.where(_ALLOC_TBL.c.resource_class_id.in_(res_classes))
|
||||
usage = usage.where(_ALLOC_TBL.c.resource_class_id.in_(rc_ids))
|
||||
usage = usage.group_by(_ALLOC_TBL.c.resource_provider_id,
|
||||
_ALLOC_TBL.c.resource_class_id)
|
||||
usage = sa.alias(usage, name='usage')
|
||||
|
||||
inv_join = sql.join(_RP_TBL, _INV_TBL,
|
||||
sql.and_(_RP_TBL.c.id == _INV_TBL.c.resource_provider_id,
|
||||
_INV_TBL.c.resource_class_id.in_(res_classes)))
|
||||
_INV_TBL.c.resource_class_id.in_(rc_ids)))
|
||||
primary_join = sql.outerjoin(inv_join, usage,
|
||||
sql.and_(
|
||||
_INV_TBL.c.resource_provider_id == usage.c.resource_provider_id,
|
||||
@ -724,7 +763,7 @@ def _check_capacity_exceeded(conn, allocs):
|
||||
sel = sa.select(cols_in_output).select_from(primary_join)
|
||||
sel = sel.where(
|
||||
sa.and_(_RP_TBL.c.uuid.in_(provider_uuids),
|
||||
_INV_TBL.c.resource_class_id.in_(res_classes)))
|
||||
_INV_TBL.c.resource_class_id.in_(rc_ids)))
|
||||
records = conn.execute(sel)
|
||||
# Create a map keyed by (rp_uuid, res_class) for the records in the DB
|
||||
usage_map = {}
|
||||
@ -738,17 +777,17 @@ def _check_capacity_exceeded(conn, allocs):
|
||||
# Ensure that all providers have existing inventory
|
||||
missing_provs = provider_uuids - provs_with_inv
|
||||
if missing_provs:
|
||||
class_str = ', '.join([fields.ResourceClass.from_index(res_class)
|
||||
for res_class in res_classes])
|
||||
class_str = ', '.join([_RC_CACHE.string_from_id(rc_id)
|
||||
for rc_id in rc_ids])
|
||||
provider_str = ', '.join(missing_provs)
|
||||
raise exception.InvalidInventory(resource_class=class_str,
|
||||
resource_provider=provider_str)
|
||||
|
||||
res_providers = {}
|
||||
for alloc in allocs:
|
||||
res_class = fields.ResourceClass.index(alloc.resource_class)
|
||||
rc_id = _RC_CACHE.id_from_string(alloc.resource_class)
|
||||
rp_uuid = alloc.resource_provider.uuid
|
||||
key = (rp_uuid, res_class)
|
||||
key = (rp_uuid, rc_id)
|
||||
usage = usage_map[key]
|
||||
amount_needed = alloc.used
|
||||
allocation_ratio = usage['allocation_ratio']
|
||||
@ -759,13 +798,13 @@ def _check_capacity_exceeded(conn, allocs):
|
||||
LOG.warning(
|
||||
_LW("Over capacity for %(rc)s on resource provider %(rp)s. "
|
||||
"Needed: %(needed)s, Used: %(used)s, Capacity: %(cap)s"),
|
||||
{'rc': fields.ResourceClass.from_index(res_class),
|
||||
{'rc': alloc.resource_class,
|
||||
'rp': rp_uuid,
|
||||
'needed': amount_needed,
|
||||
'used': used,
|
||||
'cap': capacity})
|
||||
raise exception.InvalidAllocationCapacityExceeded(
|
||||
resource_class=fields.ResourceClass.from_index(res_class),
|
||||
resource_class=alloc.resource_class,
|
||||
resource_provider=rp_uuid)
|
||||
if rp_uuid not in res_providers:
|
||||
rp = ResourceProvider(id=usage['resource_provider_id'],
|
||||
@ -815,7 +854,16 @@ class AllocationList(base.ObjectListBase, base.NovaObject):
|
||||
We must check that there is capacity for each allocation.
|
||||
If there is not we roll back the entire set.
|
||||
"""
|
||||
_ensure_rc_cache(context)
|
||||
conn = context.session.connection()
|
||||
|
||||
# Short-circuit out if there are any allocations with string
|
||||
# resource class names that don't exist.
|
||||
for alloc in allocs:
|
||||
if _RC_CACHE.id_from_string(alloc.resource_class) is None:
|
||||
raise exception.NotFound("No such resource class '%s'" %
|
||||
alloc.resource_class)
|
||||
|
||||
# Before writing any allocation records, we check that the submitted
|
||||
# allocations do not cause any inventory capacity to be exceeded for
|
||||
# any resource provider and resource class involved in the allocation
|
||||
@ -832,10 +880,10 @@ class AllocationList(base.ObjectListBase, base.NovaObject):
|
||||
# Now add the allocations that were passed in.
|
||||
for alloc in allocs:
|
||||
rp = alloc.resource_provider
|
||||
res_class = fields.ResourceClass.index(alloc.resource_class)
|
||||
rc_id = _RC_CACHE.id_from_string(alloc.resource_class)
|
||||
ins_stmt = _ALLOC_TBL.insert().values(
|
||||
resource_provider_id=rp.id,
|
||||
resource_class_id=res_class,
|
||||
resource_class_id=rc_id,
|
||||
consumer_id=alloc.consumer_id,
|
||||
used=alloc.used)
|
||||
conn.execute(ins_stmt)
|
||||
@ -881,7 +929,8 @@ class AllocationList(base.ObjectListBase, base.NovaObject):
|
||||
@base.NovaObjectRegistry.register
|
||||
class Usage(base.NovaObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Changed resource_class to allow custom strings
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'resource_class': fields.ResourceClassField(read_only=True),
|
||||
@ -895,9 +944,8 @@ class Usage(base.NovaObject):
|
||||
setattr(target, field, source[field])
|
||||
|
||||
if 'resource_class' not in target:
|
||||
target.resource_class = (
|
||||
target.fields['resource_class'].from_index(
|
||||
source['resource_class_id']))
|
||||
rc_str = _RC_CACHE.string_from_id(source['resource_class_id'])
|
||||
target.resource_class = rc_str
|
||||
|
||||
target._context = context
|
||||
target.obj_reset_changes()
|
||||
|
@ -144,7 +144,7 @@ tests:
|
||||
COWS: 12
|
||||
status: 400
|
||||
response_strings:
|
||||
- Field value COWS is invalid
|
||||
- No such resource class 'COWS'
|
||||
|
||||
- name: delete allocation
|
||||
DELETE: /allocations/599ffd2d-526a-4b2e-8683-f13ad25f9958
|
||||
|
@ -163,7 +163,7 @@ tests:
|
||||
total: 2048
|
||||
status: 400
|
||||
response_strings:
|
||||
- Bad inventory NO_CLASS_14 for resource provider $ENVIRON['RP_UUID']
|
||||
- No such resource class 'NO_CLASS_14'
|
||||
|
||||
- name: post inventory duplicated resource class
|
||||
desc: DISK_GB was already created above
|
||||
|
@ -22,9 +22,6 @@ from nova import test
|
||||
from nova.tests import fixtures
|
||||
from nova.tests import uuidsentinel
|
||||
|
||||
RESOURCE_CLASS = fields.ResourceClass.DISK_GB
|
||||
RESOURCE_CLASS_ID = fields.ResourceClass.index(
|
||||
fields.ResourceClass.DISK_GB)
|
||||
DISK_INVENTORY = dict(
|
||||
total=200,
|
||||
reserved=10,
|
||||
@ -32,13 +29,13 @@ DISK_INVENTORY = dict(
|
||||
max_unit=5,
|
||||
step_size=1,
|
||||
allocation_ratio=1.0,
|
||||
resource_class=RESOURCE_CLASS
|
||||
resource_class=fields.ResourceClass.DISK_GB
|
||||
)
|
||||
|
||||
DISK_ALLOCATION = dict(
|
||||
consumer_id=uuidsentinel.disk_consumer,
|
||||
used=2,
|
||||
resource_class=RESOURCE_CLASS
|
||||
resource_class=fields.ResourceClass.DISK_GB
|
||||
)
|
||||
|
||||
|
||||
@ -54,17 +51,18 @@ class ResourceProviderBaseCase(test.NoDBTestCase):
|
||||
|
||||
def _make_allocation(self, rp_uuid=None):
|
||||
rp_uuid = rp_uuid or uuidsentinel.allocation_resource_provider
|
||||
db_rp = objects.ResourceProvider(
|
||||
rp = objects.ResourceProvider(
|
||||
context=self.context,
|
||||
uuid=rp_uuid,
|
||||
name=rp_uuid)
|
||||
db_rp.create()
|
||||
updates = dict(DISK_ALLOCATION,
|
||||
resource_class_id=RESOURCE_CLASS_ID,
|
||||
resource_provider_id=db_rp.id)
|
||||
db_allocation = objects.Allocation._create_in_db(self.context,
|
||||
updates)
|
||||
return db_rp, db_allocation
|
||||
rp.create()
|
||||
alloc = objects.Allocation(
|
||||
self.context,
|
||||
resource_provider=rp,
|
||||
**DISK_ALLOCATION
|
||||
)
|
||||
alloc.create()
|
||||
return rp, alloc
|
||||
|
||||
|
||||
class ResourceProviderTestCase(ResourceProviderBaseCase):
|
||||
@ -566,7 +564,7 @@ class TestAllocation(ResourceProviderBaseCase):
|
||||
self.context, rp.uuid)
|
||||
self.assertEqual(1, len(allocations))
|
||||
self.assertEqual(rp.id, allocations[0].resource_provider_id)
|
||||
self.assertEqual(allocation.resource_provider_id,
|
||||
self.assertEqual(allocation.resource_provider.id,
|
||||
allocations[0].resource_provider_id)
|
||||
|
||||
allocations = objects.AllocationList._get_allocations_from_db(
|
||||
@ -579,7 +577,7 @@ class TestAllocation(ResourceProviderBaseCase):
|
||||
self.context, rp.uuid)
|
||||
self.assertEqual(1, len(allocations))
|
||||
self.assertEqual(rp.id, allocations[0].resource_provider.id)
|
||||
self.assertEqual(allocation.resource_provider_id,
|
||||
self.assertEqual(allocation.resource_provider.id,
|
||||
allocations[0].resource_provider.id)
|
||||
|
||||
def test_get_all_multiple_providers(self):
|
||||
@ -591,28 +589,33 @@ class TestAllocation(ResourceProviderBaseCase):
|
||||
self.context, rp1.uuid)
|
||||
self.assertEqual(1, len(allocations))
|
||||
self.assertEqual(rp1.id, allocations[0].resource_provider.id)
|
||||
self.assertEqual(allocation1.resource_provider_id,
|
||||
self.assertEqual(allocation1.resource_provider.id,
|
||||
allocations[0].resource_provider.id)
|
||||
|
||||
# add more allocations for the first resource provider
|
||||
# of the same class
|
||||
updates = dict(consumer_id=uuidsentinel.consumer1,
|
||||
resource_class_id=RESOURCE_CLASS_ID,
|
||||
resource_provider_id=rp1.id,
|
||||
used=2)
|
||||
objects.Allocation._create_in_db(self.context, updates)
|
||||
alloc3 = objects.Allocation(
|
||||
self.context,
|
||||
consumer_id=uuidsentinel.consumer1,
|
||||
resource_class=fields.ResourceClass.DISK_GB,
|
||||
resource_provider=rp1,
|
||||
used=2,
|
||||
)
|
||||
alloc3.create()
|
||||
allocations = objects.AllocationList.get_all_by_resource_provider_uuid(
|
||||
self.context, rp1.uuid)
|
||||
self.assertEqual(2, len(allocations))
|
||||
|
||||
# add more allocations for the first resource provider
|
||||
# of a different class
|
||||
updates = dict(consumer_id=uuidsentinel.consumer1,
|
||||
resource_class_id=fields.ResourceClass.index(
|
||||
fields.ResourceClass.IPV4_ADDRESS),
|
||||
resource_provider_id=rp1.id,
|
||||
used=4)
|
||||
objects.Allocation._create_in_db(self.context, updates)
|
||||
alloc4 = objects.Allocation(
|
||||
self.context,
|
||||
consumer_id=uuidsentinel.consumer1,
|
||||
resource_class=fields.ResourceClass.IPV4_ADDRESS,
|
||||
resource_provider=rp1,
|
||||
used=4,
|
||||
)
|
||||
alloc4.create()
|
||||
allocations = objects.AllocationList.get_all_by_resource_provider_uuid(
|
||||
self.context, rp1.uuid)
|
||||
self.assertEqual(3, len(allocations))
|
||||
@ -622,7 +625,7 @@ class TestAllocation(ResourceProviderBaseCase):
|
||||
self.context, rp2.uuid)
|
||||
self.assertEqual(1, len(allocations))
|
||||
self.assertEqual(rp2.uuid, allocations[0].resource_provider.uuid)
|
||||
self.assertIn(RESOURCE_CLASS,
|
||||
self.assertIn(fields.ResourceClass.DISK_GB,
|
||||
[allocation.resource_class
|
||||
for allocation in allocations])
|
||||
self.assertNotIn(fields.ResourceClass.IPV4_ADDRESS,
|
||||
|
@ -196,7 +196,7 @@ class TestImageSignatureTypes(TestField):
|
||||
self.assertIn(key_type, self.key_type_field.ALL)
|
||||
|
||||
|
||||
class TestResourceClass(TestField):
|
||||
class TestResourceClass(TestString):
|
||||
def setUp(self):
|
||||
super(TestResourceClass, self).setUp()
|
||||
self.field = fields.ResourceClassField()
|
||||
@ -212,43 +212,10 @@ class TestResourceClass(TestField):
|
||||
('NUMA_MEMORY_MB', 'NUMA_MEMORY_MB'),
|
||||
('IPV4_ADDRESS', 'IPV4_ADDRESS'),
|
||||
]
|
||||
self.expected_indexes = [
|
||||
('VCPU', 0),
|
||||
('MEMORY_MB', 1),
|
||||
('DISK_GB', 2),
|
||||
('PCI_DEVICE', 3),
|
||||
('SRIOV_NET_VF', 4),
|
||||
('NUMA_SOCKET', 5),
|
||||
('NUMA_CORE', 6),
|
||||
('NUMA_THREAD', 7),
|
||||
('NUMA_MEMORY_MB', 8),
|
||||
('IPV4_ADDRESS', 9),
|
||||
]
|
||||
self.coerce_bad_values = ['acme']
|
||||
self.coerce_bad_values = [object(), dict()]
|
||||
self.to_primitive_values = self.coerce_good_values[0:1]
|
||||
self.from_primitive_values = self.coerce_good_values[0:1]
|
||||
|
||||
def test_stringify(self):
|
||||
self.assertEqual("'VCPU'", self.field.stringify(
|
||||
fields.ResourceClass.VCPU))
|
||||
|
||||
def test_stringify_invalid(self):
|
||||
self.assertRaises(ValueError, self.field.stringify, 'cow')
|
||||
|
||||
def test_index(self):
|
||||
for name, index in self.expected_indexes:
|
||||
self.assertEqual(index, self.field.index(name))
|
||||
|
||||
def test_index_invalid(self):
|
||||
self.assertRaises(ValueError, self.field.index, 'cow')
|
||||
|
||||
def test_from_index(self):
|
||||
for name, index in self.expected_indexes:
|
||||
self.assertEqual(name, self.field.from_index(index))
|
||||
|
||||
def test_from_index_invalid(self):
|
||||
self.assertRaises(IndexError, self.field.from_index, 999)
|
||||
|
||||
|
||||
class TestInteger(TestField):
|
||||
def setUp(self):
|
||||
|
@ -1099,7 +1099,7 @@ object_data = {
|
||||
'AgentList': '1.0-5a7380d02c3aaf2a32fc8115ae7ca98c',
|
||||
'Aggregate': '1.3-f315cb68906307ca2d1cca84d4753585',
|
||||
'AggregateList': '1.2-fb6e19f3c3a3186b04eceb98b5dadbfa',
|
||||
'Allocation': '1.0-864506325f1822f4e4805b56faf51bbe',
|
||||
'Allocation': '1.1-27814e4c0cf1fd5ffaa3bcbc8f9734e5',
|
||||
'AllocationList': '1.1-e43fe4a9c9cbbda7438b0e48332f099e',
|
||||
'BandwidthUsage': '1.2-c6e4c779c7f40f2407e3d70022e3cd1c',
|
||||
'BandwidthUsageList': '1.2-5fe7475ada6fe62413cbfcc06ec70746',
|
||||
@ -1152,7 +1152,7 @@ object_data = {
|
||||
'InstanceNUMATopology': '1.2-d944a7d6c21e1c773ffdf09c6d025954',
|
||||
'InstancePCIRequest': '1.1-b1d75ebc716cb12906d9d513890092bf',
|
||||
'InstancePCIRequests': '1.1-65e38083177726d806684cb1cc0136d2',
|
||||
'Inventory': '1.0-84131c00c84a27ee6930d01b329c9a9d',
|
||||
'Inventory': '1.1-a2f8c7214829d623c2a7a32714d84eba',
|
||||
'InventoryList': '1.0-de53f0fd078c27cc1d43400f4e8bcef8',
|
||||
'LibvirtLiveMigrateBDMInfo': '1.0-252aabb723ca79d5469fa56f64b57811',
|
||||
'LibvirtLiveMigrateData': '1.3-2795e5646ee21e8c7f1c3e64fb6c80a3',
|
||||
@ -1196,7 +1196,7 @@ object_data = {
|
||||
'TaskLogList': '1.0-cc8cce1af8a283b9d28b55fcd682e777',
|
||||
'Tag': '1.1-8b8d7d5b48887651a0e01241672e2963',
|
||||
'TagList': '1.1-55231bdb671ecf7641d6a2e9109b5d8e',
|
||||
'Usage': '1.0-b78f18c3577a38e7a033e46a9725b09b',
|
||||
'Usage': '1.1-b738dbebeb20e3199fc0ebca6e292a47',
|
||||
'UsageList': '1.0-de53f0fd078c27cc1d43400f4e8bcef8',
|
||||
'USBDeviceBus': '1.0-e4c7dd6032e46cd74b027df5eb2d4750',
|
||||
'VirtCPUFeature': '1.0-3310718d8c72309259a6e39bdefe83ee',
|
||||
|
@ -23,6 +23,11 @@ from nova.tests import uuidsentinel as uuids
|
||||
|
||||
_RESOURCE_CLASS_NAME = 'DISK_GB'
|
||||
_RESOURCE_CLASS_ID = 2
|
||||
IPV4_ADDRESS_ID = objects.fields.ResourceClass.ALL.index(
|
||||
fields.ResourceClass.IPV4_ADDRESS)
|
||||
VCPU_ID = objects.fields.ResourceClass.ALL.index(
|
||||
fields.ResourceClass.VCPU)
|
||||
|
||||
_RESOURCE_PROVIDER_ID = 1
|
||||
_RESOURCE_PROVIDER_UUID = uuids.resource_provider
|
||||
_RESOURCE_PROVIDER_NAME = uuids.resource_name
|
||||
@ -312,9 +317,6 @@ class TestInventory(test_objects._LocalTest):
|
||||
{'total': 32})
|
||||
|
||||
# Create IPV4_ADDRESS resources for each provider.
|
||||
IPV4_ADDRESS_ID = objects.fields.ResourceClass.index(
|
||||
objects.fields.ResourceClass.IPV4_ADDRESS)
|
||||
|
||||
self._create_inventory_in_db(db_rp1.id,
|
||||
resource_provider_id=db_rp1.id,
|
||||
resource_class_id=IPV4_ADDRESS_ID,
|
||||
@ -430,16 +432,32 @@ class TestInventory(test_objects._LocalTest):
|
||||
self.assertIsNone(found)
|
||||
|
||||
# Try an integer resource class identifier...
|
||||
found = inv_list.find(fields.ResourceClass.index(
|
||||
fields.ResourceClass.VCPU))
|
||||
self.assertIsNotNone(found)
|
||||
self.assertEqual(24, found.total)
|
||||
self.assertRaises(ValueError, inv_list.find, VCPU_ID)
|
||||
|
||||
# Use an invalid string...
|
||||
error = self.assertRaises(exception.NotFound,
|
||||
inv_list.find,
|
||||
'HOUSE')
|
||||
self.assertIn('No such resource class', str(error))
|
||||
self.assertIsNone(inv_list.find('HOUSE'))
|
||||
|
||||
def test_custom_resource_raises(self):
|
||||
"""Ensure that if we send an inventory object to a backversioned 1.0
|
||||
receiver, that we raise ValueError if the inventory record contains a
|
||||
custom (non-standardized) resource class.
|
||||
"""
|
||||
values = {
|
||||
# NOTE(danms): We don't include an actual resource provider
|
||||
# here because chained backporting of that is handled by
|
||||
# the infrastructure and requires us to have a manifest
|
||||
'resource_class': 'custom_resource',
|
||||
'total': 1,
|
||||
'reserved': 0,
|
||||
'min_unit': 1,
|
||||
'max_unit': 1,
|
||||
'step_size': 1,
|
||||
'allocation_ratio': 1.0,
|
||||
}
|
||||
bdm = objects.Inventory(context=self.context, **values)
|
||||
self.assertRaises(ValueError,
|
||||
bdm.obj_to_primitive,
|
||||
target_version='1.0')
|
||||
|
||||
|
||||
class _TestAllocationNoDB(object):
|
||||
@ -470,6 +488,24 @@ class _TestAllocationNoDB(object):
|
||||
used=8)
|
||||
self.assertRaises(exception.ObjectActionError, obj.create)
|
||||
|
||||
def test_custom_resource_raises(self):
|
||||
"""Ensure that if we send an inventory object to a backversioned 1.0
|
||||
receiver, that we raise ValueError if the inventory record contains a
|
||||
custom (non-standardized) resource class.
|
||||
"""
|
||||
values = {
|
||||
# NOTE(danms): We don't include an actual resource provider
|
||||
# here because chained backporting of that is handled by
|
||||
# the infrastructure and requires us to have a manifest
|
||||
'resource_class': 'custom_resource',
|
||||
'consumer_id': uuids.consumer_id,
|
||||
'used': 1,
|
||||
}
|
||||
bdm = objects.Allocation(context=self.context, **values)
|
||||
self.assertRaises(ValueError,
|
||||
bdm.obj_to_primitive,
|
||||
target_version='1.0')
|
||||
|
||||
|
||||
class TestAllocationNoDB(test_objects._LocalTest,
|
||||
_TestAllocationNoDB):
|
||||
|
Loading…
Reference in New Issue
Block a user