]> xenbits.xensource.com Git - osstest/openstack-nova.git/commitdiff
placement: adds ResourceClass.create()
authorJay Pipes <jaypipes@gmail.com>
Fri, 14 Oct 2016 22:21:17 +0000 (18:21 -0400)
committerJay Pipes <jaypipes@gmail.com>
Mon, 21 Nov 2016 21:02:01 +0000 (16:02 -0500)
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

nova/exception.py
nova/objects/resource_provider.py
nova/tests/functional/db/test_resource_provider.py
nova/tests/unit/objects/test_resource_provider.py

index 04186b514bdbbd62b7c72d312dced58c655f8d47..d50b259a117a5bc71d71cd6c5b465fec299c9e83 100644 (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.")
index c5b17bd8f56e8372fb527e768b67c677f07413bf..18ffa5a2c844a98f2f1f850d727f52b2711feb1d 100644 (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):
index 9528e305f90620654b67669c24ec54173ef918e6..01a28b434dd425e0897e2ce9a72fb386b22e595f 100644 (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)
index 79b8f67edd3ff0dd47bb650558479592db038810..10513da42868e836e1d5ca344d9d102f4e2cad97 100644 (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))