From a795004b89d698f09f155e3bcf0a95facdae02d8 Mon Sep 17 00:00:00 2001 From: Amir Sadoughi Date: Sun, 23 Jun 2013 10:27:32 -0500 Subject: [PATCH] Implemented subnet.allocation_pools + unit tests subnet.allocation_pools depends on the IP policies implementation. --- quark/api/extensions/subnets_quark.py | 4 -- quark/ipam.py | 9 ++-- quark/plugin.py | 11 ++++ quark/plugin_views.py | 29 ++++++++++- quark/tests/test_quark_plugin.py | 73 ++++++++++++++++++++++----- 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/quark/api/extensions/subnets_quark.py b/quark/api/extensions/subnets_quark.py index 9b4079d..4ce495d 100644 --- a/quark/api/extensions/subnets_quark.py +++ b/quark/api/extensions/subnets_quark.py @@ -16,13 +16,9 @@ # under the License. from neutron.api import extensions -from neutron.api.v2 import attributes EXTENDED_ATTRIBUTES_2_0 = { 'subnets': { - "allocation_pools": {'allow_post': True, 'allow_put': True, - 'default': attributes.ATTR_NOT_SPECIFIED, - 'is_visible': False}, "enable_dhcp": {'allow_post': False, 'allow_put': False, 'default': False, 'is_visible': True}, diff --git a/quark/ipam.py b/quark/ipam.py index caf6c6b..70ee830 100644 --- a/quark/ipam.py +++ b/quark/ipam.py @@ -30,7 +30,8 @@ LOG = logging.getLogger("neutron") class QuarkIpam(object): - def _get_ip_policy_rule_set(self, subnet): + @staticmethod + def get_ip_policy_rule_set(subnet): ip_policy = subnet["ip_policy"] or \ subnet["network"]["ip_policy"] or \ dict() @@ -38,7 +39,7 @@ class QuarkIpam(object): ip_policy_rules = netaddr.IPSet( [netaddr.IPNetwork((int(ippr["address"]), ippr["prefix"])) for ippr in ip_policy_rules]) - subnet_set = netaddr.IPSet(netaddr.IPNetwork(subnet["cidr"])) + subnet_set = netaddr.IPSet([netaddr.IPNetwork(subnet["cidr"])]) ip_policy_rules = subnet_set & ip_policy_rules return ip_policy_rules @@ -57,7 +58,7 @@ class QuarkIpam(object): ip_policy_rules = None if not ip_address: - ip_policy_rules = self._get_ip_policy_rule_set(subnet) + ip_policy_rules = self.get_ip_policy_rule_set(subnet) policy_size = ip_policy_rules.size if ip_policy_rules else 0 if ipnet.size > (ips_in_subnet + policy_size): return subnet @@ -117,7 +118,7 @@ class QuarkIpam(object): subnet = self._choose_available_subnet( elevated, net_id, ip_address=ip_address, version=version) - ip_policy_rules = self._get_ip_policy_rule_set(subnet) + ip_policy_rules = self.get_ip_policy_rule_set(subnet) # Creating this IP for the first time next_ip = None diff --git a/quark/plugin.py b/quark/plugin.py index f7c0092..73c3d47 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -227,6 +227,7 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, gateway_ip = _pop_param(sub_attrs, "gateway_ip", str(cidr[1])) dns_ips = _pop_param(sub_attrs, "dns_nameservers", []) routes = _pop_param(sub_attrs, "host_routes", []) + allocation_pools = _pop_param(sub_attrs, "allocation_pools", []) new_subnet = db_api.subnet_create(context, **sub_attrs) @@ -246,6 +247,16 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, new_subnet["dns_nameservers"].append(db_api.dns_create( context, ip=netaddr.IPAddress(dns_ip))) + if allocation_pools: + exclude = netaddr.IPSet([cidr]) + for p in allocation_pools: + x = netaddr.IPSet(netaddr.IPRange(p["start"], p["end"])) + exclude = exclude - x + new_subnet["ip_policy"] = db_api.ip_policy_create(context, + exclude=exclude) + # HACK(amir): force backref for ip_policy + if not new_subnet["network"]: + new_subnet["network"] = net subnet_dict = v._make_subnet_dict(new_subnet, default_route=DEFAULT_ROUTE) subnet_dict["gateway_ip"] = gateway_ip diff --git a/quark/plugin_views.py b/quark/plugin_views.py index 81c9635..b03330a 100644 --- a/quark/plugin_views.py +++ b/quark/plugin_views.py @@ -19,6 +19,7 @@ View Helpers for Quark Plugin import netaddr +from quark.ipam import QuarkIpam from quark import network_strategy STRATEGY = network_strategy.STRATEGY @@ -42,12 +43,38 @@ def _make_subnet_dict(subnet, default_route=None, fields=None): dns_nameservers = [str(netaddr.IPAddress(dns["ip"])) for dns in subnet.get("dns_nameservers")] net_id = STRATEGY.get_parent_network(subnet["network_id"]) + + def _allocation_pools(subnet): + ip_policy_rules = QuarkIpam.get_ip_policy_rule_set(subnet) + cidr = netaddr.IPSet([netaddr.IPNetwork(subnet["cidr"])]) + allocatable = cidr - ip_policy_rules + + cidrs = allocatable.iter_cidrs() + if len(cidrs) == 0: + return [] + if len(cidrs) == 1: + return [dict(start=str(cidrs[0][0]), + end=str(cidrs[0][-1]))] + + pool_start = cidrs[0][0] + prev_cidr_end = cidrs[0][-1] + pools = [] + for cidr in cidrs[1:]: + cidr_start = cidr[0] + if prev_cidr_end + 1 != cidr_start: + pools.append(dict(start=str(pool_start), + end=str(prev_cidr_end))) + pool_start = cidr_start + prev_cidr_end = cidr[-1] + pools.append(dict(start=str(pool_start), end=str(prev_cidr_end))) + return pools + res = {"id": subnet.get('id'), "name": subnet.get('name'), "tenant_id": subnet.get('tenant_id'), "network_id": net_id, "ip_version": subnet.get('ip_version'), - "allocation_pools": [], + "allocation_pools": _allocation_pools(subnet), "dns_nameservers": dns_nameservers or [], "cidr": subnet.get('cidr'), "enable_dhcp": None} diff --git a/quark/tests/test_quark_plugin.py b/quark/tests/test_quark_plugin.py index 1bb301e..34d81e7 100644 --- a/quark/tests/test_quark_plugin.py +++ b/quark/tests/test_quark_plugin.py @@ -88,11 +88,11 @@ class TestQuarkGetSubnets(TestQuarkPlugin): for subnet in subnets: s_dict = subnet.copy() s_dict["routes"] = route_models - s = models.Subnet() + s = models.Subnet(network=models.Network()) s.update(s_dict) subnet_models.append(s) elif subnets: - mod = models.Subnet() + mod = models.Subnet(network=models.Network()) mod.update(subnets) mod["routes"] = route_models subnet_models = mod @@ -110,7 +110,7 @@ class TestQuarkGetSubnets(TestQuarkPlugin): subnet = dict(id=subnet_id, network_id=1, name=subnet_id, tenant_id=self.context.tenant_id, ip_version=4, cidr="192.168.0.0/24", gateway_ip="192.168.0.1", - allocation_pools=[], dns_nameservers=[], + dns_nameservers=[], enable_dhcp=None) expected_route = dict(destination=route["cidr"], nexthop=route["gateway"]) @@ -140,7 +140,7 @@ class TestQuarkGetSubnets(TestQuarkPlugin): subnet = dict(id=subnet_id, network_id=1, name=subnet_id, tenant_id=self.context.tenant_id, ip_version=4, cidr="192.168.0.0/24", gateway_ip="192.168.0.1", - allocation_pools=[], dns_nameservers=[], + dns_nameservers=[], enable_dhcp=None) with self._stubs(subnets=subnet, routes=[route]): @@ -173,6 +173,9 @@ class TestQuarkCreateSubnetOverlapping(TestQuarkPlugin): ) as (net_find, subnet_find, subnet_create): net_find.return_value = network subnet_find.return_value = subnet_models + subnet_create.return_value = models.Subnet( + network=models.Network(), + cidr="192.168.1.1/24") yield subnet_create def test_create_subnet_overlapping_true(self): @@ -206,13 +209,63 @@ class TestQuarkCreateSubnetOverlapping(TestQuarkPlugin): self.plugin.create_subnet(self.context, s) +class TestQuarkCreateSubnetAllocationPools(TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, subnet): + s = models.Subnet(network=models.Network(id=1, subnets=[])) + s.update(subnet) + + with contextlib.nested( + mock.patch("quark.db.api.network_find"), + mock.patch("quark.db.api.subnet_find"), + mock.patch("quark.db.api.subnet_create") + ) as (net_find, subnet_find, subnet_create): + net_find.return_value = s["network"] + subnet_find.return_value = [] + subnet_create.return_value = s + yield subnet_create + + def test_create_subnet_allocation_pools_zero(self): + s = dict(subnet=dict( + cidr="192.168.1.1/24", + network_id=1)) + with self._stubs(s["subnet"]) as subnet_create: + resp = self.plugin.create_subnet(self.context, s) + self.assertEqual(subnet_create.call_count, 1) + self.assertEqual(resp["allocation_pools"], + [dict(start="192.168.1.0", end="192.168.1.255")]) + + def test_create_subnet_allocation_pools_one(self): + pools = [dict(start="192.168.1.10", end="192.168.1.20")] + s = dict(subnet=dict( + allocation_pools=pools, + cidr="192.168.1.1/24", + network_id=1)) + with self._stubs(s["subnet"]) as subnet_create: + resp = self.plugin.create_subnet(self.context, s) + self.assertEqual(subnet_create.call_count, 1) + self.assertEqual(resp["allocation_pools"], pools) + + def test_create_subnet_allocation_pools_two(self): + pools = [dict(start="192.168.1.10", end="192.168.1.20"), + dict(start="192.168.1.40", end="192.168.1.50")] + s = dict(subnet=dict( + allocation_pools=pools, + cidr="192.168.1.1/24", + network_id=1)) + with self._stubs(s["subnet"]) as subnet_create: + resp = self.plugin.create_subnet(self.context, s) + self.assertEqual(subnet_create.call_count, 1) + self.assertEqual(resp["allocation_pools"], pools) + + # TODO(amir): Refactor the tests to test individual subnet attributes. # * copy.deepcopy was necessary to maintain tests on keys, which is a bit ugly. # * workaround is also in place for lame ATTR_NOT_SPECIFIED object() class TestQuarkCreateSubnet(TestQuarkPlugin): @contextlib.contextmanager def _stubs(self, subnet=None, network=None, routes=None, dns=None): - subnet_mod = models.Subnet() + subnet_mod = models.Subnet(network=models.Network()) dns_ips = subnet.pop("dns_nameservers", []) host_routes = subnet.pop("host_routes", []) subnet_mod.update(subnet) @@ -241,7 +294,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): subnet=dict(network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", gateway_ip="0.0.0.0", - allocation_pools=[], dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED, host_routes=neutron_attrs.ATTR_NOT_SPECIFIED, enable_dhcp=None)) @@ -280,7 +332,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): subnet=dict(network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", - allocation_pools=[], gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED, dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED, enable_dhcp=None)) @@ -315,7 +366,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): subnet=dict(network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", gateway_ip="0.0.0.0", - allocation_pools=[], dns_nameservers=["4.2.2.1", "4.2.2.2"], enable_dhcp=None)) network = dict(network_id=1) @@ -344,7 +394,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): subnet=dict(network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", gateway_ip="0.0.0.0", - allocation_pools=[], dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED, host_routes=[{"destination": "1.1.1.1/8", "nexthop": "172.16.0.4"}], @@ -378,7 +427,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): subnet=dict(network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", - allocation_pools=[], gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED, dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED, host_routes=[{"destination": "0.0.0.0/0", @@ -419,7 +467,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin): tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", gateway_ip="172.16.0.3", - allocation_pools=[], dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED, host_routes=[{"destination": "0.0.0.0/0", "nexthop": "172.16.0.4"}], @@ -472,7 +519,6 @@ class TestQuarkUpdateSubnet(TestQuarkPlugin): network_id=1, tenant_id=self.context.tenant_id, ip_version=4, cidr="172.16.0.0/24", - allocation_pools=[], host_routes=host_routes, dns_nameservers=["4.2.2.1", "4.2.2.2"], enable_dhcp=None) @@ -502,7 +548,8 @@ class TestQuarkUpdateSubnet(TestQuarkPlugin): subnet_find.return_value = subnet_mod route_find.return_value = subnet_mod["routes"][0] \ if subnet_mod["routes"] and find_routes else None - new_subnet_mod = copy.deepcopy(subnet_mod) + new_subnet_mod = models.Subnet(network=models.Network()) + new_subnet_mod.update(subnet_mod) if new_routes: new_subnet_mod["routes"] = new_routes if new_dns_servers: