Since OVN 20.06, config is stored in "Chassis.other_config"

Since OVN 20.06 [1], the OVN configuration is stored in
"Chassis.other_config".

Since OVN 22.09, the "Chassis" configuration stored in
"Chassis.other_config" will not be replicated to
"Chassis.external_ids".

The ML2/OVN plugin tries to retrieve the "Chassis"
configuration from the "other_config" field first; if this
field does not exist (in OVN versions before 20.06), the plugin
will use "external_ids" field instead. Neutron will be compatible
with the different OVN versions (with and without "other_config"
field).

[1]74d90c2223
[2]51309429cc

NOTE: this patch is similar to [1], but in this case neutron keeps
compatibility with the different OVN versions (with and without
"other_config" field). Since [2], the Neutron CI has a new job that
uses the OVN/OVS packages distributed by the operating system
installed by the CI (in this case, Ubuntu 20.04 and OVN 20.03).

[1]https://review.opendev.org/c/openstack/neutron/+/859642
[2]https://review.opendev.org/c/openstack/neutron/+/860636

Conflicts:
      neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py
      neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
      neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py
      neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py
      neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py
      neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py
      neutron/tests/unit/fake_resources.py
      neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py
      neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py

Closes-Bug: #1990229
Change-Id: I54c8fd4d065ae537f396408df16832b158ee8998
(cherry picked from commit 536498a29a)
(cherry picked from commit 8a4c62d094)
This commit is contained in:
Rodolfo Alonso Hernandez 2022-09-23 09:11:25 +02:00
parent 427af5305c
commit a007da1e9a
17 changed files with 603 additions and 90 deletions

View File

@ -594,7 +594,7 @@ def compute_address_pairs_diff(ovn_port, neutron_port):
def get_ovn_cms_options(chassis):
"""Return the list of CMS options in a Chassis."""
return [opt.strip() for opt in chassis.external_ids.get(
return [opt.strip() for opt in get_ovn_chassis_other_config(chassis).get(
constants.OVN_CMS_OPTIONS, '').split(',')]
@ -784,3 +784,11 @@ def create_neutron_pg_drop():
}]
OvsdbClientTransactCommand.run(command)
def get_ovn_chassis_other_config(chassis):
# NOTE(ralonsoh): LP#1990229 to be removed when min OVN version is 22.09
try:
return chassis.other_config
except AttributeError:
return chassis.external_ids

View File

@ -90,7 +90,8 @@ class NeutronAgent(abc.ABC):
'configurations': {
'chassis_name': self.chassis.name,
'bridge-mappings':
self.chassis.external_ids.get('ovn-bridge-mappings', '')},
ovn_utils.get_ovn_chassis_other_config(self.chassis).get(
'ovn-bridge-mappings', '')},
'start_flag': True,
'agent_type': self.agent_type,
'id': self.agent_id,
@ -142,9 +143,9 @@ class ControllerAgent(NeutronAgent):
@staticmethod # it is by default, but this makes pep8 happy
def __new__(cls, chassis_private, driver, updated_at=None):
external_ids = cls.chassis_from_private(chassis_private).external_ids
if ('enable-chassis-as-gw' in
external_ids.get('ovn-cms-options', [])):
_chassis = cls.chassis_from_private(chassis_private)
other_config = ovn_utils.get_ovn_chassis_other_config(_chassis)
if 'enable-chassis-as-gw' in other_config.get('ovn-cms-options', []):
cls = ControllerGatewayAgent
return super().__new__(cls)
@ -167,8 +168,9 @@ class ControllerAgent(NeutronAgent):
def update(self, chassis_private, updated_at=None, clear_down=False):
super().update(chassis_private, updated_at, clear_down)
external_ids = self.chassis_from_private(chassis_private).external_ids
if 'enable-chassis-as-gw' in external_ids.get('ovn-cms-options', []):
_chassis = self.chassis_from_private(chassis_private)
other_config = ovn_utils.get_ovn_chassis_other_config(_chassis)
if 'enable-chassis-as-gw' in other_config.get('ovn-cms-options', []):
self.__class__ = ControllerGatewayAgent
@ -177,9 +179,10 @@ class ControllerGatewayAgent(ControllerAgent):
def update(self, chassis_private, updated_at=None, clear_down=False):
super().update(chassis_private, updated_at, clear_down)
external_ids = self.chassis_from_private(chassis_private).external_ids
_chassis = self.chassis_from_private(chassis_private)
other_config = ovn_utils.get_ovn_chassis_other_config(_chassis)
if ('enable-chassis-as-gw' not in
external_ids.get('ovn-cms-options', [])):
other_config.get('ovn-cms-options', [])):
self.__class__ = ControllerAgent

View File

@ -181,7 +181,8 @@ class OVNMechanismDriver(api.MechanismDriver):
def get_supported_vif_types(self):
vif_types = set()
for ch in self.sb_ovn.chassis_list().execute(check_error=True):
dp_type = ch.external_ids.get('datapath-type', '')
other_config = ovn_utils.get_ovn_chassis_other_config(ch)
dp_type = other_config.get('datapath-type', '')
if dp_type == ovn_const.CHASSIS_DATAPATH_NETDEV:
vif_types.add(portbindings.VIF_TYPE_VHOST_USER)
else:

View File

