Merge "[OVN] The external networks GW chassis must the same as the GW LRP"

This commit is contained in:
Zuul
2025-11-21 15:11:58 +00:00
committed by Gerrit Code Review
7 changed files with 351 additions and 2 deletions

View File

@@ -1094,6 +1094,18 @@ def get_subnets_address_scopes(context, subnets_by_id, fixed_ips, ml2_plugin):
return address4_scope_id, address6_scope_id
def get_high_prio_chassis_in_ha_chassis_group(ha_chassis_group):
"""Returns (name, priority) of the highest priority HA_Chassis"""
hc_list = []
for ha_chassis in ha_chassis_group.ha_chassis:
hc_list.append((ha_chassis.chassis_name, ha_chassis.priority))
hc_list = sorted(hc_list, key=lambda x: x[1], reverse=True)
try:
return hc_list[0]
except IndexError:
return None, None
def _filter_candidates_for_ha_chassis_group(hcg_info):
"""Filter a list of chassis candidates for a given HA Chassis Group.
@@ -1157,6 +1169,10 @@ def _sync_ha_chassis_group(nb_idl, hcg_info, txn):
ha_ch_grp_cmd = txn.add(nb_idl.ha_chassis_group_add(
hcg_info.group_name, may_exist=True,
external_ids=hcg_info.external_ids))
else:
# Update the external_ids.
txn.add(nb_idl.db_set('HA_Chassis_Group', hcg_info.group_name,
('external_ids', hcg_info.external_ids)))
max_chassis_number = constants.MAX_CHASSIS_IN_HA_GROUP
priority = constants.HA_CHASSIS_GROUP_HIGHEST_PRIORITY
@@ -1231,7 +1247,18 @@ def sync_ha_chassis_group_router(context, nb_idl, sb_idl, router_id, txn):
@ovn_context(idl_var_name='nb_idl')
def sync_ha_chassis_group_network(context, nb_idl, sb_idl, port_id,
network_id, txn):
"""Syncs the HA Chassis Group for a given network"""
"""Syncs the HA_Chassis_Group for a given network"""
# If there is a network associated HA_Chassis_Group, the port will use it
# instead of creating a new one or updating it.
group_name = ovn_name(network_id)
hcg = nb_idl.lookup('HA_Chassis_Group', group_name, default=None)
if hcg:
router_id = hcg.external_ids.get(constants.OVN_ROUTER_ID_EXT_ID_KEY)
if router_id:
# If the HA_Chassis_Group is linked to a router, do not modify it.
ch_name, _ = get_high_prio_chassis_in_ha_chassis_group(hcg)
return hcg.uuid, ch_name
# If there are Chassis marked for hosting external ports create a HA
# Chassis Group per external port, otherwise do it at the network
# level
@@ -1247,7 +1274,6 @@ def sync_ha_chassis_group_network(context, nb_idl, sb_idl, port_id,
else:
chassis_list = sb_idl.get_gateway_chassis_from_cms_options(
name_only=False)
group_name = ovn_name(network_id)
ignore_chassis = set()
LOG.debug('HA Chassis Group %s is based on network %s',
group_name, network_id)

View File

@@ -1599,6 +1599,29 @@ class OVNClient:
'Error: %(error)s', {'router': router_id,
'error': e})
def update_router_ha_chassis_group(self, context, router_id):
"""If the router has GW, bind all external ports to the same GW chassis
If a router receives or removes the gateway, this method checks all
the connected internal ports and collects its networks. Then it updates
each network, depending on the presence or not of the router gateway.
See LP#2125553.
"""
# Retrieve all internal networks (aka: ext_ids=neutron:is_ext_gw=False)
# connected to this router.
lr_name = utils.ovn_name(router_id)
lr = self._nb_idl.lr_get(lr_name).execute(check_error=True)
network_ids = set()
for lrp in lr.ports:
ext_gw = lrp.external_ids.get(ovn_const.OVN_ROUTER_IS_EXT_GW)
if not strutils.bool_from_string(ext_gw):
net_name = lrp.external_ids[
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY]
network_ids.add(utils.get_neutron_name(net_name))
for network_id in network_ids:
self.link_network_ha_chassis_group(context, network_id, router_id)
def delete_router(self, context, router_id):
"""Delete a logical router."""
lrouter_name = utils.ovn_name(router_id)
@@ -1810,6 +1833,21 @@ class OVNClient:
lsp_address=lsp_address))
self._transaction(commands, txn=txn)
def get_router_port(self, port_id):
try:
return self._nb_idl.lrp_get(
utils.ovn_lrouter_port_name(port_id)).execute(
check_errors=True)
except idlutils.RowNotFound:
return
def get_router_gateway_ports(self, router_id):
lrps = self._nb_idl.lrp_list(utils.ovn_name(router_id)).execute(
check_errors=True)
return [lrp for lrp in lrps if
strutils.bool_from_string(
lrp.external_ids.get(ovn_const.OVN_ROUTER_IS_EXT_GW))]
def create_router_port(self, context, router_id, router_interface):
port = self._plugin.get_port(context, router_interface['port_id'])
router = self._l3_plugin.get_router(context, router_id)
@@ -2284,6 +2322,66 @@ class OVNClient:
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
db_rev.bump_revision(context, network, ovn_const.TYPE_NETWORKS)
def unlink_network_ha_chassis_group(self, network_id):
"""Unlink the network HCG to the router
If the network (including all subnets) has been detached from the
router, the "HA_Chassis_Group" in unlinked from the router by removing
the router_id tag from the external_ids dictionary.
"""
name = utils.ovn_name(network_id)
hcg = self._nb_idl.lookup('HA_Chassis_Group', name, default=None)
if hcg:
self._nb_idl.db_remove(
'HA_Chassis_Group', name, 'external_ids',
ovn_const.OVN_ROUTER_ID_EXT_ID_KEY).execute(
check_error=True)
def link_network_ha_chassis_group(self, context, network_id, router_id):
"""Link a unified HCG for all network ext. ports if connected to router
If a network is connected to a router, this method checks if the router
has a gateway port and the corresponding "Gateway_Chassis" registers.
In that case, it creates a unified "HA_Chassis_Group" for this network
and assign it to all external ports. That will collocate the external
ports in the same gateway chassis as the router gateway port, allowing
N/S communication. See LP#2125553
"""
gw_lrps = self.get_router_gateway_ports(router_id)
if not gw_lrps:
# The router has no GW ports. Remove the "neutron:router_id" tag
# from the "HA_Chassis_Group" associated, if any.
self.unlink_network_ha_chassis_group(network_id)
return
# Retrieve all "Gateway_Chassis" and build the "chassis_prio"
# dictionary.
chassis_prio = {}
for gc in gw_lrps[0].gateway_chassis:
chassis_prio[gc.chassis_name] = gc.priority
with self._nb_idl.transaction(check_error=True) as txn:
# Create the "HA_Chassis_Group" associated to this network.
hcg, _ = utils.sync_ha_chassis_group_network_unified(
context, self._nb_idl, self._sb_idl, network_id, router_id,
chassis_prio, txn)
# Retrieve all LSPs from external ports in this network.
ls = self._nb_idl.lookup('Logical_Switch',
utils.ovn_name(network_id))
for lsp in (lsp for lsp in ls.ports if
lsp.type == ovn_const.LSP_TYPE_EXTERNAL):
# NOTE(ralonsoh): this is a protection check but all external
# ports must have "HA_Chassis_Group". If the "HA_Chassis_Group"
# register is for this port only, remove it.
group_name = utils.ovn_extport_chassis_group_name(lsp.name)
if (lsp.ha_chassis_group and
lsp.ha_chassis_group[0].name == group_name):
txn.add(self._nb_idl.ha_chassis_group_del(
lsp.ha_chassis_group[0].name, if_exists=True))
txn.add(self._nb_idl.db_set('Logical_Switch_Port', lsp.uuid,
('ha_chassis_group', hcg)))
def _add_subnet_dhcp_options(self, context, subnet, network,
ovn_dhcp_options=None):
if utils.is_dhcp_options_ignored(subnet):

View File

@@ -0,0 +1,77 @@
# Copyright 2025 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib import context as neutron_context
from neutron_lib.plugins import constants
from neutron_lib.plugins import directory
from oslo_utils import strutils
from ovsdbapp.backend.ovs_idl import event as row_event
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils
class LogicalRouterPortEvent(row_event.RowEvent):
"""Logical_Router_Port create/delete event.
If a Logical_Router_Port is deleted or added, first check if this LRP is a
gateway port or not. Then update the corresponding network (or networks)
HA_Chassis_Group, matching the Logical_Router Gateway_Chassis.
See LP#2125553.
"""
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_CREATE, self.ROW_DELETE)
super().__init__(events, table, None)
def match_fn(self, event, row, old):
if event == self.ROW_DELETE:
# Check if the LR has another port in the same network. If that is
# the case, do nothing.
ls_name = row.external_ids[ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY]
lr_name = row.external_ids[ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY]
lr = self.driver._nb_ovn.lookup('Logical_Router', lr_name)
for lrp in (lrp for lrp in lr.ports if lrp.name != row.name):
if (ls_name == lrp.external_ids[
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY]):
return False
return True
# event == self.ROW_CREATE
return True
def run(self, event, row, old=None):
ext_gw = row.external_ids.get(ovn_const.OVN_ROUTER_IS_EXT_GW)
router_id = utils.get_neutron_name(
row.external_ids[ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY])
net_id = utils.get_neutron_name(
row.external_ids[ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY])
if event == self.ROW_DELETE:
if not strutils.bool_from_string(ext_gw): # LRP internal port.
self.l3_plugin._ovn_client.unlink_network_ha_chassis_group(
net_id)
else: # LRP gateway port.
self.l3_plugin._ovn_client.update_router_ha_chassis_group(
self.admin_context, router_id)
else: # event == self.ROW_CREATE
if not strutils.bool_from_string(ext_gw): # LRP internal port.
self.l3_plugin._ovn_client.link_network_ha_chassis_group(
self.admin_context, net_id, router_id)
else: # LRP gateway port.
self.l3_plugin._ovn_client.update_router_ha_chassis_group(
self.admin_context, router_id)

View File

@@ -51,6 +51,7 @@ from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.quota import resource_registry
from neutron.scheduler import l3_ovn_scheduler
from neutron.services.ovn_l3 import exceptions as ovn_l3_exc
from neutron.services.ovn_l3 import ovsdb_monitor
from neutron.services.ovn_l3.service_providers import driver_controller
from neutron.services.portforwarding.drivers.ovn import driver \
as port_forwarding
@@ -175,6 +176,11 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
if not self._nb_ovn or not self._sb_ovn:
raise ovn_l3_exc.MechanismDriverOVNNotReady()
# Register needed events.
self._nb_ovn.idl.notify_handler.watch_events([
ovsdb_monitor.LogicalRouterPortEvent(self),
])
def _add_neutron_router_interface(self, context, router_id,
interface_info):
try:

View File

@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import network_mtu as mtu_def
from neutron_lib.api.definitions import provider_net
@@ -499,3 +501,38 @@ class TestOVNClient(base.TestOVNFunctionalBase,
dhcp_options.options['ntp_server'])
self.assertEqual('1.2.3.6',
dhcp_options.options['wpad'])
def test_update_ha_chassis_group_linked_to_router(self):
# Create a router with multiple networks (internal, external). The
# method `link_network_ha_chassis_group` must be called for all
# internal networks.
num_private_subnets = 5
ovn_client = self.mech_driver._ovn_client
net_arg = {provider_net.NETWORK_TYPE: 'geneve',
external_net.EXTERNAL: True}
with self.network('external', as_admin=True,
arg_list=tuple(net_arg.keys()), **net_arg) as net:
with self.subnet(net, cidr='10.100.0.0/24'):
ext_gw = {'network_id': net['network']['id']}
with self.router(external_gateway_info=ext_gw) as router:
router_id = router['router']['id']
net_ids = []
for idx in range(num_private_subnets):
with self.network('internal' + str(idx)) as net:
net_ids.append(net['network']['id'])
with self.subnet(net, cidr=f'10.{idx}.0.0/24') as subnet:
subnet_id = subnet['subnet']['id']
self._router_interface_action(
'add', router_id, subnet_id, None)
lr_name = ovn_utils.ovn_name(router_id)
lr = self.nb_api.lookup('Logical_Router', lr_name)
self.assertEqual(num_private_subnets + 1, len(lr.ports))
with mock.patch.object(ovn_client, 'link_network_ha_chassis_group') as\
mock_link:
ovn_client.update_router_ha_chassis_group(self.context, router_id)
calls = [mock.call(self.context, net_id, router_id)
for net_id in net_ids]
self.assertEqual(num_private_subnets, len(mock_link.mock_calls))
mock_link.assert_has_calls(calls, any_order=True)

View File

@@ -0,0 +1,103 @@
# Copyright 2025 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
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 import utils as n_utils
from neutron.tests.functional import base
from neutron.tests.unit.api import test_extensions
from neutron.tests.unit.extensions import test_l3
class TestLogicalRouterPortEvent(
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_delete_gw_network(self):
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()
self._remove_external_gateway_from_router(
self.router_id, self.net_ext_id, external_gw_info={})
n_utils.wait_until_true(is_called, timeout=10)
def test_add_private_network(self):
def is_called():
try:
mock_link.assert_called_once_with(
mock.ANY, self.net_ext_id, self.router_id)
return True
except AssertionError:
return False
with mock.patch.object(
self.l3_plugin._ovn_client,
'link_network_ha_chassis_group') as mock_link:
self._router_interface_action(
'add', self.router_id, self.subnet_id, None)
n_utils.wait_until_true(is_called, timeout=10)
def test_delete_private_network(self):
def is_called():
try:
mock_unlink.assert_called_once_with(self.net_ext_id)
return True
except AssertionError:
return False
with mock.patch.object(
self.l3_plugin._ovn_client,
'link_network_ha_chassis_group'), \
mock.patch.object(
self.l3_plugin._ovn_client,
'unlink_network_ha_chassis_group') as mock_unlink:
self._router_interface_action(
'add', self.router_id, self.subnet_id, None)
self._router_interface_action(
'remove', self.router_id, self.subnet_id, None)
n_utils.wait_until_true(is_called, timeout=10)

View File

@@ -2836,6 +2836,7 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
self.sb_ovn.get_chassis_host_for_port.return_value = {
ch4.name, ch5.name}
self.nb_ovn.lookup.return_value = None
ovn_utils.sync_ha_chassis_group_network(
self.context, self.nb_ovn, self.sb_ovn, fake_port['id'],
@@ -2879,6 +2880,7 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
self.sb_ovn.get_extport_chassis_from_cms_options.return_value = []
self.sb_ovn.get_gateway_chassis_from_cms_options.return_value = [
ch0, ch1, ch2, ch3, ch4, ch5]
self.nb_ovn.lookup.return_value = None
ovn_utils.sync_ha_chassis_group_network(
self.context, self.nb_ovn, self.sb_ovn, fake_port['id'],