OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

neutron — Async processing

Processing outside the API — neutron — branch stable/2026.2

Branch stable/2026.2 @ eef1a0f7ee36 — repository https://opendev.org/openstack/neutron — analysed on 2026-10-28

Scope: the ML2/OVN reference profile of 01-rest-api_stable-2026.2.md. This file covers the server-side processes (API workers, RPC workers, OVN maintenance worker, periodic workers). The OVN metadata agent is in § 5. The OVS, Linux bridge, DHCP, L3 and metering agents are out of scope.

1. Processes and where the processing runs

ProcessEntry pointWhat it runs outside HTTP
API workers (uWSGI neutron-api, or neutron-server)neutron/wsgi/api.py, neutron/cmd/server/one OVN NB IDL with events and one OVN SB IDL with events per worker; events are dispatched by the hash ring (§ 2); the hash-ring heartbeat thread
neutron-rpc-serverneutron.cmd.server:main_rpcAMQP consumers of topics q-plugin, q-reports-plugin, q-server-resource-versions (neutron/plugins/ml2/plugin.py:452-469); OVN IDLs (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:351-354). Can be 0 workers with OVN: [DEFAULT]/rpc_workers = 0 disables all agent RPC (neutron/plugins/ml2/plugin.py:436-445)
neutron-ovn-maintenance-workerneutron.cmd.server:main_ovn_maintenance → neutron/server/ovn_maintenance.py:24start-up DB sync NB and SB (§ 4), then the maintenance thread (§ 3); holds the OVSDB lock ovn_db_inconsistencies_periodics
neutron-ovn-metadata-agent (deprecated) or neutron-ovn-agent with the metadata extension, on every compute nodeneutron/cmd/agents/ovn_metadata.py, neutron/agent/ovn/extensions/metadata.py:93SB OVSDB events, heartbeat, metadata proxy (§ 5)
neutron-periodic-workersneutron.cmd.server:main_periodic → neutron/service.py:317the plugins' other workers (quota cleanup…), grouped in one AllServicesNeutronWorker; the OVN maintenance worker is excluded (neutron/service.py:290-314)

The OVN driver connects its IDLs in every worker class listed above after the fork (post_fork_initialize, neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:399-457). API workers also register in the hash ring (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:356-397); the maintenance worker runs the DB sync and starts the maintenance thread (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:438-474).

2. Summary of triggers