@ -0,0 +1,274 @@
# Copyright 2021 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.
import itertools
from keystoneauth1 import exceptions as ks_exc
from neutron_lib import constants as n_const
from neutron_lib.placement import constants as placement_constants
from neutron_lib.placement import utils as placement_utils
from neutron_lib.plugins import constants as plugins_constants
from neutron_lib.plugins import directory
from neutron_lib.utils import helpers
from oslo_log import log as logging
from ovsdbapp.backend.ovs_idl import event as row_event
from neutron.agent.common import placement_report
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils as ovn_utils
from neutron.common import utils as common_utils
LOG = logging.getLogger(__name__)
def _parse_ovn_cms_options(chassis):
cms_options = ovn_utils.get_ovn_cms_options(chassis)
return {n_const.RP_BANDWIDTHS: _parse_bandwidths(cms_options),
n_const.RP_INVENTORY_DEFAULTS: _parse_inventory_defaults(
cms_options),
ovn_const.RP_HYPERVISORS: _parse_hypervisors(cms_options)}
def _parse_bridge_mappings(chassis):
other_config = ovn_utils.get_ovn_chassis_other_config(chassis)
bridge_mappings = other_config.get('ovn-bridge-mappings', '')
bridge_mappings = helpers.parse_mappings(bridge_mappings.split(','),
unique_values=False)
return {k: [v] for k, v in bridge_mappings.items()}
def _parse_placement_option(option_name, cms_options):
for cms_option in (cms_option for cms_option in cms_options if
option_name in cms_option):
try:
return cms_option.split('=')[1]
except IndexError:
break
def _parse_bandwidths(cms_options):
bw_values = _parse_placement_option(n_const.RP_BANDWIDTHS, cms_options)
if not bw_values:
return {}
return placement_utils.parse_rp_bandwidths(bw_values.split(';'))
def _parse_inventory_defaults(cms_options):
inv_defaults = _parse_placement_option(n_const.RP_INVENTORY_DEFAULTS,
cms_options)
if not inv_defaults:
return {}
inventory = {}
for inv_default in inv_defaults.split(';'):
for key in placement_constants.INVENTORY_OPTIONS:
if key in inv_default:
inventory[key] = inv_default.split(':')[1]
return placement_utils.parse_rp_inventory_defaults(inventory)
def _parse_hypervisors(cms_options):
hyperv = _parse_placement_option(ovn_const.RP_HYPERVISORS, cms_options)
if not hyperv:
return {}
return helpers.parse_mappings(hyperv.split(';'), unique_values=False)
def _send_deferred_batch(state):
if not state:
return
deferred_batch = state.deferred_sync()
for deferred in deferred_batch:
try:
LOG.debug('Placement client: %s', str(deferred))
deferred.execute()
except Exception:
LOG.exception('Placement client call failed: %s', str(deferred))
def dict_chassis_config(state):
if state:
return {n_const.RP_BANDWIDTHS: state._rp_bandwidths,
n_const.RP_INVENTORY_DEFAULTS: state._rp_inventory_defaults,
ovn_const.RP_HYPERVISORS: state._hypervisor_rps}
class ChassisBandwidthConfigEvent(row_event.RowEvent):
"""Chassis create update event to track the bandwidth config changes."""
def __init__(self, placement_extension):
self._placement_extension = placement_extension
# NOTE(ralonsoh): BW resource provider information is stored in
# "Chassis", not "Chassis_Private".
table = 'Chassis'
events = (self.ROW_CREATE, self.ROW_UPDATE)
super().__init__(events, table, None)
self.event_name = 'ChassisBandwidthConfigEvent'
def run(self, event, row, old):
name2uuid = self._placement_extension.name2uuid()
state = self._placement_extension.build_placement_state(row, name2uuid)
if not state:
return
_send_deferred_batch(state)
ch_config = dict_chassis_config(state)
LOG.debug('OVN chassis %(chassis)s Placement configuration modified: '
'%(config)s', {'chassis': row.name, 'config': ch_config})
@common_utils.SingletonDecorator
class OVNClientPlacementExtension(object):
"""OVN client Placement API extension"""
def __init__(self, driver):
LOG.info('Starting OVNClientPlacementExtension')
super().__init__()
self._config_event = None
self._reset(driver)
def _reset(self, driver):
"""Reset the interval members values
This class is a singleton. Once initialized, any other new instance
will return the same object reference with the same member values.
This method is used to reset all of them as when the class is initially
instantiated if needed.
"""
self._driver = driver
self._placement_plugin = None
self._plugin = None
self.uuid_ns = ovn_const.OVN_RP_UUID
self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES
if not self.enabled:
return
if not self._config_event:
self._config_event = ChassisBandwidthConfigEvent(self)
try:
self._driver._sb_idl.idl.notify_handler.watch_events(
[self._config_event])
except AttributeError:
# "sb_idl.idl.notify_handler" is not present in the
# MaintenanceWorker.
pass
@property
def placement_plugin(self):
if self._placement_plugin is None:
self._placement_plugin = directory.get_plugin(
plugins_constants.PLACEMENT_REPORT)
return self._placement_plugin
@property
def enabled(self):
return bool(self.placement_plugin)
@property
def plugin(self):
if self._plugin is None:
self._plugin = self._driver._plugin
return self._plugin
@property
def ovn_mech_driver(self):
if self._ovn_mech_driver is None:
self._ovn_mech_driver = (
self.plugin.mechanism_manager.mech_drivers['ovn'].obj)
return self._ovn_mech_driver
def get_chassis_config(self):
"""Read all Chassis BW config and returns the Placement states"""
chassis = {}
name2uuid = self.name2uuid()
for ch in self._driver._sb_idl.chassis_list().execute(
check_error=True):
state = self.build_placement_state(ch, name2uuid)
if state:
chassis[ch.name] = state
return chassis
def read_initial_chassis_config(self):
"""Read the Chassis BW configuration and update the Placement API
This method is called once from the MaintenanceWorker when the Neutron
server starts.
"""
if not self.enabled:
return
chassis = self.get_chassis_config()
for state in chassis.values():
_send_deferred_batch(state)
msg = ', '.join(['Chassis %s: %s' % (name, dict_chassis_config(state))
for (name, state) in chassis.items()]) or '(no info)'
LOG.debug('OVN chassis Placement initial configuration: %s', msg)
return chassis
def name2uuid(self, name=None):
try:
rps = self.placement_plugin._placement_client.\
list_resource_providers(name=name)['resource_providers']
except (ks_exc.HttpError, ks_exc.ClientException):
LOG.warning('Error connecting to Placement API.')
return {}
_name2uuid = {rp['name']: rp['uuid'] for rp in rps}
LOG.info('Placement information about resource providers '
'(name:uuid):%s ', _name2uuid)
return _name2uuid
def build_placement_state(self, chassis, name2uuid):
bridge_mappings = _parse_bridge_mappings(chassis)
cms_options = _parse_ovn_cms_options(chassis)
LOG.debug('Building placement options for chassis %s: %s',
chassis.name, cms_options)
hypervisor_rps = {}
for device, hyperv in cms_options[ovn_const.RP_HYPERVISORS].items():
try:
hypervisor_rps[device] = {'name': hyperv,
'uuid': name2uuid[hyperv]}
except (KeyError, AttributeError):
continue
bridges = set(itertools.chain(*bridge_mappings.values()))
# Remove "cms_options[RP_BANDWIDTHS]" not present in "hypervisor_rps"
# and "bridge_mappings". If we don't have a way to match the RP bridge
# with a host ("hypervisor_rps") or a way to match the RP bridge with
# an external network ("bridge_mappings"), this value is irrelevant.
rp_bw = cms_options[n_const.RP_BANDWIDTHS]
if rp_bw:
cms_options[n_const.RP_BANDWIDTHS] = {
device: bw for device, bw in rp_bw.items() if
device in hypervisor_rps and device in bridges}
# NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs
# will be added in a future implementation. If no RP_BANDWIDTHS values
# are present (that means there is no BW information for any interface
# in this host), no "PlacementState" is returned.
return placement_report.PlacementState(
rp_bandwidths=cms_options[n_const.RP_BANDWIDTHS],
rp_inventory_defaults=cms_options[n_const.RP_INVENTORY_DEFAULTS],
rp_pkt_processing={},
rp_pkt_processing_inventory_defaults=None,
driver_uuid_namespace=self.uuid_ns,
agent_type=ovn_const.OVN_CONTROLLER_AGENT,
hypervisor_rps=hypervisor_rps,
device_mappings=bridge_mappings,
supported_vnic_types=self.supported_vnic_types,
client=self.placement_plugin._placement_client)

