Browse Source

Centralize traffic when LB and member has FIP

When Load Balancer and its member has FIP assigned
and environment is configured to use DVR the member
FIP needs to be centralized. It is current core OVN
limitation, that should be solved in [1].

This patch adds this mechanism to OVN Client.
It covers cases:

1) FIP association on port that is a member of
   some LB - make it centralized.
2) FIP association on LB VIP - find a members
   FIPs and centralized them.
3) The reverse of each of the above cases.

Related-Bug: #1860662

[1] https://bugzilla.redhat.com/show_bug.cgi?id=1793897

Conflicts:
    networking_ovn/common/ovn_client.py
    networking_ovn/common/constants.py
    networking_ovn/tests/unit/l3/test_l3_ovn.p

Change-Id: I254f0ac28f7585b699a8238e01ffb37dd70282ef
(cherry picked from commit 57ac38921e)
changes/99/706799/2
Maciej Józefczyk 5 months ago
parent
commit
50c8db84b6
4 changed files with 324 additions and 4 deletions
  1. +16
    -0
      networking_ovn/common/constants.py
  2. +167
    -2
      networking_ovn/common/ovn_client.py
  3. +1
    -0
      networking_ovn/tests/unit/fakes.py
  4. +140
    -2
      networking_ovn/tests/unit/l3/test_l3_ovn.py

+ 16
- 0
networking_ovn/common/constants.py View File

@@ -137,6 +137,22 @@ MAINTENANCE_DELETE_TYPE_ORDER = {
# peer router port (connecting to the logical router).
DEFAULT_ADDR_FOR_LSP_WITH_PEER = 'router'

# FIP ACTIONS
FIP_ACTION_ASSOCIATE = 'fip_associate'
FIP_ACTION_DISASSOCIATE = 'fip_disassociate'

# Loadbalancer constants
LRP_PREFIX = "lrp-"
LB_VIP_PORT_PREFIX = "ovn-lb-vip-"
LB_EXT_IDS_LS_REFS_KEY = 'ls_refs'
LB_EXT_IDS_LR_REF_KEY = 'lr_ref'
LB_EXT_IDS_POOL_PREFIX = 'pool_'
LB_EXT_IDS_LISTENER_PREFIX = 'listener_'
LB_EXT_IDS_MEMBER_PREFIX = 'member_'
LB_EXT_IDS_VIP_KEY = 'neutron:vip'
LB_EXT_IDS_VIP_FIP_KEY = 'neutron:vip_fip'
LB_EXT_IDS_VIP_PORT_ID_KEY = 'neutron:vip_port_id'

# Maximum chassis count where a gateway port can be hosted
MAX_GW_CHASSIS = 5



+ 167
- 2
networking_ovn/common/ovn_client.py View File

@@ -662,6 +662,21 @@ class OVNClient(object):
if self._nb_idl.is_col_present('NAT', 'external_ids'):
columns['external_ids'] = ext_ids

# TODO(mjozefcz): Remove this workaround when OVN LB
# will support both decentralized FIPs on LB and member.
lb_member_fip = self._is_lb_member_fip(context, floatingip)
if (config.is_ovn_distributed_floating_ip() and
lb_member_fip):
LOG.warning("Port %s is configured as a member "
"of one of OVN Load_Balancers and "
"Load_Balancer has FIP assigned. "
"In order to make traffic work member "
"FIP needs to be centralized, even if "
"this environment is configured as DVR. "
"Removing logical_port and external_mac from "
"NAT entry.", floatingip['port_id'])
columns.pop('logical_port', None)
columns.pop('external_mac', None)
commands.append(self._nb_idl.add_nat_rule_in_lrouter(gw_lrouter_name,
**columns))

@@ -678,12 +693,158 @@ class OVNClient(object):
self._nb_idl.db_set('Logical_Switch_Port', private_lsp.uuid,
('external_ids', port_fip))
)
if not lb_member_fip:
commands.extend(
self._handle_lb_fip_cmds(
context, private_lsp,
action=ovn_const.FIP_ACTION_ASSOCIATE))
else:
LOG.warning("LSP for floatingip %s, has not been found! "
"Cannot set FIP on VIP.",
floatingip['id'])
self._transaction(commands, txn=txn)

def _is_lb_member_fip(self, context, fip):
port = self._plugin.get_port(
context, fip['port_id'])
member_subnet = [ip['subnet_id'] for ip in port['fixed_ips']
if ip['ip_address'] == fip['fixed_ip_address']]
if not member_subnet:
return False
member_subnet = member_subnet[0]