ProcessTriggerTypePurposeReadsWrites / emitsDefined at
API workerLogicalSwitchPortCreateEventOVSDB event NB Logical_Switch_Port CREATEreport a port that is already up when createdLSP up, enabledport status, provisioning blocks, Nova eventneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:618
API workerLogicalSwitchPortUpdateUpEventOVSDB event NB LSP UPDATE, up or enabled → truethe VM's interface came upLSPports.status ACTIVE via provisioning, network-vif-plugged to Nova, NAT external_macneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:639
API workerLogicalSwitchPortUpdateDownEventOVSDB event NB LSP UPDATE, up or enabled → falsethe interface went downLSPports.status DOWN, network-vif-unpluggedneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:673
API workerLogicalSwitchPortUpdateLogicalRouterPortEventOVSDB event NB LSP UPDATE of router portspropagate up to the sibling LRP statusLSProuter port statusneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:708
API workerFIPAddDeleteEventOVSDB event NB NAT CREATE/DELETE, or UPDATE of external_macflush stale ARP entries of a floating IPNATSB MAC_Binding rows deleted (ovsdb-client transact)neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:835
API workerFIPAddExternalMacEventOVSDB event NB NAT CREATEset external_mac of a distributed FIP when the port is already upNAT, LSPNAT external_macneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:873
API workerHAChassisGroupRouterEventOVSDB event NB HA_Chassis_Group UPDATEre-point networks' HA groups when a router's gateway chassis list changesHCGnetwork HCGsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:916
API workerChassisEventOVSDB event SB Chassis CREATE/DELETE, UPDATE of bridge mappings, gateway flag or AZskeep segment-host mappings and gateway scheduling rightChassis other_configsegmenthostmappings, gateway LRP rescheduling, HA groupsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:49
API workerPortBindingChassisEventOVSDB event SB Port_Binding UPDATE of a chassisredirect portrecord which host carries a router gatewayPort_Binding chassisgateway port binding:host_idneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:559
API workerPortBindingChassisUpdateEventOVSDB event SB Port_Binding UPDATE (chassis change while up, live migration)finish a live migration seen by OVNPort_Binding, LSPport binding, NB nb_cfg bumpneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:274
API workerPortBindingUpdateVirtualPortsEventOVSDB event SB Port_Binding UPDATE/DELETE of virtual portstrack the host of a VRRP/virtual IP ownerPort_Bindingvirtual port binding:host_idneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:770
API workerChassisAgentWriteEvent, ChassisOVNAgentWriteEvent, ChassisAgentDownEvent, ChassisAgentDeleteEvent, ChassisAgentTypeChangeEventOVSDB events SB Chassis_Private (create, nb_cfg update, delete), SB_Global.external_ids:delete_agent, Chassis type changemaintain the in-memory agent cache that answers GET /agentsChassis, Chassis_PrivateAgentCache (memory only)neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:387-557
API workerChassisBandwidthConfigEventOVSDB event SB Chassis CREATE/UPDATEreport the chassis' bandwidth inventories to PlacementChassis other_configPlacement RESTneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py:110
API workertouch_hash_ring_nodetimer, every HASH_RING_TOUCH_INTERVAL = 15 sheartbeat of this worker in the hash ringovn_hash_ringUPDATE ovn_hash_ring.updated_atneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1611
maintenancecheck_for_inconsistenciesSQL polling every 300 s, at start-up, OVSDB lockre-apply to OVN whatever failed after a commitovn_revision_numbers ⋈ standardattributesOVN NB, ovn_revision_numbersneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:488
maintenance27 one-shot and daily taskstimer 5 s until done (NeverAgain), or 86 400 sdata migrations and NB settingsNB, DBNB, DBneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:610-1534
maintenanceOvnNbSynchronizer.do_sync, OvnSbSynchronizer.do_synconce at start-upcompare the Neutron DB with NB/SB; log or repairallNB, segmenthostmappingsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py:109, 1835
anyAFTER_CREATE/AFTER_UPDATE/AFTER_DELETE callbacks of routers, router interfaces, floating IPs, port forwardings, security groups, segmentsin-process registry callbackswrite the L3 and SG objects to OVN after the DB commitDBNBneutron/services/ovn_l3/service_providers/ovn.py:46-252, neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:277-335
anyPROVISIONING_COMPLETEin-process callbackset the port ACTIVE once every provisioning entity is doneprovisioningblocksports.statusneutron/plugins/ml2/plugin.py:370
anyNova notifier batchtimer [DEFAULT]/send_events_interval (2 s)send queued port events to NovaqueuePOST /os-server-external-eventsneutron/notifiers/nova.py:79-80, 277
RPC workerq-plugin, q-reports-plugin, q-server-resource-versionsAMQP consumersserve non-OVN agents (SR-IOV, DHCP…); OVN agents do not use RPC——neutron/plugins/ml2/plugin.py:452-469

3. OVSDB events and the hash ring

How events arrive. Each worker keeps an in-memory replica of NB and SB through ovsdbapp's OvsdbIdl (monitor_cond, then pushed update2/update3 messages); these are **notifications pushed by ovsdb-server, not polling**. The NB IDL of API workers registers all tables (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1146-1150); the SB IDL registers Chassis_Private, Chassis, Encap, Port_Binding, Datapath_Binding and SB_Global.external_ids only (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1167-1175). Chassis, Port_Binding events are only watched after the first connection dump, to avoid replaying every row at start-up (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1177-1189).

Who handles an event. Every API worker receives every update, but only one handles it: the hash ring maps the row UUID to one live node of group mechanism_driver (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1079-1117). Nodes are rows of table ovn_hash_ring (one per API worker, keyed by a node UUID, with hostname, group_name, updated_at); a node is live when updated_at is younger than HASH_RING_NODES_TIMEOUT = 120 s (neutron/common/ovn/constants.py:327). The handling worker also touches its own node when its last heartbeat is older than 15 s. Events flagged GLOBAL (WaitForChassisNbCfgEvent) are delivered to all workers.

