placement: adds ResourceClass.create()
Adds in the implementation of objects.ResourceClass.create() with checks that the custom resource class being added doesn't overlap with any standard (or previously-added custom) resource classes. The implementation uses a hard-coded number 10000 to mark the start of custom resource class integer identifiers to make it easy to differentiate custom resource classes. Note that we do NOT increment the object version here because nothing as-yet calls the ResourceClass object. Also note that this patch adds a required "CUSTOM_" namespace prefix to all custom resource classes. Followup patches will also place this constraint into the JSONSchema for POST /resource_classes. The CUSTOM_ namespace is required in order to ensure that custom resource class names never conflict with future standard resource class additions. Change-Id: I4532031da19abaf87b1c2e30b5f70ff269c3ffc8 blueprint: custom-resource-classes
This commit is contained in:
parent
a58c7e5173
commit
624f18417b
|
@ -2126,6 +2126,10 @@ class InventoryWithResourceClassNotFound(NotFound):
|
|||
msg_fmt = _("No inventory of class %(resource_class)s found.")
|
||||
|
||||
|
||||
class ResourceClassExists(NovaException):
|
||||
msg_fmt = _("Resource class %(resource_class)s already exists.")
|
||||
|
||||
|
||||
class InvalidInventory(Invalid):
|
||||
msg_fmt = _("Inventory for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s' invalid.")
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import versionutils
|
||||
import six
|
||||
|
@ -1047,9 +1048,17 @@ class ResourceClass(base.NovaObject):
|
|||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
CUSTOM_NAMESPACE = 'CUSTOM_'
|
||||
"""All non-standard resource classes must begin with this string."""
|
||||
|
||||
MIN_CUSTOM_RESOURCE_CLASS_ID = 10000
|
||||
"""Any user-defined resource classes must have an identifier greater than
|
||||
or equal to this number.
|
||||
"""
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(read_only=True),
|
||||
'name': fields.ResourceClassField(read_only=True),
|
||||
'name': fields.ResourceClassField(nullable=False),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -1061,6 +1070,51 @@ class ResourceClass(base.NovaObject):
|
|||
target.obj_reset_changes()
|
||||
return target
|
||||
|
||||
@staticmethod
|
||||
@db_api.api_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:
|
||||
return ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
|
||||
else:
|
||||
return max_id + 1
|
||||
|
||||
def create(self):
|
||||
if 'id' in self:
|
||||
raise exception.ObjectActionError(action='create',
|
||||
reason='already created')
|
||||
if 'name' not in self:
|
||||
raise exception.ObjectActionError(action='create',
|
||||
reason='name is required')
|
||||
if self.name in fields.ResourceClass.STANDARD:
|
||||
raise exception.ResourceClassExists(resource_class=self.name)
|
||||
|
||||
if not self.name.startswith(self.CUSTOM_NAMESPACE):
|
||||
raise exception.ObjectActionError(
|
||||
action='create',
|
||||
reason='name must start with ' + self.CUSTOM_NAMESPACE)
|
||||
|
||||
updates = self.obj_get_changes()
|
||||
try:
|
||||
rc = self._create_in_db(self._context, updates)
|
||||
self._from_db_object(self._context, self, rc)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.ResourceClassExists(resource_class=self.name)
|
||||
|
||||
@staticmethod
|
||||
@db_api.api_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
|
||||
|
||||
|
||||
@base.NovaObjectRegistry.register
|
||||
class ResourceClassList(base.ObjectListBase, base.NovaObject):
|
||||
|
|
|
@ -1003,8 +1003,8 @@ class ResourceClassListTestCase(ResourceProviderBaseCase):
|
|||
the custom classes.
|
||||
"""
|
||||
customs = [
|
||||
('IRON_NFV', 10001),
|
||||
('IRON_ENTERPRISE', 10002),
|
||||
('CUSTOM_IRON_NFV', 10001),
|
||||
('CUSTOM_IRON_ENTERPRISE', 10002),
|
||||
]
|
||||
with self.api_db.get_engine().connect() as conn:
|
||||
for custom in customs:
|
||||
|
@ -1015,3 +1015,48 @@ class ResourceClassListTestCase(ResourceProviderBaseCase):
|
|||
rcs = objects.ResourceClassList.get_all(self.context)
|
||||
expected_count = len(fields.ResourceClass.STANDARD) + len(customs)
|
||||
self.assertEqual(expected_count, len(rcs))
|
||||
|
||||
def test_create_fail_not_using_namespace(self):
|
||||
rc = objects.ResourceClass(
|
||||
context=self.context,
|
||||
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 = objects.ResourceClass(
|
||||
context=self.context,
|
||||
name=fields.ResourceClass.VCPU,
|
||||
)
|
||||
self.assertRaises(exception.ResourceClassExists, rc.create)
|
||||
|
||||
def test_create(self):
|
||||
rc = objects.ResourceClass(
|
||||
self.context,
|
||||
name='CUSTOM_IRON_NFV',
|
||||
)
|
||||
rc.create()
|
||||
min_id = objects.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
|
||||
self.assertEqual(min_id, rc.id)
|
||||
|
||||
rc = objects.ResourceClass(
|
||||
self.context,
|
||||
name='CUSTOM_IRON_ENTERPRISE',
|
||||
)
|
||||
rc.create()
|
||||
self.assertEqual(min_id + 1, rc.id)
|
||||
|
||||
def test_create_duplicate_custom(self):
|
||||
rc = objects.ResourceClass(
|
||||
self.context,
|
||||
name='CUSTOM_IRON_NFV',
|
||||
)
|
||||
rc.create()
|
||||
self.assertEqual(objects.ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID,
|
||||
rc.id)
|
||||
rc = objects.ResourceClass(
|
||||
self.context,
|
||||
name='CUSTOM_IRON_NFV',
|
||||
)
|
||||
self.assertRaises(exception.ResourceClassExists, rc.create)
|
||||
|
|
|
@ -14,10 +14,12 @@ import uuid
|
|||
|
||||
import mock
|
||||
|
||||
from nova import context
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova.objects import fields
|
||||
from nova.objects import resource_provider
|
||||
from nova import test
|
||||
from nova.tests.unit.objects import test_objects
|
||||
from nova.tests import uuidsentinel as uuids
|
||||
|
||||
|
@ -569,3 +571,22 @@ class TestUsageNoDB(test_objects._LocalTest):
|
|||
self.assertRaises(ValueError,
|
||||
usage.obj_to_primitive,
|
||||
target_version='1.0')
|
||||
|
||||
|
||||
class TestResourceClass(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceClass, self).setUp()
|
||||
self.user_id = 'fake-user'
|
||||
self.project_id = 'fake-project'
|
||||
self.context = context.RequestContext(self.user_id, self.project_id)
|
||||
|
||||
def test_cannot_create_with_id(self):
|
||||
rc = objects.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 = objects.ResourceClass(self.context)
|
||||
exc = self.assertRaises(exception.ObjectActionError, rc.create)
|
||||
self.assertIn('name is required', str(exc))
|
||||
|
|
Loading…
Reference in New Issue