Skip to content

Commit adca13e

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Check subnet overlapping after add router interface" into stable/yoga
2 parents 32fde52 + 2032397 commit adca13e

File tree

3 files changed

+246
-30
lines changed

3 files changed

+246
-30
lines changed

neutron/db/l3_db.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# under the License.
1414

1515
import functools
16+
import itertools
1617
import random
1718

1819
import netaddr
@@ -563,6 +564,23 @@ def _check_for_external_ip_change(self, context, gw_port, ext_ips):
563564
ip_address_change = not ip_addresses == new_ip_addresses
564565
return ip_address_change
565566

567+
def _raise_on_subnets_overlap(self, subnet_1, subnet_2):
568+
cidr = subnet_1['cidr']
569+
ipnet = netaddr.IPNetwork(cidr)
570+
new_cidr = subnet_2['cidr']
571+
new_ipnet = netaddr.IPNetwork(new_cidr)
572+
match1 = netaddr.all_matching_cidrs(new_ipnet, [cidr])
573+
match2 = netaddr.all_matching_cidrs(ipnet, [new_cidr])
574+
if match1 or match2:
575+
data = {'subnet_cidr': new_cidr,
576+
'subnet_id': subnet_2['id'],
577+
'cidr': cidr,
578+
'sub_id': subnet_1['id']}
579+
msg = (_("Cidr %(subnet_cidr)s of subnet "
580+
"%(subnet_id)s overlaps with cidr %(cidr)s "
581+
"of subnet %(sub_id)s") % data)
582+
raise n_exc.BadRequest(resource='router', msg=msg)
583+
566584
def _ensure_router_not_in_use(self, context, router_id):
567585
"""Ensure that no internal network interface is attached
568586
to the router.
@@ -669,22 +687,8 @@ def _check_for_dup_router_subnets(self, context, router,
669687
subnets = self._core_plugin.get_subnets(context.elevated(),
670688
filters=id_filter)
671689
for sub in subnets:
672-
cidr = sub['cidr']
673-
ipnet = netaddr.IPNetwork(cidr)
674690
for s in new_subnets:
675-
new_cidr = s['cidr']
676-
new_ipnet = netaddr.IPNetwork(new_cidr)
677-
match1 = netaddr.all_matching_cidrs(new_ipnet, [cidr])
678-
match2 = netaddr.all_matching_cidrs(ipnet, [new_cidr])
679-
if match1 or match2:
680-
data = {'subnet_cidr': new_cidr,
681-
'subnet_id': s['id'],
682-
'cidr': cidr,
683-
'sub_id': sub['id']}
684-
msg = (_("Cidr %(subnet_cidr)s of subnet "
685-
"%(subnet_id)s overlaps with cidr %(cidr)s "
686-
"of subnet %(sub_id)s") % data)
687-
raise n_exc.BadRequest(resource='router', msg=msg)
691+
self._raise_on_subnets_overlap(sub, s)
688692

689693
def _get_device_owner(self, context, router=None):
690694
"""Get device_owner for the specified router."""
@@ -765,17 +769,7 @@ def _validate_router_port_info(self, context, router, port_id):
765769
port = self._check_router_port(context, port_id, router.id)
766770

767771
# Only allow one router port with IPv6 subnets per network id
768-
if self._port_has_ipv6_address(port):
769-
for existing_port in (rp.port for rp in router.attached_ports):
770-
if (existing_port['network_id'] == port['network_id'] and
771-
self._port_has_ipv6_address(existing_port)):
772-
msg = _("Cannot have multiple router ports with the "
773-
"same network id if both contain IPv6 "
774-
"subnets. Existing port %(p)s has IPv6 "
775-
"subnet(s) and network id %(nid)s")
776-
raise n_exc.BadRequest(resource='router', msg=msg % {
777-
'p': existing_port['id'],
778-
'nid': existing_port['network_id']})
772+
self._validate_one_router_ipv6_port_per_network(router, port)
779773

780774
fixed_ips = list(port['fixed_ips'])
781775
subnets = []
@@ -841,6 +835,20 @@ def _port_has_ipv6_address(self, port):
841835
if netaddr.IPNetwork(fixed_ip['ip_address']).version == 6:
842836
return True
843837

838+
def _validate_one_router_ipv6_port_per_network(self, router, port):
839+
if self._port_has_ipv6_address(port):
840+
for existing_port in (rp.port for rp in router.attached_ports):
841+
if (existing_port["id"] != port["id"] and
842+
existing_port["network_id"] == port["network_id"] and
843+
self._port_has_ipv6_address(existing_port)):
844+
msg = _("Router already contains IPv6 port %(p)s "
845+
"belonging to network id %(nid)s. Only one IPv6 port "
846+
"from the same network subnet can be connected to a "
847+
"router.")
848+
raise n_exc.BadRequest(resource='router', msg=msg % {
849+
'p': existing_port['id'],
850+
'nid': existing_port['network_id']})
851+
844852
def _find_ipv6_router_port_by_network(self, context, router, net_id):
845853
router_dev_owner = self._get_device_owner(context, router)
846854
for port in router.attached_ports:
@@ -959,7 +967,7 @@ def add_router_interface(self, context, router_id, interface_info=None):
959967
port=port,
960968
interface_info=interface_info)
961969
self._add_router_port(
962-
context, port['id'], router.id, device_owner)
970+
context, port['id'], router, device_owner)
963971

964972
gw_ips = []
965973
gw_network_id = None
@@ -987,18 +995,58 @@ def add_router_interface(self, context, router_id, interface_info=None):
987995
subnets[-1]['id'], [subnet['id'] for subnet in subnets])
988996

989997
@db_api.retry_if_session_inactive()
990-
def _add_router_port(self, context, port_id, router_id, device_owner):
998+
def _add_router_port(self, context, port_id, router, device_owner):
991999
l3_obj.RouterPort(
9921000
context,
9931001
port_id=port_id,
994-
router_id=router_id,
1002+
router_id=router.id,
9951003
port_type=device_owner
9961004
).create()
1005+
1006+
# NOTE(froyo): Check after create the RouterPort if we have generated
1007+
# an overlapping. Like creation of port is an ML2 plugin command it
1008+
# runs in an isolated transaction so we could not control there the
1009+
# addition of ports to different subnets that collides in cidrs. So
1010+
# make the check here an trigger the overlaping that will remove all
1011+
# created items.
1012+
router_ports = l3_obj.RouterPort.get_objects(
1013+
context, router_id=router.id)
1014+
1015+
if len(router_ports) > 1:
1016+
subnets_id = []
1017+
for rp in router_ports:
1018+
port = port_obj.Port.get_object(context.elevated(),
1019+
id=rp.port_id)
1020+
if port:
1021+
# Only allow one router port with IPv6 subnets per network
1022+
# id
1023+
self._validate_one_router_ipv6_port_per_network(
1024+
router, port)
1025+
subnets_id.extend([fixed_ip['subnet_id']
1026+
for fixed_ip in port['fixed_ips']])
1027+
else:
1028+
raise l3_exc.RouterInterfaceNotFound(
1029+
router_id=router.id, port_id=rp.port_id)
1030+
1031+
if subnets_id:
1032+
id_filter = {'id': subnets_id}
1033+
subnets = self._core_plugin.get_subnets(context.elevated(),
1034+
filters=id_filter)
1035+
1036+
# Ignore temporary Prefix Delegation CIDRs
1037+
subnets = [
1038+
s
1039+
for s in subnets
1040+
if s["cidr"] != constants.PROVISIONAL_IPV6_PD_PREFIX
1041+
]
1042+
for sub1, sub2 in itertools.combinations(subnets, 2):
1043+
self._raise_on_subnets_overlap(sub1, sub2)
1044+
9971045
# Update owner after actual process again in order to
9981046
# make sure the records in routerports table and ports
9991047
# table are consistent.
10001048
self._core_plugin.update_port(
1001-
context, port_id, {'port': {'device_id': router_id,
1049+
context, port_id, {'port': {'device_id': router.id,
10021050
'device_owner': device_owner}})
10031051

10041052
def _check_router_interface_not_in_use(self, router_id, subnet):

neutron/tests/fullstack/test_l3_agent.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import netaddr
2020
from neutron_lib import constants
21+
from neutronclient.common import exceptions
2122
from oslo_utils import uuidutils
2223

2324
from neutron.agent.l3 import ha_router
@@ -266,6 +267,29 @@ def _is_filter_set(direction):
266267
return (_is_filter_set(constants.INGRESS_DIRECTION) and
267268
_is_filter_set(constants.EGRESS_DIRECTION))
268269

270+
def _test_concurrent_router_subnet_attachment_overlapping_cidr(self,
271+
ha=False):
272+
tenant_id = uuidutils.generate_uuid()
273+
subnet_cidr = '10.100.0.0/24'
274+
network1 = self.safe_client.create_network(
275+
tenant_id, name='foo-network1')
276+
subnet1 = self.safe_client.create_subnet(
277+
tenant_id, network1['id'], subnet_cidr)
278+
network2 = self.safe_client.create_network(
279+
tenant_id, name='foo-network2')
280+
subnet2 = self.safe_client.create_subnet(
281+
tenant_id, network2['id'], subnet_cidr)
282+
router = self.safe_client.create_router(tenant_id, ha=ha)
283+
284+
funcs = [self.safe_client.add_router_interface,
285+
self.safe_client.add_router_interface]
286+
args = [(router['id'], subnet1['id']), (router['id'], subnet2['id'])]
287+
self.assertRaises(
288+
exceptions.BadRequest,
289+
self._simulate_concurrent_requests_process_and_raise,
290+
funcs,
291+
args)
292+
269293

270294
class TestLegacyL3Agent(TestL3Agent):
271295

@@ -417,6 +441,9 @@ def test_external_subnet_changed(self):
417441
def test_router_fip_qos_after_admin_state_down_up(self):
418442
self._router_fip_qos_after_admin_state_down_up()
419443

444+
def test_concurrent_router_subnet_attachment_overlapping_cidr_(self):
445+
self._test_concurrent_router_subnet_attachment_overlapping_cidr()
446+
420447

421448
class TestHAL3Agent(TestL3Agent):
422449

@@ -573,3 +600,7 @@ def test_external_subnet_changed(self):
573600

574601
def test_router_fip_qos_after_admin_state_down_up(self):
575602
self._router_fip_qos_after_admin_state_down_up(ha=True)
603+
604+
def test_concurrent_router_subnet_attachment_overlapping_cidr_(self):
605+
self._test_concurrent_router_subnet_attachment_overlapping_cidr(
606+
ha=True)

neutron/tests/unit/db/test_l3_db.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,93 @@ def test__validate_gw_info_delete_gateway_no_route(self):
514514
self.assertIsNone(
515515
self.db._validate_gw_info(mock.ANY, info, [], router))
516516

517+
def test__raise_on_subnets_overlap_does_not_raise(self):
518+
subnets = [
519+
{'id': uuidutils.generate_uuid(),
520+
'cidr': '10.1.0.0/24'},
521+
{'id': uuidutils.generate_uuid(),
522+
'cidr': '10.2.0.0/24'}]
523+
self.db._raise_on_subnets_overlap(subnets[0], subnets[1])
524+
525+
def test__raise_on_subnets_overlap_raises(self):
526+
subnets = [
527+
{'id': uuidutils.generate_uuid(),
528+
'cidr': '10.1.0.0/20'},
529+
{'id': uuidutils.generate_uuid(),
530+
'cidr': '10.1.10.0/24'}]
531+
self.assertRaises(
532+
n_exc.BadRequest, self.db._raise_on_subnets_overlap, subnets[0],
533+
subnets[1])
534+
535+
def test__validate_one_router_ipv6_port_per_network(self):
536+
port = models_v2.Port(
537+
id=uuidutils.generate_uuid(),
538+
network_id='foo_network',
539+
fixed_ips=[models_v2.IPAllocation(
540+
ip_address=str(netaddr.IPNetwork(
541+
'2001:db8::/32').ip + 1),
542+
subnet_id='foo_subnet')])
543+
rports = [l3_models.RouterPort(router_id='foo_router', port=port)]
544+
router = l3_models.Router(
545+
id='foo_router', attached_ports=rports, route_list=[],
546+
gw_port_id=None)
547+
new_port = models_v2.Port(
548+
id=uuidutils.generate_uuid(),
549+
network_id='foo_network2',
550+
fixed_ips=[models_v2.IPAllocation(
551+
ip_address=str(netaddr.IPNetwork(
552+
'2001:db8::/32').ip + 2),
553+
subnet_id='foo_subnet')])
554+
self.db._validate_one_router_ipv6_port_per_network(
555+
router, new_port)
556+
557+
def test__validate_one_router_ipv6_port_per_network_mix_ipv4_ipv6(self):
558+
port = models_v2.Port(
559+
id=uuidutils.generate_uuid(),
560+
network_id='foo_network',
561+
fixed_ips=[models_v2.IPAllocation(
562+
ip_address=str(netaddr.IPNetwork(
563+
'10.1.10.0/24').ip + 1),
564+
subnet_id='foo_subnet')])
565+
rports = [l3_models.RouterPort(router_id='foo_router', port=port)]
566+
router = l3_models.Router(
567+
id='foo_router', attached_ports=rports, route_list=[],
568+
gw_port_id=None)
569+
new_port = models_v2.Port(
570+
id=uuidutils.generate_uuid(),
571+
network_id='foo_network',
572+
fixed_ips=[models_v2.IPAllocation(
573+
ip_address=str(netaddr.IPNetwork(
574+
'2001:db8::/32').ip + 2),
575+
subnet_id='foo_subnet')])
576+
self.db._validate_one_router_ipv6_port_per_network(
577+
router, new_port)
578+
579+
def test__validate_one_router_ipv6_port_per_network_failed(self):
580+
port = models_v2.Port(
581+
id=uuidutils.generate_uuid(),
582+
network_id='foo_network',
583+
fixed_ips=[models_v2.IPAllocation(
584+
ip_address=str(netaddr.IPNetwork(
585+
'2001:db8::/32').ip + 1),
586+
subnet_id='foo_subnet')])
587+
rports = [l3_models.RouterPort(router_id='foo_router', port=port)]
588+
router = l3_models.Router(
589+
id='foo_router', attached_ports=rports, route_list=[],
590+
gw_port_id=None)
591+
new_port = models_v2.Port(
592+
id=uuidutils.generate_uuid(),
593+
network_id='foo_network',
594+
fixed_ips=[models_v2.IPAllocation(
595+
ip_address=str(netaddr.IPNetwork(
596+
'2001:db8::/32').ip + 2),
597+
subnet_id='foo_subnet')])
598+
self.assertRaises(
599+
n_exc.BadRequest,
600+
self.db._validate_one_router_ipv6_port_per_network,
601+
router,
602+
new_port)
603+
517604

518605
class L3_NAT_db_mixin(base.BaseTestCase):
519606
def setUp(self):
@@ -697,6 +784,56 @@ def test_remove_router_interface_by_subnet(self, mock_log):
697784
mock_log.warning.not_called_once()
698785
self._check_routerports((True, False))
699786

787+
@mock.patch.object(l3_db.L3_NAT_dbonly_mixin,
788+
'_check_for_dup_router_subnets')
789+
@mock.patch.object(l3_db.L3_NAT_dbonly_mixin,
790+
'_raise_on_subnets_overlap')
791+
def test_add_router_interface_by_port_overlap_detected(
792+
self, mock_raise_on_subnets_overlap, mock_check_dup):
793+
# NOTE(froyo): On a normal behaviour this overlapping would be detected
794+
# by _check_for_dup_router_subnets, in order to evalue the code
795+
# implemented to cover the race condition when two ports are added
796+
# simultaneously using colliding cidrs we need to "fake" this method
797+
# to overpass it and check we achieve the code part that cover the case
798+
mock_check_dup.return_value = True
799+
network2 = self.create_network('network2')
800+
subnet = self.create_subnet(network2, '1.1.1.1', '1.1.1.0/24')
801+
ipa = str(netaddr.IPNetwork(subnet['subnet']['cidr']).ip + 10)
802+
fixed_ips = [{'subnet_id': subnet['subnet']['id'], 'ip_address': ipa}]
803+
port = self.create_port(
804+
network2['network']['id'], {'fixed_ips': fixed_ips})
805+
self.mixin.add_router_interface(
806+
self.ctx, self.router['id'],
807+
interface_info={'port_id': port['port']['id']})
808+
mock_raise_on_subnets_overlap.assert_not_called()
809+
self.mixin.add_router_interface(
810+
self.ctx, self.router['id'],
811+
interface_info={'port_id': self.ports[0]['port']['id']})
812+
mock_raise_on_subnets_overlap.assert_called_once()
813+
814+
@mock.patch.object(l3_db.L3_NAT_dbonly_mixin,
815+
'_check_for_dup_router_subnets')
816+
@mock.patch.object(l3_db.L3_NAT_dbonly_mixin,
817+
'_raise_on_subnets_overlap')
818+
def test_add_router_interface_by_subnet_overlap_detected(
819+
self, mock_raise_on_subnets_overlap, mock_check_dup):
820+
# NOTE(froyo): On a normal behaviour this overlapping would be detected
821+
# by _check_for_dup_router_subnets, in order to evalue the code
822+
# implemented to cover the race condition when two ports are added
823+
# simultaneously using colliding cidrs we need to "fake" this method
824+
# to overpass it and check we achieve the code part that cover the case
825+
mock_check_dup.return_value = True
826+
network2 = self.create_network('network2')
827+
subnet = self.create_subnet(network2, '1.1.1.1', '1.1.1.0/24')
828+
self.mixin.add_router_interface(
829+
self.ctx, self.router['id'],
830+
interface_info={'subnet_id': subnet['subnet']['id']})
831+
mock_raise_on_subnets_overlap.assert_not_called()
832+
self.mixin.add_router_interface(
833+
self.ctx, self.router['id'],
834+
interface_info={'subnet_id': self.subnets[0]['subnet']['id']})
835+
mock_raise_on_subnets_overlap.assert_called_once()
836+
700837
@mock.patch.object(port_obj, 'LOG')
701838
def test_remove_router_interface_by_subnet_removed_rport(self, mock_log):
702839
self._add_router_interfaces()

0 commit comments

Comments
 (0)