Concurrency and failure.

  • At start-up the first API worker of a host deletes the host's older nodes and each worker adds its own (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:385-397); on SIGTERM the node is removed (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:337-344).
  • An empty ring logs HashRing is empty, error: … and drops the event (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1085-1087). A worker whose heartbeat stalls keeps receiving events assigned to it until the timeout, then the ring rebalances: events in that window can be lost; the maintenance task and the start-up sync compensate.
  • The heartbeat thread logs CRITICAL The number of nodes in the Hash Ring (%d) is higher than the number of API workers when a host has more nodes than workers (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1623-1634).
  • Exceptions inside an event are logged and swallowed (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:1116-1117).
  • cleanup_old_hash_ring_nodes (daily) deletes nodes untouched for 5 days (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1074-1083).

3.1 Port up / down (set_port_status_up, set_port_status_down)

  • Trigger: NB Logical_Switch_Port.up (written by ovn-northd from SB Port_Binding.up, itself written by ovn-controller when it claims the interface) or enabled changes (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:653-705).
  • Purpose: tell Neutron and Nova that the VM's interface is wired.
  • Flow (up) (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1453-1507): 1. for a distributed floating IP on this port, set the NAT external_mac; 2. complete the L2 provisioning entity of the port, which fires PROVISIONING_COMPLETE when no other entity is pending, and ML2 sets ports.status = ACTIVE, which records the Nova event network-vif-plugged (neutron/plugins/ml2/plugin.py:370); 3. router ports are set ACTIVE directly; compute ports whose status did not change trigger notify_port_active_direct; 4. write the host to the LSP external_ids; 5. for external ports, re-sync the network's HA chassis group.
  • Flow (down) (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1509-1538): clear the NAT external_mac, re-insert the provisioning block, set DOWN, send network-vif-unplugged to Nova.
  • Idempotency: repeated events are harmless; a port deleted meanwhile is ignored (PortNotFound, DBReferenceError).
  • Cost: one SQL transaction per event plus one Nova event (batched).

3.2 Agent liveness (GET /agents)

OVN controllers have no RPC heartbeat. GET /agents first calls ping_all_chassis, which bumps NB_Global.nb_cfg at most every agent_down_time / 2 seconds (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1620-1638, 1715-1720). ovn-northd copies it to SB, every ovn-controller echoes it into its Chassis_Private.nb_cfg, and the ChassisAgentWriteEvent updates the cache. An agent is alive when its nb_cfg is at most one generation behind NB_Global.nb_cfg, or when its last update is younger than [DEFAULT]/agent_down_time (default 75 s) (neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:96-110). The cache is per worker and in memory: two API workers can answer differently right after a change [inferred].

4. OVN maintenance worker

4.1 check_for_inconsistencies — the consistency loop

  • Trigger: futurist periodic every DB_CONSISTENCY_CHECK_INTERVAL = 300 s, first run at start-up (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:488, neutron/common/ovn/constants.py:297); not configurable.
  • Purpose: OVN is written after the Neutron DB commit, so any failure (OVSDB down, timeout, bug) leaves OVN behind. Each resource carries a revision: standardattributes.revision_number in Neutron, external_ids:neutron:revision_number in OVN, and the last revision written to OVN in table ovn_revision_numbers. The loop re-applies every resource whose two numbers differ.
  • Data read (neutron/db/ovn_revision_numbers_db.py:224-263): sql -- create/update inconsistencies, older than 60 s, ordered by type SELECT r.* FROM ovn_revision_numbers r JOIN standardattributes s ON r.standard_attr_id = s.id WHERE s.created_at < now() - INTERVAL 60 SECOND AND r.revision_number != s.revision_number ORDER BY CASE r.resource_type WHEN 'security_groups' THEN 1 … END; -- deletions not applied to OVN: the FK was set to NULL by the delete SELECT * FROM ovn_revision_numbers WHERE standard_attr_id IS NULL ORDER BY …;
  • Flow (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:318-549): for each row, read the Neutron object and the OVN object (_resources_func_map, neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:240-297); if the OVN object is missing → ovn_create; if the OVN revision differs → ovn_update; if equal → only bump ovn_revision_numbers. Subnets and segments have their own fixers. Deletions call ovn_delete, or drop the revision row when OVN has nothing left. Types: networks, ports, floating IPs (with port forwardings), routers, router ports, security groups, SG rules, address groups, subnets, segments.
  • Revision guard: OVN writes include CheckRevisionNumberCommand, which refuses to overwrite an OVN row with an older revision (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py:1103); bump_revision never lowers ovn_revision_numbers (neutron/db/ovn_revision_numbers_db.py:183-221). A stale update raises RevisionConflict "OVN revision number for … is higher than the given resource. Skipping update" (neutron/common/ovn/exceptions.py:21).
  • Concurrency: only the process that holds the OVSDB lock ovn_db_inconsistencies_periodics on the NB connection runs the tasks (has_lock_periodic, neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:64-89; lock name neutron/common/ovn/constants.py:300; set at neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:414-415). With several maintenance workers, OVSDB elects one; failover is automatic when its connection drops.
  • Error behaviour: each resource is fixed in its own try; errors are logged (Maintenance task: Failed to fix resource …) and retried at the next run (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:523-549). A resource that always fails is retried forever.
  • Cost: two SQL queries per run plus one NB transaction per inconsistent resource; with a healthy OVN, nothing else.

