[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:
committed by
Rodolfo Alonso
parent
4e7be0f639
commit
2145901d6f
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user