Move ResourceClass and ResourceClassList to own module

This is a precursor to getting rid of the ResourceClassList
class in favor of operating on native python lists.

With the move, also create new unit and functional test files
for the new module, and move resource class syncing into the new
module.

The ensure_resource_classes_sync is renamed to ensure_sync since
its membership in the resource_class module gives sufficient
namespacing.

Change-Id: I95c383e22f11fce2eb68ef29106fe045315644b6
This commit is contained in:
Chris Dent 2019-03-06 15:32:38 +00:00
parent 849c89d0e5
commit 7964cd9cbc
12 changed files with 581 additions and 512 deletions

View File

@ -21,6 +21,7 @@ from placement import db_api
from placement import fault_wrap
from placement import handler
from placement import microversion
from placement.objects import resource_class
from placement.objects import resource_provider
from placement import requestlog
from placement import resource_class_cache as rc_cache
@ -101,7 +102,7 @@ def update_database(conf):
migration.upgrade('head')
ctx = db_api.DbContext()
resource_provider.ensure_trait_sync(ctx)
resource_provider.ensure_resource_classes_sync(ctx)
resource_class.ensure_sync(ctx)
rc_cache.ensure_rc_cache(ctx)

View File

@ -19,7 +19,7 @@ import webob
from placement import exception
from placement.i18n import _
from placement import microversion
from placement.objects import resource_provider as rp_obj
from placement.objects import resource_class as rc_obj
from placement.policies import resource_class as policies
from placement.schemas import resource_class as schema
from placement import util
@ -67,7 +67,7 @@ def create_resource_class(req):
data = util.extract_json(req.body, schema.POST_RC_SCHEMA_V1_2)
try:
rc = rp_obj.ResourceClass(context, name=data['name'])
rc = rc_obj.ResourceClass(context, name=data['name'])
rc.create()
except exception.ResourceClassExists:
raise webob.exc.HTTPConflict(
@ -97,7 +97,7 @@ def delete_resource_class(req):
context = req.environ['placement.context']
context.can(policies.DELETE)
# The containing application will catch a not found here.
rc = rp_obj.ResourceClass.get_by_name(context, name)
rc = rc_obj.ResourceClass.get_by_name(context, name)
try:
rc.destroy()
except exception.ResourceClassCannotDeleteStandard as exc:
@ -125,7 +125,7 @@ def get_resource_class(req):
context.can(policies.SHOW)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
# The containing application will catch a not found here.
rc = rp_obj.ResourceClass.get_by_name(context, name)
rc = rc_obj.ResourceClass.get_by_name(context, name)
req.response.body = encodeutils.to_utf8(jsonutils.dumps(
_serialize_resource_class(req.environ, rc))
@ -153,7 +153,7 @@ def list_resource_classes(req):
context = req.environ['placement.context']
context.can(policies.LIST)
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
rcs = rp_obj.ResourceClassList.get_all(context)
rcs = rc_obj.ResourceClassList.get_all(context)
response = req.response
output, last_modified = _serialize_resource_classes(
@ -182,7 +182,7 @@ def update_resource_class(req):
data = util.extract_json(req.body, schema.PUT_RC_SCHEMA_V1_2)
# The containing application will catch a not found here.
rc = rp_obj.ResourceClass.get_by_name(context, name)
rc = rc_obj.ResourceClass.get_by_name(context, name)
rc.name = data['name']
@ -223,10 +223,10 @@ def update_resource_class(req):
status = 204
try:
rc = rp_obj.ResourceClass.get_by_name(context, name)
rc = rc_obj.ResourceClass.get_by_name(context, name)
except exception.NotFound:
try:
rc = rp_obj.ResourceClass(context, name=name)
rc = rc_obj.ResourceClass(context, name=name)
rc.create()
status = 201
# We will not see ResourceClassCannotUpdateStandard because

View File

@ -0,0 +1,259 @@
# 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.
import os_resource_classes as orc
from oslo_concurrency import lockutils
from oslo_db import api as oslo_db_api
from oslo_db import exception as db_exc
from oslo_log import log as logging
import six
import sqlalchemy as sa
from sqlalchemy import func
from placement.db.sqlalchemy import models
from placement import db_api
from placement import exception
from placement.i18n import _
from placement.objects import common as common_obj
from placement import resource_class_cache as rc_cache
_RC_TBL = models.ResourceClass.__table__
_RESOURCE_CLASSES_LOCK = 'resource_classes_sync'
_RESOURCE_CLASSES_SYNCED = False
LOG = logging.getLogger(__name__)
class ResourceClass(object):
MIN_CUSTOM_RESOURCE_CLASS_ID = 10000
"""Any user-defined resource classes must have an identifier greater than
or equal to this number.
"""
# Retry count for handling possible race condition in creating resource
# class. We don't ever want to hit this, as it is simply a race when
# creating these classes, but this is just a stopgap to prevent a potential
# infinite loop.
RESOURCE_CREATE_RETRY_COUNT = 100
def __init__(self, context, id=None, name=None, updated_at=None,
created_at=None):
self._context = context
self.id = id
self.name = name
self.updated_at = updated_at
self.created_at = created_at
@staticmethod
def _from_db_object(context, target, source):
target._context = context
target.id = source['id']
target.name = source['name']
target.updated_at = source['updated_at']
target.created_at = source['created_at']
return target
@classmethod
def get_by_name(cls, context, name):
"""Return a ResourceClass object with the given string name.
:param name: String name of the resource class to find
:raises: ResourceClassNotFound if no such resource class was found
"""
rc = rc_cache.RC_CACHE.all_from_string(name)
obj = cls(context, id=rc['id'], name=rc['name'],
updated_at=rc['updated_at'], created_at=rc['created_at'])
return obj
@staticmethod
@db_api.placement_context_manager.reader
def _get_next_id(context):
"""Utility method to grab the next resource class identifier to use for
user-defined resource classes.
"""
query = context.session.query(func.max(models.ResourceClass.id))
max_id = query.one()[0]
if not max_id or max_id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
return ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
else:
return max_id + 1
def create(self):
if self.id is not None:
raise exception.ObjectActionError(action='create',
reason='already created')
if not self.name:
raise exception.ObjectActionError(action='create',
reason='name is required')
if self.name in orc.STANDARDS:
raise exception.ResourceClassExists(resource_class=self.name)
if not self.name.startswith(orc.CUSTOM_NAMESPACE):
raise exception.ObjectActionError(
action='create',
reason='name must start with ' + orc.CUSTOM_NAMESPACE)
updates = {}
for field in ['name', 'updated_at', 'created_at']:
value = getattr(self, field, None)
if value:
updates[field] = value
# There is the possibility of a race when adding resource classes, as
# the ID is generated locally. This loop catches that exception, and
# retries until either it succeeds, or a different exception is
# encountered.
retries = self.RESOURCE_CREATE_RETRY_COUNT
while retries:
retries -= 1
try:
rc = self._create_in_db(self._context, updates)
self._from_db_object(self._context, self, rc)
break
except db_exc.DBDuplicateEntry as e:
if 'id' in e.columns:
# Race condition for ID creation; try again
continue
# The duplication is on the other unique column, 'name'. So do
# not retry; raise the exception immediately.
raise exception.ResourceClassExists(resource_class=self.name)
else:
# We have no idea how common it will be in practice for the retry
# limit to be exceeded. We set it high in the hope that we never
# hit this point, but added this log message so we know that this
# specific situation occurred.
LOG.warning("Exceeded retry limit on ID generation while "
"creating ResourceClass %(name)s",
{'name': self.name})
msg = _("creating resource class %s") % self.name
raise exception.MaxDBRetriesExceeded(action=msg)
@staticmethod
@db_api.placement_context_manager.writer
def _create_in_db(context, updates):
next_id = ResourceClass._get_next_id(context)
rc = models.ResourceClass()
rc.update(updates)
rc.id = next_id
context.session.add(rc)
return rc
def destroy(self):
if self.id is None:
raise exception.ObjectActionError(action='destroy',
reason='ID attribute not found')
# Never delete any standard resource class.
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
raise exception.ResourceClassCannotDeleteStandard(
resource_class=self.name)
self._destroy(self._context, self.id, self.name)
rc_cache.RC_CACHE.clear()
@staticmethod
@db_api.placement_context_manager.writer
def _destroy(context, _id, name):
# Don't delete the resource class if it is referred to in the
# inventories table.
num_inv = context.session.query(models.Inventory).filter(
models.Inventory.resource_class_id == _id).count()
if num_inv:
raise exception.ResourceClassInUse(resource_class=name)
res = context.session.query(models.ResourceClass).filter(
models.ResourceClass.id == _id).delete()
if not res:
raise exception.NotFound()
def save(self):
if self.id is None:
raise exception.ObjectActionError(action='save',
reason='ID attribute not found')
updates = {}
for field in ['name', 'updated_at', 'created_at']:
value = getattr(self, field, None)
if value:
updates[field] = value
# Never update any standard resource class.
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
raise exception.ResourceClassCannotUpdateStandard(
resource_class=self.name)
self._save(self._context, self.id, self.name, updates)
rc_cache.RC_CACHE.clear()
@staticmethod
@db_api.placement_context_manager.writer
def _save(context, id, name, updates):
db_rc = context.session.query(models.ResourceClass).filter_by(
id=id).first()
db_rc.update(updates)
try:
db_rc.save(context.session)
except db_exc.DBDuplicateEntry:
raise exception.ResourceClassExists(resource_class=name)
class ResourceClassList(common_obj.ObjectList):
ITEM_CLS = ResourceClass
@staticmethod
@db_api.placement_context_manager.reader
def _get_all(context):
return list(context.session.query(models.ResourceClass).all())
@classmethod
def get_all(cls, context):
resource_classes = cls._get_all(context)
return cls._set_objects(context, resource_classes)
def ensure_sync(ctx):
global _RESOURCE_CLASSES_SYNCED
# If another thread is doing this work, wait for it to complete.
# When that thread is done _RESOURCE_CLASSES_SYNCED will be true in this
# thread and we'll simply return.
with lockutils.lock(_RESOURCE_CLASSES_LOCK):
if not _RESOURCE_CLASSES_SYNCED:
_resource_classes_sync(ctx)
_RESOURCE_CLASSES_SYNCED = True
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@db_api.placement_context_manager.writer
def _resource_classes_sync(ctx):
# Create a set of all resource class in the os_resource_classes library.
sel = sa.select([_RC_TBL.c.name])
res = ctx.session.execute(sel).fetchall()
db_classes = [r[0] for r in res if not orc.is_custom(r[0])]
LOG.debug("Found existing resource classes in db: %s", db_classes)
# Determine those resource clases which are in os_resource_classes but not
# currently in the database, and insert them.
batch_args = [{'name': six.text_type(name), 'id': index}
for index, name in enumerate(orc.STANDARDS)
if name not in db_classes]
ins = _RC_TBL.insert()
if batch_args:
conn = ctx.session.connection()
if conn.engine.dialect.name == 'mysql':
# We need to do a literal insert of 0 to preserve the order
# of the resource class ids from the previous style of
# managing them. In some mysql settings a 0 is the same as
# "give me a default key".
conn.execute("SET SESSION SQL_MODE='NO_AUTO_VALUE_ON_ZERO'")
try:
ctx.session.execute(ins, batch_args)
LOG.debug("Synced resource_classes from os_resource_classes: %s",
batch_args)
except db_exc.DBDuplicateEntry:
pass # some other process sync'd, just ignore

View File

@ -21,7 +21,6 @@ import random
# not be registered and there is no need to express VERSIONs nor handle
# obj_make_compatible.
import os_resource_classes as orc
import os_traits
from oslo_concurrency import lockutils
from oslo_db import api as oslo_db_api
@ -47,13 +46,9 @@ _TRAIT_TBL = models.Trait.__table__
_ALLOC_TBL = models.Allocation.__table__
_INV_TBL = models.Inventory.__table__
_RP_TBL = models.ResourceProvider.__table__
# Not used in this file but used in tests.
_RC_TBL = models.ResourceClass.__table__
_AGG_TBL = models.PlacementAggregate.__table__
_RP_AGG_TBL = models.ResourceProviderAggregate.__table__
_RP_TRAIT_TBL = models.ResourceProviderTrait.__table__
_RESOURCE_CLASSES_LOCK = 'resource_classes_sync'
_RESOURCE_CLASSES_SYNCED = False
_TRAIT_LOCK = 'trait_sync'
_TRAITS_SYNCED = False
@ -126,47 +121,6 @@ def ensure_trait_sync(ctx):
_TRAITS_SYNCED = True
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@db_api.placement_context_manager.writer
def _resource_classes_sync(ctx):
# Create a set of all resource class in the os_resource_classes library.
sel = sa.select([_RC_TBL.c.name])
res = ctx.session.execute(sel).fetchall()
db_classes = [r[0] for r in res if not orc.is_custom(r[0])]
LOG.debug("Found existing resource classes in db: %s", db_classes)
# Determine those resource clases which are in os_resource_classes but not
# currently in the database, and insert them.
batch_args = [{'name': six.text_type(name), 'id': index}
for index, name in enumerate(orc.STANDARDS)
if name not in db_classes]
ins = _RC_TBL.insert()
if batch_args:
conn = ctx.session.connection()
if conn.engine.dialect.name == 'mysql':
# We need to do a literal insert of 0 to preserve the order
# of the resource class ids from the previous style of
# managing them. In some mysql settings a 0 is the same as
# "give me a default key".
conn.execute("SET SESSION SQL_MODE='NO_AUTO_VALUE_ON_ZERO'")
try:
ctx.session.execute(ins, batch_args)
LOG.debug("Synced resource_classes from os_resource_classes: %s",
batch_args)
except db_exc.DBDuplicateEntry:
pass # some other process sync'd, just ignore
def ensure_resource_classes_sync(ctx):
global _RESOURCE_CLASSES_SYNCED
# If another thread is doing this work, wait for it to complete.
# When that thread is done _RESOURCE_CLASSES_SYNCED will be true in this
# thread and we'll simply return.
with lockutils.lock(_RESOURCE_CLASSES_LOCK):
if not _RESOURCE_CLASSES_SYNCED:
_resource_classes_sync(ctx)
_RESOURCE_CLASSES_SYNCED = True
def _usage_select(rc_ids):
usage = sa.select([_ALLOC_TBL.c.resource_provider_id,
_ALLOC_TBL.c.resource_class_id,
@ -1700,190 +1654,6 @@ class InventoryList(common_obj.ObjectList):
return inv_list
class ResourceClass(object):
MIN_CUSTOM_RESOURCE_CLASS_ID = 10000
"""Any user-defined resource classes must have an identifier greater than
or equal to this number.
"""
# Retry count for handling possible race condition in creating resource
# class. We don't ever want to hit this, as it is simply a race when
# creating these classes, but this is just a stopgap to prevent a potential
# infinite loop.
RESOURCE_CREATE_RETRY_COUNT = 100
def __init__(self, context, id=None, name=None, updated_at=None,
created_at=None):
self._context = context
self.id = id
self.name = name
self.updated_at = updated_at
self.created_at = created_at
@staticmethod
def _from_db_object(context, target, source):
target._context = context
target.id = source['id']
target.name = source['name']
target.updated_at = source['updated_at']
target.created_at = source['created_at']
return target
@classmethod
def get_by_name(cls, context, name):
"""Return a ResourceClass object with the given string name.
:param name: String name of the resource class to find
:raises: ResourceClassNotFound if no such resource class was found
"""
rc = rc_cache.RC_CACHE.all_from_string(name)
obj = cls(context, id=rc['id'], name=rc['name'],
updated_at=rc['updated_at'], created_at=rc['created_at'])
return obj
@staticmethod
@db_api.placement_context_manager.reader
def _get_next_id(context):
"""Utility method to grab the next resource class identifier to use for
user-defined resource classes.
"""
query = context.session.query(func.max(models.ResourceClass.id))
max_id = query.one()[0]
if not max_id or max_id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
return ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
else:
return max_id + 1
def create(self):
if self.id is not None:
raise exception.ObjectActionError(action='create',
reason='already created')
if not self.name:
raise exception.ObjectActionError(action='create',
reason='name is required')
if self.name in orc.STANDARDS:
raise exception.ResourceClassExists(resource_class=self.name)
if not self.name.startswith(orc.CUSTOM_NAMESPACE):
raise exception.ObjectActionError(
action='create',
reason='name must start with ' + orc.CUSTOM_NAMESPACE)
updates = {}
for field in ['name', 'updated_at', 'created_at']:
value = getattr(self, field, None)
if value:
updates[field] = value
# There is the possibility of a race when adding resource classes, as
# the ID is generated locally. This loop catches that exception, and
# retries until either it succeeds, or a different exception is
# encountered.
retries = self.RESOURCE_CREATE_RETRY_COUNT
while retries:
retries -= 1
try:
rc = self._create_in_db(self._context, updates)
self._from_db_object(self._context, self, rc)
break
except db_exc.DBDuplicateEntry as e:
if 'id' in e.columns:
# Race condition for ID creation; try again
continue
# The duplication is on the other unique column, 'name'. So do
# not retry; raise the exception immediately.
raise exception.ResourceClassExists(resource_class=self.name)
else:
# We have no idea how common it will be in practice for the retry
# limit to be exceeded. We set it high in the hope that we never
# hit this point, but added this log message so we know that this
# specific situation occurred.
LOG.warning("Exceeded retry limit on ID generation while "
"creating ResourceClass %(name)s",
{'name': self.name})
msg = _("creating resource class %s") % self.name
raise exception.MaxDBRetriesExceeded(action=msg)
@staticmethod
@db_api.placement_context_manager.writer
def _create_in_db(context, updates):
next_id = ResourceClass._get_next_id(context)
rc = models.ResourceClass()
rc.update(updates)
rc.id = next_id
context.session.add(rc)
return rc
def destroy(self):
if self.id is None:
raise exception.ObjectActionError(action='destroy',
reason='ID attribute not found')
# Never delete any standard resource class.
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
raise exception.ResourceClassCannotDeleteStandard(
resource_class=self.name)
self._destroy(self._context, self.id, self.name)
rc_cache.RC_CACHE.clear()
@staticmethod
@db_api.placement_context_manager.writer
def _destroy(context, _id, name):
# Don't delete the resource class if it is referred to in the
# inventories table.
num_inv = context.session.query(models.Inventory).filter(
models.Inventory.resource_class_id == _id).count()
if num_inv:
raise exception.ResourceClassInUse(resource_class=name)
res = context.session.query(models.ResourceClass).filter(
models.ResourceClass.id == _id).delete()
if not res:
raise exception.NotFound()
def save(self):
if self.id is None:
raise exception.ObjectActionError(action='save',
reason='ID attribute not found')
updates = {}
for field in ['name', 'updated_at', 'created_at']:
value = getattr(self, field, None)
if value:
updates[field] = value
# Never update any standard resource class.
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
raise exception.ResourceClassCannotUpdateStandard(
resource_class=self.name)
self._save(self._context, self.id, self.name, updates)
rc_cache.RC_CACHE.clear()
@staticmethod
@db_api.placement_context_manager.writer
def _save(context, id, name, updates):
db_rc = context.session.query(models.ResourceClass).filter_by(
id=id).first()
db_rc.update(updates)
try:
db_rc.save(context.session)
except db_exc.DBDuplicateEntry:
raise exception.ResourceClassExists(resource_class=name)
class ResourceClassList(common_obj.ObjectList):
ITEM_CLS = ResourceClass
@staticmethod
@db_api.placement_context_manager.reader
def _get_all(context):
return list(context.session.query(models.ResourceClass).all())
@classmethod
def get_all(cls, context):
resource_classes = cls._get_all(context)
return cls._set_objects(context, resource_classes)
class Trait(object):
# All the user-defined traits must begin with this prefix.

View File

@ -24,6 +24,7 @@ from oslo_db.sqlalchemy import test_fixtures
from placement.db.sqlalchemy import migration
from placement import db_api as placement_db
from placement import deploy
from placement.objects import resource_class
from placement.objects import resource_provider
from placement import resource_class_cache as rc_cache
@ -75,5 +76,5 @@ class Database(test_fixtures.GeneratesSchema, test_fixtures.AdHocDbFixture):
def cleanup(self):
resource_provider._TRAITS_SYNCED = False
resource_provider._RESOURCE_CLASSES_SYNCED = False
resource_class._RESOURCE_CLASSES_SYNCED = False
rc_cache.RC_CACHE = None

View File

@ -17,6 +17,7 @@ import sqlalchemy as sa
from placement import exception
from placement import lib as placement_lib
from placement.objects import resource_class as rc_obj
from placement.objects import resource_provider as rp_obj
from placement.tests.functional.db import test_base as tb
@ -132,7 +133,7 @@ class ProviderDBHelperTestCase(tb.PlacementDbBaseTestCase):
tb.add_inventory(excl_extra_avail, orc.DISK_GB, 2000,
allocation_ratio=0.5)
tb.add_inventory(excl_extra_avail, orc.IPV4_ADDRESS, 48)
custom_special = rp_obj.ResourceClass(self.ctx, name='CUSTOM_SPECIAL')
custom_special = rc_obj.ResourceClass(self.ctx, name='CUSTOM_SPECIAL')
custom_special.create()
tb.add_inventory(excl_extra_avail, 'CUSTOM_SPECIAL', 100)
self.allocate_from_provider(excl_extra_avail, 'CUSTOM_SPECIAL', 99)
@ -949,7 +950,7 @@ class AllocationCandidatesTestCase(tb.PlacementDbBaseTestCase):
min_unit=64, allocation_ratio=1.5)
# Create a custom resource called MAGIC
magic_rc = rp_obj.ResourceClass(
magic_rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_MAGIC',
)

View File

@ -0,0 +1,274 @@
# 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.
import mock
import os_resource_classes as orc
from oslo_utils.fixture import uuidsentinel
import placement
from placement import exception
from placement.objects import resource_class as rc_obj
from placement.objects import resource_provider as rp_obj
from placement.tests.functional.db import test_base as tb
class ResourceClassListTestCase(tb.PlacementDbBaseTestCase):
def test_get_all_no_custom(self):
"""Test that if we haven't yet added any custom resource classes, that
we only get a list of ResourceClass objects representing the standard
classes.
"""
rcs = rc_obj.ResourceClassList.get_all(self.ctx)
self.assertEqual(len(orc.STANDARDS), len(rcs))
def test_get_all_with_custom(self):
"""Test that if we add some custom resource classes, that we get a list
of ResourceClass objects representing the standard classes as well as
the custom classes.
"""
customs = [
('CUSTOM_IRON_NFV', 10001),
('CUSTOM_IRON_ENTERPRISE', 10002),
]
with self.placement_db.get_engine().connect() as conn:
for custom in customs:
c_name, c_id = custom
ins = rc_obj._RC_TBL.insert().values(id=c_id, name=c_name)
conn.execute(ins)
rcs = rc_obj.ResourceClassList.get_all(self.ctx)
expected_count = (len(orc.STANDARDS) + len(customs))
self.assertEqual(expected_count, len(rcs))
class ResourceClassTestCase(tb.PlacementDbBaseTestCase):
def test_get_by_name(self):
rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
orc.VCPU
)
vcpu_id = orc.STANDARDS.index(orc.VCPU)
self.assertEqual(vcpu_id, rc.id)
self.assertEqual(orc.VCPU, rc.name)
def test_get_by_name_not_found(self):
self.assertRaises(exception.ResourceClassNotFound,
rc_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_NO_EXISTS')
def test_get_by_name_custom(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
get_rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
self.assertEqual(rc.id, get_rc.id)
self.assertEqual(rc.name, get_rc.name)
def test_create_fail_not_using_namespace(self):
rc = rc_obj.ResourceClass(
context=self.ctx,
name='IRON_NFV',
)
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('name must start with', str(exc))
def test_create_duplicate_standard(self):
rc = rc_obj.ResourceClass(
context=self.ctx,
name=orc.VCPU,
)
self.assertRaises(exception.ResourceClassExists, rc.create)
def test_create(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
min_id = rc_obj.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
self.assertEqual(min_id, rc.id)
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_ENTERPRISE',
)
rc.create()
self.assertEqual(min_id + 1, rc.id)
@mock.patch.object(placement.objects.resource_class.ResourceClass,
"_get_next_id")
def test_create_duplicate_id_retry(self, mock_get):
# This order of ID generation will create rc1 with an ID of 42, try to
# create rc2 with the same ID, and then return 43 in the retry loop.
mock_get.side_effect = (42, 42, 43)
rc1 = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc1.create()
rc2 = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_TWO',
)
rc2.create()
self.assertEqual(rc1.id, 42)
self.assertEqual(rc2.id, 43)
@mock.patch.object(placement.objects.resource_class.ResourceClass,
"_get_next_id")
def test_create_duplicate_id_retry_failing(self, mock_get):
"""negative case for test_create_duplicate_id_retry"""
# This order of ID generation will create rc1 with an ID of 44, try to
# create rc2 with the same ID, and then return 45 in the retry loop.
mock_get.side_effect = (44, 44, 44, 44)
rc1 = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc1.create()
rc2 = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_TWO',
)
rc2.RESOURCE_CREATE_RETRY_COUNT = 3
self.assertRaises(exception.MaxDBRetriesExceeded, rc2.create)
def test_create_duplicate_custom(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
self.assertEqual(rc_obj.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID,
rc.id)
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ResourceClassExists, rc.create)
def test_destroy_fail_no_id(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ObjectActionError, rc.destroy)
def test_destroy_fail_standard(self):
rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
'VCPU',
)
self.assertRaises(exception.ResourceClassCannotDeleteStandard,
rc.destroy)
def test_destroy(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rc_list = rc_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertIn(rc.id, rc_ids)
rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
rc.destroy()
rc_list = rc_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertNotIn(rc.id, rc_ids)
# Verify rc cache was purged of the old entry
self.assertRaises(exception.ResourceClassNotFound,
rc_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_IRON_NFV')
def test_destroy_fail_with_inventory(self):
"""Test that we raise an exception when attempting to delete a resource
class that is referenced in an inventory record.
"""
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rp = rp_obj.ResourceProvider(
self.ctx,
name='my rp',
uuid=uuidsentinel.rp,
)
rp.create()
inv = rp_obj.Inventory(
resource_provider=rp,
resource_class='CUSTOM_IRON_NFV',
total=1,
)
inv_list = rp_obj.InventoryList(objects=[inv])
rp.set_inventory(inv_list)
self.assertRaises(exception.ResourceClassInUse,
rc.destroy)
rp.set_inventory(rp_obj.InventoryList(objects=[]))
rc.destroy()
rc_list = rc_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertNotIn(rc.id, rc_ids)
def test_save_fail_no_id(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ObjectActionError, rc.save)
def test_save_fail_standard(self):
rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
'VCPU',
)
self.assertRaises(exception.ResourceClassCannotUpdateStandard,
rc.save)
def test_save(self):
rc = rc_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rc = rc_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
rc.name = 'CUSTOM_IRON_SILVER'
rc.save()
# Verify rc cache was purged of the old entry
self.assertRaises(exception.NotFound,
rc_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_IRON_NFV')

View File

@ -18,7 +18,6 @@ from oslo_db import exception as db_exc
from oslo_utils.fixture import uuidsentinel
import sqlalchemy as sa
import placement
from placement.db.sqlalchemy import models
from placement import exception
from placement.objects import allocation as alloc_obj
@ -1296,258 +1295,6 @@ class TestAllocation(tb.PlacementDbBaseTestCase):
allocations[0].resource_provider.id)
class ResourceClassListTestCase(tb.PlacementDbBaseTestCase):
def test_get_all_no_custom(self):
"""Test that if we haven't yet added any custom resource classes, that
we only get a list of ResourceClass objects representing the standard
classes.
"""
rcs = rp_obj.ResourceClassList.get_all(self.ctx)
self.assertEqual(len(orc.STANDARDS), len(rcs))
def test_get_all_with_custom(self):
"""Test that if we add some custom resource classes, that we get a list
of ResourceClass objects representing the standard classes as well as
the custom classes.
"""
customs = [
('CUSTOM_IRON_NFV', 10001),
('CUSTOM_IRON_ENTERPRISE', 10002),
]
with self.placement_db.get_engine().connect() as conn:
for custom in customs:
c_name, c_id = custom
ins = rp_obj._RC_TBL.insert().values(id=c_id, name=c_name)
conn.execute(ins)
rcs = rp_obj.ResourceClassList.get_all(self.ctx)
expected_count = (len(orc.STANDARDS) + len(customs))
self.assertEqual(expected_count, len(rcs))
class ResourceClassTestCase(tb.PlacementDbBaseTestCase):
def test_get_by_name(self):
rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
orc.VCPU
)
vcpu_id = orc.STANDARDS.index(orc.VCPU)
self.assertEqual(vcpu_id, rc.id)
self.assertEqual(orc.VCPU, rc.name)
def test_get_by_name_not_found(self):
self.assertRaises(exception.ResourceClassNotFound,
rp_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_NO_EXISTS')
def test_get_by_name_custom(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
get_rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
self.assertEqual(rc.id, get_rc.id)
self.assertEqual(rc.name, get_rc.name)
def test_create_fail_not_using_namespace(self):
rc = rp_obj.ResourceClass(
context=self.ctx,
name='IRON_NFV',
)
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('name must start with', str(exc))
def test_create_duplicate_standard(self):
rc = rp_obj.ResourceClass(
context=self.ctx,
name=orc.VCPU,
)
self.assertRaises(exception.ResourceClassExists, rc.create)
def test_create(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
min_id = rp_obj.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
self.assertEqual(min_id, rc.id)
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_ENTERPRISE',
)
rc.create()
self.assertEqual(min_id + 1, rc.id)
@mock.patch.object(placement.objects.resource_provider.ResourceClass,
"_get_next_id")
def test_create_duplicate_id_retry(self, mock_get):
# This order of ID generation will create rc1 with an ID of 42, try to
# create rc2 with the same ID, and then return 43 in the retry loop.
mock_get.side_effect = (42, 42, 43)
rc1 = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc1.create()
rc2 = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_TWO',
)
rc2.create()
self.assertEqual(rc1.id, 42)
self.assertEqual(rc2.id, 43)
@mock.patch.object(placement.objects.resource_provider.ResourceClass,
"_get_next_id")
def test_create_duplicate_id_retry_failing(self, mock_get):
"""negative case for test_create_duplicate_id_retry"""
# This order of ID generation will create rc1 with an ID of 44, try to
# create rc2 with the same ID, and then return 45 in the retry loop.
mock_get.side_effect = (44, 44, 44, 44)
rc1 = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc1.create()
rc2 = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_TWO',
)
rc2.RESOURCE_CREATE_RETRY_COUNT = 3
self.assertRaises(exception.MaxDBRetriesExceeded, rc2.create)
def test_create_duplicate_custom(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
self.assertEqual(rp_obj.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID,
rc.id)
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ResourceClassExists, rc.create)
def test_destroy_fail_no_id(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ObjectActionError, rc.destroy)
def test_destroy_fail_standard(self):
rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
'VCPU',
)
self.assertRaises(exception.ResourceClassCannotDeleteStandard,
rc.destroy)
def test_destroy(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rc_list = rp_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertIn(rc.id, rc_ids)
rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
rc.destroy()
rc_list = rp_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertNotIn(rc.id, rc_ids)
# Verify rc cache was purged of the old entry
self.assertRaises(exception.ResourceClassNotFound,
rp_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_IRON_NFV')
def test_destroy_fail_with_inventory(self):
"""Test that we raise an exception when attempting to delete a resource
class that is referenced in an inventory record.
"""
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rp = rp_obj.ResourceProvider(
self.ctx,
name='my rp',
uuid=uuidsentinel.rp,
)
rp.create()
inv = rp_obj.Inventory(
resource_provider=rp,
resource_class='CUSTOM_IRON_NFV',
total=1,
)
inv_list = rp_obj.InventoryList(objects=[inv])
rp.set_inventory(inv_list)
self.assertRaises(exception.ResourceClassInUse,
rc.destroy)
rp.set_inventory(rp_obj.InventoryList(objects=[]))
rc.destroy()
rc_list = rp_obj.ResourceClassList.get_all(self.ctx)
rc_ids = (r.id for r in rc_list)
self.assertNotIn(rc.id, rc_ids)
def test_save_fail_no_id(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
self.assertRaises(exception.ObjectActionError, rc.save)
def test_save_fail_standard(self):
rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
'VCPU',
)
self.assertRaises(exception.ResourceClassCannotUpdateStandard,
rc.save)
def test_save(self):
rc = rp_obj.ResourceClass(
self.ctx,
name='CUSTOM_IRON_NFV',
)
rc.create()
rc = rp_obj.ResourceClass.get_by_name(
self.ctx,
'CUSTOM_IRON_NFV',
)
rc.name = 'CUSTOM_IRON_SILVER'
rc.save()
# Verify rc cache was purged of the old entry
self.assertRaises(exception.NotFound,
rp_obj.ResourceClass.get_by_name,
self.ctx,
'CUSTOM_IRON_NFV')
class ResourceProviderTraitTestCase(tb.PlacementDbBaseTestCase):
def _assert_traits(self, expected_traits, traits_objs):

View File

@ -28,7 +28,7 @@ from placement import conf
from placement import context
from placement import deploy
from placement.objects import project as project_obj
from placement.objects import resource_provider as rp_obj
from placement.objects import resource_class as rc_obj
from placement.objects import user as user_obj
from placement import policies
from placement.tests import fixtures
@ -434,7 +434,7 @@ class GranularFixture(APIFixture):
def start_fixture(self):
super(GranularFixture, self).start_fixture()
rp_obj.ResourceClass(
rc_obj.ResourceClass(
context=self.context, name='CUSTOM_NET_MBPS').create()
os.environ['AGGA'] = uuids.aggA

View File

@ -0,0 +1,29 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from placement import exception
from placement.objects import resource_class
from placement.tests.unit.objects import base
class TestResourceClass(base.TestCase):
def test_cannot_create_with_id(self):
rc = resource_class.ResourceClass(self.context, id=1,
name='CUSTOM_IRON_NFV')
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('already created', str(exc))
def test_cannot_create_requires_name(self):
rc = resource_class.ResourceClass(self.context)
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('name is required', str(exc))

View File

@ -225,20 +225,6 @@ class TestInventoryList(base.TestCase):
self.assertIsNone(inv_list.find('HOUSE'))
class TestResourceClass(base.TestCase):
def test_cannot_create_with_id(self):
rc = resource_provider.ResourceClass(self.context, id=1,
name='CUSTOM_IRON_NFV')
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('already created', str(exc))
def test_cannot_create_requires_name(self):
rc = resource_provider.ResourceClass(self.context)
exc = self.assertRaises(exception.ObjectActionError, rc.create)
self.assertIn('name is required', str(exc))
class TestTraits(base.TestCase):
@mock.patch("placement.objects.resource_provider."

View File

@ -29,6 +29,7 @@ import six
from placement import context
from placement import lib as pl
from placement import microversion
from placement.objects import resource_class as rc_obj
from placement.objects import resource_provider as rp_obj
from placement import util
@ -292,7 +293,7 @@ class TestPlacementURLs(testtools.TestCase):
fake_context,
name=uuidsentinel.rp_name,
uuid=uuidsentinel.rp_uuid)
self.resource_class = rp_obj.ResourceClass(
self.resource_class = rc_obj.ResourceClass(
fake_context,
name='CUSTOM_BAREMETAL_GOLD',
id=1000)