From cae313d1668a2d72c6073189eb8f6b7a2db052f1 Mon Sep 17 00:00:00 2001 From: Kien Nguyen Date: Wed, 11 Apr 2018 17:01:28 +0700 Subject: [PATCH] Add Quota objects Change-Id: I9f8d906a08267a50d628e060f9a05d8694c150b5 Partial-Implements: blueprint quota-support --- zun/objects/__init__.py | 6 ++ zun/objects/quota.py | 112 +++++++++++++++++++++ zun/objects/quota_class.py | 101 +++++++++++++++++++ zun/tests/unit/db/utils.py | 44 ++++++++ zun/tests/unit/objects/test_objects.py | 2 + zun/tests/unit/objects/test_quota.py | 96 ++++++++++++++++++ zun/tests/unit/objects/test_quota_class.py | 97 ++++++++++++++++++ 7 files changed, 458 insertions(+) create mode 100644 zun/objects/quota.py create mode 100644 zun/objects/quota_class.py create mode 100644 zun/tests/unit/objects/test_quota.py create mode 100644 zun/tests/unit/objects/test_quota_class.py diff --git a/zun/objects/__init__.py b/zun/objects/__init__.py index 242c7cf31..07e020362 100644 --- a/zun/objects/__init__.py +++ b/zun/objects/__init__.py @@ -19,6 +19,8 @@ from zun.objects import image from zun.objects import numa from zun.objects import pci_device from zun.objects import pci_device_pool +from zun.objects import quota +from zun.objects import quota_class from zun.objects import resource_class from zun.objects import resource_provider from zun.objects import volume_mapping @@ -37,6 +39,8 @@ ComputeNode = compute_node.ComputeNode Capsule = capsule.Capsule PciDevice = pci_device.PciDevice PciDevicePool = pci_device_pool.PciDevicePool +Quota = quota.Quota +QuotaClass = quota_class.QuotaClass ContainerPCIRequest = container_pci_requests.ContainerPCIRequest ContainerPCIRequests = container_pci_requests.ContainerPCIRequests ContainerAction = container_action.ContainerAction @@ -55,6 +59,8 @@ __all__ = ( Capsule, PciDevice, PciDevicePool, + Quota, + QuotaClass, ContainerPCIRequest, ContainerPCIRequests, ContainerAction, diff --git a/zun/objects/quota.py b/zun/objects/quota.py new file mode 100644 index 000000000..4f47f2353 --- /dev/null +++ b/zun/objects/quota.py @@ -0,0 +1,112 @@ +# 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 oslo_versionedobjects import fields + +from zun.db import api as dbapi +from zun.objects import base + + +@base.ZunObjectRegistry.register +class Quota(base.ZunPersistentObject, base.ZunObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(), + 'project_id': fields.StringField(nullable=True), + 'resource': fields.StringField(), + 'hard_limit': fields.IntegerField(nullable=True) + } + + @staticmethod + def _from_db_object(quota, db_quota): + """Converts a database entity to a formal object""" + for field in quota.fields: + setattr(quota, field, db_quota[field]) + + quota.obj_reset_changes() + return quota + + @base.remotable_classmethod + def get(cls, context, project_id, resource): + """Find a quota based on project_id and resource + + :param project_id: the project id. + :param context: security context. + :param resource: the name of resource. + :returns: a :class:`Quota` object. + """ + db_quota = dbapi.quota_get(context, project_id, resource) + quota = Quota._from_db_object(cls(context), db_quota) + return quota + + @base.remotable_classmethod + def get_all(cls, context, project_id): + """Find all quotas associated with project + + :param context: security context. + :param project_id: the project id. + :returns: a dict + """ + return dbapi.quota_get_all_by_project(context, project_id) + + @base.remotable + def create(self, context): + """Create a Quota record in the DB. + + :param context: security context. NOTE: This should only be + used internally by the indirection api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Quota(context) + """ + values = self.obj_get_changes() + project_id = values.get('project_id') + resource = values.get('resource') + limit = values.get('hard_limit') + db_quota = dbapi.quota_create(context, project_id, resource, limit) + self._from_db_object(self, db_quota) + + @base.remotable + def destroy(self, context=None): + """Delete the Quota from the DB. + + :param context: security context. NOTE: This should only be + used internally by the indirection api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Quota(context) + """ + dbapi.quota_destroy(context, self.project_id, self.resource) + self.obj_reset_changes() + + @base.remotable + def update(self, context=None): + """Save updates to this Quota. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: security context. NOTE: This should only be + used internally by the indirection api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Quota(context) + """ + updates = self.obj_get_changes() + dbapi.quota_update(context, self.project_id, self.resource, + updates.get('hard_limit')) + self.obj_reset_changes() diff --git a/zun/objects/quota_class.py b/zun/objects/quota_class.py new file mode 100644 index 000000000..c21961fda --- /dev/null +++ b/zun/objects/quota_class.py @@ -0,0 +1,101 @@ +# 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 oslo_versionedobjects import fields + +from zun.db import api as dbapi +from zun.objects import base + + +@base.ZunObjectRegistry.register +class QuotaClass(base.ZunPersistentObject, base.ZunObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(), + 'class_name': fields.StringField(nullable=True), + 'resource': fields.StringField(nullable=True), + 'hard_limit': fields.IntegerField(nullable=True) + } + + @staticmethod + def _from_db_method(quota_class, db_quota_class): + """Convert a database entity to a format object""" + for field in quota_class.fields: + setattr(quota_class, field, db_quota_class[field]) + + quota_class.obj_reset_changes() + return quota_class + + @base.remotable_classmethod + def get(cls, context, class_name, resource): + """Find a quota class based on class_name and resource name. + + :param class_name: the name of class. + :param context: security context. + :param resource: the name of resource. + :returns: a :class:`QuotaClass` object. + """ + db_quota_class = dbapi.quota_class_get(context, class_name, resource) + quota_class = QuotaClass._from_db_method(cls(context), db_quota_class) + return quota_class + + @base.remotable_classmethod + def get_all(cls, context, class_name=None): + """Find quota based on class_name + + :param context: security context. + :param class_name: the class name. + :return a dict + """ + if class_name is None: + res = dbapi.quota_class_get_default(context) + else: + res = dbapi.quota_class_get_all_by_name(context, class_name) + return res + + @base.remotable + def create(self, context): + """Create a QuotaClass record in the DB. + + :param context: security context. NOTE: This should only be + used internally by the indirection api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: QuotaClass(context) + """ + values = self.obj_get_changes() + class_name = values.get('class_name') + resource = values.get('resource') + limit = values.get('hard_limit') + dbapi.quota_class_create(context, class_name, resource, limit) + + @base.remotable + def update(self, context=None): + """Save updates to this QuotaClass. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: security context. NOTE: This should only be + used internally by the indirection api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: QuotaClass(context) + """ + updates = self.obj_get_changes() + limit = updates.get('hard_limit') + dbapi.quota_class_update(context, self.class_name, + self.resource, limit) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 3d21ea116..76e05b8ec 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -513,3 +513,47 @@ def create_test_quota_class(**kwargs): limit = kwargs.get('limit', 100) dbapi = _get_dbapi() return dbapi.quota_class_create(context, class_name, resource, limit) + + +def get_test_quota_value(**kwargs): + quota_values = { + 'created_at': kwargs.get('created_at'), + 'updated_at': kwargs.get('updated_at'), + 'id': kwargs.get('id', 123), + 'project_id': kwargs.get('project_id', 'fake_project_id'), + 'resource': kwargs.get('resource', 'container'), + 'hard_limit': kwargs.get('hard_limit', 20) + } + + return quota_values + + +def get_test_quota(**kwargs): + quota_values = get_test_quota_value(**kwargs) + fake_quota = FakeObject() + for k, v in quota_values.items(): + setattr(fake_quota, k, v) + + return fake_quota + + +def get_test_quota_class_value(**kwargs): + quota_values = { + 'created_at': kwargs.get('created_at'), + 'updated_at': kwargs.get('updated_at'), + 'id': kwargs.get('id', 123), + 'class_name': kwargs.get('class_name', 'fake_class_name'), + 'resource': kwargs.get('resource', 'container'), + 'hard_limit': kwargs.get('hard_limit', 20) + } + + return quota_values + + +def get_test_quota_class(**kwargs): + quota_class_values = get_test_quota_class_value(**kwargs) + fake_quota_class = FakeObject() + for k, v in quota_class_values.items(): + setattr(fake_quota_class, k, v) + + return fake_quota_class diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 409629005..481b6c991 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -358,6 +358,8 @@ object_data = { 'ComputeNode': '1.11-08be22db017745f4f0bc8f873eca7db0', 'PciDevicePool': '1.0-3f5ddc3ff7bfa14da7f6c7e9904cc000', 'PciDevicePoolList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e', + 'Quota': '1.0-4daf54427ac19cb23182cad82fcde751', + 'QuotaClass': '1.0-4739583a70891fbc145031228fb8001e', 'ContainerPCIRequest': '1.0-b060f9f9f734bedde79a71a4d3112ee0', 'ContainerPCIRequests': '1.0-7b8f7f044661fe4e24e6949c035af2c4', 'ContainerAction': '1.1-b0c721f9e10c6c0d1e41e512c49eb877', diff --git a/zun/tests/unit/objects/test_quota.py b/zun/tests/unit/objects/test_quota.py new file mode 100644 index 000000000..342358fd0 --- /dev/null +++ b/zun/tests/unit/objects/test_quota.py @@ -0,0 +1,96 @@ +# 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 + +from zun import objects +from zun.tests.unit.db import base +from zun.tests.unit.db import utils + + +class TestQuotaObject(base.DbTestCase): + + def setUp(self): + super(TestQuotaObject, self).setUp() + self.fake_quota = utils.get_test_quota() + + def test_get_quota(self): + project_id = self.fake_quota['project_id'] + resource = self.fake_quota['resource'] + with mock.patch.object(self.dbapi, 'quota_get', + autospec=True) as mock_get: + mock_get.return_value = self.fake_quota + quota = objects.Quota.get(self.context, project_id, resource) + mock_get.assert_called_once_with( + self.context, project_id, resource) + self.assertEqual(self.context, quota._context) + + def test_get_all_quotas_by_project(self): + project_id = self.fake_quota['project_id'] + with mock.patch.object(self.dbapi, 'quota_get_all_by_project', + autospec=True) as mock_get_all: + mock_get_all.return_value = { + 'project_id': project_id, + 'resource_1': 10, + 'resource_2': 20 + } + quotas_dict = objects.Quota.get_all(self.context, project_id) + mock_get_all.assert_called_once_with(self.context, project_id) + self.assertEqual(project_id, quotas_dict['project_id']) + + def test_create_quota(self): + project_id = self.fake_quota['project_id'] + resource = self.fake_quota['resource'] + hard_limit = self.fake_quota['hard_limit'] + with mock.patch.object(self.dbapi, 'quota_create', + autospec=True) as mock_create: + mock_create.return_value = self.fake_quota + quota = objects.Quota( + self.context, **utils.get_test_quota_value()) + quota.create(self.context) + mock_create.assert_called_once_with( + self.context, project_id, resource, hard_limit) + + def test_destroy_quota(self): + project_id = self.fake_quota['project_id'] + resource = self.fake_quota['resource'] + with mock.patch.object(self.dbapi, 'quota_get', + autospec=True) as mock_get: + mock_get.return_value = self.fake_quota + with mock.patch.object(self.dbapi, 'quota_destroy', + autospec=True) as mock_destroy: + quota = objects.Quota.get(self.context, project_id, + resource) + quota.destroy() + mock_destroy.assert_called_once_with( + None, project_id, resource) + self.assertEqual(self.context, quota._context) + + def test_update_quota(self): + project_id = self.fake_quota['project_id'] + resource = self.fake_quota['resource'] + with mock.patch.object(self.dbapi, 'quota_get', + autospec=True) as mock_get: + mock_get.return_value = self.fake_quota + with mock.patch.object(self.dbapi, 'quota_update', + autospec=True) as mock_update: + quota = objects.Quota.get(self.context, project_id, + resource) + quota.hard_limit = 100 + quota.update() + + mock_get.assert_called_once_with(self.context, project_id, + resource) + mock_update.assert_called_once_with( + None, project_id, resource, 100) + self.assertEqual(self.context, quota._context) + self.assertEqual(100, quota.hard_limit) diff --git a/zun/tests/unit/objects/test_quota_class.py b/zun/tests/unit/objects/test_quota_class.py new file mode 100644 index 000000000..002761109 --- /dev/null +++ b/zun/tests/unit/objects/test_quota_class.py @@ -0,0 +1,97 @@ +# 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 + +from zun.db.sqlalchemy import api +from zun import objects +from zun.tests.unit.db import base +from zun.tests.unit.db import utils + + +class TestQuotaClassObject(base.DbTestCase): + + def setUp(self): + super(TestQuotaClassObject, self).setUp() + self.fake_quota_class = utils.get_test_quota_class() + + def test_get_quota_class(self): + class_name = self.fake_quota_class['class_name'] + resource = self.fake_quota_class['resource'] + with mock.patch.object(self.dbapi, 'quota_class_get', + autospec=True) as mock_get: + mock_get.return_value = self.fake_quota_class + quota_class = objects.QuotaClass.get( + self.context, class_name, resource) + mock_get.assert_called_once_with( + self.context, class_name, resource) + self.assertEqual(self.context, quota_class._context) + + def test_get_all_with_default(self): + class_name = api._DEFAULT_QUOTA_NAME + with mock.patch.object(self.dbapi, 'quota_class_get_default', + autospec=True) as mock_get_all: + mock_get_all.return_value = { + 'class_name': class_name, + 'resource_1': 10, + 'resource_2': 20 + } + quota_class_dict = objects.QuotaClass.get_all(self.context) + mock_get_all.assert_called_once_with(self.context) + self.assertEqual(class_name, quota_class_dict['class_name']) + + def test_get_all_with_class_name(self): + class_name = self.fake_quota_class['class_name'] + with mock.patch.object(self.dbapi, 'quota_class_get_all_by_name', + autospec=True) as mock_get_all: + mock_get_all.return_value = { + 'class_name': class_name, + 'resource_1': 10, + 'resource_2': 20 + } + quota_class_dict = objects.QuotaClass.get_all( + self.context, class_name) + mock_get_all.assert_called_once_with(self.context, class_name) + self.assertEqual(class_name, quota_class_dict['class_name']) + + def test_create_quota_class(self): + class_name = self.fake_quota_class['class_name'] + resource = self.fake_quota_class['resource'] + hard_limit = self.fake_quota_class['hard_limit'] + with mock.patch.object(self.dbapi, 'quota_class_create', + autospec=True) as mock_create: + mock_create.return_value = self.fake_quota_class + quota_class = objects.QuotaClass( + self.context, **utils.get_test_quota_class_value()) + quota_class.create(self.context) + mock_create.assert_called_once_with( + self.context, class_name, resource, hard_limit) + + def test_update_quota(self): + class_name = self.fake_quota_class['class_name'] + resource = self.fake_quota_class['resource'] + with mock.patch.object(self.dbapi, 'quota_class_get', + autospec=True) as mock_get: + mock_get.return_value = self.fake_quota_class + with mock.patch.object(self.dbapi, 'quota_class_update', + autospec=True) as mock_update: + quota_class = objects.QuotaClass.get( + self.context, class_name, resource) + quota_class.hard_limit = 100 + quota_class.update() + + mock_get.assert_called_once_with( + self.context, class_name, resource) + mock_update.assert_called_once_with( + None, class_name, resource, 100) + self.assertEqual(self.context, quota_class._context) + self.assertEqual(100, quota_class.hard_limit)