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:
Jay Pipes 2016-09-29 14:32:08 -04:00
parent 0e854d91ac
commit 5661f879b0
11 changed files with 229 additions and 170 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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