Set onlink routes for all subnets on an external network

The addition of the on-link routes gives us some freedom to allocate a
router's IP address from any one of multiple subnets on one external
network.  Different routers can get their IPs from different subnets and
they still have direct on-link connectivity to each other.  For example,
one router with its primary IP from 10.0.0.0/24 and another from
192.168.0.0/24 can communicate directly.  It is important that each
router has on-link routes to *all* of the subnets.

Any router can host floating ips from any of the subnets regardless of
which subnet the primary IP address comes from.

This is an alternative to the "Multiple floating IP pools" section in
the administration guide.  It is a simpler alternative that avoids
having to create multiple external networks.  It is also more flexible
because routers will no longer be restricted to getting floating IPs
from the pool to which they happen to be connected.

DocImpact
Document the procedure for adding subnets to the external network.
Potentially remove the existing procedure for "Multiple floating IP
pools" from the docs.

Change-Id: I2c283f5be0cbb6b5d350cafc1b636c300b796a7b
Closes-Bug: #1312467
This commit is contained in:
Carl Baldwin 2014-04-24 23:06:10 +00:00
parent bdb167393c
commit fc7cffedbe
6 changed files with 84 additions and 36 deletions

View File

@ -458,7 +458,7 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager):
interface_name = None
if ex_gw_port_id:
interface_name = self.get_external_device_name(ex_gw_port_id)
if ex_gw_port and not ri.ex_gw_port:
if ex_gw_port and ex_gw_port != ri.ex_gw_port:
self._set_subnet_info(ex_gw_port)
self.external_gateway_added(ri, ex_gw_port,
interface_name, internal_cidrs)
@ -646,6 +646,7 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager):
self.driver.init_l3(interface_name, [ex_gw_port['ip_cidr']],
namespace=ri.ns_name,
gateway=ex_gw_port['subnet'].get('gateway_ip'),
extra_subnets=ex_gw_port.get('extra_subnets', []),
preserve_ips=preserve_ips)
ip_address = ex_gw_port['ip_cidr'].split('/')[0]
self._send_gratuitous_arp_packet(ri, interface_name, ip_address)

View File

@ -72,7 +72,7 @@ class LinuxInterfaceDriver(object):
self.root_helper = config.get_root_helper(conf)
def init_l3(self, device_name, ip_cidrs, namespace=None,
preserve_ips=[], gateway=None):
preserve_ips=[], gateway=None, extra_subnets=[]):
"""Set the L3 settings for the interface using data from the port.
ip_cidrs: list of 'X.X.X.X/YY' strings
@ -108,6 +108,13 @@ class LinuxInterfaceDriver(object):
if gateway:
device.route.add_gateway(gateway)
new_onlink_routes = set(s['cidr'] for s in extra_subnets)
existing_onlink_routes = set(device.route.list_onlink_routes())
for route in new_onlink_routes - existing_onlink_routes:
device.route.add_onlink_route(route)
for route in existing_onlink_routes - new_onlink_routes:
device.route.delete_onlink_route(route)
def check_bridge_exists(self, bridge):
if not ip_lib.device_exists(bridge):
raise exceptions.BridgeDoesNotExist(bridge=bridge)

View File

@ -376,6 +376,22 @@ class IpRouteCommand(IpDeviceCommandBase):
'dev',
self.name)
def list_onlink_routes(self):
def iterate_routes():
output = self._run('list', 'dev', self.name, 'scope', 'link')
for line in output.split('\n'):
line = line.strip()
if line and not line.count('src'):
yield line
return [x for x in iterate_routes()]
def add_onlink_route(self, cidr):
self._as_root('replace', cidr, 'dev', self.name, 'scope', 'link')
def delete_onlink_route(self, cidr):
self._as_root('del', cidr, 'dev', self.name, 'scope', 'link')
def get_gateway(self, scope=None, filters=None):
if filters is None:
filters = []

View File

@ -924,36 +924,41 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
"""
if not ports:
return
subnet_id_ports_dict = {}
for port in ports:
fixed_ips = port.get('fixed_ips', [])
if len(fixed_ips) > 1:
LOG.info(_("Ignoring multiple IPs on router port %s"),
port['id'])
continue
elif not fixed_ips:
# Skip ports without IPs, which can occur if a subnet
# attached to a router is deleted
LOG.info(_("Skipping port %s as no IP is configure on it"),
port['id'])
continue
fixed_ip = fixed_ips[0]
my_ports = subnet_id_ports_dict.get(fixed_ip['subnet_id'], [])
my_ports.append(port)
subnet_id_ports_dict[fixed_ip['subnet_id']] = my_ports
if not subnet_id_ports_dict:
return
filters = {'id': subnet_id_ports_dict.keys()}
fields = ['id', 'cidr', 'gateway_ip']
subnet_dicts = self._core_plugin.get_subnets(context, filters, fields)
for subnet_dict in subnet_dicts:
ports = subnet_id_ports_dict.get(subnet_dict['id'], [])
def each_port_with_ip():
for port in ports:
# TODO(gongysh) stash the subnet into fixed_ips
# to make the payload smaller.
port['subnet'] = {'id': subnet_dict['id'],
'cidr': subnet_dict['cidr'],
'gateway_ip': subnet_dict['gateway_ip']}
fixed_ips = port.get('fixed_ips', [])
if len(fixed_ips) > 1:
LOG.info(_("Ignoring multiple IPs on router port %s"),
port['id'])
continue
elif not fixed_ips:
# Skip ports without IPs, which can occur if a subnet
# attached to a router is deleted
LOG.info(_("Skipping port %s as no IP is configure on it"),
port['id'])
continue
yield (port, fixed_ips[0])
network_ids = set(p['network_id'] for p, _ in each_port_with_ip())
filters = {'network_id': [id for id in network_ids]}
fields = ['id', 'cidr', 'gateway_ip', 'network_id']
subnets_by_network = dict((id, []) for id in network_ids)
for subnet in self._core_plugin.get_subnets(context, filters, fields):
subnets_by_network[subnet['network_id']].append(subnet)
for port, fixed_ip in each_port_with_ip():
port['extra_subnets'] = []
for subnet in subnets_by_network[port['network_id']]:
subnet_info = {'id': subnet['id'],
'cidr': subnet['cidr'],
'gateway_ip': subnet['gateway_ip']}
if subnet['id'] == fixed_ip['subnet_id']:
port['subnet'] = subnet_info
else:
port['extra_subnets'].append(subnet_info)
def _process_sync_data(self, routers, interfaces, floating_ips):
routers_dict = {}

