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:
parent
849c89d0e5
commit
7964cd9cbc
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
259
placement/objects/resource_class.py
Normal file
259
placement/objects/resource_class.py
Normal 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
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
)
|
||||
|
274
placement/tests/functional/db/test_resource_class.py
Normal file
274
placement/tests/functional/db/test_resource_class.py
Normal 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')
|
@ -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):
|
||||
|
@ -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
|
||||
|
29
placement/tests/unit/objects/test_resource_class.py
Normal file
29
placement/tests/unit/objects/test_resource_class.py
Normal 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))
|
@ -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."
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user