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
f3b7d64
Fix VLAN network detection to use VlanTypeDriver ranges
May 14, 2026
d540e9d
Fix PF GET/PUT parent floating IP validation
brianphaley May 21, 2026
0901bb3
Merge "Fix VLAN network detection to use VlanTypeDriver ranges" into …
May 27, 2026
8ab37df
Fix port RBAC policies to require network ownership
ralonsoh May 27, 2026
1c9bad3
Skip OVNL3 post-fork init in non-API workers
ralonsoh May 26, 2026
ec19809
Merge "Skip OVNL3 post-fork init in non-API workers" into stable/2026.1
May 28, 2026
1941988
Fix tunnel network detection to use tunnel type driver ranges
ralonsoh May 28, 2026
895376b
[OVS FW] Clean invalid conntrack entries after OF rules are applied
slawqo Jun 1, 2026
48c440b
Merge "[OVS FW] Clean invalid conntrack entries after OF rules are ap…
Jun 3, 2026
27e972e
Handle invalid MAC in metadata agent port provisioning
eduolivares Apr 28, 2026
57f12d9
Fix cross-project access to router conntrack helpers
ralonsoh Jun 4, 2026
88d6c79
Fix ``update_router:enable_default_route_*`` policies
ralonsoh Jun 9, 2026
50aa8f7
ovs: skip OF operations for ports with invalid ofport
ralonsoh Jun 9, 2026
e97c10f
ovs: defer ports with invalid ofport in ``_process_port``
ralonsoh Jun 10, 2026
a9d8055
quota: Fix quota details API error with unloaded service plugins
ralonsoh Jun 8, 2026
0b06dc0
Guard tun_br in _restore_local_vlan_map when no br-tun
xtrusia Jun 12, 2026
a34e7b2
[metadata] cleanup response headers
pshchelo Dec 11, 2025
bc7852f
Increase ``allowed_network_downtime`` to 7.0
ralonsoh Jun 22, 2026
8d11b5e
Merge "[metadata] cleanup response headers" into stable/2026.1
Jun 23, 2026
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
26 changes: 21 additions & 5 deletions neutron/agent/linux/openvswitch_firewall/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ def __init__(self, integration_bridge):
self._initialize_sg()
self._update_cookie = None
self._deferred = False
self._ports_pending_invalid_ct_cleanup = []
self.iptables_helper = iptables.Helper(self.int_br.br)
self.iptables_helper.load_driver_if_needed()
self.ipconntrack = ip_conntrack.OvsIpConntrackManager()
Expand Down Expand Up @@ -702,11 +703,22 @@ def _get_port_physical_network(self, port_name):
return get_physical_network_from_other_config(
self.int_br.br, port_name)

def _delete_invalid_conntrack_entries_for_port(self, port, of_port):
port['of_port'] = of_port
def _delete_invalid_conntrack_entries_for_port(self, ports):
for ethertype in [lib_const.IPv4, lib_const.IPv6]:
self.ipconntrack.delete_conntrack_state_by_remote_ips(
[port], ethertype, set(), mark=ovsfw_consts.CT_MARK_INVALID)
ports, ethertype, set(), mark=ovsfw_consts.CT_MARK_INVALID)

def _schedule_invalid_conntrack_entries_cleanup(self, port):
if self._deferred:
self._ports_pending_invalid_ct_cleanup.append(port)
else:
self._delete_invalid_conntrack_entries_for_port([port])

def _flush_pending_invalid_conntrack_cleanup(self):
self._delete_invalid_conntrack_entries_for_port(
self._ports_pending_invalid_ct_cleanup
)
self._ports_pending_invalid_ct_cleanup = []

