FIX OVN LB Health Monitor checks for IPv6 members

After [1] the IPv6 backend members health checks are supported,
they are mapping into field ip_port_mappings of the OVN LB entity
and translated to OVN SB DB Service_Monitor entries, same way
for IPv4 ones.

However, IPv6 backend members require being enclosed in [ ], and
this was not occurring, causing it not to translate into entries
in the Service_Monitor table. This patch fixes this issue.

Furthermore, a one-time maintenance task has been developed to fix
those existing IPv6 Health Monitors directly upon the startup of
the ovn-octavia-provider component without requiring any action
by the administrator/user.

[1] 40a686e8e7

Closes-Bug: #2055876
Change-Id: I9b97aa9e6c8d601bc9e465e6aa8895dcc2666568
This commit is contained in:
Fernando Royo 2024-03-06 09:56:30 +01:00
parent 8d3e5c7ed3
commit bd1137ad57
5 changed files with 205 additions and 13 deletions

View File

@ -16,6 +16,7 @@ import inspect
import threading
from futurist import periodics
import netaddr
from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_log import log as logging
@ -118,3 +119,42 @@ class DBInconsistenciesPeriodics(object):
raise periodics.NeverAgain()
LOG.debug('Maintenance task: device_owner and device_id checked for '
'OVN LB HM ports.')
# TODO(froyo): Remove this in the Caracal+4 cycle
@periodics.periodic(spacing=600, run_immediately=True)
def format_ip_port_mappings_ipv6(self):
"""Give correct format to `ip_port_mappings` for IPv6 backend members.
The `ip_port_mappings` field for OVN LBs should be a dictionary with
keys following the format:
`${MEMBER_IP}=${LSP_NAME_MEMBER}:${HEALTH_SRC_IP}`. However, when
`MEMBER_IP` and `HEALTH_SRC_IP` are IPv6 addresses, they should be
enclosed in `[]`.
"""
LOG.debug('Maintenance task: Ensure correct formatting of '
'ip_port_mappings for IPv6 backend members.')
ovn_lbs = self.ovn_nbdb_api.db_find_rows(
'Load_Balancer', ('ip_port_mappings', '!=', {})).execute()
for lb in ovn_lbs:
mappings = {}
for k, v in lb.ip_port_mappings.items():
try:
# If first element is IPv4 (mixing IPv4 and IPv6 not
# allowed) or get AddrFormatError (IPv6 already fixed) we
# can jump to next item
if netaddr.IPNetwork(k).version == n_const.IP_VERSION_4:
break
except netaddr.AddrFormatError:
break
port_uuid, src_ip = v.split(':', 1)
mappings[f'[{k}]'] = f'{port_uuid}:[{src_ip}]'
self.ovn_nbdb_api.db_clear('Load_Balancer', lb.uuid,
'ip_port_mappings').execute(
check_error=True)
self.ovn_nbdb_api.db_set('Load_Balancer', lb.uuid,
('ip_port_mappings', mappings)).execute(
check_error=True)
LOG.debug('Maintenance task: no more ip_port_mappings to format, '
'stopping the periodic task.')
raise periodics.NeverAgain()

View File

