diff --git a/quark/plugin.py b/quark/plugin.py index aae268d..d80af9c 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -54,7 +54,9 @@ quark_resources = [ quota.BaseResource('v4_subnets_per_network', 'quota_v4_subnets_per_network'), quota.BaseResource('v6_subnets_per_network', - 'quota_v6_subnets_per_network') + 'quota_v6_subnets_per_network'), + quota.BaseResource('fixed_ips_per_port', + 'quota_fixed_ips_per_port') ] quark_quota_opts = [ @@ -81,7 +83,10 @@ quark_quota_opts = [ help=_('Maximum v4 subnets per network')), cfg.IntOpt('quota_v6_subnets_per_network', default=1, - help=_('Maximum v6 subnets per network')) + help=_('Maximum v6 subnets per network')), + cfg.IntOpt('quota_fixed_ips_per_port', + default=5, + help=_('Maximum number of fixed IPs per port')) ] diff --git a/quark/plugin_modules/ip_addresses.py b/quark/plugin_modules/ip_addresses.py index c1328a0..7085098 100644 --- a/quark/plugin_modules/ip_addresses.py +++ b/quark/plugin_modules/ip_addresses.py @@ -14,6 +14,7 @@ # under the License. from neutron.common import exceptions +from neutron import quota from oslo.config import cfg from oslo_log import log as logging import webob @@ -65,6 +66,13 @@ def validate_ports_on_network_and_same_segment(ports, network_id): msg="Segment id's do not match.") +def validate_port_ip_quotas(context, ports): + for port in ports: + addresses = port.get("ip_addresses", []) + quota.QUOTAS.limit_check(context, context.tenant_id, + fixed_ips_per_port=len(addresses) + 1) + + def _shared_ip_request(ip_address): port_ids = ip_address.get('ip_address', {}).get('port_ids', []) return len(port_ids) > 1 @@ -117,6 +125,7 @@ def create_ip_address(context, body): net_id=network_id) validate_ports_on_network_and_same_segment(ports, network_id) + validate_port_ip_quotas(context, ports) # Shared Ips are only new IPs. Two use cases: if we got device_id # or if we got port_ids. We should check the case where we got port_ids @@ -185,6 +194,7 @@ def update_ip_address(context, id, ip_address): validate_ports_on_network_and_same_segment(ports, address["network_id"]) + validate_port_ip_quotas(context, ports) LOG.info("Updating IP address, %s, to only be used by the" "following ports: %s" % (address.address_readable, diff --git a/quark/plugin_modules/ports.py b/quark/plugin_modules/ports.py index 58212b6..fcf4975 100644 --- a/quark/plugin_modules/ports.py +++ b/quark/plugin_modules/ports.py @@ -91,6 +91,11 @@ def create_port(context, port): resource="port", msg="This device is already connected to the " "requested network via another port") + # Try to fail early on quotas and save ourselves some db overhead + if fixed_ips: + quota.QUOTAS.limit_check(context, context.tenant_id, + fixed_ips_per_port=len(fixed_ips)) + if not STRATEGY.is_parent_network(net_id): # We don't honor segmented networks when they aren't "shared" segment_id = None @@ -261,6 +266,13 @@ def update_port(context, id, port): utils.filter_body(context, port_dict, admin_only=admin_only, always_filter=always_filter) + # Pre-check the requested fixed_ips before making too many db trips. + # Note that this is the only check we need, since this call replaces + # the entirety of the IP addresses document if fixed_ips are provided. + if fixed_ips: + quota.QUOTAS.limit_check(context, context.tenant_id, + fixed_ips_per_port=len(fixed_ips)) + # TODO(anyone): security groups are not currently supported on port create, # nor on isolated networks today. Please see RM8615 new_security_groups = utils.pop_param(port_dict, "security_groups") diff --git a/quark/tests/plugin_modules/test_ip_addresses.py b/quark/tests/plugin_modules/test_ip_addresses.py index f3fab55..8d0b0ba 100644 --- a/quark/tests/plugin_modules/test_ip_addresses.py +++ b/quark/tests/plugin_modules/test_ip_addresses.py @@ -146,6 +146,45 @@ class TestIpAddresses(test_quark_plugin.TestQuarkPlugin): self.plugin.create_ip_address(self.context, ip_address) +class TestCreateIpAddressQuotaCheck(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, port, addresses): + port_model = models.Port() + port_model.update(port) + + for addr in addresses: + addr_model = models.IPAddress() + addr_model.update(addr) + port_model["ip_addresses"].append(addr_model) + + with contextlib.nested( + mock.patch("quark.db.api.port_find"), + mock.patch("quark.plugin_modules.ip_addresses.ipam_driver"), + mock.patch("quark.plugin_modules.ip_addresses.db_api" + ".port_associate_ip"), + mock.patch("quark.plugin_modules.ip_addresses" + ".validate_ports_on_network_and_same_segment") + ) as (port_find, mock_ipam, mock_port_associate_ip, validate): + port_find.return_value = port_model + yield + + def test_create_ip_address_with_port_over_quota(self): + addresses = [{"id": ip, "address": ip} for ip in xrange(5)] + port = dict(id=1, network_id=2, ip_addresses=[]) + + ip = dict(id=1, address=3232235876, address_readable="192.168.1.100", + subnet_id=1, network_id=2, version=4) + + with self._stubs(port=port, addresses=addresses): + ip_address = dict(port_ids=[port["id"]]) + ip_address['version'] = 4 + ip_address['network_id'] = 2 + + with self.assertRaises(exceptions.OverQuota): + self.plugin.create_ip_address( + self.context, dict(ip_address=ip_address)) + + @mock.patch("quark.plugin_modules.ip_addresses.v") @mock.patch("quark.plugin_modules.ip_addresses" ".validate_ports_on_network_and_same_segment") @@ -514,6 +553,55 @@ class TestQuarkUpdateIPAddress(test_quark_plugin.TestQuarkPlugin): self.assertEqual(response['port_ids'], []) +class TestQuarkUpdateIPAddressQuotaCheck(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, port, addresses): + port_models = [] + addr_model = None + + port_model = models.Port() + port_model.update(port) + port_models.append(port_model) + + for addr in addresses: + addr_model = models.IPAddress() + addr_model.update(addr) + port_model["ip_addresses"].append(addr_model) + + db_mod = "quark.db.api" + with contextlib.nested( + mock.patch("%s.port_find" % db_mod), + mock.patch("%s.ip_address_find" % db_mod), + mock.patch("%s.port_associate_ip" % db_mod), + mock.patch("%s.port_disassociate_ip" % db_mod), + mock.patch("quark.plugin_modules.ip_addresses" + ".validate_ports_on_network_and_same_segment"), + mock.patch("quark.plugin_modules.ip_addresses.ipam_driver") + ) as (port_find, ip_find, port_associate_ip, port_disassociate_ip, val, + mock_ipam): + port_find.return_value = port_models + ip_find.return_value = addr_model + port_associate_ip.side_effect = _port_associate_stub + port_disassociate_ip.side_effect = _port_disassociate_stub + mock_ipam.deallocate_ip_address.side_effect = ( + _ip_deallocate_stub) + yield + + def test_update_ip_address_port_over_quota(self): + addresses = [{"id": ip, "address": ip} for ip in xrange(5)] + + port = dict(id=1, network_id=2) + ip = dict(id=1, address=3232235876, address_readable="192.168.1.100", + subnet_id=1, network_id=2, version=4, deallocated=1, + deallocated_at='2020-01-01 00:00:00') + + with self._stubs(port=port, addresses=addresses): + ip_address = {'ip_address': {"port_ids": [port]}} + with self.assertRaises(exceptions.OverQuota): + self.plugin.update_ip_address(self.admin_context, ip['id'], + ip_address) + + class TestQuarkGetIpAddress(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager def _stubs(self, ips, ports): diff --git a/quark/tests/plugin_modules/test_ports.py b/quark/tests/plugin_modules/test_ports.py index eaa428b..91a4c13 100644 --- a/quark/tests/plugin_modules/test_ports.py +++ b/quark/tests/plugin_modules/test_ports.py @@ -712,6 +712,26 @@ class TestQuarkPortCreateQuota(test_quark_plugin.TestQuarkPlugin): self.plugin.create_port(self.context, port) +class TestQuarkPortCreateFixedIpsQuota(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, network): + network["network_plugin"] = "BASE" + network["ipam_strategy"] = "ANY" + with mock.patch("quark.db.api.network_find") as net_find: + net_find.return_value = network + yield + + def test_create_port_fixed_ips_over_quota(self): + network = {"id": 1, "tenant_id": self.context.tenant_id} + fixed_ips = [{"subnet_id": 1}, {"subnet_id": 1}, {"subnet_id": 1}, + {"subnet_id": 1}, {"subnet_id": 1}, {"subnet_id": 1}] + port = {"port": {"network_id": 1, "tenant_id": self.context.tenant_id, + "device_id": 2, "fixed_ips": fixed_ips}} + with self._stubs(network=network): + with self.assertRaises(exceptions.OverQuota): + self.plugin.create_port(self.context, port) + + class TestQuarkUpdatePort(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager def _stubs(self, port, new_ips=None, parent_net=False): @@ -728,8 +748,7 @@ class TestQuarkUpdatePort(test_quark_plugin.TestQuarkPlugin): mock.patch("quark.db.api.port_update"), mock.patch("quark.ipam.QuarkIpam.allocate_ip_address"), mock.patch("quark.ipam.QuarkIpam.deallocate_ips_by_port"), - mock.patch("neutron.quota.QuotaEngine.limit_check"), - ) as (port_find, port_update, alloc_ip, dealloc_ip, limit_check): + ) as (port_find, port_update, alloc_ip, dealloc_ip): port_find.return_value = port_model port_update.return_value = port_model if new_ips: @@ -854,6 +873,20 @@ class TestQuarkUpdatePort(test_quark_plugin.TestQuarkPlugin): self.assertEqual(port_res["fixed_ips"][1]["ip_address"], str(ip2.ipv6())) + def test_update_port_goes_over_quota(self): + fixed_ips = {"fixed_ips": [{"subnet_id": 1}, + {"subnet_id": 1}, + {"subnet_id": 1}, + {"subnet_id": 1}, + {"subnet_id": 1}, + {"subnet_id": 1}]} + with self._stubs( + port=dict(id=1, name="myport", mac_address="0:0:0:0:0:1") + ) as (port_find, port_update, alloc_ip, dealloc_ip): + new_port = {"port": fixed_ips} + with self.assertRaises(exceptions.OverQuota): + self.plugin.update_port(self.context, 1, new_port) + class TestQuarkUpdatePortSecurityGroups(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager