[OVN] Implement OVN agent metadata extension

This patch is implementing the OVN agent metadata extension, by reusing
the OVN metadata class. The class ``MetadataAgent`` is inherited in the
``MetadataExtension`` class. The goal is to use the same code in both
implementations (until the OVN metadata agent is deprecated).

The OVN agent metadata extension has a different initialization
process. The OVN and OVS IDL connections are created during the
extension initialization but are not accessible. The ``start`` method
is used to load the configuration, execute the sync process and
register the metadata extension.

This extension will replace the need of the OVN metadata agent. The
deprecation of this agent will imply the refactor of the existing code
that now is shared between both agents.

This new OVN agent will be tested in the "neutron-tempest-plugin-ovn"
CI job, after the change done in the following patch.

Needed-By: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/909860

Partial-Bug: #2017871
Change-Id: I4381a67648a9b6198a8d936db784964d74dc87a1
This commit is contained in:
Rodolfo Alonso Hernandez 2023-12-19 07:54:26 +00:00
parent 5c187e8dab
commit fe31f4fe02
8 changed files with 334 additions and 51 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -79,4 +79,4 @@ class FakeOVNAgentExtension(ext_mgr.OVNAgentExtension):
return [OVNSBChassisEvent]
def start(self):
self._is_ext_started = True
self._is_started = True

View File

@ -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)

View File

@ -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):

View File

@ -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