diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index 0579987c762..ad4fa99116f 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -405,6 +405,8 @@ LSP_OPTIONS_MCAST_FLOOD = 'mcast_flood' LSP_OPTIONS_QOS_MIN_RATE = 'qos_min_rate' LSP_OPTIONS_LOCALNET_LEARN_FDB = 'localnet_learn_fdb' +LR_OPTIONS_MAC_AGE_LIMIT = 'mac_binding_age_threshold' + LRP_OPTIONS_RESIDE_REDIR_CH = 'reside-on-redirect-chassis' LRP_OPTIONS_REDIRECT_TYPE = 'redirect-type' BRIDGE_REDIRECT_TYPE = "bridged" diff --git a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py index 600c1c755d1..02f34b452c7 100644 --- a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py @@ -224,6 +224,11 @@ ovn_opts = [ 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.')), + cfg.IntOpt('mac_binding_age_threshold', + min=0, + default=0, + help=_('The number of seconds to keep MAC_Binding entries in ' + 'the OVN DB. 0 to disable aging.')), ] nb_global_opts = [ @@ -247,6 +252,14 @@ nb_global_opts = [ '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.')), + cfg.IntOpt('mac_binding_removal_limit', + min=0, + default=0, + help=_('MAC binding aging bulk removal limit. This limits how ' + 'many entries can expire in a single transaction. ' + 'The default is 0 which is unlimited. When the limit ' + 'is reached, the next batch removal is delayed by ' + '5 seconds.')), ] @@ -370,3 +383,8 @@ def get_fdb_age_threshold(): def get_fdb_removal_limit(): return str(cfg.CONF.ovn_nb_global.fdb_removal_limit) + + +def get_ovn_mac_binding_age_threshold(): + # This value is always stored as a string in the OVN DB + return str(cfg.CONF.ovn.mac_binding_age_threshold) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py index 2f05b2e68bd..c84aa3ecfdd 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py @@ -182,6 +182,15 @@ class API(api.API, metaclass=abc.ABCMeta): :returns: :class:`Command` with no result """ + @abc.abstractmethod + def set_router_mac_age_limit(self, router=None): + """Set the OVN MAC_Binding age threshold + + :param router: The name or UUID of a router, or None for all routers + :type router: uuid.UUID or string or None + :returns: :class:`Command` with no result + """ + @abc.abstractmethod def add_acl(self, lswitch, lport, **columns): """Create an ACL for a logical port. diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py index 2faadf0aa1b..e7c4eb592e6 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py @@ -472,6 +472,40 @@ class SetLRouterPortInLSwitchPortCommand(command.BaseCommand): setattr(port, 'addresses', self.lsp_address) +class SetLRouterMacAgeLimitCommand(command.BaseCommand): + def __init__(self, api, router, threshold): + super().__init__(api) + self.router = router + self.threshold = str(threshold) # Just in case an integer sneaks in + + def run_idl(self, txn): + # Creating a Command object that iterates over the list of Routers + # from inside a transaction avoids the issue of doing two + # transactions: one for list_rows() and the other for setting the + # values on routers, which would allow routers to be added and removed + # between the two transactions. + if self.router is None: + routers = self.api.tables["Logical_Router"].rows.values() + else: + routers = [self.api.lookup("Logical_Router", self.router)] + + for router in routers: + # It's not technically necessary to check the value before setting + # it as python-ovs is smart enough to avoid sending operations to + # the server that would result in no change. The overhead of + # setkey() though is > than the overhead of checking the value here + try: + if (router.options.get(ovn_const.LR_OPTIONS_MAC_AGE_LIMIT) == + self.threshold): + continue + except AttributeError: + # The Logical_Router is newly created in this txn and has no + # "options" set yet, which the following setkey will rectify + pass + router.setkey("options", ovn_const.LR_OPTIONS_MAC_AGE_LIMIT, + self.threshold) + + class AddACLCommand(command.BaseCommand): def __init__(self, api, lswitch, lport, **columns): super(AddACLCommand, self).__init__(api) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py index f564e3da242..366d76b0426 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py @@ -857,6 +857,11 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): LOG.debug("Setting NB_Global options: %s", options) return self.db_set("NB_Global", ".", options=options) + def set_router_mac_age_limit(self, router=None): + # Set the MAC_Binding age limit on OVN Logical Routers + return cmd.SetLRouterMacAgeLimitCommand( + self, router, cfg.get_ovn_mac_binding_age_threshold()) + class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): def __init__(self, connection): 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 823ebc9fe1f..f8ec285588a 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -832,6 +832,15 @@ 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 update_mac_aging_settings(self): + """Ensure that MAC_Binding aging options are set""" + with self._nb_idl.transaction(check_error=True) as txn: + txn.add(self._nb_idl.set_router_mac_age_limit()) + 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 4bc3c9002e9..c55340906df 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 @@ -1354,7 +1354,9 @@ class OVNClient(object): lrouter_name = utils.ovn_name(router['id']) added_gw_port = None options = {'always_learn_from_arp_request': 'false', - 'dynamic_neigh_routers': 'true'} + 'dynamic_neigh_routers': 'true', + ovn_const.LR_OPTIONS_MAC_AGE_LIMIT: + ovn_conf.get_ovn_mac_binding_age_threshold()} with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.create_lrouter(lrouter_name, external_ids=external_ids, diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py index 0f0d067b69f..833247237b4 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py @@ -383,6 +383,27 @@ class TestNbApi(BaseOvnIdlTest): lsp = self.nbapi.lookup('Logical_Switch_Port', lsp_name) self.assertEqual([], lsp.ha_chassis_group) + def test_set_router_mac_aging(self): + name = "aging_router1" + with self.nbapi.transaction(check_error=True) as txn: + r = txn.add(self.nbapi.lr_add(name)) + txn.add(self.nbapi.set_router_mac_age_limit(router=name)) + self.assertEqual(r.result.options[ovn_const.LR_OPTIONS_MAC_AGE_LIMIT], + ovn_conf.get_ovn_mac_binding_age_threshold()) + + def test_set_router_mac_aging_all(self): + ovn_conf.cfg.CONF.set_override("mac_binding_age_threshold", 5, + group="ovn") + names = ["aging_router2", "aging_router3"] + with self.nbapi.transaction(check_error=True) as txn: + for name in names: + txn.add(self.nbapi.lr_add(name)) + txn.add(self.nbapi.set_router_mac_age_limit()) + for name in names: + r = self.nbapi.lookup("Logical_Router", name) + self.assertEqual(r.options[ovn_const.LR_OPTIONS_MAC_AGE_LIMIT], + ovn_conf.get_ovn_mac_binding_age_threshold()) + class TestIgnoreConnectionTimeout(BaseOvnIdlTest): @classmethod diff --git a/neutron/tests/functional/services/ovn_l3/test_plugin.py b/neutron/tests/functional/services/ovn_l3/test_plugin.py index fbaa0b8aded..527d3dc3522 100644 --- a/neutron/tests/functional/services/ovn_l3/test_plugin.py +++ b/neutron/tests/functional/services/ovn_l3/test_plugin.py @@ -26,6 +26,7 @@ from ovsdbapp.backend.ovs_idl import idlutils from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import utils as ovn_utils from neutron.common import utils as n_utils +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron.scheduler import l3_ovn_scheduler as l3_sched from neutron.tests.functional import base from neutron.tests.functional.resources.ovsdb import events @@ -595,3 +596,12 @@ class TestRouter(base.TestOVNFunctionalBase): 'Logical_Router_Port'].rows.values(): self.assertEqual(ovn_const.MAX_GW_CHASSIS, len(row.gateway_chassis)) + + def test_set_router_mac_age_limit(self): + name = "macage_router1" + router = self._create_router(name) + lr_name = ovn_utils.ovn_name(router['id']) + options = self.nb_api.db_get( + "Logical_Router", lr_name, "options").execute(check_error=True) + self.assertEqual(ovn_conf.get_ovn_mac_binding_age_threshold(), + options[ovn_const.LR_OPTIONS_MAC_AGE_LIMIT]) diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index e8d45aca6ac..6827c54c110 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -558,8 +558,11 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ''} self.l3_inst._nb_ovn.create_lrouter.assert_called_once_with( 'neutron-router-id', external_ids=external_ids, - enabled=True, options={'always_learn_from_arp_request': 'false', - 'dynamic_neigh_routers': 'true'}) + enabled=True, + options={'always_learn_from_arp_request': 'false', + 'dynamic_neigh_routers': 'true', + ovn_const.LR_OPTIONS_MAC_AGE_LIMIT: + config.get_ovn_mac_binding_age_threshold()}) self.l3_inst._nb_ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) expected_calls = [ diff --git a/releasenotes/notes/support-ovn-mac-aging-8a555c608ccf8b04.yaml b/releasenotes/notes/support-ovn-mac-aging-8a555c608ccf8b04.yaml new file mode 100644 index 00000000000..4a64b0d4103 --- /dev/null +++ b/releasenotes/notes/support-ovn-mac-aging-8a555c608ccf8b04.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + MAC address aging in the OVN ML2 mech driver is now supported and can + be configured globally with the new ``[ovn] mac_binding_aging_threshold`` + and ``[ovn_nb_global] mac_binding_removal_limit`` configuration + options. Setting the value per-router is not currently supported. This + feature is available in OVN versions >= 22.09.0+. Previous versions will + ignore the new options.