View File

@ -836,7 +836,8 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
return cls(conn)
def _get_chassis_physnets(self, chassis):
bridge_mappings = chassis.external_ids.get('ovn-bridge-mappings', '')
other_config = utils.get_ovn_chassis_other_config(chassis)
bridge_mappings = other_config.get('ovn-bridge-mappings', '')
mapping_dict = helpers.parse_mappings(bridge_mappings.split(','))
return list(mapping_dict.keys())
@ -854,7 +855,8 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
return [ch.name if name_only else ch
for ch in self.chassis_list().execute(check_error=True)
if ovn_const.CMS_OPT_CHASSIS_AS_GW in
ch.external_ids.get(ovn_const.OVN_CMS_OPTIONS, '').split(',')]
utils.get_ovn_chassis_other_config(ch).get(
ovn_const.OVN_CMS_OPTIONS, '').split(',')]
def get_chassis_and_physnets(self):
chassis_info_dict = {}
@ -874,8 +876,9 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
except StopIteration:
msg = _('Chassis with hostname %s does not exist') % hostname
raise RuntimeError(msg)
return (chassis.external_ids.get('datapath-type', ''),
chassis.external_ids.get('iface-types', ''),
other_config = utils.get_ovn_chassis_other_config(chassis)
return (other_config.get('datapath-type', ''),
other_config.get('iface-types', ''),
self._get_chassis_physnets(chassis))
def get_metadata_port_network(self, network):

View File

@ -172,12 +172,21 @@ class ChassisEvent(row_event.RowEvent):
def match_fn(self, event, row, old):
if event != self.ROW_UPDATE:
return True
# NOTE(lucasgomes): If the external_ids column wasn't updated
# (meaning, Chassis "gateway" status didn't change) just returns
if not hasattr(old, 'external_ids') and event == self.ROW_UPDATE:
# NOTE(ralonsoh): LP#1990229 to be removed when min OVN version is
# 22.09
other_config = ('other_config' if hasattr(row, 'other_config') else
'external_ids')
# NOTE(lucasgomes): If the other_config/external_ids column wasn't
# updated (meaning, Chassis "gateway" status didn't change) just
# returns
if not hasattr(old, other_config) and event == self.ROW_UPDATE:
return False
if (old.external_ids.get('ovn-bridge-mappings') !=
row.external_ids.get('ovn-bridge-mappings')):
old_br_mappings = utils.get_ovn_chassis_other_config(old).get(
'ovn-bridge-mappings')
new_br_mappings = utils.get_ovn_chassis_other_config(row).get(
'ovn-bridge-mappings')
if old_br_mappings != new_br_mappings:
return True
# Check if either the Gateway status or Availability Zones has
# changed in the Chassis
@ -192,8 +201,9 @@ class ChassisEvent(row_event.RowEvent):
def run(self, event, row, old):
host = row.hostname
phy_nets = []
new_other_config = utils.get_ovn_chassis_other_config(row)
if event != self.ROW_DELETE:
bridge_mappings = row.external_ids.get('ovn-bridge-mappings', '')
bridge_mappings = new_other_config.get('ovn-bridge-mappings', '')
mapping_dict = helpers.parse_mappings(bridge_mappings.split(','))
phy_nets = list(mapping_dict)
@ -208,9 +218,10 @@ class ChassisEvent(row_event.RowEvent):
if event == self.ROW_DELETE:
kwargs['event_from_chassis'] = row.name
elif event == self.ROW_UPDATE:
old_mappings = old.external_ids.get('ovn-bridge-mappings',
old_other_config = utils.get_ovn_chassis_other_config(old)
old_mappings = old_other_config.get('ovn-bridge-mappings',
set()) or set()
new_mappings = row.external_ids.get('ovn-bridge-mappings',
new_mappings = new_other_config.get('ovn-bridge-mappings',
set()) or set()
if old_mappings:
old_mappings = set(old_mappings.split(','))
@ -339,11 +350,17 @@ class ChassisAgentTypeChangeEvent(ChassisEvent):
events = (BaseEvent.ROW_UPDATE,)
def match_fn(self, event, row, old=None):
if not getattr(old, 'external_ids', False):
# NOTE(ralonsoh): LP#1990229 to be removed when min OVN version is
# 22.09
other_config = ('other_config' if hasattr(row, 'other_config') else
'external_ids')
if not getattr(old, other_config, False):
return False
agent_type_change = n_agent.NeutronAgent.chassis_from_private(
row).external_ids.get('ovn-cms-options', []) != (
old.external_ids.get('ovn-cms-options', []))
chassis = n_agent.NeutronAgent.chassis_from_private(row)
new_other_config = utils.get_ovn_chassis_other_config(chassis)
old_other_config = utils.get_ovn_chassis_other_config(old)
agent_type_change = new_other_config.get('ovn-cms-options', []) != (
old_other_config.get('ovn-cms-options', []))
return agent_type_change
def run(self, event, row, old):

View File

@ -414,7 +414,8 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
self._start_ovn_northd()
def add_fake_chassis(self, host, physical_nets=None, external_ids=None,
name=None, enable_chassis_as_gw=False):
name=None, enable_chassis_as_gw=False,
other_config=None):
def append_cms_options(ext_ids, value):
if 'ovn-cms-options' not in ext_ids:
ext_ids['ovn-cms-options'] = value
@ -423,14 +424,15 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
physical_nets = physical_nets or []
external_ids = external_ids or {}
other_config = other_config or {}
if enable_chassis_as_gw:
append_cms_options(external_ids, 'enable-chassis-as-gw')
append_cms_options(other_config, 'enable-chassis-as-gw')
bridge_mapping = ",".join(["%s:br-provider%s" % (phys_net, i)
for i, phys_net in enumerate(physical_nets)])
if name is None:
name = uuidutils.generate_uuid()
external_ids['ovn-bridge-mappings'] = bridge_mapping
other_config['ovn-bridge-mappings'] = bridge_mapping
# We'll be using different IP addresses every time for the Encap of
# the fake chassis as the SB schema doesn't allow to have two entries
# with same (ip,type) pairs as of OVS 2.11. This shouldn't have any
@ -441,7 +443,8 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase,
self._counter += 1
chassis = self.sb_api.chassis_add(
name, ['geneve'], '172.24.4.%d' % self._counter,
external_ids=external_ids, hostname=host).execute(check_error=True)
external_ids=external_ids, hostname=host,
other_config=other_config).execute(check_error=True)
if self.sb_api.is_table_present('Chassis_Private'):
nb_cfg_timestamp = timeutils.utcnow_ts() * 1000
self.sb_api.db_create(

View File

@ -0,0 +1,188 @@
# Copyright 2021 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 import constants as n_const
from neutron_lib.plugins import constants as plugins_constants
from neutron.common.ovn import constants as ovn_const
from neutron.common import utils as common_utils
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import placement as placement_extension
from neutron.tests.functional import base
class TestOVNClientQosExtension(base.TestOVNFunctionalBase):
EMPTY_CHASSIS = {n_const.RP_BANDWIDTHS: {},
n_const.RP_INVENTORY_DEFAULTS: {},
ovn_const.RP_HYPERVISORS: {}}
RP_BANDWIDTHS_1 = {'br-provider0': {'egress': 1000, 'ingress': 2000}}
RP_INVENTORY_DEFAULTS_1 = {'allocation_ratio': 1.0, 'min_unit': 2}
RP_HYPERVISORS_1 = {'br-provider0': {'name': 'host1', 'uuid': 'uuid1'}}
CHASSIS1 = {
'chassis1': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_1,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_1,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_1
}
}
RP_BANDWIDTHS_2 = {'br-provider0': {'egress': 3000, 'ingress': 4000}}
RP_INVENTORY_DEFAULTS_2 = {'allocation_ratio': 3.0, 'min_unit': 1}
RP_HYPERVISORS_2 = {'br-provider0': {'name': 'host2', 'uuid': 'uuid2'}}
CHASSIS2 = {
'chassis2': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_2,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_2,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_2
}
}
RP_BANDWIDTHS_3 = {'br-provider0': {'egress': 5000, 'ingress': 6000}}
RP_INVENTORY_DEFAULTS_3 = {'allocation_ratio': 1.1, 'min_unit': 1}
CHASSIS2_B = {
'chassis2': {
n_const.RP_BANDWIDTHS: RP_BANDWIDTHS_3,
n_const.RP_INVENTORY_DEFAULTS: RP_INVENTORY_DEFAULTS_3,
ovn_const.RP_HYPERVISORS: RP_HYPERVISORS_2
}
}
def setUp(self, maintenance_worker=False, service_plugins=None):
service_plugins = {plugins_constants.PLACEMENT_REPORT: 'placement'}
super().setUp(maintenance_worker=maintenance_worker,
service_plugins=service_plugins)
self.ovn_client = self.mech_driver._ovn_client
self.placement_ext = self.ovn_client.placement_extension
self.mock_name2uuid = mock.patch.object(
self.placement_ext, 'name2uuid').start()
self.mock_send_batch = mock.patch.object(
placement_extension, '_send_deferred_batch').start()
def _build_other_config(self, bandwidths, inventory_defaults, hypervisors):
options = []
if bandwidths:
options.append(n_const.RP_BANDWIDTHS + '=' + bandwidths)
if inventory_defaults:
options.append(n_const.RP_INVENTORY_DEFAULTS + '=' +
inventory_defaults)
if hypervisors:
options.append(ovn_const.RP_HYPERVISORS + '=' + hypervisors)
return {'ovn-cms-options': ','.join(options)}
def _create_chassis(self, host, name, physical_nets=None, bandwidths=None,
inventory_defaults=None, hypervisors=None):
other_config = self._build_other_config(bandwidths, inventory_defaults,
hypervisors)
self.add_fake_chassis(host, physical_nets=physical_nets,
other_config=other_config, name=name)
def _update_chassis(self, name, bandwidths=None, inventory_defaults=None,
hypervisors=None):
other_config = self._build_other_config(bandwidths, inventory_defaults,
hypervisors)
self.sb_api.db_set(
'Chassis', name, ('other_config', other_config)
).execute(check_error=True)
def _check_placement_config(self, expected_chassis):
current_chassis = None
def check_chassis():
nonlocal current_chassis
current_chassis = self.placement_ext.get_chassis_config()
current_chassis = {
chassis_name: placement_extension.dict_chassis_config(state)
for chassis_name, state in current_chassis.items()}
return current_chassis == expected_chassis
try:
common_utils.wait_until_true(check_chassis, timeout=5)
except common_utils.WaitTimeout:
self.fail('OVN client Placement extension cache does not have '
'the expected chassis information.\nExpected: %s.\n'
'Actual: %s' % (expected_chassis, current_chassis))
def test_read_initial_config_and_update(self):
self.mock_name2uuid.return_value = {'host1': 'uuid1',
'host2': 'uuid2'}
self._create_chassis(
'host1', 'chassis1', physical_nets=['phys1'],
bandwidths='br-provider0:1000:2000',
inventory_defaults='allocation_ratio:1.0;min_unit:2',
hypervisors='br-provider0:host1')
self._create_chassis(
'host2', 'chassis2', physical_nets=['phys2'],
bandwidths='br-provider0:3000:4000',
inventory_defaults='allocation_ratio:3.0;min_unit:1',
hypervisors='br-provider0:host2')
self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2})
self._update_chassis(
'chassis2',
bandwidths='br-provider0:5000:6000',
inventory_defaults='allocation_ratio:1.1;min_unit:1',
hypervisors='br-provider0:host2')
self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2_B})
def test_read_initial_empty_config_and_update(self):
self.mock_name2uuid.return_value = {'host1': 'uuid1',
'host2': 'uuid2'}
self._create_chassis('host1', 'chassis1', physical_nets=['phys1'])
self._create_chassis('host2', 'chassis2', physical_nets=['phys2'])
self._check_placement_config({**{'chassis1': self.EMPTY_CHASSIS},
**{'chassis2': self.EMPTY_CHASSIS}})
self._update_chassis(
'chassis1',
bandwidths='br-provider0:1000:2000',
inventory_defaults='allocation_ratio:1.0;min_unit:2',
hypervisors='br-provider0:host1')
self._check_placement_config({**self.CHASSIS1,
**{'chassis2': self.EMPTY_CHASSIS}})
self._update_chassis(
'chassis2',
bandwidths='br-provider0:3000:4000',
inventory_defaults='allocation_ratio:3.0;min_unit:1',
hypervisors='br-provider0:host2')
self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2})
def test_update_twice(self):
self.mock_name2uuid.return_value = {'host1': 'uuid1',
'host2': 'uuid2'}
self._create_chassis(
'host1', 'chassis1', physical_nets=['phys1'],
bandwidths='br-provider0:1000:2000',
inventory_defaults='allocation_ratio:1.0;min_unit:2',
hypervisors='br-provider0:host1')
self._create_chassis('host2', 'chassis2', physical_nets=['phys2'])
self._check_placement_config({**self.CHASSIS1,
**{'chassis2': self.EMPTY_CHASSIS}})
self._update_chassis(
'chassis2',
bandwidths='br-provider0:3000:4000',
inventory_defaults='allocation_ratio:3.0;min_unit:1',
hypervisors='br-provider0:host2')
self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2})
self._update_chassis(
'chassis2',
bandwidths='br-provider0:5000:6000',
inventory_defaults='allocation_ratio:1.1;min_unit:1',
hypervisors='br-provider0:host2')
self._check_placement_config({**self.CHASSIS1, **self.CHASSIS2_B})