4.2 One-shot and daily tasks

All use has_lock_periodic and the same thread pool of one worker (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:144-172). "One-shot" tasks run every MAINTENANCE_ONE_RUN_TASK_SPACING = 5 s while the lock is not held, up to MAINTENANCE_TASK_RETRY_LIMIT = 100 tries (neutron/common/ovn/constants.py:298-299), and stop with NeverAgain once they have done their job. They are upgrade data migrations of OVN NB content; a rewrite that creates NB rows in their final form does not need them, but must produce the same final form:

TaskWhat it ensures in NBDefined at
update_lrouter_ports_ext_ids_name_prefixLRP external_ids names carry the neutron- prefixneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:610
check_global_dhcp_opts[ovn]/ovn_dhcp4_global_options, ovn_dhcp6_global_options applied to every DHCP_Optionsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:648
check_for_igmp_snoop_supportmcast_snoop/mcast_flood_unregistered on every switchneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:679
check_localnet_port_has_learn_fdblocalnet ports learn FDB when [ovn]/localnet_learn_fdbneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:717
add_provnet_ext_id_to_localnet_portslocalnet ports carry the segment IDneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:749
check_redirect_type_router_gateway_portsgateway LRPs use redirect-type=bridged on VLAN when [ovn]/enable_distributed_floating_ipneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:798
check_provider_distributed_portsmetadata/distributed ports on provider networks have the right optionsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:872
check_fdb_aging_settings, update_mac_aging_settingsfdb_age_threshold on switches, mac_binding_age_threshold on routersneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:915, 957
check_baremetal_ports_dhcp_optionsPXE options of baremetal portsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:974
check_router_default_route_empty_dst_ipdefault routes have a valid destinationneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1025
check_fair_meter_consistencythe ACL log meter matches [network_log] rate/burstneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1058
configure_nb_global (daily)[ovn_nb_global] options copied into NB_Global.optionsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1088
update_router_distributed_flag (daily)router_extra_attributes.distributed mirrors enable_distributed_floating_ipneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1108
set_network_type_and_physnetswitches carry the network type and physnet in external_idsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1135
check_network_broadcast_arps_to_all_routersexternal switches broadcast ARPs per [ovn]/broadcast_arps_to_all_routersneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1175
set_fip_distributed_flagNAT rows record whether they are distributedneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1212
set_ovn_owned_dns_option, remove_stale_dns_recordsDNS rows owned by OVN and free of deleted portsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1237, 1263
update_ha_failoverHA chassis groups of routers have the failover optionsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1288
check_dhcp_options_consistencyeach subnet's DHCP_Options matches the DBneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1323
update_security_group_with_address_groupACLs of rules using address groups reference the address setsneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1364
update_virtual_port_parent_hostnamevirtual ports know their parent's hostneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1412
migrate_lrp_gateway_chassis_to_ha_chassis_groupgateway LRPs use HA_Chassis_Group instead of Gateway_Chassisneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1444
update_neutron_pg_drop_prioritythe drop ACLs of neutron_pg_drop have the expected priorityneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1497
check_pvlan_plugin_statusPVLAN port groups exist only when the plugin is loadedneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1534
cleanup_old_hash_ring_nodes (daily)hash ring rows untouched for 5 days deletedneutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:1074

Tasks marked rerun_on_schema_updates run again when the NB schema version changes (OVNNBDBReconnectionEvent, neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:181-226). Other service plugins can add periodics through ovn_maintenance_periodics() (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:466-472).

