[OVN] Add method `sync_ha_chassis_group_network_unified`

This new method is meant to be used when a network with external ports
is connected to a router. In this case, it is needed to create a single
scheduler for all the ports connected to the router and matching the
same chassis as the gateway router port.

This method uses the network ID to assign the group name. It also uses
a defined chassis priority (that will match the gateway router port
assignation). The created ``HA_Chassis_Group`` must have a set of
``HA_Chassis`` registers that should have the same chassis name and
priority as the gateway router port ``Gateway_Chassis``.

NOTE: in future developments, the router port ``Gateway_Chassis`` will
be replaced with a ``HA_Chassis_Group`` register.

Related-Bug: #2125553
Signed-off-by: Rodolfo Alonso Hernandez <ralonsoh@redhat.com>
Change-Id: Ia4f685077a8d72bf28f66daf21225d96f57ddef6
This commit is contained in:
Rodolfo Alonso Hernandez
2025-09-29 10:34:04 +00:00
parent 633ff6093b
commit a5ca6d2bb8
4 changed files with 139 additions and 9 deletions

View File

@@ -22,6 +22,7 @@ OVN_SG_EXT_ID_KEY = 'neutron:security_group_id'
OVN_SG_RULE_EXT_ID_KEY = 'neutron:security_group_rule_id'
OVN_ML2_MECH_DRIVER_NAME = 'ovn'
OVN_NETWORK_NAME_EXT_ID_KEY = 'neutron:network_name'
OVN_NETWORK_ID_EXT_ID_KEY = 'neutron:network_id'
OVN_NETWORK_MTU_EXT_ID_KEY = 'neutron:mtu'
OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name'
OVN_PORT_EXT_ID_KEY = 'neutron:port_id'

View File

