Send IP advertisements when plugging a new member subnet

When plugging a new subnet, the amphora sends a packet advertising the
IP address of the port. In case previously deallocated IP addresses are
re-used, it helps the other resources on the same L2 network to flush
the old ARP entries.

Closes-Bug: #2015572

Change-Id: Iecedb9e9c8f293cd4c067900e1696f5050769ee8
This commit is contained in:
Gregory Thiemonge 2023-04-07 08:04:45 -04:00
parent cad146f1bb
commit 18cd1f34c8
5 changed files with 80 additions and 3 deletions

View File

@ -24,6 +24,7 @@ import pyroute2
import webob import webob
from werkzeug import exceptions from werkzeug import exceptions
from octavia.amphorae.backends.agent.api_server import util
from octavia.common import constants as consts from octavia.common import constants as consts
@ -196,6 +197,8 @@ class Plug(object):
fixed_ips=fixed_ips, fixed_ips=fixed_ips,
mtu=mtu) mtu=mtu)
self._osutils.bring_interface_up(existing_interface, 'network') self._osutils.bring_interface_up(existing_interface, 'network')
util.send_member_advertisements(fixed_ips)
return webob.Response(json={ return webob.Response(json={
'message': "OK", 'message': "OK",
'details': "Updated existing interface {interface}".format( 'details': "Updated existing interface {interface}".format(
@ -236,6 +239,7 @@ class Plug(object):
IFLA_IFNAME=netns_interface) IFLA_IFNAME=netns_interface)
self._osutils.bring_interface_up(netns_interface, 'network') self._osutils.bring_interface_up(netns_interface, 'network')
util.send_member_advertisements(fixed_ips)
return webob.Response(json={ return webob.Response(json={
'message': "OK", 'message': "OK",

View File

@ -17,6 +17,7 @@ import os
import re import re
import stat import stat
import subprocess import subprocess
import typing as tp
import jinja2 import jinja2
from oslo_config import cfg from oslo_config import cfg
@ -411,3 +412,23 @@ def send_vip_advertisements(lb_id):
except Exception as e: except Exception as e:
LOG.debug('Send VIP advertisement failed due to :%s. ' LOG.debug('Send VIP advertisement failed due to :%s. '
'This amphora may not be the MASTER. Ignoring.', str(e)) 'This amphora may not be the MASTER. Ignoring.', str(e))
def send_member_advertisements(fixed_ips: tp.Iterable[tp.Dict[str, str]]):
"""Sends advertisements for each fixed_ip of a list
This method will send either GARP (IPv4) or neighbor advertisements (IPv6)
for the addresses of the subnets of the members.
:param fixed_ips: a list of dicts that contain 'ip_address' elements
:returns: None
"""
try:
for fixed_ip in fixed_ips:
ip_address = fixed_ip[consts.IP_ADDRESS]
interface = network_utils.get_interface_name(
ip_address, net_ns=consts.AMPHORA_NAMESPACE)
ip_advertisement.send_ip_advertisement(
interface, ip_address, net_ns=consts.AMPHORA_NAMESPACE)
except Exception as e:
LOG.debug('Send member advertisement failed due to: %s', str(e))

View File

@ -265,7 +265,10 @@ class TestPlug(base.TestCase):
"BaseOS.write_port_interface_file") "BaseOS.write_port_interface_file")
@mock.patch("octavia.amphorae.backends.agent.api_server.osutils." @mock.patch("octavia.amphorae.backends.agent.api_server.osutils."
"BaseOS.bring_interface_up") "BaseOS.bring_interface_up")
def test_plug_network(self, mock_if_up, mock_write_port_interface, @mock.patch("octavia.amphorae.backends.agent.api_server.util."
"send_member_advertisements")
def test_plug_network(self, mock_send_member_adv,
mock_if_up, mock_write_port_interface,
mock_netns, mock_iproute, mock_netns, mock_iproute,
mock_by_mac, mock_interface_exists, mock_webob): mock_by_mac, mock_interface_exists, mock_webob):
fixed_ips = [ fixed_ips = [
@ -284,6 +287,7 @@ class TestPlug(base.TestCase):
mock_write_port_interface.assert_called_once_with( mock_write_port_interface.assert_called_once_with(
interface='eth0', fixed_ips=fixed_ips, mtu=mtu) interface='eth0', fixed_ips=fixed_ips, mtu=mtu)
mock_if_up.assert_called_once_with('eth0', 'network') mock_if_up.assert_called_once_with('eth0', 'network')
mock_send_member_adv.assert_called_once_with(fixed_ips)
mock_webob.Response.assert_any_call( mock_webob.Response.assert_any_call(
json={'message': 'OK', json={'message': 'OK',
@ -300,7 +304,10 @@ class TestPlug(base.TestCase):
"BaseOS.write_port_interface_file") "BaseOS.write_port_interface_file")
@mock.patch("octavia.amphorae.backends.agent.api_server.osutils." @mock.patch("octavia.amphorae.backends.agent.api_server.osutils."
"BaseOS.bring_interface_up") "BaseOS.bring_interface_up")
def test_plug_network_existing_interface(self, mock_if_up, @mock.patch("octavia.amphorae.backends.agent.api_server.util."
"send_member_advertisements")
def test_plug_network_existing_interface(self, mock_send_member_adv,
mock_if_up,
mock_write_port_interface, mock_write_port_interface,
mock_netns, mock_by_mac, mock_netns, mock_by_mac,
mock_interface_exists, mock_interface_exists,
@ -327,6 +334,7 @@ class TestPlug(base.TestCase):
mock_write_port_interface.assert_called_once_with( mock_write_port_interface.assert_called_once_with(
interface=FAKE_INTERFACE, fixed_ips=fixed_ips, mtu=mtu) interface=FAKE_INTERFACE, fixed_ips=fixed_ips, mtu=mtu)
mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'network') mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'network')
mock_send_member_adv.assert_called_once_with(fixed_ips)
mock_webob.Response.assert_any_call( mock_webob.Response.assert_any_call(
json={'message': 'OK', json={'message': 'OK',
@ -344,8 +352,10 @@ class TestPlug(base.TestCase):
"BaseOS.write_vip_interface_file") "BaseOS.write_vip_interface_file")
@mock.patch("octavia.amphorae.backends.agent.api_server.osutils." @mock.patch("octavia.amphorae.backends.agent.api_server.osutils."
"BaseOS.bring_interface_up") "BaseOS.bring_interface_up")
@mock.patch("octavia.amphorae.backends.agent.api_server.util."
"send_member_advertisements")
def test_plug_network_on_vip( def test_plug_network_on_vip(
self, mock_if_up, mock_write_vip_interface, self, mock_send_member_adv, mock_if_up, mock_write_vip_interface,
mock_netns, mock_by_mac, mock_interface_exists, mock_webob): mock_netns, mock_by_mac, mock_interface_exists, mock_webob):
fixed_ips = [ fixed_ips = [
{'ip_address': FAKE_IP_IPV4, {'ip_address': FAKE_IP_IPV4,
@ -394,6 +404,7 @@ class TestPlug(base.TestCase):
fixed_ips=fixed_ips, mtu=mtu) fixed_ips=fixed_ips, mtu=mtu)
mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'vip') mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'vip')
mock_send_member_adv.assert_called_once_with(fixed_ips)
mock_webob.Response.assert_any_call( mock_webob.Response.assert_any_call(
json={'message': 'OK', json={'message': 'OK',

View File

@ -406,3 +406,38 @@ class TestUtil(base.TestCase):
util.send_vip_advertisements(LB_ID1) util.send_vip_advertisements(LB_ID1)
mock_get_int_name.assert_not_called() mock_get_int_name.assert_not_called()
mock_send_advert.assert_not_called() mock_send_advert.assert_not_called()
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
'send_ip_advertisement')
@mock.patch('octavia.amphorae.backends.utils.network_utils.'
'get_interface_name')
def test_send_member_advertisements(self, mock_get_int_name,
mock_send_advert):
# IPv4 fixed_ips
mock_get_int_name.side_effect = ['fake0', 'fake1']
fixed_ips = [{'ip_address': '192.0.2.1'},
{'ip_address': '192.2.0.2'}]
util.send_member_advertisements(fixed_ips)
mock_send_advert.assert_has_calls(
[mock.call('fake0', fixed_ips[0]['ip_address'],
net_ns=consts.AMPHORA_NAMESPACE),
mock.call('fake1', fixed_ips[1]['ip_address'],
net_ns=consts.AMPHORA_NAMESPACE)])
# Mixed IPv4/IPv6
mock_send_advert.reset_mock()
mock_get_int_name.side_effect = ['fake0', 'fake1']
fixed_ips = [{'ip_address': '192.0.2.1'},
{'ip_address': '2001:db8::2'}]
util.send_member_advertisements(fixed_ips)
mock_send_advert.assert_has_calls(
[mock.call('fake0', fixed_ips[0]['ip_address'],
net_ns=consts.AMPHORA_NAMESPACE),
mock.call('fake1', fixed_ips[1]['ip_address'],
net_ns=consts.AMPHORA_NAMESPACE)])
# Exception
mock_send_advert.reset_mock()
mock_get_int_name.side_effect = Exception('ERROR')
util.send_member_advertisements(fixed_ips)
mock_send_advert.assert_not_called()

View File

@ -0,0 +1,6 @@
---
fixes:
- |
When plugging a new member subnet, the amphora sends an IP advertisement of
the newly allocated IP. It allows the servers on the same L2 network to
flush the ARP entries of a previously allocated IP address.