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 ee3a5dc8aa2..07e19951394 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 @@ -1768,7 +1769,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 @@ -1798,6 +1799,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)] @@ -1806,12 +1808,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'] @@ -1835,8 +1844,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 063f3de30f1..a0c1ef07111 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"""