diff --git a/etc/neutron.conf b/etc/neutron.conf index a8dabdcc35..59dd892c1a 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -283,6 +283,9 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier # ======== end of WSGI parameters related to the API server ========== [quotas] +# Default driver to use for quota checks +# quota_driver = neutron.db.quota_db.DbQuotaDriver + # Resource name(s) that are supported in quota features # quota_items = network,subnet,port @@ -307,15 +310,31 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier # unlimited. # quota_security_group_rule = 100 +# Number of vips allowed per tenant. A negative value means unlimited. +# quota_vip = 10 + +# Number of pools allowed per tenant. A negative value means unlimited. +# quota_pool = 10 + +# Number of pool members allowed per tenant. A negative value means unlimited. +# The default is unlimited because a member is not a real resource consumer +# on Openstack. However, on back-end, a member is a resource consumer +# and that is the reason why quota is possible. +# quota_member = -1 + +# Number of health monitors allowed per tenant. A negative value means +# unlimited. +# The default is unlimited because a health monitor is not a real resource +# consumer on Openstack. However, on back-end, a member is a resource consumer +# and that is the reason why quota is possible. +# quota_health_monitors = -1 + # Number of routers allowed per tenant. A negative value means unlimited. # quota_router = 10 # Number of floating IPs allowed per tenant. A negative value means unlimited. # quota_floatingip = 50 -# Default driver to use for quota checks -# quota_driver = neutron.db.quota_db.DbQuotaDriver - [agent] # Use "sudo neutron-rootwrap /etc/neutron/rootwrap.conf" to use the real # root filter facility. diff --git a/neutron/extensions/loadbalancer.py b/neutron/extensions/loadbalancer.py index eb77473fd6..7f23704d8f 100644 --- a/neutron/extensions/loadbalancer.py +++ b/neutron/extensions/loadbalancer.py @@ -17,6 +17,7 @@ import abc +from oslo.config import cfg import six from neutron.api import extensions @@ -290,6 +291,26 @@ SUB_RESOURCE_ATTRIBUTE_MAP = { } } +lbaas_quota_opts = [ + cfg.IntOpt('quota_vip', + default=10, + help=_('Number of vips allowed per tenant. ' + 'A negative value means unlimited.')), + cfg.IntOpt('quota_pool', + default=10, + help=_('Number of pools allowed per tenant. ' + 'A negative value means unlimited.')), + cfg.IntOpt('quota_member', + default=-1, + help=_('Number of pool members allowed per tenant. ' + 'A negative value means unlimited.')), + cfg.IntOpt('quota_health_monitor', + default=-1, + help=_('Number of health monitors allowed per tenant. ' + 'A negative value means unlimited.')) +] +cfg.CONF.register_opts(lbaas_quota_opts, 'QUOTAS') + class Loadbalancer(extensions.ExtensionDescriptor): diff --git a/neutron/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py b/neutron/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py index 94acdc6e04..c5de647850 100644 --- a/neutron/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py +++ b/neutron/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py @@ -28,6 +28,7 @@ from neutron.common import config from neutron.extensions import loadbalancer from neutron.openstack.common import uuidutils from neutron.plugins.common import constants +from neutron import quota from neutron.tests.unit import test_api_v2 from neutron.tests.unit import test_extensions from neutron.tests.unit import testlib_api @@ -81,6 +82,11 @@ class LoadBalancerExtensionTestCase(testlib_api.WebTestCase): ext_mgr = LoadBalancerTestExtensionManager() self.ext_mdw = test_extensions.setup_extensions_middleware(ext_mgr) self.api = webtest.TestApp(self.ext_mdw) + + quota.QUOTAS._driver = None + cfg.CONF.set_override('quota_driver', quota.QUOTA_CONF_DRIVER, + group='QUOTAS') + super(LoadBalancerExtensionTestCase, self).setUp() def tearDown(self): diff --git a/neutron/tests/unit/services/loadbalancer/test_loadbalancer_quota_ext.py b/neutron/tests/unit/services/loadbalancer/test_loadbalancer_quota_ext.py new file mode 100644 index 0000000000..fb5ebba54d --- /dev/null +++ b/neutron/tests/unit/services/loadbalancer/test_loadbalancer_quota_ext.py @@ -0,0 +1,168 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014 OpenStack Foundation. +# All Rights Reserved. +# +# 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.config import cfg + +from neutron import context +from neutron import quota +from neutron.tests.unit import test_api_v2 +from neutron.tests.unit import test_quota_ext + +_get_path = test_api_v2._get_path + + +class LBaaSQuotaExtensionTestCase( + test_quota_ext.QuotaExtensionTestCase): + + def setUp(self): + super(LBaaSQuotaExtensionTestCase, self).setUp() + cfg.CONF.set_override( + 'quota_items', + ['vip', 'pool', 'member', 'health_monitor', 'extra1'], + group='QUOTAS') + quota.register_resources_from_config() + + +class LBaaSQuotaExtensionDbTestCase(LBaaSQuotaExtensionTestCase): + fmt = 'json' + + def setUp(self): + cfg.CONF.set_override( + 'quota_driver', + 'neutron.db.quota_db.DbQuotaDriver', + group='QUOTAS') + super(LBaaSQuotaExtensionDbTestCase, self).setUp() + + def test_quotas_loaded_right(self): + res = self.api.get(_get_path('quotas', fmt=self.fmt)) + quota = self.deserialize(res) + self.assertEqual([], quota['quotas']) + self.assertEqual(200, res.status_int) + + def test_quotas_default_values(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id)} + res = self.api.get(_get_path('quotas', id=tenant_id, fmt=self.fmt), + extra_environ=env) + quota = self.deserialize(res) + self.assertEqual(10, quota['quota']['vip']) + self.assertEqual(10, quota['quota']['pool']) + self.assertEqual(-1, quota['quota']['member']) + self.assertEqual(-1, quota['quota']['health_monitor']) + self.assertEqual(-1, quota['quota']['extra1']) + + def test_show_quotas_with_admin(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id + '2', + is_admin=True)} + res = self.api.get(_get_path('quotas', id=tenant_id, fmt=self.fmt), + extra_environ=env) + self.assertEqual(200, res.status_int) + quota = self.deserialize(res) + self.assertEqual(10, quota['quota']['vip']) + self.assertEqual(10, quota['quota']['pool']) + self.assertEqual(-1, quota['quota']['member']) + self.assertEqual(-1, quota['quota']['health_monitor']) + + def test_show_quotas_with_owner_tenant(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id, + is_admin=False)} + res = self.api.get(_get_path('quotas', id=tenant_id, fmt=self.fmt), + extra_environ=env) + self.assertEqual(200, res.status_int) + quota = self.deserialize(res) + self.assertEqual(10, quota['quota']['vip']) + self.assertEqual(10, quota['quota']['pool']) + self.assertEqual(-1, quota['quota']['member']) + self.assertEqual(-1, quota['quota']['health_monitor']) + + def test_update_quotas_to_unlimited(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id, + is_admin=True)} + quotas = {'quota': {'pool': -1}} + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), extra_environ=env, + expect_errors=False) + self.assertEqual(200, res.status_int) + + def test_update_quotas_exceeding_current_limit(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id, + is_admin=True)} + quotas = {'quota': {'pool': 120}} + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), extra_environ=env, + expect_errors=False) + self.assertEqual(200, res.status_int) + + def test_update_quotas_with_admin(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id + '2', + is_admin=True)} + quotas = {'quota': {'pool': 100}} + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), extra_environ=env) + self.assertEqual(200, res.status_int) + env2 = {'neutron.context': context.Context('', tenant_id)} + res = self.api.get(_get_path('quotas', id=tenant_id, fmt=self.fmt), + extra_environ=env2) + quota = self.deserialize(res) + self.assertEqual(10, quota['quota']['vip']) + self.assertEqual(100, quota['quota']['pool']) + self.assertEqual(-1, quota['quota']['member']) + self.assertEqual(-1, quota['quota']['health_monitor']) + + +class LBaaSQuotaExtensionDbTestCaseXML(LBaaSQuotaExtensionDbTestCase): + fmt = 'xml' + + +class LBaaSQuotaExtensionCfgTestCase( + LBaaSQuotaExtensionTestCase): + + def setUp(self): + cfg.CONF.set_override( + 'quota_driver', + 'neutron.quota.ConfDriver', + group='QUOTAS') + super(LBaaSQuotaExtensionCfgTestCase, self).setUp() + + def test_quotas_default_values(self): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id)} + res = self.api.get(_get_path('quotas', id=tenant_id, fmt=self.fmt), + extra_environ=env) + quota = self.deserialize(res) + self.assertEqual(10, quota['quota']['vip']) + self.assertEqual(10, quota['quota']['pool']) + self.assertEqual(-1, quota['quota']['member']) + self.assertEqual(-1, quota['quota']['health_monitor']) + self.assertEqual(-1, quota['quota']['extra1']) + + def test_update_quotas_forbidden(self): + tenant_id = 'tenant_id1' + quotas = {'quota': {'pool': 100}} + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), + expect_errors=True) + self.assertEqual(403, res.status_int) + + +class LBaaSQuotaExtensionCfgTestCaseXML(LBaaSQuotaExtensionCfgTestCase): + fmt = 'xml'