Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4fb3d6f
[OVN] The OVS IDL connection is global for all tests
ralonsoh Aug 5, 2025
fc1039f
Remove false-positive ACLs in OVN DB sync
brianphaley Dec 6, 2024
cf83a59
[OVN] Initialize OVN agent in ``start`` method
ralonsoh Nov 14, 2025
53c2c8d
[SGL] Ignore port groups that don't come from SGs
elvgarrui Nov 18, 2025
55f6e07
Merge "[OVN] The OVS IDL connection is global for all tests" into sta…
Nov 24, 2025
5c6576b
Merge "[OVN] Initialize OVN agent in ``start`` method" into stable/20…
Nov 24, 2025
72165d0
Replace response body in after hook for 404 error
skraynev Nov 8, 2025
d8775ea
Merge "Remove false-positive ACLs in OVN DB sync" into stable/2025.1
Nov 26, 2025
f3a3997
[OVN] Add 'qos-bw-minimum-ingress' ML2 extension
ralonsoh Nov 26, 2025
c121981
Fix incorrect log message in ovn_client
brianphaley Nov 20, 2025
167a5b8
Merge "[SGL] Ignore port groups that don't come from SGs" into stable…
Nov 26, 2025
92cdf1d
Merge "Fix incorrect log message in ovn_client" into stable/2025.1
Nov 27, 2025
7360e1f
Append content-encoding header
Oct 3, 2025
91857ed
Merge "[OVN] Add 'qos-bw-minimum-ingress' ML2 extension" into stable/…
Nov 28, 2025
abfca72
Don't prevent setting CIDRs as allowed_address_pair for port
slawqo Nov 27, 2025
607c3c4
[FT] Wait for the manager to be created (2)
ralonsoh Dec 1, 2025
1fe30fd
Merge "Append content-encoding header" into stable/2025.1
Dec 3, 2025
8116c1f
Merge "[FT] Wait for the manager to be created (2)" into stable/2025.1
Dec 3, 2025
33a318d
Update OVN_AGENT_METADATA_SB_CFG_KEY when OVN metadata agent starts
rainsun-minsu May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions neutron/agent/ovn/agent/ovn_neutron_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ def __init__(self, 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)
self.ext_manager_api = None
self.ext_manager = None

def __getitem__(self, name):
"""Return the named extension objet from ``self.ext_manager``"""
Expand Down Expand Up @@ -159,7 +158,22 @@ def update_neutron_sb_cfg_key(self):
'Chassis_Private', self.chassis,
('external_ids', external_ids)).execute(check_error=True)

def _initialize_ext_manager(self):
"""Initialize the externsion manager and the extension manager API.

This method must be called once, outside the ``__init__`` method and
at the beginning of the ``start`` method.
"""
if not self.ext_manager:
self.ext_manager_api = ext_mgr.OVNAgentExtensionAPI()
self.ext_manager = ext_mgr.OVNAgentExtensionManager(self._conf)
self.ext_manager.initialize(None, 'ovn', self)

def start(self):
# This must be the first operation in the `start` method.
self._initialize_ext_manager()

# Extension manager configuration.
self.ext_manager_api.ovs_idl = self._load_ovs_idl()
self.load_config()
# Before executing "_load_sb_idl", is is needed to execute
Expand Down
17 changes: 17 additions & 0 deletions neutron/agent/ovn/metadata/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,21 @@ def _update_chassis_private_config(self):
'Chassis_Private', self.chassis,
('external_ids', external_ids)).execute(check_error=True)

def _update_metadata_sb_cfg_key(self):
"""Update the Chassis_Private nb_cfg information in external_ids

This method should be called once the Metadata Agent has been
registered (method ``register_metadata_agent`` has been called) and
the corresponding Chassis_Private register has been created/updated
and chassis private config has been updated.
"""
nb_cfg = self.sb_idl.db_get('Chassis_Private',
self.chassis, 'nb_cfg').execute()
external_ids = {ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY: str(nb_cfg)}
self.sb_idl.db_set(
'Chassis_Private', self.chassis,
('external_ids', external_ids)).execute(check_error=True)

