Merge "Fix missing GARP with UDP listeners on SINGLE LB"

This commit is contained in:
Zuul 2024-11-20 20:47:37 +00:00 committed by Gerrit Code Review
commit 260bd20fbc
4 changed files with 140 additions and 3 deletions

View File

@ -201,6 +201,15 @@ class KeepalivedLvs(lvs_listener_base.LvsListenerApiServerBase):
f"{listener_id}"), f"{listener_id}"),
'details': e.output}, status=500) 'details': e.output}, status=500)
is_vrrp = (CONF.controller_worker.loadbalancer_topology ==
consts.TOPOLOGY_ACTIVE_STANDBY)
# TODO(gthiemonge) remove RESTART from the list (same as previous todo
# in this function)
if not is_vrrp and action in [consts.AMP_ACTION_START,
consts.AMP_ACTION_RESTART,
consts.AMP_ACTION_RELOAD]:
util.send_vip_advertisements(listener_id=listener_id)
return webob.Response( return webob.Response(
json={'message': 'OK', json={'message': 'OK',
'details': (f'keepalivedlvs listener {listener_id} ' 'details': (f'keepalivedlvs listener {listener_id} '

View File

@ -347,7 +347,37 @@ def get_haproxy_vip_addresses(lb_id):
return vips return vips
def send_vip_advertisements(lb_id): def get_lvs_vip_addresses(listener_id: str) -> list[str]:
"""Get the VIP addresses for a LVS load balancer.
:param listener_id: The listener ID to get VIP addresses from.
:returns: List of VIP addresses (IPv4 and IPv6)
"""
vips = []
# Extract the VIP addresses from keepalived configuration
# Format is
# virtual_server_group ipv<n>-group {
# vip_address1 port1
# vip_address2 port2
# }
# it can be repeated in case of dual-stack LBs
with open(keepalived_lvs_cfg_path(listener_id), encoding='utf-8') as file:
vsg_section = False
for line in file:
current_line = line.strip()
if vsg_section:
if current_line.startswith('}'):
vsg_section = False
else:
vip_address = current_line.split(' ')[0]
vips.append(vip_address)
elif line.startswith('virtual_server_group '):
vsg_section = True
return vips
def send_vip_advertisements(lb_id: tp.Optional[str] = None,
listener_id: tp.Optional[str] = None):
"""Sends address advertisements for each load balancer VIP. """Sends address advertisements for each load balancer VIP.
This method will send either GARP (IPv4) or neighbor advertisements (IPv6) This method will send either GARP (IPv4) or neighbor advertisements (IPv6)
@ -357,7 +387,10 @@ def send_vip_advertisements(lb_id):
:returns: None :returns: None
""" """
try: try:
vips = get_haproxy_vip_addresses(lb_id) if lb_id:
vips = get_haproxy_vip_addresses(lb_id)
else:
vips = get_lvs_vip_addresses(listener_id)
for vip in vips: for vip in vips:
interface = network_utils.get_interface_name( interface = network_utils.get_interface_name(

View File

@ -356,13 +356,57 @@ class TestUtil(base.TestCase):
self.assertEqual([], util.get_haproxy_vip_addresses(LB_ID1)) self.assertEqual([], util.get_haproxy_vip_addresses(LB_ID1))
mock_cfg_path.assert_called_once_with(LB_ID1) mock_cfg_path.assert_called_once_with(LB_ID1)
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'keepalived_lvs_cfg_path')
def test_get_lvs_vip_addresses(self, mock_cfg_path):
FAKE_PATH = 'fake_path'
mock_cfg_path.return_value = FAKE_PATH
self.useFixture(
test_utils.OpenFixture(FAKE_PATH, 'no match')).mock_open()
# Test with no matching lines in the config file
self.assertEqual([], util.get_lvs_vip_addresses(LB_ID1))
mock_cfg_path.assert_called_once_with(LB_ID1)
# Test with 2 matching lines
mock_cfg_path.reset_mock()
test_data = ('virtual_server_group ipv4-group {\n'
' 203.0.113.43 1\n'
' 203.0.113.44 1\n'
'}\n')
self.useFixture(
test_utils.OpenFixture(FAKE_PATH, test_data)).mock_open()
expected_result = ['203.0.113.43', '203.0.113.44']
self.assertEqual(expected_result,
util.get_lvs_vip_addresses(LB_ID1))
mock_cfg_path.assert_called_once_with(LB_ID1)
# Test with 2 groups
mock_cfg_path.reset_mock()
test_data = ('virtual_server_group ipv4-group {\n'
' 203.0.113.43 1\n'
'}\n'
'virtual_server_group ipv6-group {\n'
' 2d01:27::1 2\n'
' 2d01:27::2 2\n'
'}\n')
self.useFixture(
test_utils.OpenFixture(FAKE_PATH, test_data)).mock_open()
expected_result = ['203.0.113.43', '2d01:27::1', '2d01:27::2']
self.assertEqual(expected_result,
util.get_lvs_vip_addresses(LB_ID1))
mock_cfg_path.assert_called_once_with(LB_ID1)
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.' @mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
'send_ip_advertisement') 'send_ip_advertisement')
@mock.patch('octavia.amphorae.backends.utils.network_utils.' @mock.patch('octavia.amphorae.backends.utils.network_utils.'
'get_interface_name') 'get_interface_name')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.' @mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_haproxy_vip_addresses') 'get_haproxy_vip_addresses')
def test_send_vip_advertisements(self, mock_get_vip_addrs, @mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_lvs_vip_addresses')
def test_send_vip_advertisements(self, mock_get_lvs_vip_addrs,
mock_get_vip_addrs,
mock_get_int_name, mock_send_advert): mock_get_int_name, mock_send_advert):
mock_get_vip_addrs.side_effect = [[], ['203.0.113.46'], mock_get_vip_addrs.side_effect = [[], ['203.0.113.46'],
Exception('boom')] Exception('boom')]
@ -371,6 +415,7 @@ class TestUtil(base.TestCase):
# Test no VIPs # Test no VIPs
util.send_vip_advertisements(LB_ID1) util.send_vip_advertisements(LB_ID1)
mock_get_vip_addrs.assert_called_once_with(LB_ID1) mock_get_vip_addrs.assert_called_once_with(LB_ID1)
mock_get_lvs_vip_addrs.assert_not_called()
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()
@ -380,6 +425,7 @@ class TestUtil(base.TestCase):
mock_send_advert.reset_mock() mock_send_advert.reset_mock()
util.send_vip_advertisements(LB_ID1) util.send_vip_advertisements(LB_ID1)
mock_get_vip_addrs.assert_called_once_with(LB_ID1) mock_get_vip_addrs.assert_called_once_with(LB_ID1)
mock_get_lvs_vip_addrs.assert_not_called()
mock_get_int_name.assert_called_once_with( mock_get_int_name.assert_called_once_with(
'203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE) '203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
mock_send_advert.assert_called_once_with( mock_send_advert.assert_called_once_with(
@ -393,6 +439,48 @@ class TestUtil(base.TestCase):
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')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_haproxy_vip_addresses')
@mock.patch('octavia.amphorae.backends.agent.api_server.util.'
'get_lvs_vip_addresses')
def test_send_vip_advertisements_udp(self, mock_get_lvs_vip_addrs,
mock_get_vip_addrs,
mock_get_int_name, mock_send_advert):
mock_get_lvs_vip_addrs.side_effect = [[], ['203.0.113.46'],
Exception('boom')]
mock_get_int_name.return_value = 'fake0'
# Test no VIPs
util.send_vip_advertisements(listener_id=LISTENER_ID1)
mock_get_lvs_vip_addrs.assert_called_once_with(LISTENER_ID1)
mock_get_vip_addrs.assert_not_called()
mock_get_int_name.assert_not_called()
mock_send_advert.assert_not_called()
# Test with a VIP
mock_get_lvs_vip_addrs.reset_mock()
mock_get_int_name.reset_mock()
mock_send_advert.reset_mock()
util.send_vip_advertisements(listener_id=LISTENER_ID1)
mock_get_lvs_vip_addrs.assert_called_once_with(LISTENER_ID1)
mock_get_vip_addrs.assert_not_called()
mock_get_int_name.assert_called_once_with(
'203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
mock_send_advert.assert_called_once_with(
'fake0', '203.0.113.46', net_ns=consts.AMPHORA_NAMESPACE)
# Test with an exception (should not raise)
mock_get_lvs_vip_addrs.reset_mock()
mock_get_int_name.reset_mock()
mock_send_advert.reset_mock()
util.send_vip_advertisements(listener_id=LISTENER_ID1)
mock_get_int_name.assert_not_called()
mock_send_advert.assert_not_called()
@mock.patch('octavia.amphorae.backends.utils.ip_advertisement.' @mock.patch('octavia.amphorae.backends.utils.ip_advertisement.'
'send_ip_advertisement') 'send_ip_advertisement')
@mock.patch('octavia.amphorae.backends.utils.network_utils.' @mock.patch('octavia.amphorae.backends.utils.network_utils.'

View File

@ -0,0 +1,7 @@
---
fixes:
- |
Fixed an issue with SINGLE topology load balancer with UDP listeners, the
Amphora now sends a Gratuitous ARP packet when a UDP pool is added, it
makes the VIP address more quickly reachable after a failover or when
reusing a previously allocated IP address.