Merge "Allow the use of legacy routers within RPN segments"

This commit is contained in:
Zuul 2021-06-09 15:48:06 +00:00 committed by Gerrit Code Review
commit 8507e632b7
4 changed files with 181 additions and 6 deletions

View File

@ -524,3 +524,87 @@ one segment to a routed one.
+------------+--------------------------------------+ +------------+--------------------------------------+
| segment_id | 81e5453d-4c9f-43a5-8ddf-feaf3937e8c7 | | 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.

View File

@ -51,6 +51,7 @@ from neutron.db import models_v2
from neutron.db import standardattrdescription_db as st_attr from neutron.db import standardattrdescription_db as st_attr
from neutron.extensions import l3 from neutron.extensions import l3
from neutron.extensions import qos_fip 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 base as base_obj
from neutron.objects import port_forwarding from neutron.objects import port_forwarding
from neutron.objects import ports as port_obj 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)) query = query.filter(models_v2.Subnet.network_id.in_(network_ids))
fields = ['id', 'cidr', 'gateway_ip', 'dns_nameservers', 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): def make_subnet_dict_with_scope(row):
subnet_db, address_scope_id = 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. These ports already have fixed_ips populated.
""" """
seg_plugin_loaded = segment_ext.SegmentPluginBase.is_loaded()
network_ids = [p['network_id'] network_ids = [p['network_id']
for p in self._each_port_having_fixed_ips(ports)] for p in self._each_port_having_fixed_ips(ports)]
@ -1806,12 +1808,19 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
context, network_ids) context, network_ids)
for port in self._each_port_having_fixed_ips(ports): for port in self._each_port_having_fixed_ips(ports):
is_gw = port['device_owner'] == constants.DEVICE_OWNER_ROUTER_GW
port['subnets'] = [] port['subnets'] = []
port['extra_subnets'] = [] port['extra_subnets'] = []
port['address_scopes'] = {constants.IP_VERSION_4: None, port['address_scopes'] = {constants.IP_VERSION_4: None,
constants.IP_VERSION_6: 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 = {} scopes = {}
for subnet in subnets_by_network[port['network_id']]: for subnet in subnets_by_network[port['network_id']]:
scope = subnet['address_scope_id'] scope = subnet['address_scope_id']
@ -1835,8 +1844,16 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
fixed_ip['prefixlen'] = prefixlen fixed_ip['prefixlen'] = prefixlen
break break
else: else:
# This subnet is not used by the port. # NOTE(ralonsoh): if this is the gateway port and is
port['extra_subnets'].append(subnet_info) # 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['address_scopes'].update(scopes)
port['mtu'] = mtus_by_network.get(port['network_id'], 0) port['mtu'] = mtus_by_network.get(port['network_id'], 0)

View File

@ -248,3 +248,7 @@ class SegmentPluginBase(object, metaclass=abc.ABCMeta):
@classmethod @classmethod
def get_plugin_type(cls): def get_plugin_type(cls):
return SEGMENTS return SEGMENTS
@classmethod
def is_loaded(cls):
return cls.get_plugin_type() in directory.get_plugins()

View File

@ -15,6 +15,7 @@
from unittest import mock from unittest import mock
import ddt
import netaddr import netaddr
from neutron_lib.callbacks import events from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry from neutron_lib.callbacks import registry
@ -31,6 +32,7 @@ import testtools
from neutron.db import l3_db from neutron.db import l3_db
from neutron.db.models import l3 as l3_models 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 base as base_obj
from neutron.objects import network as network_obj from neutron.objects import network as network_obj
from neutron.objects import ports as port_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 from neutron.tests.unit.db import test_db_base_plugin_v2
@ddt.ddt
class TestL3_NAT_dbonly_mixin( class TestL3_NAT_dbonly_mixin(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase): test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
@ -135,7 +138,8 @@ class TestL3_NAT_dbonly_mixin(
ports = [{'network_id': 'net_id', ports = [{'network_id': 'net_id',
'id': 'port_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: with mock.patch.object(directory, 'get_plugin') as get_p:
get_p().get_networks.return_value = [{'id': 'net_id', 'mtu': 1446}] get_p().get_networks.return_value = [{'id': 'net_id', 'mtu': 1446}]
self.db._populate_mtu_and_subnets_for_ports(mock.sentinel.context, self.db._populate_mtu_and_subnets_for_ports(mock.sentinel.context,
@ -151,7 +155,73 @@ class TestL3_NAT_dbonly_mixin(
'mtu': 1446, 'mtu': 1446,
'network_id': 'net_id', 'network_id': 'net_id',
'subnets': [{k: subnet[k] for k in keys}], '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): def test__get_sync_floating_ips_no_query(self):
"""Basic test that no query is performed if no router ids are passed""" """Basic test that no query is performed if no router ids are passed"""