Merge "Allow the use of legacy routers within RPN segments"
This commit is contained in:
commit
8507e632b7
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
Loading…
Reference in New Issue