ls = self._nb_idl.lookup(
'Logical_Switch', utils.ovn_name(port['network_id']))
for lb in ls.load_balancer:
for ext_id in lb.external_ids.keys():
if ext_id.startswith(ovn_const.LB_EXT_IDS_POOL_PREFIX):
members = lb.external_ids[ext_id]
if not members:
continue
for member in members.split(','):
if ('%s:' % fip['fixed_ip_address'] in member and
'_%s' % member_subnet in member):
return True
return False

def _handle_lb_fip_cmds(self, context, lb_lsp,
action=ovn_const.FIP_ACTION_ASSOCIATE):
commands = []
if not config.is_ovn_distributed_floating_ip():
return commands

lb_lsp_fip_port = lb_lsp.external_ids.get(
ovn_const.OVN_PORT_NAME_EXT_ID_KEY, '')

if not lb_lsp_fip_port.startswith(ovn_const.LB_VIP_PORT_PREFIX):
return commands

# This is a FIP on LB VIP.
# Loop over members and delete FIP external_mac/logical_port enteries.
# Find all LBs with this LSP as VIP.
lbs = self._nb_idl.db_find_rows(
'Load_Balancer',
('external_ids', '=', {
ovn_const.LB_EXT_IDS_VIP_PORT_ID_KEY: lb_lsp.name})
).execute(check_error=True)
for lb in lbs:
# GET all LS where given LB is linked.
ls_linked = [
item
for item in self._nb_idl.db_find_rows(
'Logical_Switch').execute(check_error=True)
if lb in item.load_balancer]

if not ls_linked:
return

# Find out IP addresses and subnets of configured members.
members_to_verify = []
for ext_id in lb.external_ids.keys():
if ext_id.startswith(ovn_const.LB_EXT_IDS_POOL_PREFIX):
members = lb.external_ids[ext_id]
if not members:
continue
for member in members.split(','):
# NOTE(mjozefcz): Remove this workaround in W release.
# Last argument of member info is a subnet_id from
# from which member comes from.
# member_`id`_`ip`:`port`_`subnet_ip`
member_info = member.split('_')
if len(member_info) >= 4:
m = {}
m['id'] = member_info[1]
m['ip'] = member_info[2].split(':')[0]
m['subnet_id'] = member_info[3]
try:
subnet = self._plugin.get_subnet(
context, m['subnet_id'])
m['network_id'] = subnet['network_id']
members_to_verify.append(m)
except n_exc.SubnetNotFound:
LOG.debug("Cannot find subnet details "
"for OVN LB member "
"%s.", m['id'])

# Find a member LSPs from all linked LS to this LB.
for member in members_to_verify:
ls = self._nb_idl.lookup(
'Logical_Switch', utils.ovn_name(member['network_id']))
for lsp in ls.ports:
if not lsp.addresses:
continue
if member['ip'] in utils.remove_macs_from_lsp_addresses(
lsp.addresses):
member['lsp'] = lsp
nats = self._nb_idl.db_find_rows(
'NAT',
('external_ids', '=', {
ovn_const.OVN_FIP_PORT_EXT_ID_KEY: lsp.name})
).execute(check_error=True)

for nat in nats:
if action == ovn_const.FIP_ACTION_ASSOCIATE:
# NOTE(mjozefcz): We should delete logical_port
# and external_mac entries from member NAT in
# order to make traffic work.
LOG.warning(
"Port %s is configured as a member "
"of one of OVN Load_Balancers and "
"Load_Balancer has FIP assigned. "
"In order to make traffic work member "
"FIP needs to be centralized, even if "
"this environment is configured as "
"DVR. Removing logical_port and "
"external_mac from NAT entry.",
lsp.name)
commands.extend([
self._nb_idl.db_clear(
'NAT', nat.uuid, 'external_mac'),
self._nb_idl.db_clear(
'NAT', nat.uuid, 'logical_port')])
else:
# NOTE(mjozefcz): The FIP from LB VIP is
# dissassociated now. We can decentralize
# member FIPs now.
LOG.warning(
"Port %s is configured as a member "
"of one of OVN Load_Balancers and "
"Load_Balancer has FIP disassociated. "
"DVR for this port can be enabled back.",
lsp.name)
commands.append(self._nb_idl.db_set(
'NAT', nat.uuid,
('logical_port', lsp.name)))
port = self._plugin.get_port(context, lsp.name)
if port['status'] == const.PORT_STATUS_ACTIVE:
commands.append(
self._nb_idl.db_set(
'NAT', nat.uuid,
('external_mac',
port['mac_address'])))

return commands

