Skip to content

Commit 2032397

Browse files
Check subnet overlapping after add router interface
When simultaneous attempts are made to add an interface to the same router including overlapping networks in cidrs, both attempts are successful. There is a check to avoid this overlap but is performed when creating the network interface and it is done over the ports already attached to the router, so at this moment the check is not able to detect the overlapping. Furthermore, the create_port operation over the ML2 plugin must be executed in isolated transactions, so trying to control the execution context or adding additional steps to the transaction is not feasible. This patch checks once the RouterPort is created on the neutron database if there is more than one overlapping port, triggering in that case the exception that will remove the the culprit of overlapping. Conflicts: neutron/db/l3_db.py (manually cherry picked from commit 1abb77d) Closes-Bug: #1987666 Change-Id: I7cec8b53e72e7abf34012906e6adfecf079525af (cherry picked from commit 1abb77d)
1 parent 4465987 commit 2032397

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)