diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py index 9d8ccb1899f..653b6f8b458 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py @@ -404,6 +404,16 @@ class PortBindingChassisEvent(row_event.RowEvent): self.event_name = 'PortBindingChassisEvent' def run(self, event, row, old): + if len(old._data) == 1 and 'external_ids' in old._data: + # NOTE: since [1], the NB logical_router_port.external_ids are + # copied into the SB port_binding.external_ids. If only the + # external_ids are changed, this event should be dismissed or it + # will trigger the Neutron NB update (that will trigger the core + # SB update and therefore an infinite loop). + # [1] https://www.mail-archive.com/ovs-dev@openvswitch.org/ + # msg62836.html + return + if not utils.is_ovn_l3(self.l3_plugin): return router = host = None diff --git a/neutron/tests/functional/base.py b/neutron/tests/functional/base.py index d24b3b4be4f..deb5dbe8c79 100644 --- a/neutron/tests/functional/base.py +++ b/neutron/tests/functional/base.py @@ -423,17 +423,22 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase, self._start_ovn_northd() def add_fake_chassis(self, host, physical_nets=None, external_ids=None, - name=None, azs=None): + name=None, azs=None, enable_chassis_as_gw=False): + def append_cms_options(ext_ids, value): + if 'ovn-cms-options' not in ext_ids: + ext_ids['ovn-cms-options'] = value + else: + ext_ids['ovn-cms-options'] += ',' + value + physical_nets = physical_nets or [] external_ids = external_ids or {} if azs is None: azs = ['ovn'] if azs: - if 'ovn-cms-options' not in external_ids: - external_ids['ovn-cms-options'] = 'availability-zones=' - else: - external_ids['ovn-cms-options'] += ',availability-zones=' + append_cms_options(external_ids, 'availability-zones=') external_ids['ovn-cms-options'] += ':'.join(azs) + if enable_chassis_as_gw: + append_cms_options(external_ids, 'enable-chassis-as-gw') bridge_mapping = ",".join(["%s:br-provider%s" % (phys_net, i) for i, phys_net in enumerate(physical_nets)]) diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py index 9d7f327e9ad..5077cac4dd4 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py @@ -17,6 +17,7 @@ from unittest import mock import fixtures as og_fixtures from neutron_lib.api.definitions import allowedaddresspairs +from neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import portbindings from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory @@ -26,7 +27,6 @@ from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import event from ovsdbapp.backend.ovs_idl import idlutils - from neutron.common.ovn import constants as ovn_const from neutron.common import utils as n_utils from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf @@ -35,8 +35,11 @@ from neutron.plugins.ml2.drivers.ovn.agent import neutron_agent from neutron.plugins.ml2.drivers.ovn.mech_driver import mech_driver from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor from neutron.tests.functional import base +from neutron.tests.functional.resources.ovsdb import events as test_events from neutron.tests.functional.resources.ovsdb import fixtures from neutron.tests.functional.resources import process +from neutron.tests.unit.api import test_extensions +from neutron.tests.unit.extensions import test_l3 class WaitForDataPathBindingCreateEvent(event.WaitEvent): @@ -364,6 +367,64 @@ class TestNBDbMonitorOverSsl(TestNBDbMonitor): return 'ssl' +class TestSBDbMonitor(base.TestOVNFunctionalBase, test_l3.L3NatTestCaseMixin): + + def setUp(self, **kwargs): + super().setUp(**kwargs) + self.chassis = self.add_fake_chassis('ovs-host1', + enable_chassis_as_gw=True) + self.l3_plugin = directory.get_plugin(plugin_constants.L3) + ext_mgr = test_l3.L3TestExtensionManager() + self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) + self.handler = event.RowEventHandler() + self.sb_api.idl.notify = self.handler.notify + + def _find_port_binding(self, port_type): + rows = self.sb_api.db_find_rows( + 'Port_Binding', ('type', '=', port_type)).execute(check_error=True) + return rows[0] if rows else None + + def test_router_port_binding(self): + # This test will check the router GW port creation and binding. + def check_ext_ids(): + pb = self._find_port_binding(ovn_const.OVN_CHASSIS_REDIRECT) + _, lrp = list( + self.nb_api.tables['Logical_Router_Port'].rows.data.items())[0] + if pb.external_ids == {}: + # The current version of OVN installed in FT CI could not have + # [1]. In this case, the Port_Binding.external_ids value is an + # empty dictionary. + # [1]https://www.mail-archive.com/ovs-dev@openvswitch.org/ + # msg62836.html + return True + return lrp.external_ids == pb.external_ids + + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + ext_net = self._make_network(self.fmt, 'ext_net', True, **kwargs) + self._make_subnet(self.fmt, ext_net, '10.251.0.1', '10.251.0.0/24', + enable_dhcp=True) + router = self._make_router(self.fmt, self._tenant_id) + row_event = test_events.WaitForCreatePortBindingEventPerType() + self.handler.watch_event(row_event) + self._add_external_gateway_to_router(router['router']['id'], + ext_net['network']['id']) + self.assertTrue(row_event.wait()) + + # Check SB pb.external_ids == NB lrp.external_ids + port_binding = self._find_port_binding(ovn_const.OVN_CHASSIS_REDIRECT) + self.sb_api.db_set('Port_Binding', port_binding.uuid, + ('up', True)).execute(check_error=True) + try: + n_utils.wait_until_true(check_ext_ids, timeout=5) + except n_utils.WaitTimeout: + pb = self._find_port_binding(ovn_const.OVN_CHASSIS_REDIRECT) + _, lrp = list( + self.nb_api.tables['Logical_Router_Port'].rows.data.items())[0] + self.fail('pb.ext_ids: %s -- lrp.ext_ids: %s' % + (pb.external_ids, lrp.external_ids)) + + class TestAgentMonitor(base.TestOVNFunctionalBase): FAKE_CHASSIS_HOST = 'fake-chassis-host' diff --git a/neutron/tests/functional/resources/ovsdb/events.py b/neutron/tests/functional/resources/ovsdb/events.py index e4a71707711..960f46528c4 100644 --- a/neutron/tests/functional/resources/ovsdb/events.py +++ b/neutron/tests/functional/resources/ovsdb/events.py @@ -18,6 +18,8 @@ import threading from ovsdbapp.backend.ovs_idl import event from ovsdbapp.tests.functional.schema.ovn_southbound import event as test_event +from neutron.common.ovn import constants as ovn_const + class WaitForCrLrpPortBindingEvent(event.RowEvent): event_name = 'WaitForCrLrpPortBindingEvent' @@ -63,3 +65,12 @@ class WaitForUpdatePortBindingEvent(test_event.WaitForPortBindingEvent): (('logical_port', '=', port), ('mac', '=', mac)), timeout=timeout) + + +class WaitForCreatePortBindingEventPerType(event.WaitEvent): + event_name = 'WaitForCreatePortBindingEventPerType' + + def __init__(self, port_type=ovn_const.OVN_CHASSIS_REDIRECT, + timeout=5): + super().__init__((self.ROW_CREATE,), 'Port_Binding', + (('type', '=', port_type),), timeout=timeout)