def get_ofport(self, port):
port_id = port['device']
Expand Down Expand Up @@ -768,7 +780,8 @@ def prepare_port_filter(self, port):
self._update_flows_for_port(of_port, old_of_port)
else:
self._set_port_filters(of_port)
self._delete_invalid_conntrack_entries_for_port(port, of_port)
port['of_port'] = of_port
self._schedule_invalid_conntrack_entries_cleanup(port)
except exceptions.OVSFWPortNotFound as not_found_error:
LOG.info("port %(port_id)s does not exist in ovsdb: %(err)s.",
{'port_id': port['device'],
Expand Down Expand Up @@ -808,7 +821,8 @@ def update_port_filter(self, port):
else:
self._set_port_filters(of_port)

self._delete_invalid_conntrack_entries_for_port(port, of_port)
port['of_port'] = of_port
self._schedule_invalid_conntrack_entries_cleanup(port)

except exceptions.OVSFWPortNotFound as not_found_error:
LOG.info("port %(port_id)s does not exist in ovsdb: %(err)s.",
Expand Down Expand Up @@ -897,11 +911,13 @@ def remove_trusted_ports(self, port_ids):

def filter_defer_apply_on(self):
self._deferred = True
self._ports_pending_invalid_ct_cleanup = []

def filter_defer_apply_off(self):
if self._deferred:
self._cleanup_stale_sg()
self.int_br.apply_flows()
self._flush_pending_invalid_conntrack_cleanup()
self._deferred = False

@property
Expand Down
5 changes: 5 additions & 0 deletions neutron/agent/ovn/metadata/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ def _get_port_ip4_ips_and_ip6_flag(self, port):
LOG.warning("Port %s MAC column is empty, cannot retrieve IP "
"addresses", port.uuid)
return [], False
if port.mac == [ovn_const.UNKNOWN_ADDR]:
LOG.warning("Port %s MAC column value is %s, this port will "
"not have metadata service", port.uuid, port.mac)
return [], False

mac, ips = ovn_utils.get_mac_and_ips_from_port_binding(port)
if not ips:
LOG.debug("Port %s IP addresses were not retrieved from the "
Expand Down
31 changes: 23 additions & 8 deletions neutron/common/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,32 @@ 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'))

resp_headers = http_response.headers.copy()
resp_body_encoding = resp_headers.get('content-encoding')
resp_content_magic = http_response.content[:3]
if resp_content_magic == CONTENT_ENCODERS["gzip"]:
# python-requests asks for and unpacks the gzip itself,
# but leaves the content-encoding header in response untouched.
# however, specifically user_data could itself be gzipped still.
resp_headers['content-encoding'] = 'gzip'
elif (
resp_content_magic not in CONTENT_ENCODERS.values() and
resp_body_encoding in CONTENT_ENCODERS.keys()
):
# content-encoding is set but content is not actually encoded,
# this is normal for how `requests` behaves, it decodes gzip
# itself but not removes the content-encoding header from response.
resp_headers.pop('content-encoding', None)

# remove content-length and transfer-encoding, possibly from original
# gzip/deflate (python-requests does not modify those headers),
# let webob recalculate these.
resp_headers.pop('content-length', None)
resp_headers.pop('transfer-encoding', None)
_res = webob.Response(
body=http_response.content,
status=http_response.status_code,
headerlist=headerlist)
headerlist=list(resp_headers.items()))
# 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.
Expand Down
1 change: 1 addition & 0 deletions neutron/conf/policies/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
# related to the "network owner" and network isn't really parent of the subnet
# or port. Because of that, using parent owner in those cases may be
# missleading for users so it's better to keep also "network owner" rules.
NET_OWNER_MANAGER = 'role:manager and ' + RULE_NET_OWNER
NET_OWNER_MEMBER = 'role:member and ' + RULE_NET_OWNER
NET_OWNER_READER = 'role:reader and ' + RULE_NET_OWNER
ADMIN_OR_NET_OWNER_MEMBER = (
Expand Down
19 changes: 1 addition & 18 deletions neutron/conf/policies/port.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
check_str=neutron_policy.policy_or(
'not rule:network_device',
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER
),
scope_types=['project'],
Expand All @@ -119,7 +118,6 @@
name='create_port:mac_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER),
scope_types=['project'],
description='Specify ``mac_address`` attribute when creating a port',
Expand All @@ -136,7 +134,6 @@
name='create_port:fixed_ips',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER,
'rule:shared'),
scope_types=['project'],
Expand All @@ -155,7 +152,6 @@
name='create_port:fixed_ips:ip_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER),
scope_types=['project'],
description='Specify IP address in ``fixed_ips`` when creating a port',
Expand All @@ -172,7 +168,6 @@
name='create_port:fixed_ips:subnet_id',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER,
'rule:shared'),
scope_types=['project'],
Expand All @@ -191,7 +186,6 @@
name='create_port:port_security_enabled',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER),
scope_types=['project'],
description=(
Expand Down Expand Up @@ -258,7 +252,6 @@
name='create_port:allowed_address_pairs',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description=(
Expand All @@ -276,7 +269,6 @@
name='create_port:allowed_address_pairs:mac_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description=(
Expand All @@ -294,7 +286,6 @@
name='create_port:allowed_address_pairs:ip_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description=(
Expand Down Expand Up @@ -496,7 +487,6 @@
check_str=neutron_policy.policy_or(
'not rule:network_device',
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER,
),
scope_types=['project'],
Expand All @@ -515,7 +505,7 @@
name='update_port:mac_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER
base.NET_OWNER_MANAGER,
),
scope_types=['project'],
description='Update ``mac_address`` attribute of a port',
Expand All @@ -532,7 +522,6 @@
name='update_port:fixed_ips',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER
),
scope_types=['project'],
Expand All @@ -550,7 +539,6 @@
name='update_port:fixed_ips:ip_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER
),
scope_types=['project'],
Expand All @@ -571,7 +559,6 @@
name='update_port:fixed_ips:subnet_id',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER,
'rule:shared'
),
Expand All @@ -594,7 +581,6 @@
name='update_port:port_security_enabled',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_SERVICE,
base.PROJECT_MANAGER,
base.NET_OWNER_MEMBER
),
scope_types=['project'],
Expand Down Expand Up @@ -653,7 +639,6 @@
name='update_port:allowed_address_pairs',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description='Update ``allowed_address_pairs`` attribute of a port',
Expand All @@ -668,7 +653,6 @@
name='update_port:allowed_address_pairs:mac_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description=(
Expand All @@ -686,7 +670,6 @@
name='update_port:allowed_address_pairs:ip_address',
check_str=neutron_policy.policy_or(
base.ADMIN_OR_NET_OWNER_MEMBER,
base.PROJECT_MANAGER,
base.SERVICE),
scope_types=['project'],
description=(
Expand Down
4 changes: 2 additions & 2 deletions neutron/conf/policies/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,15 @@
scope_types=['project'],
description=('Specify ``enable_default_route_bfd`` attribute when '
'updating a router'),
operations=ACTION_POST,
operations=ACTION_PUT,
),
policy.DocumentedRuleDefault(
name='update_router:enable_default_route_ecmp',
check_str=base.ADMIN,
scope_types=['project'],
description=('Specify ``enable_default_route_ecmp`` attribute when '
'updating a router'),
operations=ACTION_POST,
operations=ACTION_PUT,
),
policy.DocumentedRuleDefault(
name='update_router:tags',
Expand Down
10 changes: 8 additions & 2 deletions neutron/db/quota/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ def get_detailed_project_quotas(self, context, resources, project_id):
# pass it regardless to keep the quota driver API intact
plugins = directory.get_plugins()
plugin = plugins.get(key, plugins[constants.CORE])
used = resource.count(context, plugin, project_id)
try:
used = resource.count(context, plugin, project_id)
except NotImplementedError:
LOG.info('Skipping quota resource %s: no plugin loaded '
'that supports counting it.', key)
continue

project_quota_ext[key] = {
'limit': resource.default,
Expand All @@ -108,7 +113,8 @@ def get_detailed_project_quotas(self, context, resources, project_id):
quota_objs = quota_obj.Quota.get_objects(context,
project_id=project_id)
for item in quota_objs:
project_quota_ext[item['resource']]['limit'] = item['limit']
if item['resource'] in project_quota_ext:
project_quota_ext[item['resource']]['limit'] = item['limit']
return project_quota_ext

@staticmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,10 @@ def _restore_local_vlan_map(self):
self.available_local_vlans.remove(local_vlan)
# Restore the br-tun flood output ports
# See LP #1978088
tun_ofports = self.tun_br.get_flood_to_tun_ofports(
local_vlan)
tun_ofports = set()
if self.enable_tunneling:
tun_ofports = self.tun_br.get_flood_to_tun_ofports(
local_vlan)
self._local_vlan_hints[key] = {
'vlan': local_vlan,
'tun_ofports': tun_ofports}
Expand Down Expand Up @@ -1489,7 +1491,15 @@ def port_unbound(self, vif_id, net_uuid=None):
if not lvm.vif_ports:
self.reclaim_local_vlan(net_uuid, lvm.segmentation_id)

@staticmethod
def _has_valid_ofport(port):
return port.ofport and port.ofport != ovs_lib.INVALID_OFPORT

def port_alive(self, port, log_errors=True):
if not self._has_valid_ofport(port):
LOG.warning("port_alive skipped for port %s: invalid ofport %s",
port.port_name, port.ofport)
return
cur_tag = self.int_br.db_get_val("Port", port.port_name, "tag",
log_errors=log_errors)
# Port normal vlan tag is set correctly, remove the drop flows
Expand All @@ -1505,6 +1515,10 @@ def port_dead(self, port, log_errors=True):

:param port: an ovs_lib.VifPort object.
'''
if not self._has_valid_ofport(port):
LOG.warning("port_dead skipped for port %s: invalid ofport %s",
port.port_name, port.ofport)
return
# Don't kill a port if it's already dead
cur_tag = self.int_br.db_get_val("Port", port.port_name, "tag",
log_errors=log_errors)
Expand Down Expand Up @@ -1856,9 +1870,11 @@ def _process_port(port, ports, ancillary_ports):
iface_id = self.int_br.portid_from_external_ids(
port['external_ids'])
if iface_id:
if port['ofport'] == ovs_lib.UNASSIGNED_OFPORT:
LOG.debug("Port %s not ready yet on the bridge",
iface_id)
if port['ofport'] in (ovs_lib.UNASSIGNED_OFPORT,
ovs_lib.INVALID_OFPORT):
LOG.debug("Port %s not ready yet on the bridge "
"(ofport=%s)",
iface_id, port['ofport'])
ports_not_ready_yet.add(port['name'])
return
# check if port belongs to ancillary bridge
Expand Down Expand Up @@ -1964,19 +1980,12 @@ def treat_vif_port(self, vif_port, port_id, network_id, network_type,
physical_network, segmentation_id, admin_state_up,
fixed_ips, device_owner, provisioning_needed):
port_needs_binding = True
if not vif_port.ofport:
# Log an error if the VIF port has no ofport, which indicates
# that the port might not be able to transmit traffic.
LOG.error("VIF port: %s has no ofport and might not "
"be able to transmit.", vif_port.vif_id)
elif vif_port.ofport == ovs_lib.INVALID_OFPORT:
# When the ofport is set to INVALID_OFPORT, it indicates that
# the port is in a transitional state and has not yet been fully
# configured.
LOG.info("VIF port: %s is in a transitional state and has not "
"yet been assigned a valid ofport. This is expected "
"during port initialization. (ofport=%s)",
vif_port.vif_id, vif_port.ofport)
if not self._has_valid_ofport(vif_port):
LOG.warning("VIF port: %s has no valid ofport (ofport=%s), "
"skipping OF operations; the port will be "
"retried on the next iteration.",
vif_port.vif_id, vif_port.ofport)
return False
if admin_state_up:
port_needs_binding = self.port_bound(
vif_port, network_id, network_type,
Expand Down
Loading