From dc0fabf5b0dccd6a02ff017b88aee79f358b59f3 Mon Sep 17 00:00:00 2001 From: John Perkins Date: Fri, 26 Sep 2014 20:17:09 -0500 Subject: [PATCH] Enforce subnets per ip version per network quota RM#9096 --- quark/plugin.py | 10 ++ quark/plugin_modules/networks.py | 23 ++++- quark/plugin_modules/subnets.py | 18 ++++ quark/tests/plugin_modules/test_ports.py | 2 +- quark/tests/plugin_modules/test_subnets.py | 108 ++++++++++++++++++++- 5 files changed, 156 insertions(+), 5 deletions(-) diff --git a/quark/plugin.py b/quark/plugin.py index a69c224..46b7608 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -39,6 +39,10 @@ CONF = cfg.CONF quark_resources = [ quota.BaseResource('ports_per_network', 'quota_ports_per_network'), + quota.BaseResource('v6_subnets_per_network', + 'quota_v6_subnets_per_network'), + quota.BaseResource('v4_subnets_per_network', + 'quota_v4_subnets_per_network'), quota.BaseResource('routes_per_subnet', 'quota_routes_per_subnet'), quota.BaseResource('security_rules_per_group', @@ -51,6 +55,12 @@ quark_quota_opts = [ cfg.IntOpt('quota_ports_per_network', default=250, help=_('Maximum ports per network')), + cfg.IntOpt('quota_v6_subnets_per_network', + default=1, + help=_('Maximum v6 subnets per network')), + cfg.IntOpt('quota_v4_subnets_per_network', + default=1, + help=_('Maximum v4 subnets per network')), cfg.IntOpt('quota_routes_per_subnet', default=3, help=_('Maximum routes per subnet')), diff --git a/quark/plugin_modules/networks.py b/quark/plugin_modules/networks.py index 47e3a95..15a4967 100644 --- a/quark/plugin_modules/networks.py +++ b/quark/plugin_modules/networks.py @@ -13,11 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +import netaddr from neutron.common import exceptions from neutron.extensions import providernet as pnet from neutron.openstack.common import importutils from neutron.openstack.common import log as logging from neutron.openstack.common import uuidutils +from neutron import quota from oslo.config import cfg from quark.db import api as db_api @@ -60,8 +62,25 @@ def create_network(context, network): LOG.info("create_network for tenant %s" % context.tenant_id) with context.session.begin(): - # Generate a uuid that we're going to hand to the backend and db net_attrs = network["network"] + subs = net_attrs.pop("subnets", []) + # Enforce subnet quotas + if len(subs) > 0: + v4_count, v6_count = 0, 0 + for s in subs: + version = netaddr.IPNetwork(s['subnet']['cidr']).version + if version == 6: + v6_count += 1 + else: + v4_count += 1 + if v4_count > 0: + quota.QUOTAS.limit_check(context, context.tenant_id, + v4_subnets_per_network=v4_count) + if v6_count > 0: + quota.QUOTAS.limit_check(context, context.tenant_id, + v6_subnets_per_network=v6_count) + + # Generate a uuid that we're going to hand to the backend and db net_uuid = utils.pop_param(net_attrs, "id", None) net_type = None if net_uuid and context.is_admin: @@ -96,8 +115,6 @@ def create_network(context, network): network_id=net_uuid, phys_type=pnet_type, phys_net=phys_net, segment_id=seg_id) - subs = net_attrs.pop("subnets", []) - net_attrs["id"] = net_uuid net_attrs["tenant_id"] = context.tenant_id net_attrs["network_plugin"] = default_net_type diff --git a/quark/plugin_modules/subnets.py b/quark/plugin_modules/subnets.py index 1bf4068..b451fd9 100644 --- a/quark/plugin_modules/subnets.py +++ b/quark/plugin_modules/subnets.py @@ -20,6 +20,7 @@ from neutron.common import rpc as n_rpc from neutron.openstack.common import importutils from neutron.openstack.common import log as logging from neutron.openstack.common import timeutils +from neutron import quota from oslo.config import cfg from quark import allocation_pool @@ -115,6 +116,23 @@ def create_subnet(context, subnet): err_vals["prefix"] = 31 err_msg = err % err_vals raise exceptions.InvalidInput(error_message=err_msg) + # Enforce subnet quotas + net_subnets = get_subnets(context, + filters=dict(network_id=net_id)) + + v4_count, v6_count = 0, 0 + for subnet in net_subnets: + if netaddr.IPNetwork(subnet['cidr']).version == 6: + v6_count += 1 + else: + v4_count += 1 + + if cidr.version == 6: + quota.QUOTAS.limit_check(context, context.tenant_id, + v6_subnets_per_network=v6_count + 1) + else: + quota.QUOTAS.limit_check(context, context.tenant_id, + v4_subnets_per_network=v4_count + 1) # See RM981. The default behavior of setting a gateway unless # explicitly asked to not is no longer desirable. diff --git a/quark/tests/plugin_modules/test_ports.py b/quark/tests/plugin_modules/test_ports.py index b4eda50..d91cd4e 100644 --- a/quark/tests/plugin_modules/test_ports.py +++ b/quark/tests/plugin_modules/test_ports.py @@ -240,7 +240,7 @@ class TestQuarkCreatePortFailure(test_quark_plugin.TestQuarkPlugin): self.plugin.create_port(self.context, port_2) -class TestQuarkCreatePort(test_quark_plugin.TestQuarkPlugin): +class TestQuarkCreatePortsSameDevBadRequest(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager def _stubs(self, port=None, network=None, addr=None, mac=None, limit_checks=None): diff --git a/quark/tests/plugin_modules/test_subnets.py b/quark/tests/plugin_modules/test_subnets.py index 7e4186e..786636a 100644 --- a/quark/tests/plugin_modules/test_subnets.py +++ b/quark/tests/plugin_modules/test_subnets.py @@ -1037,6 +1037,110 @@ class TestQuarkDeleteSubnet(test_quark_plugin.TestQuarkPlugin): self.plugin.delete_subnet(self.context, 1) +class TestSubnetsQuotas(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, subnet_values, deleted_at=None): + class FakeContext(object): + def __enter__(*args, **kwargs): + pass + + def __exit__(*args, **kwargs): + pass + + self.context.session.begin = FakeContext + subnets = list() + for s in subnet_values: + s["network"] = models.Network() + s["network"]["created_at"] = s["created_at"] + subnet = models.Subnet(**s) + subnets.append(subnet) + db_mod = "quark.db.api" + rpc_mod = "neutron.common.rpc" + time_mod = "neutron.openstack.common.timeutils" + sub_plugin_mod = "quark.plugin_modules.subnets" + with contextlib.nested( + mock.patch("%s.get_subnets" % sub_plugin_mod), + mock.patch("%s.subnet_find" % db_mod), + mock.patch("%s.network_find" % db_mod), + mock.patch("%s.subnet_create" % db_mod), + mock.patch("%s.subnet_delete" % db_mod), + mock.patch("%s.get_notifier" % rpc_mod), + mock.patch("%s.utcnow" % time_mod), + mock.patch("%s._validate_subnet_cidr" % sub_plugin_mod) + ) as (get_subnets, sub_find, net_find, sub_create, sub_del, notify, + time_func, sub_validate): + sub_create.return_value = subnets[0] + sub_find.return_value = subnets[0] + retsubs = [] + if len(subnets) > 1: + retsubs = subnets[1:] + get_subnets.return_value = retsubs + time_func.return_value = deleted_at + yield notify + + def test_create_subnet_v4_alongside_v6_quota_pass(self): + original_4 = cfg.CONF.QUOTAS.quota_v4_subnets_per_network + original_6 = cfg.CONF.QUOTAS.quota_v6_subnets_per_network + s = [dict(network_id=1, cidr="192.167.10.0/24", + tenant_id=1, id=1, created_at="123"), + dict(network_id=1, cidr="::0/24", + tenant_id=1, id=2, created_at="123")] + with self._stubs(s): + cfg.CONF.set_override('quota_v4_subnets_per_network', 1, "QUOTAS") + cfg.CONF.set_override('quota_v6_subnets_per_network', 1, "QUOTAS") + self.plugin.create_subnet(self.context, dict(subnet=s[0])) + cfg.CONF.set_override('quota_v4_subnets_per_network', original_4, + "QUOTAS") + cfg.CONF.set_override('quota_v6_subnets_per_network', original_6, + "QUOTAS") + + def test_create_subnet_v4_quota_pass(self): + original_4 = cfg.CONF.QUOTAS.quota_v4_subnets_per_network + s = [dict(network_id=1, cidr="192.167.10.0/24", + tenant_id=1, id=1, created_at="123")] + with self._stubs(s): + cfg.CONF.set_override('quota_v4_subnets_per_network', 1, "QUOTAS") + self.plugin.create_subnet(self.context, dict(subnet=s[0])) + cfg.CONF.set_override('quota_v4_subnets_per_network', original_4, + "QUOTAS") + + def test_create_subnet_v6_quota_pass(self): + original_6 = cfg.CONF.QUOTAS.quota_v6_subnets_per_network + s = [dict(network_id=1, cidr="::0/24", + tenant_id=1, id=1, created_at="123")] + with self._stubs(s): + cfg.CONF.set_override('quota_v6_subnets_per_network', 1, "QUOTAS") + self.plugin.create_subnet(self.context, dict(subnet=s[0])) + cfg.CONF.set_override('quota_v6_subnets_per_network', original_6, + "QUOTAS") + + def test_create_subnet_v4_quota_fail(self): + original_4 = cfg.CONF.QUOTAS.quota_v4_subnets_per_network + s = [dict(network_id=1, cidr="192.167.10.0/24", + tenant_id=1, id=1, created_at="123"), + dict(network_id=1, cidr="192.168.10.0/24", + tenant_id=1, id=2, created_at="124")] + with self._stubs(s): + cfg.CONF.set_override('quota_v4_subnets_per_network', 1, "QUOTAS") + with self.assertRaises(exceptions.OverQuota): + self.plugin.create_subnet(self.context, dict(subnet=s[0])) + cfg.CONF.set_override('quota_v4_subnets_per_network', original_4, + "QUOTAS") + + def test_create_subnet_v6_quota_fail(self): + original_6 = cfg.CONF.QUOTAS.quota_v6_subnets_per_network + s = [dict(network_id=1, cidr="::0/24", + tenant_id=1, id=1, created_at="123"), + dict(network_id=1, cidr="::1/24", + tenant_id=1, id=2, created_at="124")] + with self._stubs(s): + cfg.CONF.set_override('quota_v6_subnets_per_network', 1, "QUOTAS") + with self.assertRaises(exceptions.OverQuota): + self.plugin.create_subnet(self.context, dict(subnet=s[0])) + cfg.CONF.set_override('quota_v6_subnets_per_network', original_6, + "QUOTAS") + + class TestSubnetsNotification(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager def _stubs(self, s, deleted_at=None): @@ -1057,6 +1161,7 @@ class TestSubnetsNotification(test_quark_plugin.TestQuarkPlugin): time_mod = "neutron.openstack.common.timeutils" sub_plugin_mod = "quark.plugin_modules.subnets" with contextlib.nested( + mock.patch("%s.get_subnets" % sub_plugin_mod), mock.patch("%s.subnet_find" % db_mod), mock.patch("%s.network_find" % db_mod), mock.patch("%s.subnet_create" % db_mod), @@ -1064,9 +1169,10 @@ class TestSubnetsNotification(test_quark_plugin.TestQuarkPlugin): mock.patch("%s.get_notifier" % rpc_mod), mock.patch("%s.utcnow" % time_mod), mock.patch("%s._validate_subnet_cidr" % sub_plugin_mod) - ) as (sub_find, net_find, sub_create, sub_del, notify, + ) as (get_subnets, sub_find, net_find, sub_create, sub_del, notify, time_func, sub_validate): sub_create.return_value = subnet + get_subnets.return_value = [] sub_find.return_value = subnet time_func.return_value = deleted_at yield notify