def _delete_floatingip(self, fip, lrouter, txn=None):
commands = [self._nb_idl.delete_nat_rule_in_lrouter(
lrouter, type='dnat_and_snat',
@@ -699,8 +860,12 @@ class OVNClient(object):
self._nb_idl.db_remove(
'Logical_Switch_Port', private_lsp.uuid,
'external_ids',
(ovn_const.OVN_PORT_FIP_EXT_ID_KEY))
)
(ovn_const.OVN_PORT_FIP_EXT_ID_KEY)))
commands.extend(
self._handle_lb_fip_cmds(
n_context.get_admin_context(),
private_lsp,
action=ovn_const.FIP_ACTION_DISASSOCIATE))
except KeyError:
LOG.debug("FIP %s doesn't have external_ids.", fip)
self._transaction(commands, txn=txn)


+ 1
- 0
networking_ovn/tests/unit/fakes.py View File

@@ -144,6 +144,7 @@ class FakeOvsdbNbOvnIdl(object):
self.db_list_rows = mock.Mock()
self.lsp_list = mock.MagicMock()
self.db_find = mock.Mock()
self.db_find_rows = mock.Mock()
self.db_set = mock.Mock()
self.db_clear = mock.Mock()
self.db_remove = mock.Mock()


+ 140
- 2
networking_ovn/tests/unit/l3/test_l3_ovn.py View File

@@ -29,6 +29,7 @@ from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_utils import uuidutils

from networking_ovn.common import config
from networking_ovn.common import constants as ovn_const
@@ -58,6 +59,7 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'network_id': self.fake_network['id'],
'device_owner': 'network:router_interface',
'mac_address': 'aa:aa:aa:aa:aa:aa',
'status': constants.PORT_STATUS_ACTIVE,
'fixed_ips': [{'ip_address': '10.0.0.100',
'subnet_id': 'subnet-id'}],
'id': 'router-port-id'}
@@ -140,7 +142,7 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'port_id': 'new-port_id'}
self.fake_floating_ip_new = fakes.FakeFloatingIp.create_one_fip(
attrs=self.fake_floating_ip_new_attrs)
self.fake_ovn_nat_rule = {
self.fake_ovn_nat_rule = fakes.FakeOvsdbRow.create_one_ovsdb_row({
'logical_ip': self.fake_floating_ip['fixed_ip_address'],
'external_ip': self.fake_floating_ip['floating_ip_address'],
'type': 'dnat_and_snat',
@@ -149,8 +151,55 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
ovn_const.OVN_FIP_PORT_EXT_ID_KEY:
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id'])}}
self.fake_floating_ip['router_id'])}})
self.l3_inst = directory.get_plugin(plugin_constants.L3)
self.lb_id = uuidutils.generate_uuid()
self.member_subnet = {'id': 'subnet-id',
'ip_version': 4,
'cidr': '10.0.0.0/24',
'network_id': self.fake_network['id']}
self.member_id = uuidutils.generate_uuid()
self.member_port_id = uuidutils.generate_uuid()
self.member_address = '10.0.0.10'
self.member_l4_port = '80'
self.member_port = {
'network_id': self.fake_network['id'],
'mac_address': 'aa:aa:aa:aa:aa:aa',
'fixed_ips': [{'ip_address': self.member_address,
'subnet_id': self.member_subnet['id']}],
'id': 'fake-port-id'}
self.member_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'addresses': ['10.0.0.10 ff:ff:ff:ff:ff:ff'],
'uuid': self.member_port['id']})
self.listener_id = uuidutils.generate_uuid()
self.pool_id = uuidutils.generate_uuid()
self.ovn_lb = mock.MagicMock()
self.ovn_lb.protocol = ['tcp']
self.ovn_lb.uuid = uuidutils.generate_uuid()
self.member_line = (
'member_%s_%s:%s_%s' %
(self.member_id, self.member_address,
self.member_l4_port, self.member_subnet['id']))
self.ovn_lb.external_ids = {
ovn_const.LB_EXT_IDS_VIP_KEY: '10.22.33.4',
ovn_const.LB_EXT_IDS_VIP_FIP_KEY: '123.123.123.123',
ovn_const.LB_EXT_IDS_VIP_PORT_ID_KEY: 'foo_port',
'enabled': True,
'pool_%s' % self.pool_id: self.member_line,
'listener_%s' % self.listener_id: '80:pool_%s' % self.pool_id}
self.lb_vip_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY:
'%s%s' % (ovn_const.LB_VIP_PORT_PREFIX,
self.ovn_lb.uuid)},
'name': uuidutils.generate_uuid(),
'addresses': ['10.0.0.100 ff:ff:ff:ff:ff:ee'],
'uuid': uuidutils.generate_uuid()})
self.lb_network = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'load_balancer': [self.ovn_lb],
'name': 'neutron-%s' % self.fake_network['id'],
'ports': [self.lb_vip_lsp, self.member_lsp],
'uuid': self.fake_network['id']})
self.nb_idl = self._start_mock(
'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._ovn',
new_callable=mock.PropertyMock,
@@ -212,6 +261,10 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'networking_ovn.db.revision.bump_revision', return_value=None)
self.del_rev_p = self._start_mock(
'networking_ovn.db.revision.delete_revision', return_value=None)
self.mock_is_lb_member_fip = mock.patch(
'networking_ovn.common.ovn_client.OVNClient._is_lb_member_fip',
return_value=False)
self.mock_is_lb_member_fip.start()

