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
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)
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)
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
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)
'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 = []
"""
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 = {}
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',
'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)
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)
+ 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().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().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',