4.3 Start-up DB sync ([ovn]/neutron_sync_mode)

  • Trigger: once, in the maintenance worker before the maintenance thread starts (neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:438-452); also run by hand with neutron-ovn-db-sync-util (neutron/cmd/ovn/neutron_ovn_db_sync_util.py).
  • Modes (neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py:103-120): off; log (default) — report differences as warnings; repair — create missing NB objects and delete NB objects unknown to Neutron; migrate — used by the OVS→OVN migration.
  • NB order (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py:109-131): port groups, networks/ports/DHCP options (with metadata ports), DNS records, ACLs, PVLAN, routers and router ports (static routes, SNAT, FIPs, port forwardings), port QoS, FIP QoS, FIP DNAT rules, distributed NAT.
  • SB (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py:1835-1901): rebuild segmenthostmappings from each chassis' bridge mappings.
  • Cost: reads every network, port, router, FIP and SG of the cloud and the whole NB replica; minutes on large clouds; runs on every restart of the maintenance worker.

5. OVN metadata agent

5.1 Role and process

  • Purpose: give every VM the Nova metadata service at http://169.254.169.254 (and http://[fe80::a9fe:a9fe] for IPv6) without a router, on every compute node. For each network that has a VIF bound to the local chassis, the agent creates a namespace ovnmeta-<network id> wired to OVN through the network's metadata port (device_owner: network:distributed, LSP type: localport, created by the server, 01 § 4.1), runs one haproxy in it, and proxies the requests to nova-metadata-api after identifying the instance.
  • Processes: neutron-ovn-metadata-agent (neutron/cmd/agents/ovn_metadata.py, class MetadataAgent, neutron/agent/ovn/metadata/agent.py:335). It is deprecated: the start-up log says it "has been deprecated in 2025.2 and will be removed in 2026.2" (neutron/agent/ovn/metadata/agent.py:475-478), but it still ships in this branch. Its replacement is neutron-ovn-agent with the metadata extension, which inherits the same class (MetadataExtension(extension_manager.OVNAgentExtension, metadata_agent.MetadataAgent), neutron/agent/ovn/extensions/metadata.py:93-96) and therefore has the same behaviour, plus the SB/OVS connections shared with the other extensions (neutron/agent/ovn/extensions/metadata.py:194-212).
  • Connections (no AMQP, no SQL): the local OVS database (Open_vSwitch, Bridge, Port, Interface, neutron/agent/ovn/metadata/ovsdb.py:66-80) and the OVN SB database (Encap, Port_Binding, Datapath_Binding, SB_Global, Chassis, Chassis_Private, neutron/agent/ovn/metadata/agent.py:444-455), plus HTTP to Nova.
  • Identity: the chassis name is Open_vSwitch.external_ids:system-id, the bridge external_ids:ovn-bridge (default br-int) (neutron/agent/ovn/metadata/agent.py:491-514); the agent ID is uuid5(chassis_id, 'metadata_agent'), written in Chassis_Private.external_ids:neutron:ovn-metadata-id (neutron/agent/ovn/metadata/agent.py:482-489).

5.2 Triggers

