diff --git a/neutron/agent/ovn/agent/ovn_neutron_agent.py b/neutron/agent/ovn/agent/ovn_neutron_agent.py index a6f24d5ac06..24e3a5a77c3 100644 --- a/neutron/agent/ovn/agent/ovn_neutron_agent.py +++ b/neutron/agent/ovn/agent/ovn_neutron_agent.py @@ -54,9 +54,9 @@ 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._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) @@ -65,6 +65,10 @@ class OVNNeutronAgent(service.Service): """Return the named extension objet from ``self.ext_manager``""" return self.ext_manager[name].obj + @property + def conf(self): + return self._conf + @property def ovs_idl(self): if not self.ext_manager_api.ovs_idl: @@ -87,15 +91,27 @@ class OVNNeutronAgent(service.Service): def sb_post_fork_event(self): return self.ext_manager_api.sb_post_fork_event + @property + def chassis(self): + return self._chassis + + @property + def chassis_id(self): + return self._chassis_id + + @property + def ovn_bridge(self): + return self._ovn_bridge + def load_config(self): - self.chassis = ovsdb.get_own_chassis_name(self.ovs_idl) + self._chassis = ovsdb.get_own_chassis_name(self.ovs_idl) try: - self.chassis_id = uuid.UUID(self.chassis) + 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(self.ovs_idl) + self._chassis_id = uuid.uuid5(OVN_MONITOR_UUID_NAMESPACE, + self._chassis) + self._ovn_bridge = ovsdb.get_ovn_bridge(self.ovs_idl) LOG.info("Loaded chassis name %s (UUID: %s) and ovn bridge %s.", self.chassis, self.chassis_id, self.ovn_bridge) diff --git a/neutron/agent/ovn/extensions/extension_manager.py b/neutron/agent/ovn/extensions/extension_manager.py index 1caabb72e7f..2947200d68e 100644 --- a/neutron/agent/ovn/extensions/extension_manager.py +++ b/neutron/agent/ovn/extensions/extension_manager.py @@ -18,11 +18,13 @@ import threading from neutron_lib.agent import extension from neutron_lib import exceptions +from oslo_log import log as logging from neutron._i18n import _ from neutron.agent import agent_extensions_manager as agent_ext_mgr +LOG = logging.getLogger(__name__) OVN_AGENT_EXT_MANAGER_NAMESPACE = 'neutron.agent.ovn.extensions' @@ -45,13 +47,15 @@ class OVNAgentExtensionManager(agent_ext_mgr.AgentExtensionsManager): """Start the extensions, once the OVN agent has been initialized.""" for ext in self: ext.obj.start() + LOG.info('Extension manager: %s started', ext.obj.name) class OVNAgentExtension(extension.AgentExtension, metaclass=abc.ABCMeta): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.agent_api = None + self._is_started = False @property @abc.abstractmethod @@ -77,6 +81,11 @@ class OVNAgentExtension(extension.AgentExtension, metaclass=abc.ABCMeta): OVN agent and the extension manager API. It is executed at the end of the OVN agent ``start`` method. """ + self._is_started = True + + @property + def is_started(self): + return self._is_started @property @abc.abstractmethod diff --git a/neutron/agent/ovn/extensions/metadata.py b/neutron/agent/ovn/extensions/metadata.py new file mode 100644 index 00000000000..3c624e5e79f --- /dev/null +++ b/neutron/agent/ovn/extensions/metadata.py @@ -0,0 +1,174 @@ +# Copyright 2024 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 collections +import functools +import re + +from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_log import log +from ovsdbapp.backend.ovs_idl import vlog + +from neutron.agent.linux import external_process +from neutron.agent.ovn.extensions import extension_manager +from neutron.agent.ovn.metadata import agent as metadata_agent +from neutron.agent.ovn.metadata import server as metadata_server +from neutron.common.ovn import constants as ovn_const +from neutron.conf.agent.database import agents_db +from neutron.conf.agent.metadata import config as meta_conf +from neutron.conf.agent.ovn.metadata import config as ovn_meta +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config + + +LOG = log.getLogger(__name__) +EXT_NAME = 'metadata' +agents_db.register_db_agents_opts() +_SYNC_STATE_LOCK = lockutils.ReaderWriterLock() +CHASSIS_METADATA_LOCK = 'chassis_metadata_lock' + +SB_IDL_TABLES = ['Encap', + 'Port_Binding', + 'Datapath_Binding', + 'SB_Global', + 'Chassis', + 'Chassis_Private', + ] + +NS_PREFIX = ovn_const.OVN_METADATA_PREFIX +MAC_PATTERN = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I) +OVN_VIF_PORT_TYPES = ( + "", ovn_const.LSP_TYPE_EXTERNAL, ovn_const.LSP_TYPE_LOCALPORT) + +MetadataPortInfo = collections.namedtuple('MetadataPortInfo', ['mac', + 'ip_addresses', + 'logical_port']) + + +def _sync_lock(f): + """Decorator to block all operations for a global sync call.""" + @functools.wraps(f) + def wrapped(*args, **kwargs): + with _SYNC_STATE_LOCK.write_lock(): + return f(*args, **kwargs) + return wrapped + + +class MetadataExtension(extension_manager.OVNAgentExtension, + metadata_agent.MetadataAgent): + + def __init__(self): + super().__init__(conf=cfg.CONF) + vlog.use_python_logger(max_level=config.get_ovn_ovsdb_log_level()) + self._process_monitor = None + self._proxy = None + # We'll restart all haproxy instances upon start so that they honor + # any potential changes in their configuration. + self.restarted_metadata_proxy_set = set() + + @staticmethod + def _register_config_options(): + ovn_meta.register_meta_conf_opts(meta_conf.SHARED_OPTS) + ovn_meta.register_meta_conf_opts( + meta_conf.UNIX_DOMAIN_METADATA_PROXY_OPTS) + ovn_meta.register_meta_conf_opts(meta_conf.METADATA_PROXY_HANDLER_OPTS) + ovn_meta.register_meta_conf_opts(meta_conf.METADATA_RATE_LIMITING_OPTS, + group=meta_conf.RATE_LIMITING_GROUP) + + def initialize(self, *args): + self._register_config_options() + self._process_monitor = external_process.ProcessMonitor( + config=self.agent_api.conf, resource_type='metadata') + + @property + def name(self): + return 'Metadata OVN agent extension' + + @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 SB_IDL_TABLES + + @property + def sb_idl_events(self): + return [metadata_agent.PortBindingUpdatedEvent, + metadata_agent.PortBindingDeletedEvent, + metadata_agent.SbGlobalUpdateEvent, + metadata_agent.ChassisPrivateCreateEvent, + ] + + # NOTE(ralonsoh): the following properties are needed during the migration + # to the Metadata agent to the OVN agent, while sharing the code with + # ``metadata_agent.MetadataAgent`` + @property + def nb_idl(self): + return self.agent_api.nb_idl + + @property + def sb_idl(self): + return self.agent_api.sb_idl + + @property + def ovs_idl(self): + return self.agent_api.ovs_idl + + @property + def conf(self): + return self.agent_api.conf + + @property + def chassis(self): + return self.agent_api.chassis + + @property + def ovn_bridge(self): + return self.agent_api.ovn_bridge + + @_sync_lock + def resync(self): + """Resync the Metadata OVN agent extension. + + Reload the configuration and sync the agent again. + """ + self.agent_api.load_config() + self.sync() + + def start(self): + self._load_config() + + # Launch the server that will act as a proxy between the VM's and Nova. + self._proxy = metadata_server.UnixDomainMetadataProxy( + self.agent_api.conf, self.agent_api.chassis, + sb_idl=self.agent_api.sb_idl) + self._proxy.run() + + # Do the initial sync. + self.sync() + + # Register the agent with its corresponding Chassis + self.register_metadata_agent() + + # Raise the "is_started" flag. + self._is_started = True diff --git a/neutron/agent/ovn/metadata/agent.py b/neutron/agent/ovn/metadata/agent.py index 698947aaf16..4b060c20a1e 100644 --- a/neutron/agent/ovn/metadata/agent.py +++ b/neutron/agent/ovn/metadata/agent.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc import collections import functools from random import randint @@ -31,6 +32,7 @@ from ovsdbapp.backend.ovs_idl import vlog from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib from neutron.agent.linux import iptables_manager +from neutron.agent.ovn.agent import ovn_neutron_agent from neutron.agent.ovn.metadata import driver as metadata_driver from neutron.agent.ovn.metadata import ovsdb from neutron.agent.ovn.metadata import server as metadata_server @@ -84,11 +86,40 @@ class ConfigException(Exception): """ -class PortBindingEvent(row_event.RowEvent): - def __init__(self, metadata_agent): - self.agent = metadata_agent +class _OVNExtensionEvent(metaclass=abc.ABCMeta): + """Implements a method to retrieve the correct caller agent + + The events inheriting from this class could be called from the OVN metadata + agent or as part of an extension of the OVN agent ("metadata" extension, + for example). In future releases, the OVN metadata agent will be superseded + by the OVN agent (with the "metadata" extension) and this class removed, + keeping only the compatibility with the OVN agent (to be removed in C+2). + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._agent_or_extension = None + self._agent = None + + @property + def agent(self): + """This method provide support for the OVN agent + + This event can be used in the OVN metadata agent and in the OVN + agent metadata extension. + """ + if not self._agent_or_extension: + if isinstance(self._agent, ovn_neutron_agent.OVNNeutronAgent): + self._agent_or_extension = self._agent['metadata'] + else: + self._agent_or_extension = self._agent + return self._agent_or_extension + + +class PortBindingEvent(_OVNExtensionEvent, row_event.RowEvent): + def __init__(self, agent): table = 'Port_Binding' super().__init__((self.__class__.EVENT,), table, None) + self._agent = agent self.event_name = self.__class__.__name__ self._log_msg = ( "PortBindingEvent matched for logical port %s and network %s") @@ -269,7 +300,7 @@ class PortBindingDeletedEvent(PortBindingEvent): return True -class ChassisPrivateCreateEvent(row_event.RowEvent): +class ChassisPrivateCreateEvent(_OVNExtensionEvent, row_event.RowEvent): """Row create event - Chassis name == our_chassis. On connection, we get a dump of all chassis so if we catch a creation @@ -277,12 +308,15 @@ class ChassisPrivateCreateEvent(row_event.RowEvent): to do a full sync to make sure that we capture all changes while the connection to OVSDB was down. """ - def __init__(self, metadata_agent): - self.agent = metadata_agent + def __init__(self, agent): + self._extension = None self.first_time = True events = (self.ROW_CREATE,) - super(ChassisPrivateCreateEvent, self).__init__( - events, 'Chassis_Private', (('name', '=', self.agent.chassis),)) + super().__init__(events, 'Chassis_Private', None) + # NOTE(ralonsoh): ``self._agent`` needs to be assigned before being + # used in the property ``self.agent``. + self._agent = agent + self.conditions = (('name', '=', self.agent.chassis),) self.event_name = self.__class__.__name__ def run(self, event, row, old): @@ -297,14 +331,14 @@ class ChassisPrivateCreateEvent(row_event.RowEvent): self.agent.sync() -class SbGlobalUpdateEvent(row_event.RowEvent): +class SbGlobalUpdateEvent(_OVNExtensionEvent, row_event.RowEvent): """Row update event on SB_Global table.""" - def __init__(self, metadata_agent): - self.agent = metadata_agent + def __init__(self, agent): table = 'SB_Global' events = (self.ROW_UPDATE,) super(SbGlobalUpdateEvent, self).__init__(events, table, None) + self._agent = agent self.event_name = self.__class__.__name__ self.first_run = True @@ -337,16 +371,21 @@ class SbGlobalUpdateEvent(row_event.RowEvent): class MetadataAgent(object): def __init__(self, conf): - self.conf = conf + self._conf = conf vlog.use_python_logger(max_level=config.get_ovn_ovsdb_log_level()) self._process_monitor = external_process.ProcessMonitor( - config=self.conf, + config=self._conf, resource_type='metadata') self._sb_idl = None self._post_fork_event = threading.Event() # We'll restart all haproxy instances upon start so that they honor # any potential changes in their configuration. self.restarted_metadata_proxy_set = set() + self._chassis = None + + @property + def conf(self): + return self._conf @property def sb_idl(self): @@ -358,15 +397,27 @@ class MetadataAgent(object): def sb_idl(self, val): self._sb_idl = val + @property + def chassis(self): + return self._chassis + + @property + def chassis_id(self): + return self._chassis_id + + @property + def ovn_bridge(self): + return self._ovn_bridge + def _load_config(self): - self.chassis = self._get_own_chassis_name() + self._chassis = self._get_own_chassis_name() try: - self.chassis_id = uuid.UUID(self.chassis) + 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_METADATA_UUID_NAMESPACE, - self.chassis) - self.ovn_bridge = self._get_ovn_bridge() + self._chassis_id = uuid.uuid5(OVN_METADATA_UUID_NAMESPACE, + self._chassis) + self._ovn_bridge = self._get_ovn_bridge() LOG.debug("Loaded chassis name %s (UUID: %s) and ovn bridge %s.", self.chassis, self.chassis_id, self.ovn_bridge) @@ -408,14 +459,14 @@ class MetadataAgent(object): self._post_fork_event.clear() self.sb_idl = ovsdb.MetadataAgentOvnSbIdl( - chassis=self.chassis, tables=tables, events=events).start() + chassis=self._chassis, tables=tables, events=events).start() # Now IDL connections can be safely used. self._post_fork_event.set() # Launch the server that will act as a proxy between the VM's and Nova. self._proxy = metadata_server.UnixDomainMetadataProxy( - self.conf, self.chassis, sb_idl=self.sb_idl) + self.conf, self._chassis, sb_idl=self.sb_idl) self._proxy.run() # Do the initial sync. @@ -661,7 +712,7 @@ class MetadataAgent(object): metadata_port.logical_port) chassis_ports = self.sb_idl.get_ports_on_chassis( - self.chassis, include_additional_chassis=True) + self._chassis, include_additional_chassis=True) datapath_ports_ips = [] for chassis_port in self._vif_ports(chassis_ports): if str(chassis_port.datapath.uuid) == datapath_uuid: 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 index 8c2df06c1e9..82caf8211e8 100644 --- a/neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py +++ b/neutron/tests/functional/agent/ovn/agent/fake_ovn_agent_extension.py @@ -79,4 +79,4 @@ class FakeOVNAgentExtension(ext_mgr.OVNAgentExtension): return [OVNSBChassisEvent] def start(self): - self._is_ext_started = True + self._is_started = True 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 index 6ab54fe1eb5..dd53e70714c 100644 --- a/neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py +++ b/neutron/tests/functional/agent/ovn/agent/test_ovn_neutron_agent.py @@ -14,47 +14,57 @@ # under the License. from unittest import mock +import uuid 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.agent.ovn.metadata import agent as metadata_agent +from neutron.common.ovn import constants as ovn_const from neutron.common import utils as n_utils from neutron.tests.common import net_helpers from neutron.tests.functional import base TEST_EXTENSION = 'testing' +METADATA_EXTENSION = 'metadata' +EXTENSION_NAMES = {TEST_EXTENSION: 'Fake OVN agent extension', + METADATA_EXTENSION: 'Metadata OVN agent extension', + } -class TestOVNNeutronAgent(base.TestOVNFunctionalBase): +class TestOVNNeutronAgentBase(base.TestOVNFunctionalBase): - OVN_BRIDGE = 'br-int' - FAKE_CHASSIS_HOST = 'ovn-host-fake' - - def setUp(self, **kwargs): + def setUp(self, extensions, **kwargs): super().setUp(**kwargs) + self.host_name = 'host-' + uuidutils.generate_uuid()[:5] + self.chassis_name = self.add_fake_chassis(self.host_name) + self.extensions = extensions self.mock_chassis_name = mock.patch.object( - agent_ovsdb, 'get_own_chassis_name').start() - self.ovn_agent = self._start_ovn_neutron_agent() + agent_ovsdb, 'get_own_chassis_name', + return_value=self.chassis_name).start() + with mock.patch.object(metadata_agent.MetadataAgent, + '_get_own_chassis_name', + return_value=self.chassis_name): + self.ovn_agent = self._start_ovn_neutron_agent() def _check_loaded_and_started_extensions(self, ovn_agent): - loaded_ext = ovn_agent[TEST_EXTENSION] - self.assertEqual('Fake OVN agent extension', loaded_ext.name) - self.assertTrue(loaded_ext._is_ext_started) + for _ext in self.extensions: + loaded_ext = ovn_agent[_ext] + self.assertEqual(EXTENSION_NAMES.get(_ext), loaded_ext.name) + self.assertTrue(loaded_ext.is_started) def _start_ovn_neutron_agent(self): conf = self.useFixture(fixture_config.Config()).conf - conf.set_override('extensions', TEST_EXTENSION, group='agent') + conf.set_override('extensions', ','.join(self.extensions), + 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 = uuidutils.generate_uuid() - self.mock_chassis_name.return_value = self.chassis_name - agt = ovn_neutron_agent.OVNNeutronAgent(conf) agt.test_ovs_idl = [] agt.test_ovn_sb_idl = [] @@ -62,8 +72,6 @@ class TestOVNNeutronAgent(base.TestOVNFunctionalBase): agt.start() self._check_loaded_and_started_extensions(agt) - self.add_fake_chassis(self.FAKE_CHASSIS_HOST, name=self.chassis_name) - 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) @@ -71,6 +79,12 @@ class TestOVNNeutronAgent(base.TestOVNFunctionalBase): self.addCleanup(agt.ext_manager_api.nb_idl.ovsdb_connection.stop) return agt + +class TestOVNNeutronAgentFakeAgent(TestOVNNeutronAgentBase): + + def setUp(self, **kwargs): + super().setUp(extensions=[TEST_EXTENSION], **kwargs) + def test_ovs_and_ovs_events(self): # Test the OVS IDL is attending the provided events. bridge = self.useFixture(net_helpers.OVSBridgeFixture()).bridge @@ -95,4 +109,22 @@ class TestOVNNeutronAgent(base.TestOVNFunctionalBase): exc = Exception('Logical Switch %s not added or not detected by ') n_utils.wait_until_true( lambda: lswitch_name in self.ovn_agent.test_ovn_nb_idl, - timeout=10) + timeout=10, exception=exc) + + +class TestOVNNeutronAgentMetadataExtension(TestOVNNeutronAgentBase): + + def setUp(self, **kwargs): + super().setUp(extensions=[METADATA_EXTENSION], **kwargs) + + def test_check_metadata_started(self): + # Check the metadata extension is registered. + chassis_id = uuid.UUID(self.chassis_name) + agent_id = uuid.uuid5(chassis_id, 'metadata_agent') + ext_ids = {ovn_const.OVN_AGENT_METADATA_ID_KEY: str(agent_id)} + ch_private = self.sb_api.lookup('Chassis_Private', self.chassis_name) + self.assertEqual(ext_ids, ch_private.external_ids) + + # Check Unix proxy is running. + metadata_extension = self.ovn_agent[METADATA_EXTENSION] + self.assertIsNotNone(metadata_extension._proxy.server) diff --git a/neutron/tests/unit/agent/ovn/metadata/test_agent.py b/neutron/tests/unit/agent/ovn/metadata/test_agent.py index 03a449b8221..672999e375a 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py @@ -80,8 +80,8 @@ class TestMetadataAgent(base.BaseTestCase): self.agent.sb_idl = mock.Mock() self.agent.ovs_idl = mock.Mock() self.agent.ovs_idl.transaction = mock.MagicMock() - self.agent.chassis = 'chassis' - self.agent.ovn_bridge = 'br-int' + self.agent._chassis = 'chassis' + self.agent._ovn_bridge = 'br-int' self.ports = [] for i in range(0, 3): diff --git a/setup.cfg b/setup.cfg index 08aaddd25fd..aca634fc417 100644 --- a/setup.cfg +++ b/setup.cfg @@ -146,6 +146,7 @@ neutron.agent.l3.extensions = conntrack_helper = neutron.agent.l3.extensions.conntrack_helper:ConntrackHelperAgentExtension ndp_proxy = neutron.agent.l3.extensions.ndp_proxy:NDPProxyAgentExtension neutron.agent.ovn.extensions = + metadata = neutron.agent.ovn.extensions.metadata:MetadataExtension qos_hwol = neutron.agent.ovn.extensions.qos_hwol:QoSHardwareOffloadExtension noop = neutron.agent.ovn.extensions.noop:NoopOVNAgentExtension testing = neutron.tests.functional.agent.ovn.agent.fake_ovn_agent_extension:FakeOVNAgentExtension