summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRodolfo Alonso Hernandez <ralonsoh@redhat.com>2023-01-06 04:05:13 +0100
committerRodolfo Alonso Hernandez <ralonsoh@redhat.com>2023-01-26 07:41:38 +0100
commitd0c7bb653af16ddf310579966d2f6583da866f4c (patch)
treedc289ef22d750e86e7960f442438a0ad1ab8b1f3
parent65bd4a24f5543053ccfe9e9ab37fd516a11dc14b (diff)
downloadneutron-d0c7bb653af16ddf310579966d2f6583da866f4c.tar.gz
[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
-rw-r--r--etc/oslo-config-generator/ovn_agent.ini6
-rw-r--r--neutron/agent/ovn/agent/__init__.py0
-rw-r--r--neutron/agent/ovn/agent/ovn_neutron_agent.py127
-rw-r--r--neutron/agent/ovn/agent/ovsdb.py142
-rw-r--r--neutron/agent/ovn/extensions/__init__.py0
-rw-r--r--neutron/agent/ovn/extensions/extension_manager.py130
-rw-r--r--neutron/agent/ovn/extensions/noop.py39
-rw-r--r--neutron/agent/ovn/ovn_neutron_agent.py43
-rw-r--r--neutron/cmd/eventlet/agents/ovn_neutron_agent.py26
-rw-r--r--neutron/conf/agent/ovn/ovn_neutron_agent/__init__.py0
-rw-r--r--neutron/conf/agent/ovn/ovn_neutron_agent/config.py56
-rw-r--r--neutron/tests/functional/agent/ovn/agent/__init__.py0
-rw-r--r--neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py75
-rw-r--r--neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py77
-rw-r--r--releasenotes/notes/ovn-agent-added-84fc31c0fba02be9.yaml8
-rw-r--r--setup.cfg5
16 files changed, 734 insertions, 0 deletions
diff --git a/etc/oslo-config-generator/ovn_agent.ini b/etc/oslo-config-generator/ovn_agent.ini
new file mode 100644
index 0000000000..b4f2885867
--- /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 0000000000..e69de29bb2
--- /dev/null
+++ b/neutron/agent/ovn/agent/__init__.py
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 0000000000..a3925eeebc
--- /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 0000000000..e5cf757119
--- /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 0000000000..e69de29bb2
--- /dev/null
+++ b/neutron/agent/ovn/extensions/__init__.py
diff --git a/neutron/agent/ovn/extensions/extension_manager.py b/neutron/agent/ovn/extensions/extension_manager.py
new file mode 100644
index 0000000000..447e1f1e5d
--- /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 0000000000..c972e6fd04
--- /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 0000000000..421adbb2e3
--- /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 0000000000..ce3424ecbc
--- /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 0000000000..e69de29bb2
--- /dev/null
+++ b/neutron/conf/agent/ovn/ovn_neutron_agent/__init__.py
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 0000000000..69c693d0dc
--- /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 0000000000..e69de29bb2
--- /dev/null
+++ b/neutron/tests/functional/agent/ovn/agent/__init__.py
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 0000000000..a294d49320
--- /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 0000000000..a6a8c09f54
--- /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 0000000000..ed2a04d860
--- /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 ce30c6a760..34b53e0528 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