@@ -16,6 +16,7 @@ import functools
import inspect
import os
import random
import typing
import netaddr
from neutron_lib.api.definitions import external_net
@@ -64,13 +65,34 @@ PortExtraDHCPValidation = collections.namedtuple(
BPInfo = collections.namedtuple(
'BPInfo', ['bp_param', 'vnic_type', 'capabilities'])
HAChassisGroupInfo = collections.namedtuple(
'HAChassisGroupInfo', ['group_name', 'chassis_list', 'az_hints',
'ignore_chassis', 'external_ids'])
_OVS_PERSIST_UUID = _SENTINEL = object()
class HAChassisGroupInfo:
def __init__(self,
group_name: str,
chassis_list: list[typing.Any],
az_hints: list[str],
ignore_chassis: set[str],
external_ids: dict[str, typing.Any],
priority: dict[str, typing.Any] | None = None):
if priority:
# If present, the "priority" dictionary must contain all the
# chassis names present in "chassis_list".
ch_name_list = [ch.name for ch in chassis_list]
if sorted(ch_name_list) != sorted(list(priority.keys())):
raise RuntimeError(_(
'In a "HAChassisGroupInfo", the "chassis_list" must have '
'the same chassis as the "priority" dictionary'))
self.group_name = group_name
self.chassis_list = chassis_list
self.az_hints = az_hints
self.ignore_chassis = ignore_chassis
self.external_ids = external_ids
self.priority = priority
class OvsdbClientCommand:
_CONNECTION = 0
_PRIVATE_KEY = 1
@@ -1112,6 +1134,16 @@ def _sync_ha_chassis_group(nb_idl, hcg_info, txn):
:returns: The HA Chassis Group UUID or the HA Chassis Group command object,
The name of the Chassis with the highest priority (could be None)
"""
def get_priority(ch_name):
nonlocal priority
nonlocal hcg_info
if hcg_info.priority:
return hcg_info.priority[ch_name]
_priority = int(priority)
priority -= 1
return _priority
# If there are Chassis marked for hosting external ports create a HA
# Chassis Group per external port, otherwise do it at the network level
candidates = _filter_candidates_for_ha_chassis_group(hcg_info)
@@ -1164,8 +1196,7 @@ def _sync_ha_chassis_group(nb_idl, hcg_info, txn):
ch_ordered_list = [ch[0] for ch in ch_ordered_list] + ch_add_list
for ch in ch_ordered_list:
txn.add(nb_idl.ha_chassis_group_add_chassis(
hcg_info.group_name, ch, priority=priority))
priority -= 1
hcg_info.group_name, ch, priority=get_priority(ch)))
if not high_prio_ch_name:
high_prio_ch_name = ch
@@ -1222,13 +1253,60 @@ def sync_ha_chassis_group_network(context, nb_idl, sb_idl, port_id,
plugin = directory.get_plugin()
resource = plugin.get_network(context, network_id)
az_hints = common_utils.get_az_hints(resource)
external_ids = {constants.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(az_hints)}
external_ids = {constants.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(az_hints),
constants.OVN_NETWORK_ID_EXT_ID_KEY: network_id,
}
hcg_info = HAChassisGroupInfo(
group_name=group_name, chassis_list=chassis_list, az_hints=az_hints,
ignore_chassis=ignore_chassis, external_ids=external_ids)
return _sync_ha_chassis_group(nb_idl, hcg_info, txn)
@ovn_context(idl_var_name='nb_idl')
def sync_ha_chassis_group_network_unified(context, nb_idl, sb_idl, network_id,
router_id, chassis_prio, txn):
"""Creates a single HA_Chassis_Group for a given network
This method creates a single HA_Chassis_Group for a network. This method
is called when a network with external ports is connected to a router;
in order to provide N/S connectivity all external ports need to be bound
to the same chassis as the gateway Logical_Router_Port.
The chassis list and the priority is already provided. This method checks
if all gateway chassis provided have external connectivity to this network.
"""
chassis_physnets = sb_idl.get_chassis_and_physnets()
group_name = ovn_name(network_id)
ls = nb_idl.get_lswitch(group_name)
# It is expected to be called for a non-tunnelled network with a physical
# network assigned.
physnet = ls.external_ids.get(constants.OVN_PHYSNET_EXT_ID_KEY)
if physnet:
missing_mappings = set()
for ch_name in chassis_prio:
if physnet not in chassis_physnets[ch_name]:
missing_mappings.add(ch_name)
if missing_mappings:
LOG.warning('The following chassis do not have mapped the '
f'physical network {physnet}: {missing_mappings}')
chassis_list = [sb_idl.lookup('Chassis', ch_name, None)
for ch_name in chassis_prio.keys()]
plugin = directory.get_plugin()
resource = plugin.get_network(context, network_id)
az_hints = common_utils.get_az_hints(resource)
external_ids = {constants.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(az_hints),
constants.OVN_NETWORK_ID_EXT_ID_KEY: network_id,
constants.OVN_ROUTER_ID_EXT_ID_KEY: router_id,
}
hcg_info = HAChassisGroupInfo(
group_name=group_name, chassis_list=chassis_list, az_hints=az_hints,
ignore_chassis=set(), external_ids=external_ids, priority=chassis_prio)
return _sync_ha_chassis_group(nb_idl, hcg_info, txn)
def get_port_type_virtual_and_parents(subnets_by_id, fixed_ips, network_id,
port_id, nb_idl):
"""Returns if a port is type virtual and its corresponding parents.

View File

@@ -15,6 +15,7 @@
import ddt
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
from oslo_utils import uuidutils
from ovsdbapp.backend.ovs_idl import event
from ovsdbapp.backend.ovs_idl import idlutils
@@ -212,6 +213,54 @@ class TestSyncHaChassisGroup(base.TestOVNFunctionalBase):
self.nb_api.ha_chassis_group_get(hcg_name).execute,
check_error=True)
def _test_sync_unify_ha_chassis_group_network(self, create_hcg=False):
physnet = 'physnet1'
net_ext_args = {provider_net.NETWORK_TYPE: 'vlan',
provider_net.PHYSICAL_NETWORK: physnet,
external_net.EXTERNAL: True}
net_ext = self._make_network(self.fmt, 'test-ext-net', True,
as_admin=True,
arg_list=tuple(net_ext_args.keys()),
**net_ext_args)['network']
other_config = {'ovn-bridge-mappings': physnet + ':br-ex'}
ch1 = self.add_fake_chassis('host1', azs=[], enable_chassis_as_gw=True,
other_config=other_config)
ch2 = self.add_fake_chassis('host2', azs=[], enable_chassis_as_gw=True,
other_config=other_config)
ch3 = self.add_fake_chassis('host3', azs=[], enable_chassis_as_gw=True)
group_name = utils.ovn_name(net_ext['id'])
# Create a pre-existing HCG.
if create_hcg:
chassis_list = [self.sb_api.lookup('Chassis', ch2)]
hcg_info = utils.HAChassisGroupInfo(
group_name=group_name, chassis_list=chassis_list,
az_hints=[], ignore_chassis=set(), external_ids={})
with self.nb_api.transaction(check_error=True) as txn:
utils._sync_ha_chassis_group(self.nb_api, hcg_info, txn)
hcg = self.nb_api.lookup('HA_Chassis_Group', group_name)
self.assertEqual(1, len(hcg.ha_chassis))
self.assertEqual(ovn_const.HA_CHASSIS_GROUP_HIGHEST_PRIORITY,
hcg.ha_chassis[0].priority)
# Invoke the sync method
chassis_prio = {ch1: 10, ch2: 20, ch3: 30}
with self.nb_api.transaction(check_error=True) as txn:
utils.sync_ha_chassis_group_network_unified(
self.context, self.nb_api, self.sb_api, net_ext['id'],
'router-id', chassis_prio, txn)
hcg = self.nb_api.lookup('HA_Chassis_Group', group_name)
self.assertEqual(3, len(hcg.ha_chassis))
for hc in hcg.ha_chassis:
self.assertEqual(chassis_prio[hc.chassis_name], hc.priority)
def test_sync_unify_ha_chassis_group_network_no_hcg(self):
self._test_sync_unify_ha_chassis_group_network()
def test_sync_unify_ha_chassis_group_network_existing_hcg(self):
self._test_sync_unify_ha_chassis_group_network(create_hcg=True)
@utils.ovn_context()
def method_with_idl_and_default_txn(ls_name, idl, txn=None):

View File

@@ -2993,8 +2993,10 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
'fake-net-id', fake_txn)
# Assert it creates the HA Chassis Group
ext_ids = {ovn_const.OVN_AZ_HINTS_EXT_ID_KEY:
','.join(hcg_info.az_hints)}
ext_ids = {
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(hcg_info.az_hints),
ovn_const.OVN_NETWORK_ID_EXT_ID_KEY: 'fake-net-id',
}
self.nb_ovn.ha_chassis_group_add.assert_called_once_with(
hcg_info.group_name, may_exist=True, external_ids=ext_ids)