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

Reverse-spec · Services

neutron-vpnaas — Async processing

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-lib stable/2026.2 @ 1ee72052251a; neutron: = neutron stable/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

#TriggerFamilyProcessProfileSheet
1vpnservice_updatedAMQP cast, topic ipsec_agent, server = agent hostagent (device driver)both§ 1
2get_vpn_services_on_hostAMQP call, topic ipsec_driverneutron-server (service driver callback)both§ 2
3update_statusAMQP call, topic ipsec_driverneutron-serverboth§ 3
4get_vpn_transit_network_details, get_subnet_infoAMQP call, topic ipsec_driverneutron-serverOVN§ 4
5vpnservice_added_to_agent, vpnservice_removed_from_agent, agent_updatedAMQP call/cast, topic ipsec_agent, server = agent hostOVN VPN agentOVN§ 5
6report_status every [ipsec] ipsec_status_check_interval (60 s)timer + process state polling + cacheagentboth§ 6
7check_vpn_agents every max(agent_down_time // 2, 1) speriodic (futurist, OVN maintenance worker, DB lock)neutron-serverOVN, if allow_automatic_vpnagent_failover§ 7
8ChassisVPNAgentWriteEvent (Chassis_Private create/update)OVSDB event (SB)neutron-server, every workerOVN§ 8
9SbGlobalUpdateEvent (SB_Global update)OVSDB event (SB)OVN VPN agentOVN§ 9
10ChassisPrivateCreateEvent (own Chassis_Private created)OVSDB event (SB)OVN VPN agentOVN§ 10
11L3 agent extension hooks add_router, update_router, delete_router, ha_state_changein-process calls from the L3 agentL3 agentlegacy§ 11
12Neutron callbacks (router gateway/interface/update, subnet/port/network delete, router precommit update)neutron_lib.callbacks registryneutron-serverboth / OVN§ 12
13Agent start-up: initial sync, agent registrationprocess startagentsboth§ 13

RPC topology

Topic (exchange neutron, the control_exchange)Server sideCreated atClient side
ipsec_driver (no server, shared queue)IPsecVpnDriverCallBack (legacy) / IPsecVpnOvnDriverCallBack (OVN) in neutron-serverlegacy: 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 startneutron-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, method vpnservice_updated, args {"router": {"id": "<router_id>"}} (neutron_vpnaas/services/vpn/service_drivers/__init__.py:106-110).
  • Emitter: the service driver after PUT/DELETE vpnservice and POST/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 after POST vpnservice. Target hosts: legacy → every L3 agent hosting the router with admin_state_up and alive (neutron_vpnaas/services/vpn/service_drivers/__init__.py:92-104); OVN → every live VPN agent bound to the router in routervpnagentbindings (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): calls sync(context, [router]), see § 13 for sync itself: one get_vpn_services_on_host call, then reconciliation of the local daemons, then report_status.
  • Concurrency and idempotency: sync holds the process-wide lock neutron-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 to get_vpn_services_on_host].
  • Error behaviour: exceptions inside sync are raised in the RPC dispatcher and logged by oslo.messaging; nothing is retried.
  • Cost: 1 cast per hosting agent per API change, then 1 call back 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 every vpnservice_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 (type L3 agent); returns nothing if it is disabled or in dvr mode (VPN runs on dvr_snat/legacy nodes only). Then SELECT 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): the VPN Agent of that host from the agent list; **if vpn_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 on routervpnagentbindings.vpn_agent_id instead of the L3 binding, without the provider filter;
    • plus the map subnet id → cidr of 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).
  • 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, and ipsec_site_connections[] with all connection columns (**including psk**), nested ikepolicy and ipsecpolicy rows (DB column names: lifetime_units, lifetime_value), peer_cidrs, local_cidrs, local_ip_vers, and external_ip chosen by the peer's IP version. When peer_id (and local_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, step rpc-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's sync fails 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_CREATE and keep ACTIVE/DOWN in sync with the real tunnels.
  • Flow (neutron_vpnaas/db/vpn/vpn_db.py:733-768), in one write transaction:
    1. for each service: read it; deleted → LOG.warning, skip; if it is not pending, or the agent set updated_pending_status, write status;
    2. for each connection: read it; deleted → skip silently; same pending rule (neutron_vpnaas/db/vpn/vpn_db.py:285-298).
  • 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 backup state 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 IP 169.254.0.1 and 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, None when not found).
  • Reply format: gw_port and transit_port are **SQLAlchemy Port rows**, 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 least id, mac_address, network_id and fixed_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 → AttributeError on None.gw_port (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:76-77) → RemoteError on the agent, sync aborted [inferred].

