Resource Quota - Limit clusters per project

Currently there is no limit on the number of clusters that can
be created in a project. This change limits number of clusters
in a project by checking cluster quota on cluster-create.

Change-Id: Ifa17d12692751fc6929e62be8bb59d481a2fd205
Partially-Implements: blueprint resource-quota
This commit is contained in:
Vijendar Komalla 2017-01-19 16:52:50 -06:00
parent aa56874bfb
commit 206d17f8ca
7 changed files with 74 additions and 2 deletions

View File

@ -33,11 +33,14 @@ from magnum.common import clients
from magnum.common import exception from magnum.common import exception
from magnum.common import name_generator from magnum.common import name_generator
from magnum.common import policy from magnum.common import policy
import magnum.conf
from magnum.i18n import _
from magnum.i18n import _LW from magnum.i18n import _LW
from magnum import objects from magnum import objects
from magnum.objects import fields from magnum.objects import fields
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = magnum.conf.CONF
class ClusterID(wtypes.Base): class ClusterID(wtypes.Base):
@ -353,6 +356,24 @@ class ClustersController(base.Controller):
return cluster 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) @expose.expose(ClusterID, body=Cluster, status_code=202)
def post(self, cluster): def post(self, cluster):
"""Create a new cluster. """Create a new cluster.
@ -362,6 +383,9 @@ class ClustersController(base.Controller):
context = pecan.request.context context = pecan.request.context
policy.enforce(context, 'cluster:create', policy.enforce(context, 'cluster:create',
action='cluster:create') action='cluster:create')
self._check_cluster_quota_limit(context)
temp_id = cluster.cluster_template_id temp_id = cluster.cluster_template_id
cluster_template = objects.ClusterTemplate.get_by_uuid(context, cluster_template = objects.ClusterTemplate.get_by_uuid(context,
temp_id) temp_id)

View File

@ -387,6 +387,10 @@ class QuotaNotFound(ResourceNotFound):
message = _("Quota could not be found: %(msg)s") message = _("Quota could not be found: %(msg)s")
class ResourceLimitExceeded(NotAuthorized):
message = _('Resource limit exceeded: %(msg)s')
class RegionsListFailed(MagnumException): class RegionsListFailed(MagnumException):
message = _("Failed to list regions.") message = _("Failed to list regions.")

View File

@ -115,6 +115,15 @@ class Connection(object):
:returns: clusters, nodes count. :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 @abc.abstractmethod
def destroy_cluster(self, cluster_id): def destroy_cluster(self, cluster_id):
"""Destroy a cluster and all associated interfaces. """Destroy a cluster and all associated interfaces.

View File

@ -208,6 +208,12 @@ class Connection(api.Connection):
nodes = int(nquery.one()[0]) if nquery.one()[0] else 0 nodes = int(nquery.one()[0]) if nquery.one()[0] else 0
return clusters, nodes 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): def destroy_cluster(self, cluster_id):
session = get_session() session = get_session()
with session.begin(): with session.begin():

View File

@ -41,8 +41,9 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject,
# Version 1.10: Added 'keypair' field # Version 1.10: Added 'keypair' field
# Version 1.11: Added 'RESUME_FAILED' in status field # Version 1.11: Added 'RESUME_FAILED' in status field
# Version 1.12: Added 'get_stats' method # 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() dbapi = dbapi.get_instance()
@ -137,6 +138,10 @@ class Cluster(base.MagnumPersistentObject, base.MagnumObject,
cluster = Cluster._from_db_object(cls(context), db_cluster) cluster = Cluster._from_db_object(cls(context), db_cluster)
return 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 @base.remotable_classmethod
def get_by_name(cls, context, name): def get_by_name(cls, context, name):
"""Find a cluster based on name and return a Cluster object. """Find a cluster based on name and return a Cluster object.

View File

@ -22,12 +22,15 @@ from magnum.api import attr_validator
from magnum.api.controllers.v1 import cluster as api_cluster from magnum.api.controllers.v1 import cluster as api_cluster
from magnum.common import exception from magnum.common import exception
from magnum.conductor import api as rpcapi from magnum.conductor import api as rpcapi
import magnum.conf
from magnum import objects from magnum import objects
from magnum.tests import base from magnum.tests import base
from magnum.tests.unit.api import base as api_base from magnum.tests.unit.api import base as api_base
from magnum.tests.unit.api import utils as apiutils from magnum.tests.unit.api import utils as apiutils
from magnum.tests.unit.objects import utils as obj_utils from magnum.tests.unit.objects import utils as obj_utils
CONF = magnum.conf.CONF
class TestClusterObject(base.TestCase): class TestClusterObject(base.TestCase):
def test_cluster_init(self): def test_cluster_init(self):
@ -478,6 +481,27 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
self.assertTrue(uuidutils.is_uuid_like(response.json['uuid'])) 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): def test_create_cluster_set_project_id_and_user_id(self):
bdict = apiutils.cluster_post_data() bdict = apiutils.cluster_post_data()

View File

@ -355,7 +355,7 @@ class TestObject(test_base.TestCase, _TestObject):
# For more information on object version testing, read # For more information on object version testing, read
# http://docs.openstack.org/developer/magnum/objects.html # http://docs.openstack.org/developer/magnum/objects.html
object_data = { object_data = {
'Cluster': '1.12-73881c0604a6c90d7ecfeb5abd380f7e', 'Cluster': '1.13-87f9b6ff2090663d69a1de2e95c50a27',
'ClusterTemplate': '1.17-65a95ef932dd08800a83871eb3cf312b', 'ClusterTemplate': '1.17-65a95ef932dd08800a83871eb3cf312b',
'Certificate': '1.1-1924dc077daa844f0f9076332ef96815', 'Certificate': '1.1-1924dc077daa844f0f9076332ef96815',
'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',