@_sync_lock
def resync(self):
"""Resync the agent.
Expand All @@ -439,6 +454,7 @@ def resync(self):
"""
self._load_config()
self._update_chassis_private_config()
self._update_metadata_sb_cfg_key()
self.sync()

def start(self):
Expand Down Expand Up @@ -474,6 +490,7 @@ def start(self):
# Register the agent with its corresponding Chassis
self.register_metadata_agent()
self._update_chassis_private_config()
self._update_metadata_sb_cfg_key()

self._proxy.wait()

Expand Down
17 changes: 13 additions & 4 deletions neutron/common/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
PROXY_SERVICE_NAME = 'haproxy'
PROXY_SERVICE_CMD = 'haproxy'

CONTENT_ENCODERS = ('gzip', 'deflate')
CONTENT_ENCODERS = {
'gzip': b'\x1f\x8b\x08',
'deflate': b'\x1f\x8b\x08'
}


class InvalidUserOrGroupException(Exception):
Expand Down Expand Up @@ -159,15 +162,21 @@ class MetadataProxyHandlerBaseSocketServer(
metaclass=abc.ABCMeta):
@staticmethod
def _http_response(http_response, request):
headerlist = list(http_response.headers.items())
# We detect if content is compressed by magic signature,
# when `content-encoding` is not present.
if not http_response.headers.get('content-encoding'):
if http_response.content[:3] == CONTENT_ENCODERS['gzip']:
headerlist.append(('content-encoding', 'gzip'))

_res = webob.Response(
body=http_response.content,
status=http_response.status_code,
content_type=http_response.headers['content-type'],
charset=http_response.encoding)
headerlist=headerlist)
# The content of the response is decoded depending on the
# "Context-Enconding" header, if present. The operation is limited to
# ("gzip", "deflate"), as is in the ``webob.response.Response`` class.
if _res.content_encoding in CONTENT_ENCODERS:
if _res.content_encoding in CONTENT_ENCODERS.keys():
_res.decode_content()

# NOTE(ralonsoh): there should be a better way to format the HTTP
Expand Down
2 changes: 2 additions & 0 deletions neutron/common/ovn/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from neutron_lib.api.definitions import qinq
from neutron_lib.api.definitions import qos
from neutron_lib.api.definitions import qos_bw_limit_direction
from neutron_lib.api.definitions import qos_bw_minimum_ingress
from neutron_lib.api.definitions import qos_default
from neutron_lib.api.definitions import qos_gateway_ip
from neutron_lib.api.definitions import qos_rule_type_details
Expand Down Expand Up @@ -171,6 +172,7 @@
qinq.ALIAS,
qos.ALIAS,
qos_bw_limit_direction.ALIAS,
qos_bw_minimum_ingress.ALIAS,
qos_default.ALIAS,
qos_rule_type_details.ALIAS,
qos_rule_type_filter.ALIAS,
Expand Down
9 changes: 9 additions & 0 deletions neutron/pecan_wsgi/hooks/policy_enforcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from neutron_lib import constants as const
from oslo_log import log as logging
from oslo_policy import policy as oslo_policy
from oslo_serialization import jsonutils
from oslo_utils import excutils
from pecan import hooks
import webob
Expand Down Expand Up @@ -190,6 +191,14 @@ def after(self, state):
# we have to set the status_code here to prevent the catch_errors
# middleware from turning this into a 500.
state.response.status_code = 404
# replace the original body on NotFound body
error_message = {
'type': 'HTTPNotFound',
'message': 'The resource could not be found.',
'detail': ''
}
state.response.text = jsonutils.dumps(error_message)
state.response.content_type = 'application/json'
return

if is_single:
Expand Down
10 changes: 9 additions & 1 deletion neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,14 +655,22 @@ def _get_common_ips(ip_addresses, ip_networks):
common_ips.add(str(ip_address))
return common_ips

