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:
Jay Pipes 2016-10-14 18:21:17 -04:00
parent a58c7e5173
commit 624f18417b
4 changed files with 127 additions and 3 deletions

View File

@ -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.")

View File

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

View File

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

View File

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