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:
parent
aa56874bfb
commit
206d17f8ca
@ -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)
|
||||
|
@ -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.")
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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():
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user