TriggerTypePurposeReadsWrites / emitsDefined at
start-up sync(provision=False) then sync() on reconnectiononce / OVSDB Chassis_Private CREATE of the own chassis after the first dumpremove namespaces of networks without local VIFs; provision the otherssystem namespaces, SB Port_Binding on this chassisnamespaces, veth, OVS ports, haproxyneutron/agent/ovn/metadata/agent.py:525-557, 268-295
PortBindingCreateWithChassisOVSDB SB Port_Binding CREATE already bound to this chassisprovision the port's networkPort_Bindingprovision_datapathneutron/agent/ovn/metadata/agent.py:117-124
PortBindingUpdatedEventOVSDB SB Port_Binding UPDATE: chassis set to / removed from this chassis, live-migration additional_chassis added/removed, metadata localport neutron:cidrs changed(un)provision the networkPort_Bindingprovision_datapath (which tears down when nothing is left)neutron/agent/ovn/metadata/agent.py:127-244
PortBindingDeletedEventOVSDB SB Port_Binding DELETE of a row bound to this chassisre-evaluate the networkPort_Bindingprovision_datapath / teardownneutron/agent/ovn/metadata/agent.py:247-265
SbGlobalUpdateEventOVSDB SB SB_Global UPDATE (nb_cfg bumped by the server's liveness ping)heartbeat: prove the agent is aliveSB_Global.nb_cfgChassis_Private.external_ids:neutron:ovn-metadata-sb-cfg = <nb_cfg> after a random delay of 0 to max(min(agent_down_time // 3, 10), 3) secondsneutron/agent/ovn/metadata/agent.py:298-332
HTTP request from a VMhaproxy → Unix socket [DEFAULT]/metadata_proxy_socket ($state_path/metadata_proxy)serve metadataSB Port_BindingHTTP to Novaneutron/agent/ovn/metadata/server_socket.py:27-94

Every event handler runs under one global re-entrant lock shared with sync, so provisioning is serialised per agent (neutron/agent/ovn/metadata/agent.py:45, 61-67, 100-114). A ConfigException (the configured OVN bridge does not exist) triggers a full resync (neutron/agent/ovn/metadata/agent.py:107-114, 428-437).

5.3 provision_datapath — wiring a network

(neutron/agent/ovn/metadata/agent.py:690-866)

  1. Find the network's metadata port: the SB Port_Binding of type localport with external_ids:neutron:device_owner = network:distributed on the datapath (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py:1095-1106). No port, or no MAC → tear down and stop (the namespace disappears until the server creates the port).
  2. Collect the IPv4 addresses of the VIFs of this network bound to this chassis (types "", external, localport); none and no IPv6 → tear down (neutron/agent/ovn/metadata/agent.py:735-750).
  3. Create the veth pair tap<first 10 chars of the datapath name>0 / …1 with the inner end in ovnmeta-<network id>, set the metadata port's MAC and the network MTU (neutron:mtu from the port's external_ids), bring both ends up (neutron/agent/ovn/metadata/agent.py:776-804).
  4. Addresses inside the namespace: 169.254.169.254/32, plus fe80::a9fe:a9fe/128 and the link-local address derived from the MAC when IPv6 is used, plus the metadata port's IP in each subnet that has a local VIF; addresses no longer needed are removed (neutron/agent/ovn/metadata/agent.py:645-688, 806-823).
  5. Plug the outer end into the OVN bridge with Interface.external_ids:iface-id = <metadata port id> (removing it from any other bridge), which makes ovn-controller bind the localport (neutron/agent/ovn/metadata/agent.py:828-850); on DPDK (datapath_type = netdev) add an iptables CHECKSUM --checksum-fill rule in the namespace (neutron/agent/ovn/metadata/agent.py:600-617).
  6. Start (or keep) a monitored haproxy in the namespace, listening on 169.254.169.254:80 (and [fe80::a9fe:a9fe]:80 on the veth) and forwarding to the Unix socket with the header X-OVN-Network-ID: <network id> (neutron/agent/ovn/metadata/agent.py:857-866, neutron/agent/ovn/metadata/driver.py:18-31, neutron/agent/metadata/driver_base.py:42-46); haproxy runs as [DEFAULT]/metadata_proxy_user/group, its configuration lives in $state_path/ovn-metadata-proxy/ and the process monitor respawns it (neutron/agent/metadata/driver_base.py:259-335).

Teardown (neutron/agent/ovn/metadata/agent.py:575-598): stop haproxy, delete the OVS port and the veth, delete the namespace.

Privileges: namespace, veth, address and iptables operations go through ip_lib / iptables_manager, which use oslo.privsep (neutron/privileged/) and rootwrap; haproxy is started as root and drops to the configured user [inferred].

Cost: one SB lookup and a few netlink calls per event; sync lists every namespace and every port of the chassis.

5.4 Serving a metadata request