5. Agent scheduling notifications (OVN)

MethodModeArgsEmitted whenAgent handler
vpnservice_added_to_agentcall, 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_agentcast{"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_updatedcast{"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, option neutron_vpnaas/services/vpn/device_drivers/ipsec.py:52-54). Also called at the end of every sync (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 in backup state 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, ROUTED or CONNECTING = DOWN (neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:68-74);
      • Libreswan: … --cmd=ipsec,whack,--status; a line NNN #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);
    • the service is ACTIVE if the status output contains at least one connection, else DOWN (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).
  • Flow:
    1. 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 status None) every connection is flagged updated_pending_status=True so that the server accepts it even if pending (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:795-798);
    2. connections present in the cache but no longer in the daemon output are reported DOWN with the pending flag (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:738-752);
    3. update the cache, clear the pending flags (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:801-806);
    4. if anything changed: one call update_status with all entries (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:808-811), see § 3.
  • Concurrency and idempotency: runs in its own green thread, without the neutron-vpn-agent lock used by sync; 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 (RuntimeError caught, 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 + ipsec process 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 default agent_down_time = 75. It is added to Neutron's OVN MaintenanceThread through the plugin hook ovn_maintenance_periodics (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py:464-471), **only if allow_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):
    1. the agents of type VPN Agent with alive = false. On ML2/OVN, core_plugin.get_agents reads Neutron's in-memory AgentCache and first calls ping_all_chassis(), which bumps NB_Global.nb_cfg at most every agent_down_time // 2 s (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 if NB_Global.nb_cfg − <neutron:ovn-vpnagent-sb-cfg> ≤ 1, or if its last update is younger than agent_down_time (neutron:neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:96-107, neutron_vpnaas/services/vpn/ovn/agent_monitor.py:28-31);
    2. SELECT * FROM routervpnagentbindings WHERE vpn_agent_id IN (:dead_agent_ids).
  • Flow: for each binding, re-check the agent (get_agent): alive again → skip that agent for the rest of the loop. Otherwise LOG.warning(Rescheduling vpn services …) and reschedule_router (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:240-264):
    1. 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;
    2. after commit: cast vpnservice_removed_from_agent to the old host, call vpnservice_added_to_agent to the new host; if the call fails, delete the new binding and raise RouterReschedulingFailed (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:266-284);
    3. rebind the VPN gateway and namespace ports to the new host (§ 5). Observed: with vpn-host-1 dead and vpn-host-2 alive, the router moved and the two RPC messages were sent (_rewrite/exchanges-ovn_stable-2026.2.json, step sched-agents-after-reschedule).
  • Concurrency: has_lock_periodic runs 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: RouterReschedulingFailed and RemoteError are 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_failover moves routers, and the new L3 agent's add_router hook starts the VPN (§ 11).

8. ChassisVPNAgentWriteEvent — server OVSDB event (OVN)

  • Trigger: OVN Southbound table Chassis_Private (inherited from ChassisAgentEvent, neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:387-396), events ROW_CREATE and ROW_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 has external_ids:neutron:ovn-vpnagent-id; on create always; on update only when external_ids:neutron:ovn-vpnagent-sb-cfg changed.
  • Emitter: the OVN VPN agent writing its keys (§ 9, § 13).
  • Functional purpose: register / refresh the VPN Agent (binary neutron-ovn-vpn-agent) in Neutron's AgentCache, which is what GET /v2.0/agents, the scheduler and check_vpn_agents see (neutron_vpnaas/services/vpn/ovn/agent_monitor.py:24-45).
  • Flow: AgentCache().update("VPN Agent", row, clear_down=True): records the row and updated_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 from post_fork_initialize (neutron_vpnaas/services/vpn/ovn_plugin.py:49-57) on the L3 plugin's SB IDL if it is an OvnSbIdl (neutron_vpnaas/services/vpn/ovn/agent_monitor.py:77-89). The agent ID is uuid5(chassis_id, 'vpn_agent') (neutron_vpnaas/agent/ovn/vpn/agent.py:146).
  • Agent down: Neutron's own ChassisAgentDownEvent marks every agent of a deleted or orphaned Chassis_Private down (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py:399-427).
  • Cost: one in-memory update per VPN agent per nb_cfg bump, 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_cfg into SB_Global.nb_cfg after Neutron's ping_all_chassis (§ 7).
  • Functional purpose: the OVN VPN agent's heartbeat: acknowledge the new nb_cfg so 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 // 2 s).

10. ChassisPrivateCreateEvent — agent OVSDB event (reconnection)

  • Trigger: SB Chassis_Private, ROW_CREATE, condition name == <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_add of neutron:ovn-vpnagent-id, retried by ovn_utils.retry), update_vpn_sb_cfg_key, then a full sync (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:

HookCalled byEffect
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:698sync(context, [ri]) (neutron_vpnaas/services/vpn/agent.py:70-78)
delete_router(context, router)neutron:neutron/agent/l3/agent.py:485destroy_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 / eventCallbackPurposeProfile
ROUTER_GATEWAY BEFORE_DELETEvpn_router_gateway_callback → check_router_in_useforbid 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_DELETEvpn_router_gateway_callback → check_subnet_in_useforbid 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_UPDATEmigration_callback → check_router_in_useforbid 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_DELETEsubnet_callback → check_subnet_in_use_by_endpoint_groupforbid 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_updateforbid 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_callbackprotect 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_INITpost_fork_initializestart 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 (default LibreSwanDriver, 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)

  • main registers options, parses config, sets up privsep, launches OvnVpnAgent with restart_method='mutate' (neutron_vpnaas/services/vpn/ovn_agent.py:65-72).
  • start (neutron_vpnaas/agent/ovn/vpn/agent.py:101-128):
    1. ProcessMonitor, device drivers (default OvnStrongSwanDriver, neutron_vpnaas/services/vpn/ovn_agent.py:31-39);
    2. 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);
    3. 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);
    4. register (external_ids:neutron:ovn-vpnagent-id) and acknowledge nb_cfg immediately, so the server sees the agent alive;
    5. initial sync.

