[OVN] Use stateless NAT rules for FIPs

Using stateless NAT in OVN should always be a better choice for
floating IPs in some deployments because it allows to avoid hitting
conntrack, potentially improving NAT performance.

The only limitation for using stateless NAT in OVN is that it requires
1:1 IP mapping; which is always the case for FIPs.

This functionality was introduced in OVN in [1], provided in v20.03.0.
Neutron implies this version is used and does not check it.

This functionality is configurable via Neutron config file. The new
option introduced is ``[ovn]stateless_nat_enabled``, disabled by
default to keep the previous behaviour.

NOTE: this patch is also reducing the cover rate to 78%. cover job only
considers unit tests, not functional tests.

[1]cc87c4827f

Closes-Bug: #2111899
Signed-off-by: Rodolfo Alonso Hernandez <ralonsoh@redhat.com>
Change-Id: I3551babe7986f1aef59080aba35a2a1586e40af5
This commit is contained in:
Rodolfo Alonso Hernandez
2025-06-02 09:48:35 +00:00
committed by Rodolfo Alonso
parent 4e7be0f639
commit 2145901d6f
10 changed files with 202 additions and 19 deletions

View File

@@ -252,6 +252,12 @@ ovn_opts = [
"A migrated port is immediately activated on "
"the destination host.")],
help=_('Activation strategy to use for live migration.')),
cfg.BoolOpt('stateless_nat_enabled',
default=False,
help=_('If enabled, the floating IP NAT rules will be '
'stateless, instead of using the conntrack OVN '
'actions. This strategy is faster in some '
'environments, like for example DPDK deployments.')),
]
nb_global_opts = [
@@ -427,3 +433,7 @@ def is_broadcast_arps_to_all_routers_enabled():
def is_ovn_router_indirect_snat_enabled():
return cfg.CONF.ovn.ovn_router_indirect_snat
def is_stateless_nat_enabled():
return cfg.CONF.ovn.stateless_nat_enabled

View File

@@ -449,7 +449,8 @@ class OVNMechanismDriver(api.MechanismDriver):
self.nb_ovn,
self.sb_ovn,
ovn_conf.get_ovn_neutron_sync_mode(),
self
self,
is_maintenance=True,
)
self.nb_synchronizer.sync()

View File

@@ -812,6 +812,13 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
result = fip.execute(check_error=True)
return result[0] if result else None
def get_floatingips(self):
cmd = self.db_find('NAT',
('external_ids', '!=', {ovn_const.OVN_FIP_EXT_ID_KEY: ''}),
('type', '=', 'dnat_and_snat')
)
return cmd.execute(check_error=True)
def check_revision_number(self, name, resource, resource_type,
if_exists=True):
return cmd.CheckRevisionNumberCommand(

View File

@@ -924,11 +924,16 @@ class OVNClient:
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: gw_lrouter_name,
ovn_const.OVN_FIP_EXT_MAC_KEY: port_db['mac_address'],
ovn_const.OVN_FIP_NET_ID: floatingip['floating_network_id']}
stateless_nat = ('true' if ovn_conf.is_stateless_nat_enabled() else
'false')
options = {'stateless': stateless_nat}
columns = {'type': 'dnat_and_snat',
'logical_ip': floatingip['fixed_ip_address'],
'external_ip': floatingip['floating_ip_address'],
'logical_port': floatingip['port_id'],
'external_ids': ext_ids}
'external_ids': ext_ids,
'options': options,
}
# If OVN supports gateway_port column for NAT rules set gateway port
# uuid to floating IP without gw port reference - LP#2035281.

View File