View File

@ -52,11 +52,11 @@ class TestSbApi(BaseOvnIdlTest):
super(TestSbApi, self).setUp()
self.data = {
'chassis': [
{'external_ids': {'ovn-bridge-mappings':
{'other_config': {'ovn-bridge-mappings':
'public:br-ex,private:br-0'}},
{'external_ids': {'ovn-bridge-mappings':
{'other_config': {'ovn-bridge-mappings':
'public:br-ex,public2:br-ex2'}},
{'external_ids': {'ovn-bridge-mappings':
{'other_config': {'ovn-bridge-mappings':
'public:br-ex'}},
]
}
@ -70,7 +70,7 @@ class TestSbApi(BaseOvnIdlTest):
txn.add(self.api.chassis_add(
chassis['name'], ['geneve'], chassis['hostname'],
hostname=chassis['hostname'],
external_ids=chassis['external_ids']))
other_config=chassis['other_config']))
def test_get_chassis_hostname_and_physnets(self):
mapping = self.api.get_chassis_hostname_and_physnets()
@ -104,7 +104,7 @@ class TestSbApi(BaseOvnIdlTest):
def test_multiple_physnets_in_one_bridge(self):
self.data = {
'chassis': [
{'external_ids': {'ovn-bridge-mappings': 'p1:br-ex,p2:br-ex'}}
{'other_config': {'ovn-bridge-mappings': 'p1:br-ex,p2:br-ex'}}
]
}
self.load_test_data()

View File

@ -437,9 +437,8 @@ class TestAgentMonitor(base.TestOVNFunctionalBase):
chassis_name, self.mech_driver.agent_chassis_table)
self.mech_driver.sb_ovn.idl.notify_handler.watch_event(row_event)
self.chassis_name = self.add_fake_chassis(
self.FAKE_CHASSIS_HOST,
external_ids={'ovn-cms-options': 'enable-chassis-as-gw'},
name=chassis_name)
self.FAKE_CHASSIS_HOST, name=chassis_name,
enable_chassis_as_gw=True)
self.assertTrue(row_event.wait())
n_utils.wait_until_true(
lambda: len(list(neutron_agent.AgentCache())) == 1)
@ -447,11 +446,11 @@ class TestAgentMonitor(base.TestOVNFunctionalBase):
def test_agent_change_controller(self):
self.assertEqual(neutron_agent.ControllerGatewayAgent,
type(neutron_agent.AgentCache()[self.chassis_name]))
self.sb_api.db_set('Chassis', self.chassis_name, ('external_ids',
self.sb_api.db_set('Chassis', self.chassis_name, ('other_config',
{'ovn-cms-options': ''})).execute(check_error=True)
n_utils.wait_until_true(lambda:
neutron_agent.AgentCache()[self.chassis_name].
chassis.external_ids['ovn-cms-options'] == '')
chassis.other_config['ovn-cms-options'] == '')
self.assertEqual(neutron_agent.ControllerAgent,
type(neutron_agent.AgentCache()[self.chassis_name]))

View File

@ -59,12 +59,12 @@ class TestPortBinding(base.TestOVNFunctionalBase):
self.add_fake_chassis(self.ovs_host)
self.add_fake_chassis(
self.dpdk_host,
external_ids={'datapath-type': 'netdev',
other_config={'datapath-type': 'netdev',
'iface-types': 'dummy,dummy-internal,dpdkvhostuser'})
self.add_fake_chassis(
self.invalid_dpdk_host,
external_ids={'datapath-type': 'netdev',
other_config={'datapath-type': 'netdev',
'iface-types': 'dummy,dummy-internal,geneve,vxlan'})
self.n1 = self._make_network(self.fmt, 'n1', True)
res = self._create_subnet(self.fmt, self.n1['network']['id'],

View File

@ -129,7 +129,7 @@ class TestRouter(base.TestOVNFunctionalBase):
# Test if chassis3 is selected as candidate or not.
self.chassis3 = self.add_fake_chassis(
'ovs-host3', physical_nets=['physnet1'],
external_ids={'ovn-cms-options': 'enable-chassis-as-gw'})
other_config={'ovn-cms-options': 'enable-chassis-as-gw'})
self._check_gateway_chassis_candidates([self.chassis3])
def test_gateway_chassis_with_cms_and_no_bridge_mappings(self):
@ -137,7 +137,7 @@ class TestRouter(base.TestOVNFunctionalBase):
# chassis3 is having enable-chassis-as-gw, but no bridge mappings.
self.chassis3 = self.add_fake_chassis(
'ovs-host3',
external_ids={'ovn-cms-options': 'enable-chassis-as-gw'})
other_config={'ovn-cms-options': 'enable-chassis-as-gw'})
ovn_client = self.l3_plugin._ovn_client
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
@ -482,7 +482,7 @@ class TestRouter(base.TestOVNFunctionalBase):
self.skipTest('L3 HA not supported')
ovn_client = self.l3_plugin._ovn_client
chassis4 = self.add_fake_chassis(
'ovs-host4', physical_nets=['physnet4'], external_ids={
'ovs-host4', physical_nets=['physnet4'], other_config={
'ovn-cms-options': 'enable-chassis-as-gw'})
ovn_client._ovn_scheduler = l3_sched.OVNGatewayLeastLoadedScheduler()
ext1 = self._create_ext_network(
@ -504,7 +504,7 @@ class TestRouter(base.TestOVNFunctionalBase):
# Add another chassis as a gateway chassis
chassis5 = self.add_fake_chassis(
'ovs-host5', physical_nets=['physnet4'], external_ids={
'ovs-host5', physical_nets=['physnet4'], other_config={
'ovn-cms-options': 'enable-chassis-as-gw'})
# Add a node as compute node. Compute node wont be
# used to schedule the router gateway ports therefore
@ -534,8 +534,7 @@ class TestRouter(base.TestOVNFunctionalBase):
chassis_list.append(
self.add_fake_chassis(
'ovs-host%s' % i, physical_nets=['physnet1'],
external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'}))
other_config={'ovn-cms-options': 'enable-chassis-as-gw'}))
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")

View File

@ -61,33 +61,31 @@ class TestUtils(base.BaseTestCase):
def test_is_gateway_chassis(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': 'enable-chassis-as-gw'}})
'other_config': {'ovn-cms-options': 'enable-chassis-as-gw'}})
non_gw_chassis_0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': ''}})
non_gw_chassis_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={})
non_gw_chassis_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {}})
'other_config': {'ovn-cms-options': ''}})
non_gw_chassis_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'other_config': {}})
self.assertTrue(utils.is_gateway_chassis(chassis))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_0))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_1))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_2))
def test_get_chassis_availability_zones_no_azs(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': 'enable-chassis-as-gw'}})
'other_config': {'ovn-cms-options': 'enable-chassis-as-gw'}})
self.assertEqual(set(), utils.get_chassis_availability_zones(chassis))
def test_get_chassis_availability_zones_one_az(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options':
'other_config': {'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0'}})
self.assertEqual(
{'az0'}, utils.get_chassis_availability_zones(chassis))
def test_get_chassis_availability_zones_multiple_az(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {
'other_config': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0:az1 :az2:: :'}})
self.assertEqual(
@ -96,7 +94,7 @@ class TestUtils(base.BaseTestCase):
def test_get_chassis_availability_zones_malformed(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options':
'other_config': {'ovn-cms-options':
'enable-chassis-as-gw,availability-zones:az0'}})
self.assertEqual(
set(), utils.get_chassis_availability_zones(chassis))
@ -155,16 +153,16 @@ class TestUtils(base.BaseTestCase):
def test_get_chassis_in_azs(self):
ch0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch0',
'external_ids': {
'other_config': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0:az1:az2'}})
ch1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch1',
'external_ids': {
'other_config': {
'ovn-cms-options': 'enable-chassis-as-gw'}})
ch2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch2',
'external_ids': {
'other_config': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az1:az5'}})
@ -182,21 +180,21 @@ class TestUtils(base.BaseTestCase):
def test_get_gateway_chassis_without_azs(self):
ch0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch0',
'external_ids': {
'other_config': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0:az1:az2'}})
ch1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch1',
'external_ids': {
'other_config': {
'ovn-cms-options': 'enable-chassis-as-gw'}})
ch2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch2',
'external_ids': {
'other_config': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az1:az5'}})
ch3 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'name': 'ch3',
'external_ids': {}})
'other_config': {}})
chassis_list = [ch0, ch1, ch2, ch3]
self.assertEqual(

View File

@ -827,20 +827,23 @@ class FakeChassis(object):
if chassis_as_gw:
cms_opts.append(ovn_const.CMS_OPT_CHASSIS_AS_GW)
external_ids = {}
# NOTE(ralonsoh): LP#1990229, once min OVN version >= 20.06, the CMS
# options and the bridge mappings should be stored only in
# "other_config".
other_config = {}
if cms_opts:
external_ids[ovn_const.OVN_CMS_OPTIONS] = ','.join(cms_opts)
other_config[ovn_const.OVN_CMS_OPTIONS] = ','.join(cms_opts)
attrs = {
chassis_attrs = {
'encaps': [],
'external_ids': external_ids,
'external_ids': '',
'hostname': '',
'name': uuidutils.generate_uuid(),
'nb_cfg': 0,
'other_config': {},
'other_config': other_config,
'transport_zones': [],
'vtep_logical_switches': []}
# Overwrite default attributes.
attrs.update(attrs)
return type('Chassis', (object, ), attrs)
chassis_attrs.update(attrs)
return type('Chassis', (object, ), chassis_attrs)

View File

@ -31,7 +31,7 @@ class AgentCacheTestCase(base.BaseTestCase):
self.names_ref = []
for i in range(10): # Add 10 agents.
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'name': 'chassis' + str(i)})
attrs={'name': 'chassis' + str(i), 'other_config': {}})
self.agent_cache.update(ovn_const.OVN_CONTROLLER_AGENT,
chassis_private)
self.names_ref.append('chassis' + str(i))

