From a007da1e9a68821de71ee274353b008c1fb6d4d6 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 23 Sep 2022 09:11:25 +0200 Subject: [PATCH] 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]https://github.com/ovn-org/ovn/commit/74d90c2223d0a8c123823fb849b4c2de58c296e4 [2]https://github.com/ovn-org/ovn/commit/51309429cc3032a0cb422603e7bbda4905ca01ae 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 536498a29a4e7662a4d0b1bb923e2521509ad77a) (cherry picked from commit 8a4c62d094a5419d878c38aa9017e40931a08f21) --- neutron/common/ovn/utils.py | 12 +- .../ml2/drivers/ovn/agent/neutron_agent.py | 19 +- .../drivers/ovn/mech_driver/mech_driver.py | 3 +- .../mech_driver/ovsdb/extensions/placement.py | 274 ++++++++++++++++++ .../ovn/mech_driver/ovsdb/impl_idl_ovn.py | 11 +- .../ovn/mech_driver/ovsdb/ovsdb_monitor.py | 41 ++- neutron/tests/functional/base.py | 11 +- .../ovsdb/extensions/test_placement.py | 188 ++++++++++++ .../ovn/mech_driver/ovsdb/test_impl_idl.py | 10 +- .../mech_driver/ovsdb/test_ovsdb_monitor.py | 9 +- .../ovn/mech_driver/test_mech_driver.py | 4 +- .../functional/services/ovn_l3/test_plugin.py | 11 +- neutron/tests/unit/common/ovn/test_utils.py | 32 +- neutron/tests/unit/fake_resources.py | 17 +- .../drivers/ovn/agent/test_neutron_agent.py | 2 +- .../mech_driver/ovsdb/test_ovsdb_monitor.py | 39 ++- ...chassis-other-config-7db15b9d10bf7f04.yaml | 10 + 17 files changed, 603 insertions(+), 90 deletions(-) create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py create mode 100644 neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py create mode 100644 releasenotes/notes/ovn-chassis-other-config-7db15b9d10bf7f04.yaml diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index edc46b9da53..286b6ca4754 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -594,8 +594,8 @@ 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( - constants.OVN_CMS_OPTIONS, '').split(',')] + return [opt.strip() for opt in get_ovn_chassis_other_config(chassis).get( + constants.OVN_CMS_OPTIONS, '').split(',')] def is_gateway_chassis(chassis): @@ -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 diff --git a/neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py b/neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py index 77d45d56be0..5578bd9894c 100644 --- a/neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py +++ b/neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py index ef5ca1b1566..81df09b616a 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py @@ -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: diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py new file mode 100644 index 00000000000..f372807fcad --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py index eed3a9e95ff..b628e823323 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py @@ -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): 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 969384cc89a..87e1fecb564 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 @@ -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): diff --git a/neutron/tests/functional/base.py b/neutron/tests/functional/base.py index b0e84388da8..b6faf6747c2 100644 --- a/neutron/tests/functional/base.py +++ b/neutron/tests/functional/base.py @@ -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( diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py new file mode 100644 index 00000000000..327718d7c93 --- /dev/null +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_placement.py @@ -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}) diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py index cc85252f9bd..c5eb8b23aa1 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py @@ -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() 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 b6867ba54ba..03724708c5a 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 @@ -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])) diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index 0df43892e8e..22b59c09ab7 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -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'], diff --git a/neutron/tests/functional/services/ovn_l3/test_plugin.py b/neutron/tests/functional/services/ovn_l3/test_plugin.py index 37fd0c329cc..6ad26a2d0e6 100644 --- a/neutron/tests/functional/services/ovn_l3/test_plugin.py +++ b/neutron/tests/functional/services/ovn_l3/test_plugin.py @@ -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") diff --git a/neutron/tests/unit/common/ovn/test_utils.py b/neutron/tests/unit/common/ovn/test_utils.py index 63109835b6d..24a038c24ca 100644 --- a/neutron/tests/unit/common/ovn/test_utils.py +++ b/neutron/tests/unit/common/ovn/test_utils.py @@ -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( diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 3934b157c5c..16928fb9619 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -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) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/agent/test_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/agent/test_neutron_agent.py index ed434cab186..cd92c937534 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/agent/test_neutron_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/agent/test_neutron_agent.py @@ -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)) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py index 8bade0eb617..26b56858b6b 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py @@ -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)) diff --git a/releasenotes/notes/ovn-chassis-other-config-7db15b9d10bf7f04.yaml b/releasenotes/notes/ovn-chassis-other-config-7db15b9d10bf7f04.yaml new file mode 100644 index 00000000000..8123a03be6d --- /dev/null +++ b/releasenotes/notes/ovn-chassis-other-config-7db15b9d10bf7f04.yaml @@ -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).