@@ -73,10 +73,12 @@ class OvnDbSynchronizer(metaclass=abc.ABCMeta):
class OvnNbSynchronizer(OvnDbSynchronizer):
"""Synchronizer class for NB."""
def __init__(self, core_plugin, ovn_api, sb_ovn, mode, ovn_driver):
def __init__(self, core_plugin, ovn_api, sb_ovn, mode, ovn_driver,
is_maintenance=False):
super().__init__(
core_plugin, ovn_api, ovn_driver)
self.mode = mode
self.is_maintenance = is_maintenance
self.l3_plugin = directory.get_plugin(plugin_constants.L3)
self.pf_plugin = directory.get_plugin(plugin_constants.PORTFORWARDING)
if not self.pf_plugin:
@@ -125,6 +127,7 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
self.sync_routers_and_rports(ctx)
self.sync_port_qos_policies(ctx)
self.sync_fip_qos_policies(ctx)
self.sync_fip_dnat_rules()
LOG.debug("OVN-Northbound DB sync process completed @ %s",
str(datetime.now()))
@@ -1384,6 +1387,34 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
LOG.debug('OVN-NB Sync Floating IP QoS policies completed @ %s',
str(datetime.now()))
def sync_fip_dnat_rules(self):
"""Sync all FIPs NAT rules, setting the configured stateless option"""
LOG.debug('OVN-NB Sync Floating IP NAT rules started @ %s',
str(datetime.now()))
stateless_nat = ('true' if ovn_conf.is_stateless_nat_enabled() else
'false')
nat_rules = []
for nat_rule in self.ovn_api.get_floatingips():
if nat_rule.get('options', {}).get('stateless') != stateless_nat:
nat_rules.append(nat_rule)
if not nat_rules:
# Nothing to do.
pass
elif not (self.mode == ovn_const.OVN_DB_SYNC_MODE_REPAIR or
self.is_maintenance):
LOG.warning('The floating IP NAT rules must be updated to match '
'the ``stateless_nat_enabled`` configuration flag.')
else:
with self.ovn_api.transaction(check_error=True) as txn:
for nat_rule in nat_rules:
txn.add(self.ovn_api.db_set(
'NAT', nat_rule['_uuid'],
('options', {'stateless': stateless_nat})))
LOG.debug('OVN-NB Sync Floating IP NAT rules completed @ %s',
str(datetime.now()))
class OvnSbSynchronizer(OvnDbSynchronizer):
"""Synchronizer class for SB."""

View File

@@ -895,6 +895,47 @@ class TestNbApi(BaseOvnIdlTest):
lrp = self.nbapi.lrp_get(lrp_name).execute(check_error=True)
self.assertEqual(hcg.uuid, lrp.ha_chassis_group[0].uuid)
def test_get_floatingips(self):
lr_name = uuidutils.generate_uuid()
self.nbapi.lr_add(lr_name).execute(check_error=True)
# SNAT rule
nat = {'external_ip': '10.0.0.1', 'logical_ip': '10.10.0.1',
'type': 'snat'}
self.nbapi.add_nat_rule_in_lrouter(lr_name, **nat).execute(
check_error=True)
# DNAT rule
nat = {'external_ip': '10.0.0.2', 'logical_ip': '10.10.0.2',
'type': 'dnat'}
self.nbapi.add_nat_rule_in_lrouter(lr_name, **nat).execute(
check_error=True)
# DNAT_AND_SNAT rule, not external_ids reference
nat = {'external_ip': '10.0.0.3', 'logical_ip': '10.10.0.3',
'type': 'dnat_and_snat'}
self.nbapi.add_nat_rule_in_lrouter(lr_name, **nat).execute(
check_error=True)
# DNAT_AND_SNAT rules with external_ids reference
nat = {'external_ip': '10.0.0.4', 'logical_ip': '10.10.0.4',
'type': 'dnat_and_snat',
'external_ids': {ovn_const.OVN_FIP_EXT_ID_KEY: 'id1'}}
self.nbapi.add_nat_rule_in_lrouter(lr_name, **nat).execute(
check_error=True)
nat = {'external_ip': '10.0.0.5', 'logical_ip': '10.10.0.5',
'type': 'dnat_and_snat',
'external_ids': {ovn_const.OVN_FIP_EXT_ID_KEY: 'id2'}}
self.nbapi.add_nat_rule_in_lrouter(lr_name, **nat).execute(
check_error=True)
nat_fips = self.nbapi.get_floatingips()
self.assertEqual(2, len(nat_fips))
for nat_fip in nat_fips:
self.assertIn(
nat_fip['external_ids'][ovn_const.OVN_FIP_EXT_ID_KEY],
('id1', 'id2')
)
class TestIgnoreConnectionTimeout(BaseOvnIdlTest):
@classmethod

