diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index 3de372b2f7b..9f04d75f94f 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -402,6 +402,9 @@ LRP_OPTIONS_RESIDE_REDIR_CH = 'reside-on-redirect-chassis' LRP_OPTIONS_REDIRECT_TYPE = 'redirect-type' BRIDGE_REDIRECT_TYPE = "bridged" +# FDB AGE Settings +LS_OPTIONS_FDB_AGE_THRESHOLD = 'fdb_age_threshold' + # Port Binding types PB_TYPE_VIRTUAL = 'virtual' diff --git a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py index 290dd4dce05..6265920af1c 100644 --- a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py @@ -226,6 +226,12 @@ ovn_opts = [ 'flooding for traffic towards unknown IPs when port ' 'security is disabled. It requires OVN 22.09 or ' 'newer.')), + cfg.IntOpt('fdb_age_threshold', + min=0, + default=0, + help=_('The number of seconds to keep FDB entries in the OVN ' + 'DB. The value defaults to 0, which means disabled. ' + 'This is supported by OVN >= 23.09.')), ] nb_global_opts = [ @@ -241,6 +247,14 @@ nb_global_opts = [ 'is not an issue, setting it to True can reduce ' 'the load and latency of the control plane. ' 'The default value is False.')), + cfg.IntOpt('fdb_removal_limit', + min=0, + default=0, + help=_('FDB aging bulk removal limit. This limits how many ' + 'rows can expire in a single transaction. Default ' + 'is 0, which is unlimited. When the limit is reached, ' + 'the next batch removal is delayed by 5 seconds. ' + 'This is supported by OVN >= 23.09.')), ] @@ -360,3 +374,11 @@ def is_ovn_dhcp_disabled_for_baremetal(): def is_learn_fdb_enabled(): return cfg.CONF.ovn.localnet_learn_fdb + + +def get_fdb_age_threshold(): + return str(cfg.CONF.ovn.fdb_age_threshold) + + +def get_fdb_removal_limit(): + return str(cfg.CONF.ovn_nb_global.fdb_removal_limit) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py index b639af08713..69ed63ef96f 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -798,6 +798,39 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase): txn.add(cmd) raise periodics.NeverAgain() + # A static spacing value is used here, but this method will only run + # once per lock due to the use of periodics.NeverAgain(). + @has_lock_periodic(spacing=600, run_immediately=True) + def check_fdb_aging_settings(self): + """Check FDB aging settings + Ensure FDB aging settings are enforced. + """ + context = n_context.get_admin_context() + cmds = [] + + config_fdb_age_threshold = ovn_conf.get_fdb_age_threshold() + # Get provider networks + nets = self._ovn_client._plugin.get_networks(context) + for net in nets: + if not utils.is_provider_network(net): + continue + ls_name = utils.ovn_name(net['id']) + ls = self._nb_idl.get_lswitch(ls_name) + ls_fdb_age_threshold = ls.other_config.get( + ovn_const.LS_OPTIONS_FDB_AGE_THRESHOLD) + + if config_fdb_age_threshold != ls_fdb_age_threshold: + other_config = {ovn_const.LS_OPTIONS_FDB_AGE_THRESHOLD: + config_fdb_age_threshold} + cmds.append(self._nb_idl.db_set( + 'Logical_Switch', ls_name, + ('other_config', other_config))) + if cmds: + with self._nb_idl.transaction(check_error=True) as txn: + for cmd in cmds: + txn.add(cmd) + raise periodics.NeverAgain() + # TODO(fnordahl): Remove this in the B+3 cycle. This method removes the # now redundant "external_ids:OVN_GW_NETWORK_EXT_ID_KEY" and # "external_ids:OVN_GW_PORT_EXT_ID_KEY" from to each router. diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 9e2bde5497c..34045673b66 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -1914,6 +1914,9 @@ class OVNClient(object): params['other_config'] = {ovn_const.MCAST_SNOOP: value, ovn_const.MCAST_FLOOD_UNREGISTERED: 'false', ovn_const.VLAN_PASSTHRU: vlan_transparent} + if utils.is_provider_network(network): + params['other_config'][ovn_const.LS_OPTIONS_FDB_AGE_THRESHOLD] = ( + ovn_conf.get_fdb_age_threshold()) return params def create_network(self, context, network): diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 20129fafb99..eadc9383b82 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -20,6 +20,7 @@ from oslo_config import cfg from futurist import periodics from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib.api.definitions import floating_ip_port_forwarding as pf_def +from neutron_lib.api.definitions import provider_net as provnet_apidef from neutron_lib import constants as n_const from neutron_lib import context as n_context from neutron_lib.exceptions import l3 as lib_l3_exc @@ -60,8 +61,12 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase): ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY) == name): return row - def _create_network(self, name, external=False): - data = {'network': {'name': name, extnet_apidef.EXTERNAL: external}} + def _create_network(self, name, external=False, provider=None): + data = {'network': {'name': name, + extnet_apidef.EXTERNAL: external}} + if provider: + data['network'][provnet_apidef.NETWORK_TYPE] = 'flat' + data['network'][provnet_apidef.PHYSICAL_NETWORK] = provider req = self.new_create_request('networks', data, self.fmt, as_admin=True) res = req.get_response(self.api) @@ -756,6 +761,26 @@ class TestMaintenance(_TestMaintenanceHelper): self.assertEqual( 'false', ls['other_config'][ovn_const.MCAST_FLOOD_UNREGISTERED]) + def test_check_for_aging_settings(self): + net = self._create_network('net', provider='datacentre') + ls = self.nb_api.get_lswitch(utils.ovn_name(net['id'])) + + self.assertEqual( + '0', ls.other_config.get(ovn_const.LS_OPTIONS_FDB_AGE_THRESHOLD)) + + # Change the value of the configuration + cfg.CONF.set_override('fdb_age_threshold', 5, group='ovn') + + # Call the maintenance task and check that the value has been + # updated in the Logical Switch + self.assertRaises(periodics.NeverAgain, + self.maint.check_fdb_aging_settings) + + ls = self.nb_api.get_lswitch(utils.ovn_name(net['id'])) + + self.assertEqual( + '5', ls.other_config.get(ovn_const.LS_OPTIONS_FDB_AGE_THRESHOLD)) + def test_floating_ip(self): ext_net = self._create_network('ext_networktest', external=True) ext_subnet = self._create_subnet( diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 0788576f105..f73a66c325a 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -891,6 +891,37 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight, self.fake_ovn_client._nb_idl.db_set.assert_has_calls( expected_calls) + def test_check_fdb_aging_settings(self): + cfg.CONF.set_override('fdb_age_threshold', 5, group='ovn') + networks = [{'id': 'foo', + 'provider:physical_network': 'datacentre'}] + self.fake_ovn_client._plugin.get_networks.return_value = networks + fake_ls = mock.Mock(other_config={}) + self.fake_ovn_client._nb_idl.get_lswitch.return_value = fake_ls + + self.assertRaises( + periodics.NeverAgain, + self.periodic.check_fdb_aging_settings) + + self.fake_ovn_client._nb_idl.db_set.assert_called_once_with( + 'Logical_Switch', 'neutron-foo', + ('other_config', {constants.LS_OPTIONS_FDB_AGE_THRESHOLD: '5'})) + + def test_check_fdb_aging_settings_with_threshold_set(self): + cfg.CONF.set_override('fdb_age_threshold', 5, group='ovn') + networks = [{'id': 'foo', + 'provider:network_type': n_const.TYPE_VLAN}] + self.fake_ovn_client._plugin.get_networks.return_value = networks + fake_ls = mock.Mock(other_config={ + constants.LS_OPTIONS_FDB_AGE_THRESHOLD: '5'}) + self.fake_ovn_client._nb_idl.get_lswitch.return_value = fake_ls + + self.assertRaises( + periodics.NeverAgain, + self.periodic.check_fdb_aging_settings) + + self.fake_ovn_client._nb_idl.db_set.assert_not_called() + def test_remove_gw_ext_ids_from_logical_router(self): nb_idl = self.fake_ovn_client._nb_idl # lr0: GW port ID, not GW network ID --> we need to remove port ID. diff --git a/releasenotes/notes/support-fdb-aging-b9ab82d75db81bbc.yaml b/releasenotes/notes/support-fdb-aging-b9ab82d75db81bbc.yaml new file mode 100644 index 00000000000..2855046e534 --- /dev/null +++ b/releasenotes/notes/support-fdb-aging-b9ab82d75db81bbc.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + In OVN 22.09 the option ``localnet_learn_fdb`` was added, enabling localnet + ports to learn MAC addresses and store them at the FDB table. There was no + aging mechanism for those MACs until OVN 23.06, where the configuration + option ``fdb_age_threshold`` was added. This enables to set the maximum + time the learned MACs will stay in the FDB table (in seconds). When the + ``localnet_learn_fdb`` configuration option is enabled, the proper value + for ``fdb_age_threshold`` should also be set, to avoid + performance/scalability issues due to the table growing too much -- + especially when provider networks are large. In addition the configuration + option ``fdb_removal_limit`` was also added to avoid removing a large + number of entries at once.