Scenarios — neutron-vpnaas — branch stable/2026.2
Branch
stable/2026.2@6ce5da102cf1— repository https://opendev.org/openstack/neutron-vpnaas — analysed on 2026-10-29.neutron:= neutronstable/2026.2@eef1a0f7ee36. Sheets referenced as "05 § n" are in async processing.
1. OVN: create a VPN service and a site connection until the tunnel is ACTIVE
sequenceDiagram
autonumber
participant C as Client
participant S as neutron-server (VPNOVNDriverPlugin + IPsecOvnVPNDriver)
participant DB as MariaDB
participant NB as OVN NB (via ML2/OVN)
participant MQ as RabbitMQ
participant A as neutron-ovn-vpn-agent (vpn-host-1)
participant K as qvpn-<router> namespace (strongSwan)
C->>S: POST /v2.0/vpn/vpnservices {router_id}
S->>DB: SELECT routers / ports (gateway check)
S->>DB: INSERT vpnservices (PENDING_CREATE), providerresourceassociations
S->>S: schedule_router: live VPN agents from AgentCache
alt no live VPN agent
S->>DB: UPDATE vpnservices SET status=ERROR
S-->>C: 409 RouterReschedulingFailed (row kept in ERROR)
else agent found
S->>DB: INSERT routervpnagentbindings, vpn_ext_gws (PENDING_CREATE)
S->>DB: INSERT port vpn-gw-<router> (external net), network/subnet 169.254.0.0/28, port vpn-ns-<router> 169.254.0.2
S->>NB: Logical_Switch(_Port), Logical_Router_Port (via ML2/OVN)
S->>DB: UPDATE vpn_ext_gws SET status=ACTIVE; UPDATE vpnservices SET external_v4_ip
S-->>C: 201 {status: PENDING_CREATE, external_v4_ip}
end
Note over S,A: no RPC yet: a service without connection is not sent to agents
C->>S: POST /v2.0/vpn/ipsec-site-connections {vpnservice_id, ikepolicy_id, ipsecpolicy_id, local/peer EPG, peer_address, psk}
S->>S: validations (mode, groups, IP versions, DPD, MTU, DNS for FQDN)
alt invalid
S-->>C: 400 / 404 (02-errors)
else valid
S->>DB: INSERT ipsec_site_connections (PENDING_CREATE)
S->>DB: router extra routes: peer CIDRs via 169.254.0.2
S->>NB: Logical_Router_Static_Route (via ML2/OVN)
S->>MQ: cast vpnservice_updated {router:{id}} → ipsec_agent.vpn-host-1
S-->>C: 201 {status: PENDING_CREATE}
end
MQ->>A: vpnservice_updated
A->>MQ: call get_vpn_services_on_host {host} → ipsec_driver
MQ->>S: get_vpn_services_on_host
S->>DB: join vpnservices/connections/routervpnagentbindings (+ auto-schedule)
S-->>A: [service + connections + policies + CIDRs + psk]
A->>S: call get_vpn_transit_network_details {router_id}
S-->>A: {gw_port, transit_port, 169.254.0.1, external_network}
A->>K: ip netns add qvpn-<router>; plug vg<port> (external) and vr<port> (transit); routes local CIDRs via 169.254.0.1
A->>K: write ipsec.conf/secrets; ipsec start; ipsec stroke up-nb <conn>
A->>K: ipsec status
A->>S: call update_status [{service ACTIVE|DOWN, connections …, updated_pending_status: true}]
S->>DB: UPDATE vpnservices / ipsec_site_connections status
C->>S: GET /v2.0/vpn/ipsec-site-connections/{id}
S-->>C: 200 {status: ACTIVE}Where to look when it fails: 409 at service creation → no live VPN agent (GET /v2.0/agents?agent_type=VPN Agent); connection stuck PENDING_CREATE → the agent never received the cast or failed in sync (agent log), or get_vpn_transit_network_details failed (no vpn_ext_gws row); DOWN → strongSwan negotiation (ipsec status in qvpn-<router>, syslog). Code: neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:373-447, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:307-320.
2. Legacy: a site connection served by the L3 agent
sequenceDiagram
autonumber
participant C as Client
participant S as neutron-server (VPNDriverPlugin + IPsecVPNDriver)
participant DB as MariaDB
participant MQ as RabbitMQ
participant L as neutron-l3-agent + vpnaas extension
participant R as qrouter-<router> (strongSwan / Libreswan)
C->>S: POST /v2.0/vpn/ipsec-site-connections
S->>S: IpsecVpnValidator: ah-esp policy?
alt ah-esp
S-->>C: 400 IpsecValidationFailure
else
S->>DB: INSERT ipsec_site_connections (PENDING_CREATE)
S->>DB: SELECT L3 agents hosting the router (alive, admin up)
S->>MQ: cast vpnservice_updated → ipsec_agent.<l3 host> (one per hosting agent, both for HA)
S-->>C: 201 PENDING_CREATE
end
MQ->>L: vpnservice_updated
L->>S: call get_vpn_services_on_host (only if the L3 agent is not dvr mode)
S-->>L: services
L->>R: iptables nat POSTROUTING -s local -d peer -m policy --pol ipsec -j ACCEPT
alt HA router in backup state
L->>R: disable (stop daemon, remove config)
else master / non-HA
L->>R: render config; ipsec start (or reload)
end
L->>S: call update_status (report_status at end of sync)
S->>DB: UPDATE statusesThe L3 agent also starts the VPN on its own when the router arrives on the host (add_router hook) and flips it on keepalived transitions (ha_state_change), 05 § 11.
3. Tunnel goes down and comes back (status polling)
sequenceDiagram
autonumber
participant T as report_status timer (every ipsec_status_check_interval = 60 s)
participant A as agent device driver
participant K as ipsec daemon (namespace)
participant S as neutron-server
participant DB as MariaDB
loop every 60 s
T->>A: report_status()
A->>K: ip netns exec … neutron-vpn-netns-wrapper --cmd=ipsec,status
K-->>A: "<conn> … INSTALLED" / "ROUTED" / exit code 1|3
A->>A: compare with process_status_cache
alt something changed
A->>A: update cache first
A->>S: call update_status [changed services only]
alt server reachable
S->>DB: UPDATE status (ACTIVE ↔ DOWN)
else MessagingTimeout
Note over A,S: change lost until the next change (cache already updated)
end
else no change
Note over A: no RPC
end
endCode: neutron_vpnaas/services/vpn/device_drivers/ipsec.py:760-811, neutron_vpnaas/db/vpn/vpn_db.py:733-768.
4. OVN: a VPN agent dies, its routers move to another agent
Precondition: allow_automatic_vpnagent_failover = true (default false).
sequenceDiagram
autonumber
participant M as Maintenance worker (holds OVN NB lock)
participant NB as OVN NB/SB
participant AC as AgentCache (server memory)
participant DB as MariaDB
participant MQ as RabbitMQ
participant A1 as VPN agent host-1 (dead)
participant A2 as VPN agent host-2
Note over A1: process stopped: no more neutron:ovn-vpnagent-sb-cfg updates
loop every max(agent_down_time // 2, 1) s
M->>NB: ping_all_chassis: NB_Global.nb_cfg++ (rate-limited)
NB-->>A2: SB_Global.nb_cfg updated (SbGlobalUpdateEvent)
A2->>NB: Chassis_Private external_ids:neutron:ovn-vpnagent-sb-cfg = nb_cfg
NB-->>AC: ChassisVPNAgentWriteEvent → AgentCache.update(host-2)
M->>AC: get_agents(VPN Agent, alive=False) → host-1 (nb_cfg lag > 1 and last update > agent_down_time)
M->>DB: SELECT routervpnagentbindings WHERE vpn_agent_id IN (host-1)
M->>DB: BEGIN; DELETE binding(router, host-1); choose least-loaded live agent; INSERT binding(router, host-2); COMMIT
M->>MQ: cast vpnservice_removed_from_agent → ipsec_agent.host-1 (lost)
M->>MQ: call vpnservice_added_to_agent {router_ids} → ipsec_agent.host-2
alt host-2 answers
MQ->>A2: sync([router]): build qvpn-<router>, start strongSwan
M->>DB: update_port binding:host_id=host-2 (vpn-gw and vpn-ns ports)
NB-->>NB: ML2/OVN moves the ports to host-2's chassis
else call fails twice
M->>DB: DELETE binding(router, host-2)
Note over M: RouterReschedulingFailed logged; router unhosted until an agent auto-schedules it
end
endObserved in the capture (_rewrite/exchanges-ovn_stable-2026.2.json, step sched-agents-after-reschedule): cast vpnservice_removed_from_agent → vpn-host-1, call vpnservice_added_to_agent → vpn-host-2, then the router's agent list shows vpn-host-2. Code: neutron_vpnaas/services/vpn/ovn/maintenance.py:34-35, neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:82-118, neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:240-284.
Without failover, a dead agent's routers stay bound to it: tunnels stay down until an operator moves them (DELETE /v2.0/agents/{dead}/vpn-routers/{router} then the next agent sync auto-schedules, or POST /v2.0/agents/{live}/vpn-routers).
5. OVN: delete the connection and the service (gateway cleanup)
sequenceDiagram
autonumber
participant C as Client
participant S as neutron-server
participant DB as MariaDB
participant MQ as RabbitMQ
participant A as VPN agent
C->>S: DELETE /v2.0/vpn/ipsec-site-connections/{id}
S->>DB: DELETE ipsec_site_connections (+ ipsecpeercidrs cascade)
S->>DB: remove router extra routes no longer needed
S->>MQ: cast vpnservice_updated
S-->>C: 204
MQ->>A: sync: service has no connection → not returned → destroy process, delete qvpn-<router>
C->>S: DELETE /v2.0/vpn/vpnservices/{id}
alt connections remain
S-->>C: 409 VPNServiceInUse
else
S->>DB: DELETE vpnservices, provider association
S->>MQ: cast vpnservice_updated
opt last service on the router
S->>DB: vpn_ext_gws → PENDING_DELETE
S->>DB: delete vpn-gw port, vpn-ns port, transit router interface, subnet, network
S->>DB: DELETE vpn_ext_gws
end
S-->>C: 204
endCode: neutron_vpnaas/services/vpn/plugin.py:177-228, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:407-451. Observed: after the last delete, GET /v2.0/ports?device_owner=network:vpn_router_gateway is empty (ovn-ports-after-delete).
6. OVN VPN agent start-up and heartbeat
sequenceDiagram
autonumber
participant A as neutron-ovn-vpn-agent
participant OVS as local OVSDB
participant SB as OVN SB
participant S as neutron-server workers
participant MQ as RabbitMQ
A->>OVS: get Open_vSwitch external_ids:system-id → chassis name
A->>SB: connect (retry up to 180 s back-off), monitor SB_Global, Chassis, own Chassis_Private
A->>SB: db_add Chassis_Private external_ids neutron:ovn-vpnagent-id = uuid5(chassis, 'vpn_agent')
A->>SB: db_set external_ids neutron:ovn-vpnagent-sb-cfg = Chassis_Private.nb_cfg
SB-->>S: ChassisVPNAgentWriteEvent (create) → AgentCache: "VPN Agent" alive
A->>MQ: call get_vpn_services_on_host (initial sync) → auto-schedules unhosted routers to this agent
loop on every SB_Global.nb_cfg change
SB-->>A: SbGlobalUpdateEvent
A->>SB: db_set neutron:ovn-vpnagent-sb-cfg = nb_cfg
SB-->>S: ChassisVPNAgentWriteEvent (update)
end
opt ovn-controller restarted (own Chassis_Private re-created)
SB-->>A: ChassisPrivateCreateEvent
A->>SB: re-register + ack nb_cfg
A->>MQ: full sync
endCode: neutron_vpnaas/agent/ovn/vpn/agent.py:35-168, neutron_vpnaas/services/vpn/ovn/agent_monitor.py:48-89.
Source: reverse-spec-neutron-vpnaas/04-scenarios_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.