View File

@@ -27,6 +27,7 @@ from neutron_lib.db import api as db_api
from neutron_lib.services.logapi import constants as log_const
from neutron_lib.services.qos import constants as qos_const
from oslo_config import cfg
from oslo_utils import strutils
from oslo_utils import uuidutils
from ovsdbapp.backend.ovs_idl import idlutils
from ovsdbapp import constants as ovsdbapp_const
@@ -1703,11 +1704,14 @@ class TestOvnNbSync(testlib_api.MySQLTestCaseMixin,
nb_synchronizer.sync_port_qos_policies(ctx)
self._validate_qos_records()
def _create_floatingip(self, fip_network_id, port_id, qos_policy_id):
def _create_floatingip(self, fip_network_id, port_id, qos_policy_id=None):
body = {'tenant_id': self._tenant_id,
'floating_network_id': fip_network_id,
'port_id': port_id,
'qos_policy_id': qos_policy_id}
}
if qos_policy_id:
body['qos_policy_id'] = qos_policy_id
return self.l3_plugin.create_floatingip(self.context,
{'floatingip': body})
@@ -1863,6 +1867,51 @@ class TestOvnNbSync(testlib_api.MySQLTestCaseMixin,
self._test_sync_acls_helper(test_log=True,
log_event=log_const.DROP_EVENT)
def test_sync_fip_dnat_rules(self):
res = self._create_network(self.fmt, 'n1_ext', True, as_admin=True,
arg_list=('router:external',),
**{'router:external': True})
net_ext = self.deserialize(self.fmt, res)['network']
res = self._create_subnet(self.fmt, net_ext['id'], '10.0.0.0/24')
subnet_ext = self.deserialize(self.fmt, res)['subnet']
res = self._create_network(self.fmt, 'n1_int', True)
net_int = self.deserialize(self.fmt, res)['network']
self._create_subnet(self.fmt, net_int['id'], '10.10.0.0/24')
# Create a router with net_ext as GW network and net_int as internal
# one, and a floating IP on the external network.
data = {'name': 'r1', 'admin_state_up': True,
'tenant_id': self._tenant_id,
'external_gateway_info': {
'enable_snat': True,
'network_id': net_ext['id'],
'external_fixed_ips': [{'ip_address': '10.0.0.5',
'subnet_id': subnet_ext['id']}]}
}
router = self.l3_plugin.create_router(self.context, {'router': data})
net_int_prtr = self._make_port(self.fmt, net_int['id'],
name='n1_int-p-rtr')['port']
self.l3_plugin.add_router_interface(
self.context, router['id'], {'port_id': net_int_prtr['id']})
ovn_config.cfg.CONF.set_override('stateless_nat_enabled', True,
group='ovn')
fip = self._create_floatingip(net_ext['id'], net_int_prtr['id'])
nat = self.nb_api.get_floatingip(fip['id'])
stateless = strutils.bool_from_string(nat['options']['stateless'])
self.assertTrue(stateless)
for value in (False, True):
ovn_config.cfg.CONF.set_override('stateless_nat_enabled', value,
group='ovn')
nb_synchronizer = ovn_db_sync.OvnNbSynchronizer(
self.plugin, self.mech_driver.nb_ovn, self.mech_driver.sb_ovn,
ovn_const.OVN_DB_SYNC_MODE_REPAIR, self.mech_driver)
nb_synchronizer.sync_fip_dnat_rules()
nat = self.nb_api.get_floatingip(fip['id'])
stateless = strutils.bool_from_string(nat['options']['stateless'])
self.assertEqual(value, stateless)
class TestOvnSbSync(base.TestOVNFunctionalBase):

View File

@@ -114,6 +114,8 @@ class FakeOvsdbNbOvnIdl:
self.ls_set_dns_records = mock.Mock()
self.get_floatingip = mock.Mock()
self.get_floatingip.return_value = None
self.get_floatingips = mock.Mock()
self.get_floatingips.return_value = []
self.check_revision_number = mock.Mock()
self.lookup = mock.MagicMock()
self.get_router_floatingip_lbs = mock.Mock()

View File

@@ -1157,7 +1157,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.0.0.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_distributed(self):
self.l3_inst._nb_ovn.is_col_present.return_value = True
@@ -1187,7 +1189,9 @@ class BaseTestOVNL3RouterPluginMixin():
'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10',
external_ip='192.168.0.10', external_mac='00:01:02:03:04:05',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_distributed_logical_port_down(self):
# Check that when the port is down, the external_mac field is not
@@ -1221,7 +1225,9 @@ class BaseTestOVNL3RouterPluginMixin():
'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_external_ip_present_in_nat_rule(self):
self.l3_inst._nb_ovn.is_col_present.return_value = True
@@ -1252,7 +1258,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.0.0.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_external_ip_present_type_snat(self):
self.l3_inst._nb_ovn.is_col_present.return_value = True
@@ -1284,7 +1292,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.0.0.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_lsp_external_id(self):
foo_lport = fake_resources.FakeOvsdbRow.create_one_ovsdb_row()
@@ -1335,7 +1345,9 @@ class BaseTestOVNL3RouterPluginMixin():
external_ip='192.168.0.10',
logical_ip='10.0.0.10',
type='dnat_and_snat',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_lb_vip_fip(self):
config.cfg.CONF.set_override(
@@ -1372,7 +1384,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.0.0.10',
logical_port='port_id',
type='dnat_and_snat',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
self.l3_inst._nb_ovn.db_find_rows.assert_called_with(
'NAT', ('external_ids', '=', {ovn_const.OVN_FIP_PORT_EXT_ID_KEY:
self.member_lsp.name}))
@@ -1425,7 +1439,9 @@ class BaseTestOVNL3RouterPluginMixin():
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids,
gateway_port=lrp.uuid)
gateway_port=lrp.uuid,
options={'stateless': 'false'},
)
else:
_nb_ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
@@ -1433,7 +1449,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.0.0.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
def test_create_floatingip_with_gateway_port(self):
self._test_create_floatingip_gateway_port_option(True)
@@ -1550,7 +1568,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.10.10.10',
external_ip='192.168.0.10',
logical_port='new-port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
@mock.patch.object(extraroute_db.ExtraRoute_dbonly_mixin,
'update_floatingip')
@@ -1607,7 +1627,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.10.10.10',
external_ip='192.168.0.10',
logical_port='new-port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
@mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_network')
@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
@@ -1649,7 +1671,9 @@ class BaseTestOVNL3RouterPluginMixin():
'neutron-new-router-id', type='dnat_and_snat',
logical_ip='10.10.10.10', external_ip='192.168.0.10',
external_mac='00:01:02:03:04:05', logical_port='new-port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
'update_floatingip')
@@ -1691,7 +1715,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.10.10.10',
external_ip='192.168.0.10',
logical_port='foo',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
'update_floatingip')
@@ -1735,7 +1761,9 @@ class BaseTestOVNL3RouterPluginMixin():
logical_ip='10.10.10.10',
external_ip='192.168.0.10',
logical_port='port_id',
external_ids=expected_ext_ids)
external_ids=expected_ext_ids,
options={'stateless': 'false'},
)
@mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.'
'update_floatingip_status')

View File

@@ -0,0 +1,9 @@
---
features:
- |
The ML2/OVN driver can now use stateless NAT for floating IP addresses.
This functionality is configurable using the new boolean config option
``[ovn]stateless_nat_enabled``. By default, this option is disabled,
keeping the current behaviour. This functionality improves the performance
in some deployments (DPDK based, for example) by avoiding hitting
conntrack OVN actions.