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 fd1d483fe41..1f1b81d7bec 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 @@ -89,6 +89,15 @@ class ChassisEvent(row_event.RowEvent): 'HA_Chassis_Group').execute(check_error=True): if not hcg.name.startswith(ovn_const.OVN_NAME_PREFIX): continue + + net_id = hcg.external_ids.get(ovn_const.OVN_NETWORK_ID_EXT_ID_KEY) + router_id = hcg.external_ids.get( + ovn_const.OVN_ROUTER_ID_EXT_ID_KEY) + if net_id and router_id: + # This HA_Chassis_Group is linked to a router, it will be + # updated matching the router Gateway_Chassis registers. + continue + # The filter() is to get rid of the empty string in # the list that is returned because of split() azs = {az for az in diff --git a/neutron/services/ovn_l3/ovsdb_monitor.py b/neutron/services/ovn_l3/ovsdb_monitor.py index 69a5cfe2f7e..31d01c13457 100644 --- a/neutron/services/ovn_l3/ovsdb_monitor.py +++ b/neutron/services/ovn_l3/ovsdb_monitor.py @@ -75,3 +75,32 @@ class LogicalRouterPortEvent(row_event.RowEvent): else: # LRP gateway port. self.l3_plugin._ovn_client.update_router_ha_chassis_group( self.admin_context, router_id) + + +class LogicalRouterPortGatewayChassisEvent(row_event.RowEvent): + """Logical_Router_Port Gateway_Chassis change event. + + When the Gateway_Chassis list of a Logical_Router_Port changes, it is + needed to update the linked HA_Chassis_Group registers. + """ + def __init__(self, driver): + self.driver = driver + self.l3_plugin = directory.get_plugin(constants.L3) + self.admin_context = neutron_context.get_admin_context() + table = 'Logical_Router_Port' + events = (self.ROW_UPDATE, ) + super().__init__(events, table, None) + + def match_fn(self, event, row, old): + if hasattr(old, 'gateway_chassis'): + # NOTE: when a Gateway_Chassis register is deleted, is no longer + # present in the old.gateway_chassis list. + return True + + return False + + def run(self, event, row, old=None): + lr_name = row.external_ids.get(ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY) + router_id = utils.get_neutron_name(lr_name) + self.l3_plugin._ovn_client.update_router_ha_chassis_group( + self.admin_context, router_id) diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index 67249aaa276..b116140a516 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -179,6 +179,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, # Register needed events. self._nb_ovn.idl.notify_handler.watch_events([ ovsdb_monitor.LogicalRouterPortEvent(self), + ovsdb_monitor.LogicalRouterPortGatewayChassisEvent(self), ]) def _add_neutron_router_interface(self, context, router_id, diff --git a/neutron/tests/functional/services/ovn_l3/test_ovsdb_monitor.py b/neutron/tests/functional/services/ovn_l3/test_ovsdb_monitor.py index 0a0338f642d..b636fe1fa81 100644 --- a/neutron/tests/functional/services/ovn_l3/test_ovsdb_monitor.py +++ b/neutron/tests/functional/services/ovn_l3/test_ovsdb_monitor.py @@ -18,6 +18,7 @@ from neutron_lib.api.definitions import external_net from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory +from neutron.common.ovn import utils as ovn_utils from neutron.common import utils as n_utils from neutron.tests.functional import base from neutron.tests.unit.api import test_extensions @@ -101,3 +102,80 @@ class TestLogicalRouterPortEvent( self._router_interface_action( 'remove', self.router_id, self.subnet_id, None) n_utils.wait_until_true(is_called, timeout=10) + + def test_delete_router(self): + # The ``Logical_Router`` deletion triggers the + # ``LogicalRouterPortEvent`` event, but nothing is executed/called. + def is_called(): + try: + mock_update_router.assert_called_once_with( + mock.ANY, self.router_id) + return True + except AssertionError: + return False + + with mock.patch.object( + self.l3_plugin._ovn_client, + 'update_router_ha_chassis_group') as mock_update_router: + self._add_external_gateway_to_router(self.router_id, + self.net_ext_id) + n_utils.wait_until_true(is_called, timeout=10) + mock_update_router.reset_mock() + req = self.new_delete_request('routers', self.router_id) + req.get_response(self.api) + self.assertRaises(n_utils.WaitTimeout, n_utils.wait_until_true, + is_called, timeout=5) + + +class TestLogicalRouterPortGatewayChassisEvent( + base.TestOVNFunctionalBase, + test_l3.L3NatTestCaseMixin): + + def setUp(self, **kwargs): + super().setUp(**kwargs) + self.chassis = self.add_fake_chassis('ovs-host1') + self.l3_plugin = directory.get_plugin(plugin_constants.L3) + self.l3_plugin._post_fork_initialize(mock.ANY, mock.ANY, mock.ANY) + self.ext_api = test_extensions.setup_extensions_middleware( + test_l3.L3TestExtensionManager()) + kwargs = {'arg_list': (external_net.EXTERNAL,), + external_net.EXTERNAL: True} + self.net_ext = self._make_network( + self.fmt, 'net_ext', True, as_admin=True, **kwargs) + self.subnet = self._make_subnet(self.fmt, self.net_ext, '20.0.10.1', + '20.0.10.0/24') + self.router = self._make_router(self.fmt, self._tenant_id) + self.router_id = self.router['router']['id'] + self.net_ext_id = self.net_ext['network']['id'] + self.subnet_id = self.subnet['subnet']['id'] + + def test_add_and_remove_gateway_chassis(self): + def is_called(): + try: + mock_update_router.assert_called_once_with( + mock.ANY, self.router_id) + return True + except AssertionError: + return False + + ch_list = [] + for idx in range(5): + ch_list.append(self.add_fake_chassis(f'host-{idx}')) + self._add_external_gateway_to_router(self.router_id, self.net_ext_id) + lr = self.l3_plugin._nb_ovn.lookup('Logical_Router', + ovn_utils.ovn_name(self.router_id)) + lrp_gw = lr.ports[0] + with mock.patch.object( + self.l3_plugin._ovn_client, + 'update_router_ha_chassis_group') as mock_update_router: + for ch_name in ch_list: + self.l3_plugin._nb_ovn.lrp_set_gateway_chassis( + lrp_gw.uuid, ch_name).execute(check_error=True) + n_utils.wait_until_true(is_called, timeout=10) + mock_update_router.reset_mock() + + for ch_name in ch_list: + self.l3_plugin._nb_ovn.lrp_del_gateway_chassis( + lrp_gw.uuid, ch_name).execute(check_error=True) + n_utils.wait_until_true(is_called, timeout=10) + mock_update_router.reset_mock()