From f450886ff93a201c6520d84088c3a97330814140 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 13 May 2021 06:13:57 +0000 Subject: [PATCH] Allow the use of legacy routers within RPN segments This patch adds to legacy routers (no HA nor DVR) to be connected to a router provider network segment through the gateway interface. The router will be connected to one single segment of the RPN; that means the router will have L2 connectivity to one single subnet. The gateway router port will have an IP address on the subnet CIDR; that will provide connectivity to the broadcast domain of this CIDR (as usual, that doesn't change). The router, in other scenarios, adds the other subnet CIDRs to the router namespace routing table. That allows to SNAT any packet to those CIDRs through the gateway port. In the RPN case those routes are not added because there is no broadcast connectivity with the other subnets. Any packet that needs to reach these other subents, should go through the local segment gateway IP address. This default route is added always into the router namespace. Closes-Bug: #1923592 Change-Id: Ib66b1d7b60eb0ac0a9e3dfd08aae29cb03abde34 --- doc/source/admin/config-routed-networks.rst | 84 +++++++++++++++++++++ neutron/db/l3_db.py | 25 +++++- neutron/extensions/segment.py | 4 + neutron/tests/unit/db/test_l3_db.py | 74 +++++++++++++++++- 4 files changed, 181 insertions(+), 6 deletions(-) diff --git a/doc/source/admin/config-routed-networks.rst b/doc/source/admin/config-routed-networks.rst index 8c78d9a9ea2..5b033764eb7 100644 --- a/doc/source/admin/config-routed-networks.rst +++ b/doc/source/admin/config-routed-networks.rst @@ -524,3 +524,87 @@ one segment to a routed one. +------------+--------------------------------------+ | segment_id | 81e5453d-4c9f-43a5-8ddf-feaf3937e8c7 | +------------+--------------------------------------+ + + +Routed provider networks as external networks for tenant routed networks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + This section applies only to legacy routers, not DVR nor HA routers. A + legacy router has a single instance that is hosted in one single host. + +One of the consequences of this feature is the externalization of any routing +operation. The communication (routing) between segments is done using the +underlying network infrastructure, not managed by Neutron. + +Could be the case that the user needs to split the communication between +several hosts. It is possible to create tenant networks and connect them using +a router. To access to the router provider network, it should be connected +as router gateway. + +.. code-block:: bash + + Tenant net1 ┌─────────────────────┐ + ─────────────┤ │ + │ │ Routed provided network + │ GW port ├──────────────────────── + Tenant net2 │ │ + ─────────────┤ │ + └─────────────────────┘ + +The routed provider network, acting as router gateway, contains all subnets +associated to the segments. In a deployment without router provided networks, +the gateway port has L2 connectivity to all subnet CIDRs. In this case, the +gateway port has only connectivity to the attached segment subnets and its +L2 broadcast domains. + +The L3 agent will create, inside the router namespace, a default route in the +gateway port fixed IP CIDR. For each other subnet no belonging to the port +fixed IP address, a onlink route is created. These routes use the gateway port +as routing device and allow to route any packet with destination on these +CIDRs through this port. + +The problem in the case of connecting the gatewat port to a routed provider +network is that it will have broadcast connectivity only to those subnets +that belong to the host segment: + +* One of those subnets will provide the port IP address. The gateway IP address + of this subnet will be the default route, through the gateway port. +* Any other subnet belonging to this segment will create a onlink route, using + the gateway port as route device. + +For example, let's consider the following configuration: + +* Two tenant networks with CIDRs 10.1.0.0/24 and 10.2.0.0/24. +* A RPN with two segments; each segment with two subnets: segment 1 with + 10.51.0.0/24 and 10.52.0.0/24, segment 2 with 10.53.0.0/24 and 10.54.0.0/24. +* The router is connected to the first segment and the gateway port has an IP + address in the range of 10.51.0.0/24. This is why the default route uses + an IP address in this range. + +Without considering that the gateway network is a router provided network, this +is the routing table set in the router namespace: + +.. code-block:: bash + + $ ip netns exec $r ip r + default via 10.51.0.1 dev qg-gwport proto static + 10.1.0.0/24 dev qr-tenant1 proto kernel scope link src 10.1.0.1 + 10.2.0.0/24 dev qr-tenant2 proto kernel scope link src 10.2.0.1 + 10.51.0.0/24 dev qg-gwport proto kernel scope link src 10.100.0.15 + 10.52.0.0/24 dev qg-gwport proto static scope link + 10.53.0.0/24 dev qg-gwport proto static scope link <-- should be removed, belongs to segment 2 + 10.54.0.0/24 dev qg-gwport proto static scope link <-- should be removed, belongs to segment 2 + +Those packets sent to 10.53.0.0/24 and 10.54.0.0/24 (the second RPN subnet +CIDRs), don't have L2 connectivity and the ARP packets won't be replied. In the +case of having a RPN as gateway network, all packets exiting the router through +the gateway, must be sent to the gateway IP address, in this case 10.51.0.1. +This is why the L3 plugin does not send the information of other segments +subnets L3 agent when: + +* The network is the router gateway. +* The "segments" plugin is enabled; this plugin is needed for routed provided + networks. +* The network is connected to a segment. diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index e89f9fa885e..59e5b525efb 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -51,6 +51,7 @@ from neutron.db import models_v2 from neutron.db import standardattrdescription_db as st_attr from neutron.extensions import l3 from neutron.extensions import qos_fip +from neutron.extensions import segment as segment_ext from neutron.objects import base as base_obj from neutron.objects import port_forwarding from neutron.objects import ports as port_obj @@ -1767,7 +1768,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, query = query.filter(models_v2.Subnet.network_id.in_(network_ids)) fields = ['id', 'cidr', 'gateway_ip', 'dns_nameservers', - 'network_id', 'ipv6_ra_mode', 'subnetpool_id'] + 'network_id', 'ipv6_ra_mode', 'subnetpool_id', 'segment_id'] def make_subnet_dict_with_scope(row): subnet_db, address_scope_id = row @@ -1797,6 +1798,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, These ports already have fixed_ips populated. """ + seg_plugin_loaded = segment_ext.SegmentPluginBase.is_loaded() network_ids = [p['network_id'] for p in self._each_port_having_fixed_ips(ports)] @@ -1805,12 +1807,19 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, context, network_ids) for port in self._each_port_having_fixed_ips(ports): - + is_gw = port['device_owner'] == constants.DEVICE_OWNER_ROUTER_GW port['subnets'] = [] port['extra_subnets'] = [] port['address_scopes'] = {constants.IP_VERSION_4: None, constants.IP_VERSION_6: None} + gw_port_segment = None + if is_gw and seg_plugin_loaded: + for subnet in subnets_by_network[port['network_id']]: + if subnet['id'] == port['fixed_ips'][0]['subnet_id']: + gw_port_segment = subnet['segment_id'] + break + scopes = {} for subnet in subnets_by_network[port['network_id']]: scope = subnet['address_scope_id'] @@ -1834,8 +1843,16 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, fixed_ip['prefixlen'] = prefixlen break else: - # This subnet is not used by the port. - port['extra_subnets'].append(subnet_info) + # NOTE(ralonsoh): if this is the gateway port and is + # connected to a router provider network, it will have L2 + # connectivity only to the subnets in the same segment. + # The router will add only the onlink routes to those + # subnet CIDRs. + if is_gw and seg_plugin_loaded: + if subnet['segment_id'] == gw_port_segment: + port['extra_subnets'].append(subnet_info) + else: + port['extra_subnets'].append(subnet_info) port['address_scopes'].update(scopes) port['mtu'] = mtus_by_network.get(port['network_id'], 0) diff --git a/neutron/extensions/segment.py b/neutron/extensions/segment.py index dbc3b462141..2d38c52cb32 100644 --- a/neutron/extensions/segment.py +++ b/neutron/extensions/segment.py @@ -248,3 +248,7 @@ class SegmentPluginBase(object, metaclass=abc.ABCMeta): @classmethod def get_plugin_type(cls): return SEGMENTS + + @classmethod + def is_loaded(cls): + return cls.get_plugin_type() in directory.get_plugins() diff --git a/neutron/tests/unit/db/test_l3_db.py b/neutron/tests/unit/db/test_l3_db.py index 70cb49320dd..e55dc006d8b 100644 --- a/neutron/tests/unit/db/test_l3_db.py +++ b/neutron/tests/unit/db/test_l3_db.py @@ -15,6 +15,7 @@ from unittest import mock +import ddt import netaddr from neutron_lib.callbacks import events from neutron_lib.callbacks import registry @@ -31,6 +32,7 @@ import testtools from neutron.db import l3_db from neutron.db.models import l3 as l3_models +from neutron.extensions import segment as segment_ext from neutron.objects import base as base_obj from neutron.objects import network as network_obj from neutron.objects import ports as port_obj @@ -40,6 +42,7 @@ from neutron.tests import base from neutron.tests.unit.db import test_db_base_plugin_v2 +@ddt.ddt class TestL3_NAT_dbonly_mixin( test_db_base_plugin_v2.NeutronDbPluginV2TestCase): @@ -135,7 +138,8 @@ class TestL3_NAT_dbonly_mixin( ports = [{'network_id': 'net_id', 'id': 'port_id', - 'fixed_ips': [{'subnet_id': mock.sentinel.subnet_id}]}] + 'fixed_ips': [{'subnet_id': mock.sentinel.subnet_id}], + 'device_owner': 'compute:nova'}] with mock.patch.object(directory, 'get_plugin') as get_p: get_p().get_networks.return_value = [{'id': 'net_id', 'mtu': 1446}] self.db._populate_mtu_and_subnets_for_ports(mock.sentinel.context, @@ -151,7 +155,73 @@ class TestL3_NAT_dbonly_mixin( 'mtu': 1446, 'network_id': 'net_id', 'subnets': [{k: subnet[k] for k in keys}], - 'address_scopes': address_scopes}], ports) + 'address_scopes': address_scopes, + 'device_owner': 'compute:nova'}], ports) + + @ddt.unpack + @ddt.data({'plugin_loaded': False, 'seg1': None, 'seg2': None}, + {'plugin_loaded': True, 'seg1': None, 'seg2': None}, + {'plugin_loaded': True, 'seg1': 'seg1', 'seg2': 'seg2'}) + @mock.patch.object(l3_db.L3_NAT_dbonly_mixin, + '_get_subnets_by_network_list') + def test__populate_ports_for_subnets_gw_port(self, get_subnets_by_network, + plugin_loaded, seg1, seg2): + subnets = [ + {'id': uuidutils.generate_uuid(), + 'cidr': '10.1.0.0/24', + 'gateway_ip': mock.sentinel.gateway_ip, + 'dns_nameservers': mock.sentinel.dns_nameservers, + 'ipv6_ra_mode': mock.sentinel.ipv6_ra_mode, + 'subnetpool_id': mock.sentinel.subnetpool_id, + 'address_scope_id': mock.sentinel.address_scope_id, + 'segment_id': seg1}, + {'id': uuidutils.generate_uuid(), + 'cidr': '10.2.0.0/24', + 'gateway_ip': mock.sentinel.gateway_ip, + 'dns_nameservers': mock.sentinel.dns_nameservers, + 'ipv6_ra_mode': mock.sentinel.ipv6_ra_mode, + 'subnetpool_id': mock.sentinel.subnetpool_id, + 'address_scope_id': mock.sentinel.address_scope_id, + 'segment_id': seg1}, + {'id': uuidutils.generate_uuid(), + 'cidr': '10.3.0.0/24', + 'gateway_ip': mock.sentinel.gateway_ip, + 'dns_nameservers': mock.sentinel.dns_nameservers, + 'ipv6_ra_mode': mock.sentinel.ipv6_ra_mode, + 'subnetpool_id': mock.sentinel.subnetpool_id, + 'address_scope_id': mock.sentinel.address_scope_id, + 'segment_id': seg2}] + get_subnets_by_network.return_value = {'net_id': subnets} + + ports = [{'network_id': 'net_id', + 'id': 'port_id', + 'fixed_ips': [{'subnet_id': subnets[0]['id']}], + 'device_owner': n_const.DEVICE_OWNER_ROUTER_GW}] + with mock.patch.object(directory, 'get_plugin') as get_p, \ + mock.patch.object(segment_ext.SegmentPluginBase, + 'is_loaded', return_value=plugin_loaded): + get_p().get_networks.return_value = [{'id': 'net_id', 'mtu': 1446}] + self.db._populate_mtu_and_subnets_for_ports(mock.sentinel.context, + ports) + keys = ('id', 'cidr', 'gateway_ip', 'ipv6_ra_mode', + 'subnetpool_id', 'dns_nameservers') + address_scopes = {4: mock.sentinel.address_scope_id, 6: None} + reference = {'fixed_ips': [{'subnet_id': subnets[0]['id'], + 'prefixlen': 24}], + 'id': 'port_id', + 'mtu': 1446, + 'network_id': 'net_id', + 'subnets': [{k: subnets[0][k] for k in keys}], + 'address_scopes': address_scopes, + 'device_owner': n_const.DEVICE_OWNER_ROUTER_GW, + 'extra_subnets': [{k: subnets[1][k] for k in keys}]} + # If RPN plugin is not enabled or the network subnets do not have + # associated segments (that means this is not a RPN), all subnets + # should be passed in "subnets" + "extra_subnets". + if not plugin_loaded or subnets[0]['segment_id'] is None: + reference['extra_subnets'].append( + {k: subnets[2][k] for k in keys}) + self.assertEqual([reference], ports) def test__get_sync_floating_ips_no_query(self): """Basic test that no query is performed if no router ids are passed"""