# NOTE(slaweq): We can safely ignore any CIDR larger than /32 (for
# IPv4) or /128 (for IPv6) in the allowed_address_pairs, since such
# CIDRs cannot be set as a Virtual IP in OVN.
# Only /32 and /128 CIDRs are allowed to be set as Virtual IPs in OVN.
address_pairs_to_check = [
ip_net for ip_net in port_allowed_address_pairs_ip_addresses
if ip_net.size == 1]

for distributed_port in distributed_ports:
distributed_port_ip_addresses = [
netaddr.IPAddress(fixed_ip['ip_address']) for fixed_ip in
distributed_port.get('fixed_ips', [])]

common_ips = _get_common_ips(
distributed_port_ip_addresses,
port_allowed_address_pairs_ip_addresses)
address_pairs_to_check)

if common_ips:
err_msg = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def update_lsp_host_info(self, context, db_port, up=True):
if up:
if not port_up:
LOG.warning('Logical_Switch_Port %s host information not '
'updated, the port state is down')
'updated, the port state is down', db_port.id)
return

if not db_port.port_bindings:
Expand All @@ -333,7 +333,7 @@ def update_lsp_host_info(self, context, db_port, up=True):
else:
if port_up:
LOG.warning('Logical_Switch_Port %s host information not '
'removed, the port state is up')
'removed, the port state is up', db_port.id)
return

