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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',