Processing outside the API — neutron-vpnaas — branch stable/2026.2
Branch
stable/2026.2@6ce5da102cf1— repository https://opendev.org/openstack/neutron-vpnaas — analysed on 2026-10-29.neutron-lib:= neutron-libstable/2026.2@1ee72052251a;neutron:= neutronstable/2026.2@eef1a0f7ee36.
The API only records intent (PENDING_CREATE). Everything that makes a tunnel exist, and every status change, happens here: RPC between the service driver (in neutron-server) and the agents, timers on the agents and on the server, OVSDB events, and Neutron in-process callbacks.
Overview
| # | Trigger | Family | Process | Profile | Sheet |
|---|---|---|---|---|---|
| 1 | vpnservice_updated | AMQP cast, topic ipsec_agent, server = agent host | agent (device driver) | both | § 1 |
| 2 | get_vpn_services_on_host | AMQP call, topic ipsec_driver | neutron-server (service driver callback) | both | § 2 |
| 3 | update_status | AMQP call, topic ipsec_driver | neutron-server | both | § 3 |
| 4 | get_vpn_transit_network_details, get_subnet_info | AMQP call, topic ipsec_driver | neutron-server | OVN | § 4 |
| 5 | vpnservice_added_to_agent, vpnservice_removed_from_agent, agent_updated | AMQP call/cast, topic ipsec_agent, server = agent host | OVN VPN agent | OVN | § 5 |
| 6 | report_status every [ipsec] ipsec_status_check_interval (60 s) | timer + process state polling + cache | agent | both | § 6 |
| 7 | check_vpn_agents every max(agent_down_time // 2, 1) s | periodic (futurist, OVN maintenance worker, DB lock) | neutron-server | OVN, if allow_automatic_vpnagent_failover | § 7 |
| 8 | ChassisVPNAgentWriteEvent (Chassis_Private create/update) | OVSDB event (SB) | neutron-server, every worker | OVN | § 8 |
| 9 | SbGlobalUpdateEvent (SB_Global update) | OVSDB event (SB) | OVN VPN agent | OVN | § 9 |
| 10 | ChassisPrivateCreateEvent (own Chassis_Private created) | OVSDB event (SB) | OVN VPN agent | OVN | § 10 |
| 11 | L3 agent extension hooks add_router, update_router, delete_router, ha_state_change | in-process calls from the L3 agent | L3 agent | legacy | § 11 |
| 12 | Neutron callbacks (router gateway/interface/update, subnet/port/network delete, router precommit update) | neutron_lib.callbacks registry | neutron-server | both / OVN | § 12 |
| 13 | Agent start-up: initial sync, agent registration | process start | agents | both | § 13 |
RPC topology
Topic (exchange neutron, the control_exchange) | Server side | Created at | Client side |
|---|---|---|---|
ipsec_driver (no server, shared queue) | IPsecVpnDriverCallBack (legacy) / IPsecVpnOvnDriverCallBack (OVN) in neutron-server | legacy: driver __init__ → create_rpc_conn (neutron_vpnaas/services/vpn/service_drivers/ipsec.py:34-39), in every API worker; OVN: start_rpc_listeners (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:510-515), i.e. only in Neutron's RPC workers (neutron_vpnaas/services/vpn/plugin.py:80-85) | agents, IPsecVpnDriverApi (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:429-455), IPsecOvnDriverApi (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:206-219) |
ipsec_agent.<host> | the device driver object itself (endpoints = [self]) in the agent (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:478-487) | agent start | neutron-server: IPsecVpnAgentApi / IPsecOvnVpnAgentApi (prepare(server=host) on topic ipsec_agent), VPNAgentNotifyAPI (neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:26-52) |
Connection.create_consumer(topic, endpoints, fanout=False) builds Target(topic=topic, server=cfg.CONF.host) (neutron-lib:neutron_lib/rpc.py:346-350). The agent therefore listens on topic ipsec_agent.<host> (and ipsec_agent.<host>.<host>); the server's prepare(server=<host>) on topic ipsec_agent routes to queue ipsec_agent.<host>, which matches. All targets have version 1.0. Because the endpoint is the whole device-driver object, every public method of the driver is callable over RPC (sync, create_router, destroy_router, report_status…), not only the three that the server uses.
1. vpnservice_updated — agent consumer
- Trigger: AMQP cast, topic
ipsec_agent, server<agent host>, version 1.0, methodvpnservice_updated, args{"router": {"id": "<router_id>"}}(neutron_vpnaas/services/vpn/service_drivers/__init__.py:106-110). - Emitter: the service driver after
PUT/DELETE vpnserviceandPOST/PUT/DELETE ipsec-site-connection(neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:117-131,neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:185-189); not afterPOST vpnservice. Target hosts: legacy → every L3 agent hosting the router withadmin_state_upand alive (neutron_vpnaas/services/vpn/service_drivers/__init__.py:92-104); OVN → every live VPN agent bound to the router inroutervpnagentbindings(neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:488-500). An HA router with two L3 agents gets two casts. Observed frames:_rewrite/rpc-frames_stable-2026.2.json. - Functional purpose: tell the agent "the VPN configuration of this router changed, re-read it".
- Flow (
neutron_vpnaas/services/vpn/device_drivers/ipsec.py:583-591): callssync(context, [router]), see § 13 forsyncitself: oneget_vpn_services_on_hostcall, then reconciliation of the local daemons, thenreport_status. - Concurrency and idempotency:
syncholds the process-wide lockneutron-vpn-agent(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:815); messages are idempotent (they carry no data, the agent pulls the whole state). A lost cast is repaired by the next event or agent restart only: there is no periodic full resync on the agents (the status timer does not call the server for configuration) [inferred from the absence of any periodic call toget_vpn_services_on_host]. - Error behaviour: exceptions inside
syncare raised in the RPC dispatcher and logged by oslo.messaging; nothing is retried. - Cost: 1 cast per hosting agent per API change, then 1
callback to the server.
2. get_vpn_services_on_host — server consumer
- Trigger: AMQP call, topic
ipsec_driver, args{"host": "<agent host>"}(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:438-445). - Emitter: every agent
sync(§ 13), i.e. on everyvpnservice_updated, router event, agent start and OVN reconnection. - Functional purpose: return the complete VPN configuration the agent must run: services with their connections, IKE/IPsec policies, local and peer CIDRs, and the tunnel IP.
- Data read:
- legacy (
neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:46-84): the L3 agent of that host (typeL3 agent); returns nothing if it is disabled or indvrmode (VPN runs ondvr_snat/legacynodes only). ThenSELECT vpnservices.* FROM vpnservices JOIN ipsec_site_connections ON … JOIN routerl3agentbindings ON routerl3agentbindings.router_id = vpnservices.router_id JOIN providerresourceassociations ON resource_id = vpnservices.id WHERE routerl3agentbindings.l3_agent_id = :agent AND providerresourceassociations.provider_name = :driver_name; - OVN (
neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:94-112): theVPN Agentof that host from the agent list; **ifvpn_auto_schedule(default true) it first auto-schedules every router whose VPN services have no agent** to this agent (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:338-353,neutron_vpnaas/scheduler/vpn_agent_scheduler.py:68-84); then the same join onroutervpnagentbindings.vpn_agent_idinstead of the L3 binding, without the provider filter; - plus the map
subnet id → cidrof every subnet used in any local endpoint group (neutron_vpnaas/db/vpn/vpn_db.py:718-731, a query over all connections, not only this host's). Services without connections are not returned (inner join).
- legacy (
- Reply: a list built by
make_vpnservice_dict(neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:203-267): the service columns,subnet({"cidr": …}built from the first local endpoint for endpoint-group services, for older agents),external_ip, andipsec_site_connections[]with all connection columns (**includingpsk**), nestedikepolicyandipsecpolicyrows (DB column names:lifetime_units,lifetime_value),peer_cidrs,local_cidrs,local_ip_vers, andexternal_ipchosen by the peer's IP version. Whenpeer_id(andlocal_id) are not IP addresses, they are prefixed with@so that strongSwan/Libreswan treat them as FQDN identities (neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:226-235). A recorded reply is in_rewrite/exchanges-ovn_stable-2026.2.json, steprpc-get_vpn_services_on_host. - Concurrency:
@db_api.CONTEXT_WRITER(a write transaction, because of the auto-scheduling) (neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:77). - Error behaviour: an exception is returned to the agent as a
RemoteError; the agent'ssyncfails and the configuration is not applied. - Cost: one join per call + one query over all local endpoints; on OVN possibly scheduling writes.
3. update_status — server consumer
- Trigger: AMQP call, topic
ipsec_driver, args{"status": [ {"id": <vpnservice_id>, "status": "ACTIVE|DOWN|ERROR", "updated_pending_status": bool, "ipsec_site_connections": {<conn_id>: {"status": …, "updated_pending_status": bool}}} ]}(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:448-455). - Emitter: the agent's
report_status(§ 6), only when something changed. - Functional purpose: move services and connections out of
PENDING_CREATEand keepACTIVE/DOWNin sync with the real tunnels. - Flow (
neutron_vpnaas/db/vpn/vpn_db.py:733-768), in one write transaction:- for each service: read it; deleted →
LOG.warning, skip; if it is not pending, or the agent setupdated_pending_status, writestatus; - for each connection: read it; deleted → skip silently; same pending rule (
neutron_vpnaas/db/vpn/vpn_db.py:285-298).
- for each service: read it; deleted →
- Reply:
null. - Concurrency and idempotency: idempotent (absolute values). Two agents reporting the same service (HA router, both L3 agents) could flip the status [inferred]; the legacy agent skips routers in HA
backupstate to avoid this (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:765-772). - Cost: 1 + N SELECT/UPDATE per service.
4. get_vpn_transit_network_details, get_subnet_info — server consumers (OVN)
- Trigger: AMQP call, topic
ipsec_driver, args{"router_id"}/{"subnet_id"}, sent with an admin context (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:206-219). - Emitter: OVN VPN agent, for each router to (re)configure (
neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:313-318), and for each subnet of the gateway port (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:143-145). - Functional purpose: give the agent what it needs to build the
qvpn-<router_id>namespace: the VPN gateway port (MAC, fixed IPs on the external network), the transit port (169.254.0.2), the transit gateway IP169.254.0.1and the external network (MTU) (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:75-86); and a subnet's CIDR and gateway (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:88-92,Nonewhen not found). - Reply format:
gw_portandtransit_portare **SQLAlchemyPortrows**, serialised by oslo.messaging's JSON encoder into the full ORM dict (fixed_ips,port_bindings,standard_attr,security_groups…), not Neutron's API port dict (recorded in_rewrite/rpc-frames_stable-2026.2.json). A re-implementation must reproduce at leastid,mac_address,network_idandfixed_ips[].{ip_address, subnet_id}, which are the fields the agent reads (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:120-179). - Error behaviour: no VPN gateway for the router →
AttributeErroronNone.gw_port(neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:76-77) →RemoteErroron the agent, sync aborted [inferred].
5. Agent scheduling notifications (OVN)
| Method | Mode | Args | Emitted when | Agent handler |
|---|---|---|---|---|
vpnservice_added_to_agent | call, retried ×2 (neutron:neutron/api/rpc/agentnotifiers/utils.py:54) | {"router_ids": [...]} | manual POST /agents/{id}/vpn-routers (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:206-210); rescheduling to another host (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:278-284) | sync(context, [{"id": r} …]) (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:335-337) |
vpnservice_removed_from_agent | cast | {"router_id"} | manual DELETE …/vpn-routers/{id}; rescheduling (old host) | destroy_process(router_id) under the neutron-vpn-agent lock: stop the daemon, unplug the ports, delete qvpn-<router_id> (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:331-333, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:263-276) |
agent_updated | cast | {"payload": {"admin_state_up": bool}} | never by neutron-vpnaas; Neutron calls agent_notifiers[type].agent_updated when an agent's admin state changes, which ML2/OVN refuses for OVN agents (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1738-1740) | no handler in the device drivers [inferred: the agent would answer NoSuchMethod] |
The notifier is registered only by the OVN plugin (neutron_vpnaas/services/vpn/ovn_plugin.py:46-47); after any binding change the plugin rebinds the VPN gateway and namespace ports to the new host with two update_port calls (neutron_vpnaas/services/vpn/ovn_plugin.py:78-84, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:459-473), which makes ML2/OVN move the ports' chassis.
6. report_status — agent timer (status reporting)
- Trigger:
FixedIntervalLoopingCall(report_status)started in the device driver constructor, interval[ipsec] ipsec_status_check_interval(default 60 s), first run immediately (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:489-492, optionneutron_vpnaas/services/vpn/device_drivers/ipsec.py:52-54). Also called at the end of everysync(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:853). - Emitter: none (local timer).
- Functional purpose: detect that a tunnel came up or went down and tell the server.
- Data read (process state polling + in-memory cache):
- for every router with a VPN process (
self.processes), skipping HA routers inbackupstate and services of other projects when the context is not admin (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:760-775); - one status command per router, captured once per cycle so that the reported and cached values agree (
neutron_vpnaas/services/vpn/device_drivers/ipsec.py:688-707):- strongSwan:
ip netns exec <ns> neutron-vpn-netns-wrapper --mount_paths=/etc:<cfg>/etc,<piddir>:<cfg>/var/run --cmd=ipsec,status(neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:147-163,neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:196-198); a line<conn-id>… INSTALLED=ACTIVE,ROUTEDorCONNECTING=DOWN(neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:68-74); - Libreswan:
… --cmd=ipsec,whack,--status; a lineNNN #n: "<conn-id>… established … newest=ACTIVE,"<conn-id>"… unrouted|erouted;without SA =DOWN(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:170-179,neutron_vpnaas/services/vpn/device_drivers/ipsec.py:378-401); - exit code 1 or 3 (
Command: … ipsec … status … Exit code: 1|3) = daemon not running → no connection (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:175,neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:74);
- strongSwan:
- the service is
ACTIVEif the status output contains at least one connection, elseDOWN(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:296-313); - compared with
process_status_cache[router_id](neutron_vpnaas/services/vpn/device_drivers/ipsec.py:656-663).
- for every router with a VPN process (
- Flow:
- for each process: if the process has a pending flag, or the service status or any connection status differs from the cache → build the report entry (
neutron_vpnaas/services/vpn/device_drivers/ipsec.py:665-681,neutron_vpnaas/services/vpn/device_drivers/ipsec.py:709-736); on the first report after start (cache statusNone) every connection is flaggedupdated_pending_status=Trueso that the server accepts it even if pending (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:795-798); - connections present in the cache but no longer in the daemon output are reported
DOWNwith the pending flag (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:738-752); - update the cache, clear the pending flags (
neutron_vpnaas/services/vpn/device_drivers/ipsec.py:801-806); - if anything changed: one
call update_statuswith all entries (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:808-811), see § 3.
- for each process: if the process has a pending flag, or the service status or any connection status differs from the cache → build the report entry (
- Concurrency and idempotency: runs in its own green thread, without the
neutron-vpn-agentlock used bysync; it iterates over a snapshot (list(self.processes.items())) [inferred: a status read can overlap a reconfiguration]. The cache lives in memory: after an agent restart the first report re-sends everything. - Error behaviour: a failing status command marks the router
DOWN(RuntimeErrorcaught,neutron_vpnaas/services/vpn/device_drivers/ipsec.py:306-312); an RPC failure raises out of the looping call iteration (logged by oslo.service), and since the cache was updated first, the lost report is not repeated (02 § Asynchronous errors). - Cost: one
ip netns exec+ netns wrapper +ipsecprocess per router per interval (3 processes, with a rootwrap filter check); one RPC per interval only if something changed.
7. check_vpn_agents — server periodic task (dead-agent rescheduling, OVN)
- Trigger: futurist periodic
@maintenance.has_lock_periodic(spacing=max(cfg.CONF.agent_down_time // 2, 1), run_immediately=False)(neutron_vpnaas/services/vpn/ovn/maintenance.py:34-35): every 37 s with the defaultagent_down_time = 75. It is added to Neutron's OVNMaintenanceThreadthrough the plugin hookovn_maintenance_periodics(neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:464-471), **only ifallow_automatic_vpnagent_failover = true** (default false,neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:46-48,neutron_vpnaas/services/vpn/ovn_plugin.py:62-65). - Emitter: none (timer).
- Functional purpose: detect dead
neutron-ovn-vpn-agents and move the VPN services of their routers to a live agent. - Data read (
neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:82-130):- the agents of type
VPN Agentwithalive = false. On ML2/OVN,core_plugin.get_agentsreads Neutron's in-memoryAgentCacheand first callsping_all_chassis(), which bumpsNB_Global.nb_cfgat most everyagent_down_time // 2s (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1620-1638,neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:1715-1720). An OVN VPN agent is alive ifNB_Global.nb_cfg − <neutron:ovn-vpnagent-sb-cfg> ≤ 1, or if its last update is younger thanagent_down_time(neutron:neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:96-107,neutron_vpnaas/services/vpn/ovn/agent_monitor.py:28-31); SELECT * FROM routervpnagentbindings WHERE vpn_agent_id IN (:dead_agent_ids).
- the agents of type
- Flow: for each binding, re-check the agent (
get_agent): alive again → skip that agent for the rest of the loop. OtherwiseLOG.warning(Rescheduling vpn services …)andreschedule_router(neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:240-264):- in one transaction: DELETE the binding (nothing deleted → another server did it, stop), then
schedule_router→ choose a live agent (least routers by default;ChanceScheduler= random;AZLeastRoutersScheduler= filter by router AZ hints), INSERT the new binding; - after commit:
cast vpnservice_removed_from_agentto the old host,call vpnservice_added_to_agentto the new host; if the call fails, delete the new binding and raiseRouterReschedulingFailed(neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:266-284); - rebind the VPN gateway and namespace ports to the new host (§ 5). Observed: with
vpn-host-1dead andvpn-host-2alive, the router moved and the two RPC messages were sent (_rewrite/exchanges-ovn_stable-2026.2.json, stepsched-agents-after-reschedule).
- in one transaction: DELETE the binding (nothing deleted → another server did it, stop), then
- Concurrency:
has_lock_periodicruns the task only in the worker holding the OVN NB IDL lock (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py:64-90), i.e. one Neutron server in the cluster; the binding DELETE is the arbiter between servers. - Error behaviour:
RouterReschedulingFailedandRemoteErrorare logged per router and the loop continues; any other exception is logged and the whole run ends (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:106-118); retried next period. - Cost: one agent listing (+ possible NB transaction for the ping), one SELECT, and per dead binding: 2 DB transactions, 2 RPC messages, 2 port updates.
- Legacy profile: no equivalent in neutron-vpnaas; VPN follows the router, so Neutron's own
allow_automatic_l3agent_failovermoves routers, and the new L3 agent'sadd_routerhook starts the VPN (§ 11).
8. ChassisVPNAgentWriteEvent — server OVSDB event (OVN)
- Trigger: OVN Southbound table
Chassis_Private(inherited fromChassisAgentEvent,neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:387-396), eventsROW_CREATEandROW_UPDATE, no static condition,GLOBAL = True(runs in every worker) (neutron_vpnaas/services/vpn/ovn/agent_monitor.py:48-74). - Match (
neutron_vpnaas/services/vpn/ovn/agent_monitor.py:61-70): the row hasexternal_ids:neutron:ovn-vpnagent-id; on create always; on update only whenexternal_ids:neutron:ovn-vpnagent-sb-cfgchanged. - Emitter: the OVN VPN agent writing its keys (§ 9, § 13).
- Functional purpose: register / refresh the
VPN Agent(binaryneutron-ovn-vpn-agent) in Neutron'sAgentCache, which is whatGET /v2.0/agents, the scheduler andcheck_vpn_agentssee (neutron_vpnaas/services/vpn/ovn/agent_monitor.py:24-45). - Flow:
AgentCache().update("VPN Agent", row, clear_down=True): records the row andupdated_at = nb_cfg_timestamp, clears the "set down" flag (neutron:neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:51-57). - Registration:
watch_agent_events()is called frompost_fork_initialize(neutron_vpnaas/services/vpn/ovn_plugin.py:49-57) on the L3 plugin's SB IDL if it is anOvnSbIdl(neutron_vpnaas/services/vpn/ovn/agent_monitor.py:77-89). The agent ID isuuid5(chassis_id, 'vpn_agent')(neutron_vpnaas/agent/ovn/vpn/agent.py:146). - Agent down: Neutron's own
ChassisAgentDownEventmarks every agent of a deleted or orphanedChassis_Privatedown (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:399-427). - Cost: one in-memory update per VPN agent per
nb_cfgbump, in every API/RPC worker.
9. SbGlobalUpdateEvent — agent OVSDB event (OVN heartbeat)
- Trigger: SB table
SB_Global,ROW_UPDATE, no condition (neutron_vpnaas/agent/ovn/vpn/agent.py:65-76). - Emitter: ovn-northd copies
NB_Global.nb_cfgintoSB_Global.nb_cfgafter Neutron'sping_all_chassis(§ 7). - Functional purpose: the OVN VPN agent's heartbeat: acknowledge the new
nb_cfgso that Neutron sees the agent alive. - Flow:
db_set Chassis_Private <chassis> external_ids:neutron:ovn-vpnagent-sb-cfg=<SB_Global.nb_cfg>(neutron_vpnaas/agent/ovn/vpn/agent.py:151-158) → triggers § 8 on the server. - Cost: one OVSDB transaction per ping (at most every
agent_down_time // 2s).
10. ChassisPrivateCreateEvent — agent OVSDB event (reconnection)
- Trigger: SB
Chassis_Private,ROW_CREATE, conditionname == <own chassis>(neutron_vpnaas/agent/ovn/vpn/agent.py:35-62). The IDL only monitors its own row (set_table_condition('Chassis_Private', [['name', '==', chassis]]),neutron_vpnaas/agent/ovn/vpn/ovsdb.py:47-49). - Functional purpose: after an OVSDB reconnection or an ovn-controller restart (which re-creates the row and loses the agent's
external_ids), re-register the agent and resynchronise. - Flow: the first event (initial dump) is ignored; later ones:
register_vpn_agent(db_addofneutron:ovn-vpnagent-id, retried byovn_utils.retry),update_vpn_sb_cfg_key, then a fullsync(neutron_vpnaas/agent/ovn/vpn/agent.py:52-62).
11. L3 agent extension hooks (legacy)
The vpnaas L3 agent extension (neutron.agent.l3.extensions entry point, pyproject.toml:51-52) is called in-process by Neutron's L3 agent:
| Hook | Called by | Effect |
|---|---|---|
add_router(context, router) | neutron:neutron/agent/l3/agent.py:638 (router created on this agent) | for each device driver: create_router(ri) (remember the RouterInfo, re-apply NAT rules and start the daemon if a process already exists and the router is not HA-backup), then sync(context, [ri]) (neutron_vpnaas/services/vpn/agent.py:59-68, neutron_vpnaas/services/vpn/device_drivers/ipsec.py:615-631) |
update_router(context, router) | neutron:neutron/agent/l3/agent.py:698 | sync(context, [ri]) (neutron_vpnaas/services/vpn/agent.py:70-78) |
delete_router(context, router) | neutron:neutron/agent/l3/agent.py:485 | destroy_router(id): disable the process, remove the NAT rules, forget the router (neutron_vpnaas/services/vpn/agent.py:80-83, neutron_vpnaas/services/vpn/device_drivers/ipsec.py:633-654) |
ha_state_change(context, {router_id, state}) | neutron:neutron/agent/l3/ha.py:210 (keepalived transition) | update ha_state; master/primary → process.enable(), else process.disable() (neutron_vpnaas/services/vpn/agent.py:85-102) |
In the legacy path, the daemon runs in qrouter-<router_id>, or in snat-<router_id> for a distributed router (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:494-509), and the driver inserts one NAT rule per (local CIDR, peer CIDR) IPv4 pair so that tunnelled traffic is not SNATed: -A POSTROUTING -s <local> -d <peer> -m policy --dir out --pol ipsec -j ACCEPT, tagged vpnaas, on top of the router's nat table (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:529-567). The OVN driver has no NAT handling (get_router_based_iptables_manager = None, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:231).
12. Neutron in-process callbacks (server)
| Resource / event | Callback | Purpose | Profile |
|---|---|---|---|
ROUTER_GATEWAY BEFORE_DELETE | vpn_router_gateway_callback → check_router_in_use | forbid clearing the gateway of a router with VPN services (RouterInUse, 409) (neutron_vpnaas/db/vpn/vpn_db.py:771-778, neutron_vpnaas/db/vpn/vpn_db.py:565-575) | legacy only (OVN override is a no-op, neutron_vpnaas/services/vpn/ovn_plugin.py:53-54) |
ROUTER_INTERFACE BEFORE_DELETE | vpn_router_gateway_callback → check_subnet_in_use | forbid detaching a subnet used by a legacy service or a local endpoint group (neutron_vpnaas/db/vpn/vpn_db.py:577-608) | both |
ROUTER BEFORE_UPDATE | migration_callback → check_router_in_use | forbid changing the distributed flag (DVR migration) of a router with VPN services; Neutron publishes this event only from that migration path (neutron:neutron/db/l3_dvr_db.py:149-157) (neutron_vpnaas/db/vpn/vpn_db.py:784-790) | legacy only |
SUBNET BEFORE_DELETE | subnet_callback → check_subnet_in_use_by_endpoint_group | forbid deleting a subnet listed in a subnet endpoint group (neutron_vpnaas/db/vpn/vpn_db.py:793-799) | both |
ROUTER PRECOMMIT_UPDATE | _handle_router_precommit_update | forbid removing an extra route whose destination is a VPN peer CIDR (RouteInUseByVPN) (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:135-155) | OVN |
PORT, SUBNET, NETWORK BEFORE_DELETE | _prevent_vpn_*_delete_callback | protect the VPN gateway port, the namespace port, the transit subnet and network while their gateway is not PENDING_DELETE (neutron_vpnaas/db/vpn/vpn_ext_gw_db.py:95-165) | OVN |
PROCESS AFTER_INIT | post_fork_initialize | start watching VPN agent rows in the SB DB (§ 8) (neutron_vpnaas/services/vpn/ovn_plugin.py:49-57) | OVN |
The subscriptions of vpn_db are made at import time and again in the plugin constructor (idempotent) (neutron_vpnaas/db/vpn/vpn_db.py:802-823, neutron_vpnaas/services/vpn/plugin.py:74).
13. Agent start-up and sync
Legacy L3 agent extension
L3WithVPNaaS.__init__→VPNService.load_device_drivers(host)(neutron_vpnaas/services/vpn/agent.py:108-115,neutron_vpnaas/services/vpn/vpn_service.py:32-46): one driver per[vpnagent] vpn_device_driver(defaultLibreSwanDriver,neutron_vpnaas/services/vpn/agent.py:27-38). Each driver starts its RPC consumer and its status timer.
OVN VPN agent (neutron-ovn-vpn-agent)
mainregisters options, parses config, sets up privsep, launchesOvnVpnAgentwithrestart_method='mutate'(neutron_vpnaas/services/vpn/ovn_agent.py:65-72).start(neutron_vpnaas/agent/ovn/vpn/agent.py:101-128):ProcessMonitor, device drivers (defaultOvnStrongSwanDriver,neutron_vpnaas/services/vpn/ovn_agent.py:31-39);- local OVS IDL (
Open_vSwitch,Bridge,Port,Interface) → chassis name =Open_vSwitch.external_ids:system-id(neutron_vpnaas/agent/ovn/vpn/agent.py:160-168); - SB IDL on
SB_Global,Chassis,Chassis_Private(own row only), with the two events (neutron_vpnaas/agent/ovn/vpn/ovsdb.py:29-62); the schema fetch is retried with exponential back-off up to 180 s (neutron_vpnaas/agent/ovn/vpn/ovsdb.py:53-57); - register (
external_ids:neutron:ovn-vpnagent-id) and acknowledgenb_cfgimmediately, so the server sees the agent alive; - initial
sync.
sync(context, routers) — common reconciliation (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:815-853)
call get_vpn_services_on_host(§ 2).- For each returned service whose router has no process yet, or is in the
routersargument: create/update the process object (ensure_process), then- legacy: re-apply NAT rules; if the router is known locally: HA backup →
disable, elseupdate(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:855-873); - OVN:
get_vpn_transit_network_details, ensure namespaceqvpn-<router_id>(created withsysctl net.ipv4.ip_forward=1), plug the external portvg<port-id>on the external network and the internal portvr<port-id>on the transit network (veth to OVS via the configured interface driver), set their IPs (/28for the transit), default route via the external subnet gateway, and **static routes to every local CIDR via169.254.0.1** (protostatic, metric 100), removing stale ones; thenupdate(neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:250-261,neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:287-320,neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:120-179).
- legacy: re-apply NAT rules; if the router is known locally: HA backup →
- Destroy processes of routers in
routersthat have no service any more. - Destroy processes of routers that the server no longer returns (stale), and on OVN also every leftover
qvpn-*namespace (neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:322-328). report_status(§ 6).
process.update() — the daemon (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:315-340)
- If the service is admin-down or has no admin-up connection →
disable: stop the daemon if running,rm -rf <config_base_dir>/<router_id>. - Else
enable: write the configuration, thenrestartif the daemon answers, elsestart(neutron_vpnaas/services/vpn/device_drivers/ipsec.py:342-353). - Configuration (
[ipsec] config_base_dir=$state_path/ipsec/<router_id>/): directoriesetc,var/run,log,etc/ipsec.d/*; values translated to the daemon dialect (aes-128→aes128,group14→modp2048,group31→curve25519,bi-directional→start,response-only→add,v1/v2→ikev1/ikev2for strongSwan,never/insistfor Libreswan…) (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:137-166,neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:76-123,neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:61-92); PSK base64-encoded with the0sprefix (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:216-227);local_iddefaults to the tunnel IP. - strongSwan (
neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:173-242): rendersipsec.conf(oneconn <connection-id>per connection,auto=route,ike=<enc>-<auth>-<dh>,esp=…,dpdaction/dpddelay/dpdtimeout,keyexchange),strongswan.conf,ipsec.secrets(mode 0600), copies/etc/strongswan.d;start=ipsec startthenipsec stroke up-nb <conn-id>per connection;restart=ipsec rereadsecrets+ipsec reload;stop=ipsec stop. Every command goes throughip netns exec <ns> neutron-vpn-netns-wrapper, which bind-mounts<cfg>/etcon/etcand<cfg>/var/runon the strongSwan piddir inside the namespace, checks the command against the rootwrap filters and runs it (neutron_vpnaas/services/vpn/common/netns_wrapper.py:112-156). - Libreswan (
neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:135-385): rendersipsec.conf(withvirtual_private, admin-down connections skipped) andipsec.secrets,chown root:root, initialises NSS (ipsec _stackmanager startoripsec start, thenipsec checknss);start=ipsec pluto --use-xfrm --uniqueids [--perpeerlog …],ipsec whack --listen,ipsec whack --name <id> --asynchronous --initiatefor each admin-upbi-directionalconnection;restart= stop, wait for pluto to exit ([pluto] shutdown_check_timeout×shutdown_check_back_off,shutdown_check_retries), start — skipped when[pluto] restart_check_configis true and the files are unchanged;stop=whack --terminateper established connection,whack --shutdown.
Source: reverse-spec-neutron-vpnaas/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.