@ -13,6 +13,8 @@
import atexit
import contextlib
import netaddr
from neutron_lib import constants as n_const
from neutron_lib import exceptions as n_exc
from oslo_log import log
from ovsdbapp.backend import ovs_idl
@ -137,6 +139,53 @@ class GetLrsCommand(command.ReadOnlyCommand):
self.api.tables['Logical_Router'].rows.values()]
# NOTE(froyo): remove this class once ovsdbapp manages the IPv6 into [ ]
# https://bugs.launchpad.net/ovsdbapp/+bug/2057471
class DelBackendFromIPPortMapping(command.BaseCommand):
table = 'Load_Balancer'
def __init__(self, api, lb, backend_ip):
super().__init__(api)
self.lb = lb
if netaddr.IPNetwork(backend_ip).version == n_const.IP_VERSION_6:
self.backend_ip = f'[{backend_ip}]'
else:
self.backend_ip = backend_ip
def run_idl(self, txn):
try:
ovn_lb = self.api.lookup(self.table, self.lb)
ovn_lb.delkey('ip_port_mappings', self.backend_ip)
except Exception:
LOG.exception("Error deleting backend %s from ip_port_mappings "
"for LB uuid %s", str(self.backend_ip), str(self.lb))
# NOTE(froyo): remove this class once ovsdbapp manages the IPv6 into [ ]
# https://bugs.launchpad.net/ovsdbapp/+bug/2057471
class AddBackendToIPPortMapping(command.BaseCommand):
table = 'Load_Balancer'
def __init__(self, api, lb, backend_ip, port_name, src_ip):
super().__init__(api)
self.lb = lb
self.backend_ip = backend_ip
self.port_name = port_name
self.src_ip = src_ip
if netaddr.IPNetwork(backend_ip).version == n_const.IP_VERSION_6:
self.backend_ip = f'[{backend_ip}]'
self.src_ip = f'[{src_ip}]'
def run_idl(self, txn):
try:
lb = self.api.lookup(self.table, self.lb)
lb.setkey('ip_port_mappings', self.backend_ip,
'%s:%s' % (self.port_name, self.src_ip))
except Exception:
LOG.exception("Error adding backend %s to ip_port_mappings "
"for LB uuid %s", str(self.backend_ip), str(self.lb))
class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def __init__(self, connection):
super().__init__(connection)
@ -172,6 +221,15 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def get_lrs(self):
return GetLrsCommand(self)
# NOTE(froyo): remove this method once ovsdbapp manages the IPv6 into [ ]
def lb_del_ip_port_mapping(self, lb_uuid, backend_ip):
return DelBackendFromIPPortMapping(self, lb_uuid, backend_ip)
# NOTE(froyo): remove this method once ovsdbapp manages the IPv6 into [ ]
def lb_add_ip_port_mapping(self, lb_uuid, backend_ip, port_name, src_ip):
return AddBackendToIPPortMapping(self, lb_uuid, backend_ip, port_name,
src_ip)
class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
def __init__(self, connection):

View File

@ -253,6 +253,28 @@ class FakeOVNRouter():
return type('Logical_Router', (object, ), router_attrs)
class FakeOVNLB():
@staticmethod
def create_one_lb(attrs=None):
fake_uuid = uuidutils.generate_uuid()
lb_attrs = {
'uuid': fake_uuid,
'external_ids': {},
'health_check': [],
'ip_port_mappings': {},
'name': '',
'options': {},
'protocol': 'tcp',
'selection_fields': [],
'vips': {}
}
# Overwrite default attributes.
lb_attrs.update(attrs)
return type('Load_Balancer', (object, ), lb_attrs)
class FakePort():
"""Fake one or more ports."""

View File

@ -326,26 +326,40 @@ class TestOvnProviderHelper(ovn_base.TestOvnOctaviaBase):
mock.call(self.ovn_hm_lb.uuid, 'address2'),
mock.ANY])
def test__update_ip_port_mappings(self):
def test__update_ip_port_mappings_del_backend_member(self):
src_ip = '10.22.33.4'
fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'ip': self.member_address,
'logical_port': 'a-logical-port',
'src_ip': src_ip,
'port': self.member_port,
'protocol': self.ovn_hm_lb.protocol,
'status': ovn_const.HM_EVENT_MEMBER_PORT_ONLINE})
self.helper._update_ip_port_mappings(
self.ovn_lb, self.member_address, 'a-logical-port', src_ip)
self.helper.ovn_nbdb_api.lb_add_ip_port_mapping.\
assert_called_once_with(self.ovn_lb.uuid, self.member_address,
'a-logical-port', src_ip)
self.helper._update_ip_port_mappings(
self.ovn_lb, self.member_address, 'a-logical-port', src_ip,
delete=True)
self.helper.ovn_nbdb_api.lb_del_ip_port_mapping.\
assert_called_once_with(self.ovn_lb.uuid, self.member_address)
def test__update_ip_port_mappings_add_backend_member(self):
src_ip = '10.22.33.4'
self.helper._update_ip_port_mappings(
self.ovn_lb, self.member_address, 'a-logical-port', src_ip)
self.helper.ovn_nbdb_api.lb_add_ip_port_mapping.\
assert_called_once_with(self.ovn_lb.uuid, self.member_address,
'a-logical-port', src_ip)
def test__update_ip_port_mappings_del_backend_member_ipv6(self):
member_address = 'fda2:918e:5869:0:f816:3eff:feab:cdef'
src_ip = 'fda2:918e:5869:0:f816:3eff:fecd:398a'
self.helper._update_ip_port_mappings(
self.ovn_lb, member_address, 'a-logical-port', src_ip,
delete=True)
self.helper.ovn_nbdb_api.lb_del_ip_port_mapping.\
assert_called_once_with(self.ovn_lb.uuid, member_address)
def test__update_ip_port_mappings_add_backend_member_ipv6(self):
member_address = 'fda2:918e:5869:0:f816:3eff:feab:cdef'
src_ip = 'fda2:918e:5869:0:f816:3eff:fecd:398a'
self.helper._update_ip_port_mappings(
self.ovn_lb, member_address, 'a-logical-port', src_ip)
self.helper.ovn_nbdb_api.lb_add_ip_port_mapping.\
assert_called_once_with(
self.ovn_lb.uuid, member_address, 'a-logical-port', src_ip)
def test__update_external_ids_member_status(self):
self.helper._update_external_ids_member_status(
self.ovn_lb, self.member_id, constants.NO_MONITOR)