View File

@ -160,6 +160,7 @@ class TestBasicRouterOperations(base.BaseTestCase):
ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30',
'subnet_id': _uuid()}],
'subnet': {'gateway_ip': '20.0.0.1'},
'extra_subnets': [{'cidr': '172.16.0.0/24'}],
'id': _uuid(),
'network_id': _uuid(),
'mac_address': 'ca:fe:de:ad:be:ef',
@ -179,7 +180,8 @@ class TestBasicRouterOperations(base.BaseTestCase):
'20.0.0.30')
kwargs = {'preserve_ips': ['192.168.1.34/32'],
'namespace': 'qrouter-' + router_id,
'gateway': '20.0.0.1'}
'gateway': '20.0.0.1',
'extra_subnets': [{'cidr': '172.16.0.0/24'}]}
self.mock_driver.init_l3.assert_called_with(interface_name,
['20.0.0.30/24'],
**kwargs)

View File

@ -81,15 +81,32 @@ class TestABCDriver(TestBase):
addresses = [dict(ip_version=4, scope='global',
dynamic=False, cidr='172.16.77.240/24')]
self.ip_dev().addr.list = mock.Mock(return_value=addresses)
self.ip_dev().route.list_onlink_routes.return_value = []
bc = BaseChild(self.conf)
ns = '12345678-1234-5678-90ab-ba0987654321'
bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns,
extra_subnets=[{'cidr': '172.20.0.0/24'}])
self.ip_dev.assert_has_calls(
[mock.call('tap0', 'sudo', namespace=ns),
mock.call().addr.list(scope='global', filters=['permanent']),
mock.call().addr.add(4, '192.168.1.2/24', '192.168.1.255'),
mock.call().addr.delete(4, '172.16.77.240/24'),
mock.call().route.list_onlink_routes(),
mock.call().route.add_onlink_route('172.20.0.0/24')])
def test_l3_init_delete_onlink_routes(self):
addresses = [dict(ip_version=4, scope='global',
dynamic=False, cidr='172.16.77.240/24')]
self.ip_dev().addr.list = mock.Mock(return_value=addresses)
self.ip_dev().route.list_onlink_routes.return_value = ['172.20.0.0/24']
bc = BaseChild(self.conf)
ns = '12345678-1234-5678-90ab-ba0987654321'
bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns)
self.ip_dev.assert_has_calls(
[mock.call('tap0', 'sudo', namespace=ns),
mock.call().addr.list(scope='global', filters=['permanent']),
mock.call().addr.add(4, '192.168.1.2/24', '192.168.1.255'),
mock.call().addr.delete(4, '172.16.77.240/24')])
[mock.call().route.list_onlink_routes(),
mock.call().route.delete_onlink_route('172.20.0.0/24')])
def test_l3_init_with_preserve(self):
addresses = [dict(ip_version=4, scope='global',