cmd.append(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,33 @@ def sync_acls(self, ctx):
add_acls.append(na)
n_index += 1

# Check any ACLs we found to add against existing ACLs, ignoring the
# SG rule ID key. This eliminates any false-positives where the
# normalized cidr for two SG rules is the same value, since there
# will only be a single ACL that matches exactly with the SG rule ID.
if add_acls:
def copy_acl_rem_id_key(acl):
acl_copy = acl.copy()
del acl_copy[ovn_const.OVN_SG_RULE_EXT_ID_KEY]
return acl_copy

add_rem_acls = []
# Make a list of non-default rule ACLs (they have a security group
# rule id). See ovn_default_acls code/comment above for more info.
nd_ovn_acls = [copy_acl_rem_id_key(oa) for oa in ovn_acls
if ovn_const.OVN_SG_RULE_EXT_ID_KEY in oa]
# We must copy here since we need to keep the original
# 'add_acl' intact for removal
for add_acl in add_acls:
add_acl_copy = copy_acl_rem_id_key(add_acl)
if add_acl_copy in nd_ovn_acls:
add_rem_acls.append(add_acl)

# Remove any of the false-positive ACLs
LOG.warning('False-positive ACLs to remove: (%s)', add_rem_acls)
for add_rem in add_rem_acls:
add_acls.remove(add_rem)

if n_index < neutron_num:
# We didn't find the OVN ACLs matching the Neutron ACLs
# in "ovn_acls" and we are just adding the pending Neutron ACLs.
Expand Down
10 changes: 10 additions & 0 deletions neutron/services/logapi/drivers/ovn/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ def _set_acls_log(self, pgs, context, ovn_txn, actions_enabled, log_name):
for pg in pgs:
meter_name = self.meter_name
if pg["name"] != ovn_const.OVN_DROP_PORT_GROUP_NAME:
if ovn_const.OVN_SG_EXT_ID_KEY not in pg["external_ids"]:
LOG.info("Port Group %s is not part of any security "
"group, skipping its network log "
"setting...", pg["name"])
continue
sg = sg_obj.SecurityGroup.get_sg_by_id(
context, pg["external_ids"][ovn_const.OVN_SG_EXT_ID_KEY])
if not sg:
Expand Down Expand Up @@ -426,6 +431,11 @@ def _set_neutron_acls_log(self, pgs, context, actions_enabled, log_name,
for pg in pgs:
meter_name = self.meter_name
if pg['name'] != ovn_const.OVN_DROP_PORT_GROUP_NAME:
if ovn_const.OVN_SG_EXT_ID_KEY not in pg["external_ids"]:
LOG.info("Port Group %s is not part of any security "
"group, skipping its network log "
"setting...", pg["name"])
continue
sg = sg_obj.SecurityGroup.get_sg_by_id(context,
pg['external_ids'][ovn_const.OVN_SG_EXT_ID_KEY])
if not sg:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from neutron.agent.ovn.agent import ovsdb as agent_ovsdb
from neutron.agent.ovn.metadata import agent as metadata_agent
from neutron.agent.ovn.metadata import server_socket
from neutron.agent.ovsdb import impl_idl
from neutron.common.ovn import constants as ovn_const
from neutron.common import utils as n_utils
from neutron.tests.common import net_helpers
Expand All @@ -46,6 +47,7 @@ def setUp(self, extensions, **kwargs):
self.mock_chassis_name = mock.patch.object(
agent_ovsdb, 'get_own_chassis_name',
return_value=self.chassis_name).start()
self.ovs_idl_events = []
with mock.patch.object(metadata_agent.MetadataAgent,
'_get_own_chassis_name',
return_value=self.chassis_name):
Expand All @@ -57,6 +59,21 @@ def _check_loaded_and_started_extensions(self, ovn_agent):
self.assertEqual(EXTENSION_NAMES.get(_ext), loaded_ext.name)
self.assertTrue(loaded_ext.is_started)

def _create_ovs_idl(self, ovn_agent):
for extension in ovn_agent.ext_manager:
self.ovs_idl_events += extension.obj.ovs_idl_events
self.ovs_idl_events = [e(ovn_agent) for e in
set(self.ovs_idl_events)]
ovsdb = impl_idl.api_factory()
ovsdb.idl.notify_handler.watch_events(self.ovs_idl_events)

ovn_agent.ext_manager_api.ovs_idl = ovsdb
return ovsdb

def _clear_events_ovs_idl(self):
self.ovn_agent.ovs_idl.idl_monitor.notify_handler.unwatch_events(
self.ovs_idl_events)

def _start_ovn_neutron_agent(self):
conf = self.useFixture(fixture_config.Config()).conf
conf.set_override('extensions', ','.join(self.extensions),
Expand All @@ -75,7 +92,11 @@ def _start_ovn_neutron_agent(self):
# Once eventlet is completely removed, this mock can be deleted.
with mock.patch.object(ovn_neutron_agent.OVNNeutronAgent, 'wait'), \
mock.patch.object(server_socket.UnixDomainMetadataProxy,
'wait'):
'wait'), \
mock.patch.object(ovn_neutron_agent.OVNNeutronAgent,
'_load_ovs_idl') as mock_load_ovs_idl:
agt._initialize_ext_manager()
mock_load_ovs_idl.return_value = self._create_ovs_idl(agt)
agt.start()
external_ids = agt.sb_idl.db_get(
'Chassis_Private', agt.chassis, 'external_ids').execute(
Expand All @@ -85,8 +106,7 @@ def _start_ovn_neutron_agent(self):
'0')

self._check_loaded_and_started_extensions(agt)

self.addCleanup(agt.ext_manager_api.ovs_idl.ovsdb_connection.stop)
self.addCleanup(self._clear_events_ovs_idl)
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ def _start_metadata_agent(self):
check_error=True)
self.assertEqual(external_ids[ovn_const.OVN_AGENT_OVN_BRIDGE],
self.OVN_BRIDGE)
self.assertEqual(
external_ids[ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY],
'0')

# Metadata agent will open connections to OVS and SB databases.
# Close connections to them when the test ends,
Expand All @@ -129,16 +132,6 @@ def _start_metadata_agent(self):
return agt

def test_metadata_agent_healthcheck(self):
chassis_row = self.sb_api.db_find(
AGENT_CHASSIS_TABLE,
('name', '=', self.chassis_name)).execute(
check_error=True)[0]

# Assert that, prior to creating a resource the metadata agent
# didn't populate the external_ids from the Chassis
self.assertNotIn(ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY,
chassis_row['external_ids'])

# Let's list the agents to force the nb_cfg to be bumped on NB
# db, which will automatically increment the nb_cfg counter on
# NB_Global and make ovn-controller copy it over to SB_Global. Upon
Expand Down
Loading