View File

@ -81,6 +81,9 @@ OVN_SB_SCHEMA = {
"name": {"type": "string"},
"hostname": {"type": "string"},
"external_ids": {
"type": {"key": "string", "value": "string",
"min": 0, "max": "unlimited"}},
"other_config": {
"type": {"key": "string", "value": "string",
"min": 0, "max": "unlimited"}}},
"isRoot": True,
@ -524,7 +527,7 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
self.row_json = {
"name": "fake-name",
"hostname": "fake-hostname",
"external_ids": ['map', [["ovn-bridge-mappings",
"other_config": ['map', [["ovn-bridge-mappings",
"fake-phynet1:fake-br1"]]]
}
self._mock_hash_ring = mock.patch.object(
@ -548,14 +551,18 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
self.sb_idl.notify_handler.notify_loop()
def test_chassis_create_event(self):
self._test_chassis_helper('create', self.row_json)
old_row_json = {'other_config': ['map', []]}
self._test_chassis_helper('create', self.row_json,
old_row_json=old_row_json)
self.mech_driver.update_segment_host_mapping.assert_called_once_with(
'fake-hostname', ['fake-phynet1'])
self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with(
event_from_chassis=None)
def test_chassis_delete_event(self):
self._test_chassis_helper('delete', self.row_json)
old_row_json = {'other_config': ['map', []]}
self._test_chassis_helper('delete', self.row_json,
old_row_json=old_row_json)
self.mech_driver.update_segment_host_mapping.assert_called_once_with(
'fake-hostname', [])
self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with(
@ -563,7 +570,7 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
def test_chassis_update_event(self):
old_row_json = copy.deepcopy(self.row_json)
old_row_json['external_ids'][1][0][1] = (
old_row_json['other_config'][1][0][1] = (
"fake-phynet2:fake-br2")
self._test_chassis_helper('update', self.row_json, old_row_json)
self.mech_driver.update_segment_host_mapping.assert_called_once_with(
@ -572,9 +579,9 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
event_from_chassis=None)
def test_chassis_update_event_reschedule_not_needed(self):
self.row_json['external_ids'][1].append(['foo_field', 'foo_value_new'])
self.row_json['other_config'][1].append(['foo_field', 'foo_value_new'])
old_row_json = copy.deepcopy(self.row_json)
old_row_json['external_ids'][1][1][1] = (
old_row_json['other_config'][1][1][1] = (
"foo_value")
self._test_chassis_helper('update', self.row_json, old_row_json)
self.mech_driver.update_segment_host_mapping.assert_not_called()
@ -582,14 +589,14 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
def test_chassis_update_event_reschedule_lost_physnet(self):
old_row_json = copy.deepcopy(self.row_json)
self.row_json['external_ids'][1][0][1] = ''
self.row_json['other_config'][1][0][1] = ''
self._test_chassis_helper('update', self.row_json, old_row_json)
self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with(
event_from_chassis='fake-name')
def test_chassis_update_event_reschedule_add_physnet(self):
old_row_json = copy.deepcopy(self.row_json)
self.row_json['external_ids'][1][0][1] += ',foo_physnet:foo_br'
self.row_json['other_config'][1][0][1] += ',foo_physnet:foo_br'
self._test_chassis_helper('update', self.row_json, old_row_json)
self.mech_driver.update_segment_host_mapping.assert_called_once_with(
'fake-hostname', ['fake-phynet1', 'foo_physnet'])
@ -598,7 +605,7 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
def test_chassis_update_event_reschedule_add_and_remove_physnet(self):
old_row_json = copy.deepcopy(self.row_json)
self.row_json['external_ids'][1][0][1] = 'foo_physnet:foo_br'
self.row_json['other_config'][1][0][1] = 'foo_physnet:foo_br'
self._test_chassis_helper('update', self.row_json, old_row_json)
self.mech_driver.update_segment_host_mapping.assert_called_once_with(
'fake-hostname', ['foo_physnet'])
@ -607,7 +614,7 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
def test_chassis_update_empty_no_external_ids(self):
old_row_json = copy.deepcopy(self.row_json)
old_row_json.pop('external_ids')
old_row_json.pop('other_config')
with mock.patch(
'neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.'
'ovsdb_monitor.ChassisEvent.'
@ -640,10 +647,10 @@ class TestChassisEvent(base.BaseTestCase):
def _test_handle_ha_chassis_group_changes_create(self, event):
# Chassis
ext_ids = {
other_config = {
'ovn-cms-options': 'enable-chassis-as-gw,availability-zones=az-0'}
row = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'name': 'SpongeBob', 'external_ids': ext_ids})
attrs={'name': 'SpongeBob', 'other_config': other_config})
# HA Chassis
ch0 = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'priority': 10})
@ -675,10 +682,10 @@ class TestChassisEvent(base.BaseTestCase):
def _test_handle_ha_chassis_group_changes_delete(self, event):
# Chassis
ext_ids = {
other_config = {
'ovn-cms-options': 'enable-chassis-as-gw,availability-zones=az-0'}
row = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'name': 'SpongeBob', 'external_ids': ext_ids})
attrs={'name': 'SpongeBob', 'other_config': other_config})
# HA Chassis
ha_ch = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'priority': 10})
@ -702,10 +709,10 @@ class TestChassisEvent(base.BaseTestCase):
def test_handle_ha_chassis_group_changes_update_no_changes(self):
# Assert nothing was done because the update didn't
# change the gateway chassis status or the availability zones
ext_ids = {
other_config = {
'ovn-cms-options': 'enable-chassis-as-gw,availability-zones=az-0'}
new = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'name': 'SpongeBob', 'external_ids': ext_ids})
attrs={'name': 'SpongeBob', 'other_config': other_config})
old = new
self.assertIsNone(self.event.handle_ha_chassis_group_changes(
self.event.ROW_UPDATE, new, old))

View File

@ -0,0 +1,10 @@
---
other:
- |
Since OVN 20.06, the "Chassis" register configuration is stored in the
"other_config" field and replicated into "external_ids". This replication
is stopped in OVN 22.09. The ML2/OVN plugin tries to retrieve the "Chassis"
configuration from the "other_config" field first; if this field does not
exist (in OVN versions before 20.06), the plugin will use "external_ids"
field instead. Neutron will be compatible with the different OVN versions
(with and without "other_config" field).