(neutron/agent/metadata/proxy_base.py:121-251, neutron/agent/ovn/metadata/server_socket.py:43-63)

  1. haproxy forwards the VM's request with X-Forwarded-For: <VM IP> and X-OVN-Network-ID.
  2. The handler looks for the SB Port_Binding of that network whose mac column contains the IP (or, for an IPv6 link-local source, the MAC derived from it) and that is bound to a chassis (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py:1116-1150). It reads neutron:device_id (instance UUID) and neutron:project_id from its external_ids. **This lists every Port_Binding row of the local SB replica for each request**: the cost grows with the cloud size.
  3. Exactly one port → forward to [DEFAULT]/nova_metadata_protocol://nova_metadata_host:nova_metadata_port (default http://127.0.0.1:8775) with the original path and query, headers X-Forwarded-For, X-Instance-ID, X-Tenant-ID and X-Instance-ID-Signature = HMAC-SHA256(metadata_proxy_shared_secret, instance_id) in hex (neutron/common/utils.py:1090-1094), optional client certificate (nova_client_cert, nova_client_priv_key) and CA (auth_ca_cert), timeout=60 s.
  4. Responses: Nova 200 → body and content type returned; 403 → 403 with WARNING "…shared secrets do not match"; 500 → 500; 400, 404, 409, 502, 503, 504 → same status without body; connection error → 503 "The remote metadata server is temporarily unavailable. Please try again later."; any other status → RuntimeError → 500 "An unknown error has occurred. Please try your request again.". Zero matching ports → 404 (WARNING No port found in network %s with IP address %s); several → 404 and ERROR asking to run neutron-ovn-db-sync-util.
  5. The Unix server handles requests in a thread pool of [DEFAULT]/metadata_workers threads; with ML2/OVN the option defaults to 0, and requests are then handled one at a time in the server's own thread (socketserver.UnixStreamServer.process_request), so a slow Nova blocks all VMs of the node for up to 60 s per request [inferred from the code; set metadata_workers > 0 in production]. Listen backlog: metadata_backlog (4096) a listen backlog of metadata_backlog (4096) (neutron/agent/metadata/proxy_base.py:44-79, neutron/agent/ovn/metadata/server_socket.py:75-86). The socket mode follows metadata_proxy_socket_mode (neutron/agent/metadata/proxy_base.py:259-275). No cache (has_cache=False).

5.5 Liveness as seen by GET /agents

The server lists the agent as OVN Metadata agent from Chassis_Private.external_ids:neutron:ovn-metadata-id; it is alive when neutron:ovn-metadata-sb-cfg follows NB_Global.nb_cfg (neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:182-215, 05 § 3.2). The random delay of the heartbeat spreads the SB writes of thousands of nodes.

6. Notifications to Nova and Placement

6.1 Nova external events

  • Trigger: port status changes recorded by ML2 (record_port_status_changed, neutron/notifiers/nova.py:209-246), the BEFORE_RESPONSE callback of port and floating IP API calls (neutron/notifiers/nova.py:124-154), and notify_port_active_direct (neutron/notifiers/nova.py:253-269). Only ports whose device_owner starts with compute: and only with [DEFAULT]/notify_nova_on_port_status_changes / notify_nova_on_port_data_changes (both default True).
  • Events: network-vif-plugged (DOWN/unknown → ACTIVE, status completed), network-vif-unplugged (ACTIVE → DOWN), network-vif-deleted (port deleted), network-changed (fixed IPs or floating IP changed) (neutron/notifiers/nova.py:43-45, 112-121).
  • Delivery: queued in a BatchNotifier and sent every [DEFAULT]/send_events_interval seconds (2) as one POST /v2.1/os-server-external-events (neutron/notifiers/nova.py:79-80, 277-300), with Keystone credentials from [nova]. Errors are logged; events are not re-queued. See 03.

6.2 Placement

  • ChassisBandwidthConfigEvent (SB Chassis create/update) reads ovn-cms-options resource_provider_bandwidths, resource_provider_inventory_defaults, resource_provider_hypervisors and pushes resource providers, inventories and traits to Placement through the placement service plugin's batch notifier (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py:110-140, neutron/services/placement_report/plugin.py:58-202). Used by QoS minimum bandwidth.

7. Router gateway scheduling (OVN L3)

OVN routers have no L3 agent. A router's gateway port (the LRP on the external network) must run on one "gateway chassis" at a time, with standby chassis for failover; OVN does the failover itself (BFD between chassis), Neutron only chooses the chassis and their priorities.

