diff --git a/magnum/api/controllers/v1/cluster.py b/magnum/api/controllers/v1/cluster.py index a7d9c9f232..19818a9ac8 100644 --- a/magnum/api/controllers/v1/cluster.py +++ b/magnum/api/controllers/v1/cluster.py @@ -33,11 +33,14 @@ from magnum.common import clients from magnum.common import exception from magnum.common import name_generator from magnum.common import policy +import magnum.conf +from magnum.i18n import _ from magnum.i18n import _LW from magnum import objects from magnum.objects import fields LOG = logging.getLogger(__name__) +CONF = magnum.conf.CONF class ClusterID(wtypes.Base): @@ -353,6 +356,24 @@ class ClustersController(base.Controller): return cluster + def _check_cluster_quota_limit(self, context): + try: + # Check if there is any explicit quota limit set in Quotas table + quota = objects.Quota.get_quota_by_project_id_resource( + context, + context.project_id, + 'Cluster') + cluster_limit = quota.hard_limit + except exception.QuotaNotFound: + # If explicit quota was not set for the project, use default limit + cluster_limit = CONF.quotas.max_clusters_per_project + + if objects.Cluster.get_count_all(context) >= cluster_limit: + msg = _("You have reached the maximum clusters per project, " + "%d. You may delete a cluster to make room for a new " + "one.") % cluster_limit + raise exception.ResourceLimitExceeded(msg=msg) + @expose.expose(ClusterID, body=Cluster, status_code=202) def post(self, cluster): """Create a new cluster. @@ -362,6 +383,9 @@ class ClustersController(base.Controller): context = pecan.request.context policy.enforce(context, 'cluster:create', action='cluster:create') + + self._check_cluster_quota_limit(context) + temp_id = cluster.cluster_template_id cluster_template = objects.ClusterTemplate.get_by_uuid(context, temp_id) diff --git a/magnum/common/exception.py b/magnum/common/exception.py index fd847b76a7..9a8c347bd9 100644 --- a/magnum/common/exception.py +++ b/magnum/common/exception.py @@ -387,6 +387,10 @@ class QuotaNotFound(ResourceNotFound): message = _("Quota could not be found: %(msg)s") +class ResourceLimitExceeded(NotAuthorized): + message = _('Resource limit exceeded: %(msg)s') + + class RegionsListFailed(MagnumException): message = _("Failed to list regions.") diff --git a/magnum/db/api.py b/magnum/db/api.py index ade4afb174..cbfd122e38 100644 --- a/magnum/db/api.py +++ b/magnum/db/api.py @@ -115,6 +115,15 @@ class Connection(object): :returns: clusters, nodes count. """ + @abc.abstractmethod + def get_cluster_count_all(self, context, filters=None): + """Get count of matching clusters. + + :param context: The security context + :param filters: Filters to apply. Defaults to None. + :returns: Count of matching clusters. + """ + @abc.abstractmethod def destroy_cluster(self, cluster_id): """Destroy a cluster and all associated interfaces. diff --git a/magnum/db/sqlalchemy/api.py b/magnum/db/sqlalchemy/api.py index 93db8f1431..2f0fb2f859 100644 --- a/magnum/db/sqlalchemy/api.py +++ b/magnum/db/sqlalchemy/api.py @@ -208,6 +208,12 @@ class Connection(api.Connection): nodes = int(nquery.one()[0]) if nquery.one()[0] else 0 return clusters, nodes + def get_cluster_count_all(self, context, filters=None): + query = model_query(models.Cluster) + query = self._add_tenant_filters(context, query) + query = self._add_clusters_filters(query, filters) + return query.count() + def destroy_cluster(self, cluster_id): session = get_session() with session.begin(): diff --git a/magnum/objects/cluster.py b/magnum/objects/cluster.py index 4715f3b409..5b1c04b3aa 100644 --- a/magnum/objects/cluster.py +++ b/magnum/objects/cluster.py @@ -41,8 +41,9 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject, # Version 1.10: Added 'keypair' field # Version 1.11: Added 'RESUME_FAILED' in status field # Version 1.12: Added 'get_stats' method + # Version 1.13: Added get_count_all method - VERSION = '1.12' + VERSION = '1.13' dbapi = dbapi.get_instance() @@ -137,6 +138,10 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject, cluster = Cluster._from_db_object(cls(context), db_cluster) return cluster + @base.remotable_classmethod + def get_count_all(cls, context, **kwargs): + return cls.dbapi.get_cluster_count_all(context, **kwargs) + @base.remotable_classmethod def get_by_name(cls, context, name): """Find a cluster based on name and return a Cluster object. diff --git a/magnum/tests/unit/api/controllers/v1/test_cluster.py b/magnum/tests/unit/api/controllers/v1/test_cluster.py index ff754b9f1f..aab217dd60 100644 --- a/magnum/tests/unit/api/controllers/v1/test_cluster.py +++ b/magnum/tests/unit/api/controllers/v1/test_cluster.py @@ -22,12 +22,15 @@ from magnum.api import attr_validator from magnum.api.controllers.v1 import cluster as api_cluster from magnum.common import exception from magnum.conductor import api as rpcapi +import magnum.conf from magnum import objects from magnum.tests import base from magnum.tests.unit.api import base as api_base from magnum.tests.unit.api import utils as apiutils from magnum.tests.unit.objects import utils as obj_utils +CONF = magnum.conf.CONF + class TestClusterObject(base.TestCase): def test_cluster_init(self): @@ -478,6 +481,27 @@ class TestPost(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertTrue(uuidutils.is_uuid_like(response.json['uuid'])) + @mock.patch('oslo_utils.timeutils.utcnow') + def test_create_cluster_resource_limit_reached(self, mock_utcnow): + # override max_cluster_per_project to 1 + CONF.set_override('max_clusters_per_project', 1, group='quotas') + + bdict = apiutils.cluster_post_data() + test_time = datetime.datetime(2000, 1, 1, 0, 0) + mock_utcnow.return_value = test_time + + # create first cluster + response = self.post_json('/clusters', bdict) + self.assertEqual('application/json', response.content_type) + self.assertEqual(202, response.status_int) + self.assertTrue(uuidutils.is_uuid_like(response.json['uuid'])) + + # now try to create second cluster and make sure it fails + response = self.post_json('/clusters', bdict, expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(403, response.status_int) + self.assertTrue(response.json['errors']) + def test_create_cluster_set_project_id_and_user_id(self): bdict = apiutils.cluster_post_data() diff --git a/magnum/tests/unit/objects/test_objects.py b/magnum/tests/unit/objects/test_objects.py index 6012db6409..ca2cabf5bf 100644 --- a/magnum/tests/unit/objects/test_objects.py +++ b/magnum/tests/unit/objects/test_objects.py @@ -355,7 +355,7 @@ class TestObject(test_base.TestCase, _TestObject): # For more information on object version testing, read # http://docs.openstack.org/developer/magnum/objects.html object_data = { - 'Cluster': '1.12-73881c0604a6c90d7ecfeb5abd380f7e', + 'Cluster': '1.13-87f9b6ff2090663d69a1de2e95c50a27', 'ClusterTemplate': '1.17-65a95ef932dd08800a83871eb3cf312b', 'Certificate': '1.1-1924dc077daa844f0f9076332ef96815', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',