@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface')
def test_add_router_interface(self, func):
@@ -911,6 +964,55 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'192.168.0.10'}))]
self.l3_inst._ovn.db_set.assert_has_calls(calls)

@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip')
@mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port')
def test_create_floatingip_lb_member_fip(self, gp, gf):
config.cfg.CONF.set_override(
'enable_distributed_floating_ip', True, group='ovn')
# Stop this mock.
self.mock_is_lb_member_fip.stop()
gp.return_value = self.member_port
gf.return_value = self.fake_floating_ip
self.l3_inst._ovn.lookup.return_value = self.lb_network
self.l3_inst._ovn.get_lswitch_port.return_value = self.member_lsp
self.l3_inst.create_floatingip(self.context, 'floatingip')
# Validate that there is no external_mac and logical_port while
# setting the NAT entry.
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
external_ip='192.168.0.10',
logical_ip='10.0.0.10',
type='dnat_and_snat')

@mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet')
def test_create_floatingip_lb_vip_fip(self, gs):
config.cfg.CONF.set_override(
'enable_distributed_floating_ip', True, group='ovn')
gs.return_value = self.member_subnet
self.l3_inst._ovn.get_lswitch_port.return_value = self.lb_vip_lsp
self.l3_inst._ovn.db_find_rows.return_value.execute.side_effect = [
[self.ovn_lb],
[self.lb_network],
[self.fake_ovn_nat_rule],
]
self.l3_inst._ovn.lookup.return_value = self.lb_network

self.l3_inst.create_floatingip(self.context, 'floatingip')
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
external_ip='192.168.0.10',
external_mac='aa:aa:aa:aa:aa:aa',
logical_ip='10.0.0.10',
logical_port='port_id',
type='dnat_and_snat')
self.l3_inst._ovn.db_find_rows.assert_called_with(
'NAT', ('external_ids', '=', {ovn_const.OVN_FIP_PORT_EXT_ID_KEY:
self.member_lsp.name}))
# Validate that it clears external_mac/logical_port for member NAT.
self.l3_inst._ovn.db_clear.assert_has_calls([
mock.call('NAT', self.fake_ovn_nat_rule.uuid, 'external_mac'),
mock.call('NAT', self.fake_ovn_nat_rule.uuid, 'logical_port')])

@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_floatingip')
def test_delete_floatingip(self, df):
self.l3_inst._ovn.get_floatingip.return_value = (
@@ -922,6 +1024,42 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
logical_ip='10.0.0.10',
external_ip='192.168.0.10')

@mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet')
@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip')
@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_floatingip')
def test_delete_floatingip_lb_vip_fip(self, df, gf, gs):
config.cfg.CONF.set_override(
'enable_distributed_floating_ip', True, group='ovn')
gs.return_value = self.member_subnet
gf.return_value = self.fake_floating_ip
self.l3_inst._ovn.get_floatingip.return_value = (
self.fake_ovn_nat_rule)
self.l3_inst._ovn.get_lswitch_port.return_value = self.lb_vip_lsp
self.l3_inst._ovn.db_find_rows.return_value.execute.side_effect = [
[self.ovn_lb],
[self.lb_network],
[self.fake_ovn_nat_rule],
]
self.l3_inst._ovn.lookup.return_value = self.lb_network

self.l3_inst.delete_floatingip(self.context, 'floatingip-id')
self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
type='dnat_and_snat',
logical_ip='10.0.0.10',
external_ip='192.168.0.10')
self.l3_inst._ovn.db_find_rows.assert_called_with(
'NAT', ('external_ids', '=',
{ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.member_lsp.name}))
self.l3_inst._plugin.get_port.assert_called_once_with(
mock.ANY, self.member_lsp.name)
# Validate that it adds external_mac/logical_port back.
self.l3_inst._ovn.db_set.assert_has_calls([
mock.call('NAT', self.fake_ovn_nat_rule.uuid,
('logical_port', self.member_lsp.name)),
mock.call('NAT', self.fake_ovn_nat_rule.uuid,
('external_mac', 'aa:aa:aa:aa:aa:aa'))])

@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip')
@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_floatingip')
def test_delete_floatingip_lsp_external_id(self, df, gf):


Loading…
Cancel
Save