From d0c7bb653af16ddf310579966d2f6583da866f4c Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 6 Jan 2023 04:05:13 +0100 Subject: [PATCH] [OVN] Implementation of OVN Neutron Agent This patch implements the OVN Neutron Agent executable, the extension manager engine, the agent extension abstract class and the configuration section. Related-Bug: #1998608 Change-Id: I94bb98217e03f9ac314cb9723da277a23368649c --- etc/oslo-config-generator/ovn_agent.ini | 6 + neutron/agent/ovn/agent/__init__.py | 0 neutron/agent/ovn/agent/ovn_neutron_agent.py | 127 ++++++++++++++++ neutron/agent/ovn/agent/ovsdb.py | 142 ++++++++++++++++++ neutron/agent/ovn/extensions/__init__.py | 0 .../agent/ovn/extensions/extension_manager.py | 130 ++++++++++++++++ neutron/agent/ovn/extensions/noop.py | 39 +++++ neutron/agent/ovn/ovn_neutron_agent.py | 43 ++++++ .../cmd/eventlet/agents/ovn_neutron_agent.py | 26 ++++ .../agent/ovn/ovn_neutron_agent/__init__.py | 0 .../agent/ovn/ovn_neutron_agent/config.py | 56 +++++++ .../functional/agent/ovn/agent/__init__.py | 0 .../ovn/agent/fake_ovn_agent_extension.py | 75 +++++++++ .../agent/ovn/agent/test_ovn_neutron_agent.py | 77 ++++++++++ .../ovn-agent-added-84fc31c0fba02be9.yaml | 8 + setup.cfg | 5 + 16 files changed, 734 insertions(+) create mode 100644 etc/oslo-config-generator/ovn_agent.ini create mode 100644 neutron/agent/ovn/agent/__init__.py create mode 100644 neutron/agent/ovn/agent/ovn_neutron_agent.py create mode 100644 neutron/agent/ovn/agent/ovsdb.py create mode 100644 neutron/agent/ovn/extensions/__init__.py create mode 100644 neutron/agent/ovn/extensions/extension_manager.py create mode 100644 neutron/agent/ovn/extensions/noop.py create mode 100644 neutron/agent/ovn/ovn_neutron_agent.py create mode 100644 neutron/cmd/eventlet/agents/ovn_neutron_agent.py create mode 100644 neutron/conf/agent/ovn/ovn_neutron_agent/__init__.py create mode 100644 neutron/conf/agent/ovn/ovn_neutron_agent/config.py create mode 100644 neutron/tests/functional/agent/ovn/agent/__init__.py create mode 100644 neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py create mode 100644 neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py create mode 100644 releasenotes/notes/ovn-agent-added-84fc31c0fba02be9.yaml diff --git a/etc/oslo-config-generator/ovn_agent.ini b/etc/oslo-config-generator/ovn_agent.ini new file mode 100644 index 00000000000..b4f28858671 --- /dev/null +++ b/etc/oslo-config-generator/ovn_agent.ini @@ -0,0 +1,6 @@ +[DEFAULT] +output_file = etc/ovn_agent.ini.sample +wrap_width = 79 + +namespace = neutron.ml2.ovn.agent +namespace = oslo.log diff --git a/neutron/agent/ovn/agent/__init__.py b/neutron/agent/ovn/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/agent/ovn/agent/ovn_neutron_agent.py b/neutron/agent/ovn/agent/ovn_neutron_agent.py new file mode 100644 index 00000000000..a3925eeebc9 --- /dev/null +++ b/neutron/agent/ovn/agent/ovn_neutron_agent.py @@ -0,0 +1,127 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 uuid + +from oslo_log import log as logging +from oslo_service import service +from ovsdbapp.backend.ovs_idl import event as row_event + +from neutron.agent.ovn.agent import ovsdb +from neutron.agent.ovn.extensions import extension_manager as ext_mgr +from neutron.common.ovn import constants as ovn_const + + +LOG = logging.getLogger(__name__) +OVN_MONITOR_UUID_NAMESPACE = uuid.UUID('fd7e0970-7164-11ed-80f0-00000003158a') + + +class SbGlobalUpdateEvent(row_event.RowEvent): + """Row update event on SB_Global table. + + This event will trigger the OVN Neutron Agent update of the + 'neutron:ovn-neutron-agent-sb-cfg' key in 'SB_Global', that is used to + determine the agent status. + """ + + def __init__(self, ovn_agent): + self.ovn_agent = ovn_agent + table = 'SB_Global' + events = (self.ROW_UPDATE, ) + super().__init__(events, table, None) + self.event_name = self.__class__.__name__ + + def run(self, event, row, old): + ext_ids = {ovn_const.OVN_AGENT_NEUTRON_SB_CFG_KEY: str(row.nb_cfg)} + self.ovn_agent.sb_idl.db_set('Chassis_Private', self.ovn_agent.chassis, + ('external_ids', ext_ids)).execute() + + +class OVNNeutronAgent(service.Service): + + def __init__(self, conf): + super().__init__() + self._conf = conf + self.chassis = None + self.chassis_id = None + self.ovn_bridge = None + self.ext_manager_api = ext_mgr.OVNAgentExtensionAPI() + self.ext_manager = ext_mgr.OVNAgentExtensionManager(self._conf) + self.ext_manager.initialize(None, 'ovn', self.ext_manager_api) + + @property + def sb_idl(self): + return self.ext_manager_api.sb_idl + + def _load_config(self, ovs_idl): + self.chassis = ovsdb.get_own_chassis_name(ovs_idl) + try: + self.chassis_id = uuid.UUID(self.chassis) + except ValueError: + # OVS system-id could be a non UUID formatted string. + self.chassis_id = uuid.uuid5(OVN_MONITOR_UUID_NAMESPACE, + self.chassis) + self.ovn_bridge = ovsdb.get_ovn_bridge(ovs_idl) + LOG.info("Loaded chassis name %s (UUID: %s) and ovn bridge %s.", + self.chassis, self.chassis_id, self.ovn_bridge) + + def _load_ovs_idl(self): + events = [] + for extension in self.ext_manager: + events += extension.obj.ovs_idl_events + events = [e(self) for e in set(events)] + return ovsdb.MonitorAgentOvsIdl(set(events)).start() + + def _load_nb_idl(self): + events = [] + tables = [] + for extension in self.ext_manager: + events += extension.obj.nb_idl_events + tables += extension.obj.nb_idl_tables + + if not (tables or events): + # If there is no need to retrieve any table nor attend to any + # event, the IDL object is not created to save a DB connection. + return None + + events = [e(self) for e in set(events)] + tables = set(tables) + return ovsdb.MonitorAgentOvnNbIdl(tables, events).start() + + def _load_sb_idl(self): + events = [SbGlobalUpdateEvent] + tables = ['SB_Global'] + for extension in self.ext_manager: + events += extension.obj.sb_idl_events + tables += extension.obj.sb_idl_tables + + events = [e(self) for e in set(events)] + tables = set(tables) + return ovsdb.MonitorAgentOvnSbIdl(tables, events, + chassis=self.chassis).start() + + def start(self): + self.ext_manager_api.ovs_idl = self._load_ovs_idl() + # Before executing "_load_config", it is needed to create the OVS IDL. + self._load_config(self.ext_manager_api.ovs_idl) + # Before executing "_load_sb_idl", is is needed to execute + # "_load_config" to populate self.chassis. + self.ext_manager_api.sb_idl = self._load_sb_idl() + self.ext_manager_api.nb_idl = self._load_nb_idl() + LOG.info('Starting OVN Neutron Agent') + + def stop(self, graceful=True): + LOG.info('Stopping OVN Neutron Agent') + super().stop(graceful) diff --git a/neutron/agent/ovn/agent/ovsdb.py b/neutron/agent/ovn/agent/ovsdb.py new file mode 100644 index 00000000000..e5cf7571193 --- /dev/null +++ b/neutron/agent/ovn/agent/ovsdb.py @@ -0,0 +1,142 @@ +# Copyright 2023 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 oslo_log import log +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.schema.open_vswitch import impl_idl as impl_idl_ovs + +from neutron.agent.ovsdb.native import connection as ovsdb_conn +from neutron.common.ovn import utils as ovn_utils +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor + + +LOG = log.getLogger(__name__) + + +class MonitorAgentOvnSbIdl(ovsdb_monitor.OvnIdl): + + SCHEMA = 'OVN_Southbound' + + def __init__(self, tables, events, chassis=None): + connection_string = config.get_ovn_sb_connection() + ovsdb_monitor._check_and_set_ssl_files(self.SCHEMA) + helper = self._get_ovsdb_helper(connection_string) + for table in tables: + helper.register_table(table) + try: + super().__init__(None, connection_string, helper, + leader_only=False) + except TypeError: + # TODO(twilson) We can remove this when we require ovs>=2.12.0 + super().__init__(None, connection_string, helper) + if chassis: + for table in set(tables).intersection({'Chassis', + 'Chassis_Private'}): + self.set_table_condition(table, [['name', '==', chassis]]) + if events: + self.notify_handler.watch_events(events) + + @ovn_utils.retry(max_=180) + def _get_ovsdb_helper(self, connection_string): + return idlutils.get_schema_helper(connection_string, self.SCHEMA) + + @ovn_utils.retry() + def start(self): + LOG.info('Getting OvsdbSbOvnIdl for OVN monitor with retry') + conn = connection.Connection( + self, timeout=config.get_ovn_ovsdb_timeout()) + return impl_idl_ovn.OvsdbSbOvnIdl(conn) + + def post_connect(self): + pass + + +class MonitorAgentOvnNbIdl(ovsdb_monitor.OvnIdl): + + SCHEMA = 'OVN_Northbound' + + def __init__(self, tables, events): + connection_string = config.get_ovn_nb_connection() + ovsdb_monitor._check_and_set_ssl_files(self.SCHEMA) + helper = self._get_ovsdb_helper(connection_string) + for table in tables: + helper.register_table(table) + try: + super().__init__(None, connection_string, helper, + leader_only=False) + except TypeError: + # TODO(twilson) We can remove this when we require ovs>=2.12.0 + super().__init__(None, connection_string, helper) + if events: + self.notify_handler.watch_events(events) + + @ovn_utils.retry(max_=180) + def _get_ovsdb_helper(self, connection_string): + return idlutils.get_schema_helper(connection_string, self.SCHEMA) + + @ovn_utils.retry() + def start(self): + LOG.info('Getting OvsdbNbOvnIdl for OVN monitor with retry') + conn = connection.Connection( + self, timeout=config.get_ovn_ovsdb_timeout()) + return impl_idl_ovn.OvsdbNbOvnIdl(conn) + + def post_connect(self): + pass + + +class MonitorAgentOvsIdl(ovsdb_conn.OvsIdl): + + def __init__(self, events): + super().__init__() + if events: + self.notify_handler.watch_events(events) + + @ovn_utils.retry() + def start(self): + LOG.info('Getting OvsdbIdl for OVN monitor with retry') + conn = connection.Connection(self, + timeout=config.get_ovn_ovsdb_timeout()) + return impl_idl_ovs.OvsdbIdl(conn) + + def post_connect(self): + pass + + +def get_ovn_bridge(ovs_idl): + """Return the external_ids:ovn-bridge value of the Open_vSwitch table. + + This is the OVS bridge used to plug the metadata ports to. + If the key doesn't exist, this method will return 'br-int' as default. + """ + ext_ids = ovs_idl.db_get('Open_vSwitch', '.', 'external_ids').execute() + try: + return ext_ids['ovn-bridge'] + except KeyError: + LOG.warning("Can't read ovn-bridge external-id from OVSDB. Using " + "br-int instead.") + return 'br-int' + + +def get_own_chassis_name(ovs_idl): + """Return the external_ids:system-id value of the Open_vSwitch table. + + As long as ovn-controller is running on this node, the key is + guaranteed to exist and will include the chassis name. + """ + ext_ids = ovs_idl.db_get('Open_vSwitch', '.', 'external_ids').execute() + return ext_ids['system-id'] diff --git a/neutron/agent/ovn/extensions/__init__.py b/neutron/agent/ovn/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/agent/ovn/extensions/extension_manager.py b/neutron/agent/ovn/extensions/extension_manager.py new file mode 100644 index 00000000000..447e1f1e5d9 --- /dev/null +++ b/neutron/agent/ovn/extensions/extension_manager.py @@ -0,0 +1,130 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 abc +import threading + +from neutron_lib.agent import extension +from neutron_lib import exceptions + +from neutron._i18n import _ +from neutron.agent import agent_extensions_manager as agent_ext_mgr + + +OVN_AGENT_EXT_MANAGER_NAMESPACE = 'neutron.agent.ovn.extensions' + + +class ConfigException(exceptions.NeutronException): + """Misconfiguration of the OVN Neutron Agent""" + message = _('Error configuring the OVN Neutron Agent: %(description)s.') + + +class OVNAgentExtensionManager(agent_ext_mgr.AgentExtensionsManager): + + def __init__(self, conf): + super().__init__(conf, OVN_AGENT_EXT_MANAGER_NAMESPACE) + for ext in self: + if not isinstance(ext.obj, OVNAgentExtension): + desc = ('Extension %s class is not inheriting from ' + '"OVNAgentExtension"') + raise ConfigException(description=desc) + + +class OVNAgentExtension(extension.AgentExtension, metaclass=abc.ABCMeta): + + def __init__(self): + super().__init__() + self.agent_api = None + + def initialize(self, *args): + """Initialize agent extension.""" + self.agent_api = None + + def consume_api(self, agent_api): + """Configure the Agent API. + + Allows an extension to gain access to resources internal to the + neutron agent and otherwise unavailable to the extension. + """ + self.agent_api = agent_api + + @property + @abc.abstractmethod + def ovs_idl_events(self): + pass + + @property + @abc.abstractmethod + def nb_idl_tables(self): + pass + + @property + @abc.abstractmethod + def nb_idl_events(self): + pass + + @property + @abc.abstractmethod + def sb_idl_tables(self): + pass + + @property + @abc.abstractmethod + def sb_idl_events(self): + pass + + +class OVNAgentExtensionAPI(object): + """Implements the OVN Neutron Agent API""" + + def __init__(self): + self._nb_idl = None + self._sb_idl = None + self._has_chassis_private = None + self._ovs_idl = None + self.sb_post_fork_event = threading.Event() + self.sb_post_fork_event.clear() + self.nb_post_fork_event = threading.Event() + self.nb_post_fork_event.clear() + + @property + def ovs_idl(self): + return self._ovs_idl + + @ovs_idl.setter + def ovs_idl(self, val): + self._ovs_idl = val + + @property + def nb_idl(self): + if not self._nb_idl: + self.nb_post_fork_event.wait() + return self._nb_idl + + @nb_idl.setter + def nb_idl(self, val): + self.nb_post_fork_event.set() + self._nb_idl = val + + @property + def sb_idl(self): + if not self._sb_idl: + self.sb_post_fork_event.wait() + return self._sb_idl + + @sb_idl.setter + def sb_idl(self, val): + self.sb_post_fork_event.set() + self._sb_idl = val diff --git a/neutron/agent/ovn/extensions/noop.py b/neutron/agent/ovn/extensions/noop.py new file mode 100644 index 00000000000..c972e6fd041 --- /dev/null +++ b/neutron/agent/ovn/extensions/noop.py @@ -0,0 +1,39 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.agent.ovn.extensions import extension_manager + + +class NoopOVNAgentExtension(extension_manager.OVNAgentExtension): + + @property + def ovs_idl_events(self): + return [] + + @property + def nb_idl_tables(self): + return [] + + @property + def nb_idl_events(self): + return [] + + @property + def sb_idl_tables(self): + return [] + + @property + def sb_idl_events(self): + return [] diff --git a/neutron/agent/ovn/ovn_neutron_agent.py b/neutron/agent/ovn/ovn_neutron_agent.py new file mode 100644 index 00000000000..421adbb2e32 --- /dev/null +++ b/neutron/agent/ovn/ovn_neutron_agent.py @@ -0,0 +1,43 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 sys + +from neutron.common import config +from neutron.common import utils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import service + +from neutron.agent.ovn.agent import ovn_neutron_agent +from neutron.conf.agent.ovn.ovn_neutron_agent import config as config_ovn_agent + + +LOG = logging.getLogger(__name__) + + +def main(): + config.register_common_config_options() + config_ovn_agent.register_opts() + config.init(sys.argv[1:]) + config.setup_logging() + utils.log_opt_values(LOG) + config_ovn_agent.setup_privsep() + + ovn_agent = ovn_neutron_agent.OVNNeutronAgent(cfg.CONF) + + LOG.info('OVN Neutron Agent initialized successfully, now running... ') + launcher = service.launch(cfg.CONF, ovn_agent, restart_method='mutate') + launcher.wait() diff --git a/neutron/cmd/eventlet/agents/ovn_neutron_agent.py b/neutron/cmd/eventlet/agents/ovn_neutron_agent.py new file mode 100644 index 00000000000..ce3424ecbc3 --- /dev/null +++ b/neutron/cmd/eventlet/agents/ovn_neutron_agent.py @@ -0,0 +1,26 @@ +# 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 setproctitle + +from neutron.agent.ovn import ovn_neutron_agent + + +# TODO(ralonsoh): move to ``neutron_lib.constants``. +AGENT_PROCESS_OVN_NEUTRON_AGENT = 'neutron-ovn-agent' + + +def main(): + proctitle = "%s (%s)" % (AGENT_PROCESS_OVN_NEUTRON_AGENT, + setproctitle.getproctitle()) + setproctitle.setproctitle(proctitle) + ovn_neutron_agent.main() diff --git a/neutron/conf/agent/ovn/ovn_neutron_agent/__init__.py b/neutron/conf/agent/ovn/ovn_neutron_agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/conf/agent/ovn/ovn_neutron_agent/config.py b/neutron/conf/agent/ovn/ovn_neutron_agent/config.py new file mode 100644 index 00000000000..69c693d0dc7 --- /dev/null +++ b/neutron/conf/agent/ovn/ovn_neutron_agent/config.py @@ -0,0 +1,56 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 +import shlex + +from neutron.conf.agent import ovsdb_api +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from oslo_config import cfg +from oslo_privsep import priv_context + +from neutron._i18n import _ + + +OVS_OPTS = [ + cfg.IntOpt( + 'ovsdb_connection_timeout', + default=180, + help=_('Timeout in seconds for the OVSDB connection transaction')) +] + + +def list_ovn_neutron_agent_opts(): + return [ + ('ovn', ovn_conf.ovn_opts), + ('ovs', itertools.chain(OVS_OPTS, + ovsdb_api.API_OPTS, + ) + ), + ] + + +def register_opts(): + cfg.CONF.register_opts(ovn_conf.ovn_opts, group='ovn') + cfg.CONF.register_opts(OVS_OPTS, group='ovs') + cfg.CONF.register_opts(ovsdb_api.API_OPTS, group='ovs') + + +def get_root_helper(conf): + return conf.AGENT.root_helper + + +def setup_privsep(): + priv_context.init(root_helper=shlex.split(get_root_helper(cfg.CONF))) diff --git a/neutron/tests/functional/agent/ovn/agent/__init__.py b/neutron/tests/functional/agent/ovn/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py b/neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py new file mode 100644 index 00000000000..a294d493204 --- /dev/null +++ b/neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py @@ -0,0 +1,75 @@ +# Copyright (c) 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 ovsdbapp.backend.ovs_idl import event as row_event + +from neutron.agent.ovn.extensions import extension_manager as ext_mgr + + +class OVSInterfaceEvent(row_event.RowEvent): + + def __init__(self, ovn_agent): + self.ovn_agent = ovn_agent + events = (self.ROW_CREATE, ) + table = 'Interface' + super().__init__(events, table, None) + + def run(self, event, row, old): + self.ovn_agent.test_ovs_idl = row.name + + +class OVNSBChassisEvent(row_event.RowEvent): + def __init__(self, ovn_agent): + self.ovn_agent = ovn_agent + events = (self.ROW_CREATE, ) + table = 'Chassis' + super().__init__(events, table, None) + + def run(self, event, row, old): + self.ovn_agent.test_ovn_sb_idl = row.name + + +class OVNNBLogicalSwitchEvent(row_event.RowEvent): + def __init__(self, ovn_agent): + self.ovn_agent = ovn_agent + events = (self.ROW_CREATE, ) + table = 'Logical_Switch' + super().__init__(events, table, None) + + def run(self, event, row, old): + self.ovn_agent.test_ovn_nb_idl = row.name + + +class FakeOVNAgentExtension(ext_mgr.OVNAgentExtension): + + @property + def ovs_idl_events(self): + return [OVSInterfaceEvent] + + @property + def nb_idl_tables(self): + return ['Logical_Switch'] + + @property + def nb_idl_events(self): + return [OVNNBLogicalSwitchEvent] + + @property + def sb_idl_tables(self): + return ['Chassis', 'Chassis_Private'] + + @property + def sb_idl_events(self): + return [OVNSBChassisEvent] diff --git a/neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py b/neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py new file mode 100644 index 00000000000..a6a8c09f54f --- /dev/null +++ b/neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py @@ -0,0 +1,77 @@ +# Copyright 2023 Red Hat, Inc. +# All Rights Reserved. +# +# 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 oslo_config import fixture as fixture_config +from oslo_utils import uuidutils + +from neutron.agent.ovn.agent import ovn_neutron_agent +from neutron.agent.ovn.agent import ovsdb as agent_ovsdb +from neutron.common import utils as n_utils +from neutron.tests.common import net_helpers +from neutron.tests.functional import base + + +class TestOVNNeutronAgent(base.TestOVNFunctionalBase): + + OVN_BRIDGE = 'br-int' + FAKE_CHASSIS_HOST = 'ovn-host-fake' + + def setUp(self, **kwargs): + super().setUp(**kwargs) + self.mock_chassis_name = mock.patch.object( + agent_ovsdb, 'get_own_chassis_name').start() + self.ovn_agent = self._start_ovn_neutron_agent() + + def _start_ovn_neutron_agent(self): + conf = self.useFixture(fixture_config.Config()).conf + conf.set_override('extensions', 'testing', group='agent') + ovn_nb_db = self.ovsdb_server_mgr.get_ovsdb_connection_path('nb') + conf.set_override('ovn_nb_connection', ovn_nb_db, group='ovn') + ovn_sb_db = self.ovsdb_server_mgr.get_ovsdb_connection_path('sb') + conf.set_override('ovn_sb_connection', ovn_sb_db, group='ovn') + self.chassis_name = self.add_fake_chassis(self.FAKE_CHASSIS_HOST) + self.mock_chassis_name.return_value = self.chassis_name + + agt = ovn_neutron_agent.OVNNeutronAgent(conf) + agt.start() + self.addCleanup(agt.ext_manager_api.ovs_idl.ovsdb_connection.stop) + if agt.ext_manager_api.sb_idl: + self.addCleanup(agt.ext_manager_api.sb_idl.ovsdb_connection.stop) + if agt.ext_manager_api.nb_idl: + self.addCleanup(agt.ext_manager_api.nb_idl.ovsdb_connection.stop) + return agt + + def test_ovs_and_ovs_events(self): + # Test the OVS IDL is attending the provided events. + bridge = self.useFixture(net_helpers.OVSBridgeFixture()).bridge + n_utils.wait_until_true( + lambda: bridge.br_name == self.ovn_agent.test_ovs_idl, + timeout=10) + + # Test the OVN SB IDL is attending the provided events. The chassis is + # created before the OVN SB IDL connection is created but the creation + # event is received during the subscription. + n_utils.wait_until_true( + lambda: self.chassis_name == self.ovn_agent.test_ovn_sb_idl, + timeout=10) + + # Test the OVN SN IDL is attending the provided events. + lswitch_name = 'ovn-' + uuidutils.generate_uuid() + self.nb_api.ls_add(lswitch_name).execute(check_error=True) + n_utils.wait_until_true( + lambda: lswitch_name == self.ovn_agent.test_ovn_nb_idl, + timeout=10) diff --git a/releasenotes/notes/ovn-agent-added-84fc31c0fba02be9.yaml b/releasenotes/notes/ovn-agent-added-84fc31c0fba02be9.yaml new file mode 100644 index 00000000000..ed2a04d8601 --- /dev/null +++ b/releasenotes/notes/ovn-agent-added-84fc31c0fba02be9.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added a new agent: the OVN Agent. This new agent will run on a compute or + a controller node using OVN as network backend, similar to other ML2 + mechanism drivers as ML2/OVS or ML2/SRIOV. This new agent will perform + those actions that the ovn-controller service cannot execute. The agent + functionality will be plugable and added via configuration knob. diff --git a/setup.cfg b/setup.cfg index ce30c6a760b..34b53e05284 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ console_scripts = neutron-sriov-nic-agent = neutron.cmd.eventlet.plugins.sriov_nic_neutron_agent:main neutron-sanity-check = neutron.cmd.sanity_check:main neutron-status = neutron.cmd.status:main + neutron-ovn-agent = neutron.cmd.eventlet.agents.ovn_neutron_agent:main neutron-ovn-metadata-agent = neutron.cmd.eventlet.agents.ovn_metadata:main neutron-ovn-migration-mtu = neutron.cmd.ovn.migration_mtu:main neutron-ovn-db-sync-util = neutron.cmd.ovn.neutron_ovn_db_sync_util:main @@ -140,6 +141,9 @@ neutron.agent.l3.extensions = snat_log = neutron.agent.l3.extensions.snat_log:SNATLoggingExtension conntrack_helper = neutron.agent.l3.extensions.conntrack_helper:ConntrackHelperAgentExtension ndp_proxy = neutron.agent.l3.extensions.ndp_proxy:NDPProxyAgentExtension +neutron.agent.ovn.extensions = + noop = neutron.agent.ovn.extensions.noop:NoopOVNAgentExtension + testing = neutron.tests.functional.agent.ovn.agent.fake_ovn_agent_extension:FakeOVNAgentExtension neutron.services.logapi.drivers = ovs = neutron.services.logapi.drivers.openvswitch.ovs_firewall_log:OVSFirewallLoggingDriver neutron.qos.agent_drivers = @@ -170,6 +174,7 @@ oslo.config.opts = neutron.ml2.ovn = neutron.conf.plugins.ml2.drivers.ovn.ovn_conf:list_opts neutron.ml2.ovs.agent = neutron.opts:list_ovs_opts neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts + neutron.ovn.agent = neutron.conf.agent.ovn.ovn_neutron_agent.config:list_ovn_neutron_agent_opts neutron.ovn.metadata.agent = neutron.conf.agent.ovn.metadata.config:list_metadata_agent_opts nova.auth = neutron.opts:list_nova_auth_opts placement.auth = neutron.opts:list_placement_auth_opts