Refactor configuring of floating ips on a router.
This approach to configuring floating ips is stateless and idempotent. This allows it to handle corner cases, such as reusing a floating ip address with a different floating ip id in a way that is easier to understand. The concept is to wipe the floating ips clean and rebuild them each time with the following optimizations. To avoid bad performance in manipulating iptables, it is called in the context of a call to defer_apply_on. To avoid a disruption in network flow a set difference is use to determine the set of addresses that no longer belong on the inteface rather than removing them all blindly. Change-Id: I0cfb58d487b1925e0a0db2a701c5ea3c56a0b2b5 Fixes: Bug #1209011
This commit is contained in:
parent
cba5f28267
commit
2449dc53b6
@ -95,7 +95,6 @@ class RouterInfo(object):
|
||||
self._snat_enabled = None
|
||||
self._snat_action = None
|
||||
self.internal_ports = []
|
||||
self.floating_ips = []
|
||||
self.root_helper = root_helper
|
||||
self.use_namespaces = use_namespaces
|
||||
# Invoke the setter for establishing initial SNAT action
|
||||
@ -410,45 +409,47 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager):
|
||||
ri.iptables_manager.apply()
|
||||
|
||||
def process_router_floating_ips(self, ri, ex_gw_port):
|
||||
floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, [])
|
||||
existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips])
|
||||
cur_floating_ip_ids = set([fip['id'] for fip in floating_ips])
|
||||
"""Configure the router's floating IPs
|
||||
Configures floating ips in iptables and on the router's gateway device.
|
||||
|
||||
id_to_fip_map = {}
|
||||
Cleans up floating ips that should not longer be configured.
|
||||
"""
|
||||
cidr_suffix = '/32'
|
||||
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
||||
device = ip_lib.IPDevice(interface_name, self.root_helper,
|
||||
namespace=ri.ns_name())
|
||||
|
||||
for fip in floating_ips:
|
||||
if fip['port_id']:
|
||||
if fip['id'] not in existing_floating_ip_ids:
|
||||
ri.floating_ips.append(fip)
|
||||
self.floating_ip_added(ri, ex_gw_port,
|
||||
fip['floating_ip_address'],
|
||||
fip['fixed_ip_address'])
|
||||
# Clear out all iptables rules for these chains.
|
||||
for chain, rule in self.floating_forward_rules(None, None):
|
||||
ri.iptables_manager.ipv4['nat'].empty_chain(chain)
|
||||
|
||||
# store to see if floatingip was remapped
|
||||
id_to_fip_map[fip['id']] = fip
|
||||
existing_cidrs = set([addr['cidr'] for addr in device.addr.list()])
|
||||
new_cidrs = set()
|
||||
|
||||
floating_ip_ids_to_remove = (existing_floating_ip_ids -
|
||||
cur_floating_ip_ids)
|
||||
for fip in ri.floating_ips:
|
||||
if fip['id'] in floating_ip_ids_to_remove:
|
||||
ri.floating_ips.remove(fip)
|
||||
self.floating_ip_removed(ri, ri.ex_gw_port,
|
||||
fip['floating_ip_address'],
|
||||
fip['fixed_ip_address'])
|
||||
else:
|
||||
# handle remapping of a floating IP
|
||||
new_fip = id_to_fip_map[fip['id']]
|
||||
new_fixed_ip = new_fip['fixed_ip_address']
|
||||
existing_fixed_ip = fip['fixed_ip_address']
|
||||
if (new_fixed_ip and existing_fixed_ip and
|
||||
new_fixed_ip != existing_fixed_ip):
|
||||
floating_ip = fip['floating_ip_address']
|
||||
self.floating_ip_removed(ri, ri.ex_gw_port,
|
||||
floating_ip, existing_fixed_ip)
|
||||
self.floating_ip_added(ri, ri.ex_gw_port,
|
||||
floating_ip, new_fixed_ip)
|
||||
ri.floating_ips.remove(fip)
|
||||
ri.floating_ips.append(new_fip)
|
||||
# Loop once to ensure that floating ips are configured.
|
||||
for fip in ri.router.get(l3_constants.FLOATINGIP_KEY, []):
|
||||
fip_ip = fip['floating_ip_address']
|
||||
ip_cidr = str(fip_ip) + cidr_suffix
|
||||
|
||||
new_cidrs.add(ip_cidr)
|
||||
|
||||
if ip_cidr not in existing_cidrs:
|
||||
net = netaddr.IPNetwork(ip_cidr)
|
||||
device.addr.add(net.version, ip_cidr, str(net.broadcast))
|
||||
self._send_gratuitous_arp_packet(ri, interface_name, fip_ip)
|
||||
|
||||
# Rebuild iptables rules for the floating ip.
|
||||
fixed = fip['fixed_ip_address']
|
||||
for chain, rule in self.floating_forward_rules(fip_ip, fixed):
|
||||
ri.iptables_manager.ipv4['nat'].add_rule(chain, rule)
|
||||
|
||||
ri.iptables_manager.apply()
|
||||
|
||||
# Clean up addresses that no longer belong on the gateway interface.
|
||||
for ip_cidr in existing_cidrs - new_cidrs:
|
||||
if ip_cidr.endswith(cidr_suffix):
|
||||
net = netaddr.IPNetwork(ip_cidr)
|
||||
device.addr.delete(net.version, ip_cidr)
|
||||
|
||||
def _get_ex_gw_port(self, ri):
|
||||
return ri.router.get('gw_port')
|
||||
@ -567,34 +568,6 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager):
|
||||
(internal_cidr, ex_gw_ip))]
|
||||
return rules
|
||||
|
||||
def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
|
||||
ip_cidr = str(floating_ip) + '/32'
|
||||
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
||||
device = ip_lib.IPDevice(interface_name, self.root_helper,
|
||||
namespace=ri.ns_name())
|
||||
|
||||
if ip_cidr not in [addr['cidr'] for addr in device.addr.list()]:
|
||||
net = netaddr.IPNetwork(ip_cidr)
|
||||
device.addr.add(net.version, ip_cidr, str(net.broadcast))
|
||||
self._send_gratuitous_arp_packet(ri, interface_name, floating_ip)
|
||||
|
||||
for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
|
||||
ri.iptables_manager.ipv4['nat'].add_rule(chain, rule)
|
||||
ri.iptables_manager.apply()
|
||||
|
||||
def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
|
||||
ip_cidr = str(floating_ip) + '/32'
|
||||
net = netaddr.IPNetwork(ip_cidr)
|
||||
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
||||
|
||||
device = ip_lib.IPDevice(interface_name, self.root_helper,
|
||||
namespace=ri.ns_name())
|
||||
device.addr.delete(net.version, ip_cidr)
|
||||
|
||||
for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
|
||||
ri.iptables_manager.ipv4['nat'].remove_rule(chain, rule)
|
||||
ri.iptables_manager.apply()
|
||||
|
||||
def floating_forward_rules(self, floating_ip, fixed_ip):
|
||||
return [('PREROUTING', '-d %s -j DNAT --to %s' %
|
||||
(floating_ip, fixed_ip)),
|
||||
|
@ -186,47 +186,6 @@ class TestBasicRouterOperations(base.BaseTestCase):
|
||||
def testAgentRemoveExternalGateway(self):
|
||||
self._test_external_gateway_action('remove')
|
||||
|
||||
def _test_floating_ip_action(self, action):
|
||||
router_id = _uuid()
|
||||
ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
|
||||
self.conf.use_namespaces, None)
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
floating_ip = '20.0.0.100'
|
||||
fixed_ip = '10.0.0.23'
|
||||
ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30',
|
||||
'subnet_id': _uuid()}],
|
||||
'subnet': {'gateway_ip': '20.0.0.1'},
|
||||
'id': _uuid(),
|
||||
'mac_address': 'ca:fe:de:ad:be:ef',
|
||||
'ip_cidr': '20.0.0.30/24'}
|
||||
interface_name = agent.get_external_device_name(ex_gw_port['id'])
|
||||
|
||||
if action == 'add':
|
||||
self.device_exists.return_value = False
|
||||
agent.floating_ip_added(ri, ex_gw_port, floating_ip, fixed_ip)
|
||||
arping_cmd = ['arping', '-A', '-U',
|
||||
'-I', interface_name,
|
||||
'-c', self.conf.send_arp_for_ha,
|
||||
floating_ip]
|
||||
if self.conf.use_namespaces:
|
||||
self.mock_ip.netns.execute.assert_any_call(
|
||||
arping_cmd, check_exit_code=True)
|
||||
else:
|
||||
self.utils_exec.assert_any_call(
|
||||
check_exit_code=True, root_helper=self.conf.root_helper)
|
||||
|
||||
elif action == 'remove':
|
||||
self.device_exists.return_value = True
|
||||
agent.floating_ip_removed(ri, ex_gw_port, floating_ip, fixed_ip)
|
||||
else:
|
||||
raise Exception("Invalid action %s" % action)
|
||||
|
||||
def testAgentAddFloatingIP(self):
|
||||
self._test_floating_ip_action('add')
|
||||
|
||||
def testAgentRemoveFloatingIP(self):
|
||||
self._test_floating_ip_action('remove')
|
||||
|
||||
def _check_agent_method_called(self, agent, calls, namespace):
|
||||
if namespace:
|
||||
self.mock_ip.netns.execute.assert_has_calls(
|
||||
@ -381,6 +340,7 @@ class TestBasicRouterOperations(base.BaseTestCase):
|
||||
|
||||
def testProcessRouter(self):
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
agent.process_router_floating_ips = mock.Mock()
|
||||
router = self._prepare_router_data()
|
||||
fake_floatingips1 = {'floatingips': [
|
||||
{'id': _uuid(),
|
||||
@ -407,6 +367,87 @@ class TestBasicRouterOperations(base.BaseTestCase):
|
||||
del router['gw_port']
|
||||
agent.process_router(ri)
|
||||
|
||||
agent.process_router_floating_ips.assert_called_with(ri, None)
|
||||
|
||||
@mock.patch('neutron.agent.linux.ip_lib.IPDevice')
|
||||
def test_process_router_floating_ip_add(self, IPDevice):
|
||||
fip = {
|
||||
'id': _uuid(), 'port_id': _uuid(),
|
||||
'floating_ip_address': '15.1.2.3',
|
||||
'fixed_ip_address': '192.168.0.1'
|
||||
}
|
||||
|
||||
IPDevice.return_value = device = mock.Mock()
|
||||
device.addr.list.return_value = []
|
||||
|
||||
ri = mock.MagicMock()
|
||||
ri.router.get.return_value = [fip]
|
||||
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
agent._send_gratuitous_arp_packet = mock.Mock()
|
||||
|
||||
agent.process_router_floating_ips(ri, {'id': _uuid()})
|
||||
|
||||
device.addr.add.assert_called_once_with(4, '15.1.2.3/32', '15.1.2.3')
|
||||
self.assertTrue(agent._send_gratuitous_arp_packet.called)
|
||||
|
||||
nat = ri.iptables_manager.ipv4['nat']
|
||||
rules = agent.floating_forward_rules('15.1.2.3', '192.168.0.1')
|
||||
for chain, rule in rules:
|
||||
nat.empty_chain.assert_any_call(chain)
|
||||
nat.add_rule.assert_any_call(chain, rule)
|
||||
|
||||
@mock.patch('neutron.agent.linux.ip_lib.IPDevice')
|
||||
def test_process_router_floating_ip_remove(self, IPDevice):
|
||||
IPDevice.return_value = device = mock.Mock()
|
||||
device.addr.list.return_value = [{'cidr': '15.1.2.3/32'}]
|
||||
|
||||
ri = mock.MagicMock()
|
||||
ri.router.get.return_value = []
|
||||
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
agent._send_gratuitous_arp_packet = mock.Mock()
|
||||
|
||||
agent.process_router_floating_ips(ri, {'id': _uuid()})
|
||||
|
||||
device.addr.delete.assert_called_once_with(4, '15.1.2.3/32')
|
||||
self.assertFalse(agent._send_gratuitous_arp_packet.called)
|
||||
|
||||
nat = ri.iptables_manager.ipv4['nat']
|
||||
nat = ri.iptables_manager.ipv4['nat']
|
||||
for chain, rule in agent.floating_forward_rules(None, None):
|
||||
nat.empty_chain.assert_any_call(chain)
|
||||
self.assertFalse(nat.add_rule.called)
|
||||
|
||||
@mock.patch('neutron.agent.linux.ip_lib.IPDevice')
|
||||
def test_process_router_floating_ip_remap(self, IPDevice):
|
||||
fip = {
|
||||
'id': _uuid(), 'port_id': _uuid(),
|
||||
'floating_ip_address': '15.1.2.3',
|
||||
'fixed_ip_address': '192.168.0.2'
|
||||
}
|
||||
|
||||
IPDevice.return_value = device = mock.Mock()
|
||||
device.addr.list.return_value = [{'cidr': '15.1.2.3/32'}]
|
||||
ri = mock.MagicMock()
|
||||
|
||||
ri.router.get.return_value = [fip]
|
||||
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
agent._send_gratuitous_arp_packet = mock.Mock()
|
||||
|
||||
agent.process_router_floating_ips(ri, {'id': _uuid()})
|
||||
|
||||
self.assertFalse(device.addr.add.called)
|
||||
self.assertFalse(device.addr.delete.called)
|
||||
self.assertFalse(agent._send_gratuitous_arp_packet.called)
|
||||
|
||||
nat = ri.iptables_manager.ipv4['nat']
|
||||
rules = agent.floating_forward_rules('15.1.2.3', '192.168.0.2')
|
||||
for chain, rule in rules:
|
||||
nat.empty_chain.assert_any_call(chain)
|
||||
nat.add_rule.assert_any_call(chain, rule)
|
||||
|
||||
def test_process_router_snat_disabled(self):
|
||||
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
|
||||
router = self._prepare_router_data()
|
||||
|
Loading…
Reference in New Issue
Block a user