View File

@ -132,3 +132,61 @@ class TestDBInconsistenciesPeriodics(ovn_base.TestOvnOctaviaBase):
]
net_cli.assert_has_calls(expected_call)
self.maint.ovn_nbdb_api.db_find_rows.assert_not_called()
def test_format_ip_port_mappings_ipv6_no_ip_port_mappings_to_change(self):
self.maint.ovn_nbdb_api.db_find_rows.return_value.\
execute.return_value = []
self.assertRaises(periodics.NeverAgain,
self.maint.format_ip_port_mappings_ipv6)
self.maint.ovn_nbdb_api.db_clear.assert_not_called()
self.maint.ovn_nbdb_api.db_set.assert_not_called()
@mock.patch('ovn_octavia_provider.common.clients.get_neutron_client')
def test_format_ip_port_mappings_ipv6(self, net_cli):
ovn_lbs = [
fakes.FakeOVNLB.create_one_lb(
attrs={
'uuid': 'foo1',
'ip_port_mappings': {
'fda2:918e:5869:0:f816:3eff:fe64:adf7':
'f2b97caf-da62-4db9-91da-bc11f2ac3934:'
'fda2:918e:5869:0:f816:3eff:fe81:61d0',
'fda2:918e:5869:0:f816:3eff:fe64:adf8':
'f2b97caf-da62-4db9-91da-bc11f2ac3935:'
'fda2:918e:5869:0:f816:3eff:fe81:61d0'}}),
fakes.FakeOVNLB.create_one_lb(
attrs={
'uuid': 'foo2',
'ip_port_mappings': {
'192.168.1.50':
'f2b97caf-da62-4db9-91da-bc11f2ac3934:'
'192.168.1.3'}}),
fakes.FakeOVNLB.create_one_lb(
attrs={
'uuid': 'foo3',
'ip_port_mappings': {
'[fda2:918e:5869:0:f816:3eff:fe64:adf7]':
'f2b97caf-da62-4db9-91da-bc11f2ac3934:'
'[fda2:918e:5869:0:f816:3eff:fe81:61d0]'}}),
]
self.maint.ovn_nbdb_api.db_find_rows.return_value.\
execute.return_value = ovn_lbs
self.assertRaises(periodics.NeverAgain,
self.maint.format_ip_port_mappings_ipv6)
mapping1 = {
'[fda2:918e:5869:0:f816:3eff:fe64:adf7]':
'f2b97caf-da62-4db9-91da-bc11f2ac3934:'
'[fda2:918e:5869:0:f816:3eff:fe81:61d0]',
'[fda2:918e:5869:0:f816:3eff:fe64:adf8]':
'f2b97caf-da62-4db9-91da-bc11f2ac3935:'
'[fda2:918e:5869:0:f816:3eff:fe81:61d0]'}
expected_call_db_clear = [
mock.call('Load_Balancer', 'foo1', 'ip_port_mappings')]
self.maint.ovn_nbdb_api.db_clear.assert_has_calls(
expected_call_db_clear)
expected_call_db_set = [
mock.call('Load_Balancer', 'foo1', ('ip_port_mappings', mapping1))]
self.maint.ovn_nbdb_api.db_set.assert_has_calls(
expected_call_db_set)