sync(context, routers) — common reconciliation (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:815-853)

  1. call get_vpn_services_on_host (§ 2).
  2. For each returned service whose router has no process yet, or is in the routers argument: create/update the process object (ensure_process), then
    • legacy: re-apply NAT rules; if the router is known locally: HA backup → disable, else update (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:855-873);
    • OVN: get_vpn_transit_network_details, ensure namespace qvpn-<router_id> (created with sysctl net.ipv4.ip_forward=1), plug the external port vg<port-id> on the external network and the internal port vr<port-id> on the transit network (veth to OVS via the configured interface driver), set their IPs (/28 for the transit), default route via the external subnet gateway, and **static routes to every local CIDR via 169.254.0.1** (proto static, metric 100), removing stale ones; then update (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).
  3. Destroy processes of routers in routers that have no service any more.
  4. 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).
  5. 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, then restart if the daemon answers, else start (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:342-353).
  • Configuration ([ipsec] config_base_dir = $state_path/ipsec/<router_id>/): directories etc, 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/ikev2 for strongSwan, never/insist for 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 the 0s prefix (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:216-227); local_id defaults to the tunnel IP.
  • strongSwan (neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:173-242): renders ipsec.conf (one conn <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 start then ipsec stroke up-nb <conn-id> per connection; restart = ipsec rereadsecrets + ipsec reload; stop = ipsec stop. Every command goes through ip netns exec <ns> neutron-vpn-netns-wrapper, which bind-mounts <cfg>/etc on /etc and <cfg>/var/run on 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): renders ipsec.conf (with virtual_private, admin-down connections skipped) and ipsec.secrets, chown root:root, initialises NSS (ipsec _stackmanager start or ipsec start, then ipsec checknss); start = ipsec pluto --use-xfrm --uniqueids [--perpeerlog …], ipsec whack --listen, ipsec whack --name <id> --asynchronous --initiate for each admin-up bi-directional connection; restart = stop, wait for pluto to exit ([pluto] shutdown_check_timeout × shutdown_check_back_off, shutdown_check_retries), start — skipped when [pluto] restart_check_config is true and the files are unchanged; stop = whack --terminate per 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.