7.1 Choosing the chassis when a gateway is added

  • Trigger: creation of the gateway LRP in _create_lrouter_port (router create with external_gateway_info, PUT /routers/{id} setting the gateway, add_external_gateways) (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:1996-2056).
  • External network without physical network (Geneve external network): no scheduling by physnet; the router is pinned through the HA chassis group of the router and Logical_Router.options:chassis (gateway-router mode) (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:2030-2041).
  • External network on a physnet (flat/VLAN, the usual case): ScheduleNewGatewayCommand runs inside the same NB transaction (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py:558-591):
    1. Candidates (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:1807-1847): SB chassis whose other_config:ovn-cms-options contains enable-chassis-as-gw and whose ovn-bridge-mappings contain the physnet; if the router has availability_zone_hints, only chassis whose ovn-cms-options availability-zones= list one of them.
    2. Selection by [ovn]/ovn_l3_scheduler (leastloaded by default, or chance) (neutron/scheduler/l3_ovn_scheduler.py:271-272), at most MAX_GW_CHASSIS = 5 chassis (neutron/common/ovn/constants.py:333).
    3. Priorities: the selected list is turned into priorities from 32 767 (HA_CHASSIS_GROUP_HIGHEST_PRIORITY) downwards and written as an HA_Chassis_Group named after the router, referenced by the LRP's ha_chassis_group (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py:580-591, neutron/common/ovn/constants.py:460-461). The highest-priority live chassis carries the gateway.
  • **leastloaded algorithm** (neutron/scheduler/l3_ovn_scheduler.py:165-263), to be reproduced by a rewrite that wants the same placement: bindings = for each candidate chassis: list of (LRP name, priority) it already hosts if the target router already has LRPs on some chassis: anti_affinity = MAX_GW_CHASSIS * number of LRPs of the router # avoid putting two # gateways of one router on one chassis for priority from highest to lowest (one slot per priority, up to 5): load(c) = number of LRPs hosted by c at exactly this priority (+ anti_affinity if c already hosts an LRP of this router) keep the chassis with the minimum load, excluding chassis already chosen tie-break: prefer chassis with MORE gateways at lower priorities (they are standby elsewhere), then hash(sha512("<gateway name>_<priority>")) XOR hash(chassis name) finally reorder the list so that the first chassis of each availability zone comes first The final tie-break uses Python's hash() of the chassis name, which is randomised per process (PYTHONHASHSEED) (neutron/scheduler/l3_ovn_scheduler.py:244-257): two API workers can break the same tie differently. A rewrite does not need to reproduce the exact tie-break, only the load balancing.
  • **chance**: random shuffle, then the same AZ reordering (neutron/scheduler/l3_ovn_scheduler.py:139-153).
  • No candidate: WARNING Gateway %s was not scheduled on any chassis, no candidates are available; the router exists but has no external connectivity until a chassis appears (neutron/scheduler/l3_ovn_scheduler.py:71-74).

7.2 Rescheduling

  • Trigger: SB Chassis events — a chassis appears, disappears, changes its bridge mappings, its gateway flag or its AZs (ChassisEvent, neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:162-223) → schedule_unhosted_gateways (neutron/services/ovn_l3/plugin.py:347-370).
  • What: for each gateway LRP (restricted to the HA groups that contained the removed chassis when the event is a removal), drop chassis that are no longer valid (not a gateway, physnet not mapped, AZ no longer matching), keep the remaining order, and fill up to 5 with new candidates using the same scheduler (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py:493-555, neutron/services/ovn_l3/plugin.py:372-392). Existing primaries are not moved when they stay valid, so adding a gateway chassis does not rebalance existing routers.
  • Concurrency: runs in the API worker chosen by the hash ring for the Chassis row; all LRPs are rescheduled in one NB transaction.
  • Cost: reads every gateway LRP and its HA group from the local NB replica, then one transaction.

7.3 Which host reports the gateway

PortBindingChassisEvent (05 § 2) writes the host of the active chassisredirect port binding as binding:host_id of the router gateway port, so GET /ports?device_owner=network:router_gateway and GET /routers/{id}/l3-agents show where the gateway is active (neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:598-608, neutron/services/ovn_l3/plugin.py:315-345, 500-518).

7.4 Networks attached to a router (external ports, VLAN)

When a router gains or loses its gateway, each internal network gets the router's HA chassis group so that external ports (SR-IOV, baremetal) and VLAN tenant networks are served by the same chassis as the gateway (update_router_ha_chassis_group, neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:1770-1795). HAChassisGroupRouterEvent keeps them aligned when the router's chassis list changes (05 § 2).

Source: reverse-spec-neutron/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.