Rewrite pack — 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;openstacksdk:= openstacksdkstable/2026.2@fbdc40381.
Target: an OpenKeel (Go, Kubernetes-native) re-implementation of the VPN service on the OVN path, keeping the Networking v2.0 /vpn API compatible for openstacksdk/OSC/Horizon, and either keeping or replacing the agent side. The legacy L3-agent path is documented for completeness but is not a target.
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[routes-ovn_stable-2026.2.json](_rewrite/routes-ovn_stable-2026.2.json), [routes-legacy_stable-2026.2.json](_rewrite/routes-legacy_stable-2026.2.json) | route table extracted from the running neutron-server per profile: method, path, handler file:line, policy, success status, shadowed (registered again by another extension, never dispatched), routes_artifact (Routes new/edit), and the effective attribute maps of the five collections after all extensions were applied (validators, defaults, allow_post/put, is_visible, is_filter, is_sort_key) |
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json) | OpenAPI 3.0.3 built from the OVN route table and the observed exchanges (14 paths, 35 operations); x-neutron keeps the original attribute definition, x-handler, x-policy, x-profiles, x-not-implemented (the six scheduler routes that answer 500). Types and enums are authoritative; behaviour is in files 01–05 |
[exchanges-legacy_stable-2026.2.json](_rewrite/exchanges-legacy_stable-2026.2.json), [exchanges-ovn_stable-2026.2.json](_rewrite/exchanges-ovn_stable-2026.2.json) | 85 + 108 recorded request/response pairs (scenario steps, user, roles), each with the RPC messages it emitted, plus the replies of the server-side RPC endpoints called as an agent would (kind: rpc-server) — the reference for differential testing |
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md) | 49 distinct (operation, status, type, message) combinations, UUIDs templated |
[rpc-frames_stable-2026.2.json](_rewrite/rpc-frames_stable-2026.2.json) | every VPN RPC message (topic, server, version, method, args, caller context) and every agent→server reply recorded in the scenarios |
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json) | the 25 documented VPN operations of neutron-lib's api-ref, with parameters and samples (outdated on algorithms, see § 8) |
[schema-mysql-migrated_stable-2026.2.sql](_rewrite/schema-mysql-migrated_stable-2026.2.sql) | the 9 VPN tables as created by neutron-db-manage upgrade heads on MariaDB 11.4 — the production schema |
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql), [schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md) | DDL from the ORM models; the diff against the migrated schema is empty (columns, enum values and order, nullability, PK, unique, FK and ON DELETE) |
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml) | the 20 default rules (ADMIN_OR_PROJECT_MEMBER, project scope) with their deprecated forms |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | server options (vpn_scheduler_driver, vpn_auto_schedule, allow_automatic_vpnagent_failover, [service_providers]) |
[config-ovn-vpn-agent_stable-2026.2.ini](_rewrite/config-ovn-vpn-agent_stable-2026.2.ini), [config-vpn-agent_stable-2026.2.ini](_rewrite/config-vpn-agent_stable-2026.2.ini) | agent option samples ([vpnagent], [ipsec], [strongswan], [libreswan], [pluto], [ovs], [ovn]); template paths shown as <site-packages>/… |
tools/vpnaas_capture.py, tools/summarize.py, tools/build_openapi.py | the reproducible extraction: vpnaas_capture.py <legacy|ovn> <vpnaas> <neutron> <neutron-lib> <out> <slug> loads Neutron's paste pipeline with a reference profile, walks the Routes mapper, replays the scenario, intercepts oslo.messaging cast/call |
[rewrite-pack-index_stable-2026.2.md](_rewrite/rewrite-pack-index_stable-2026.2.md) | index |
Extraction environment: Python 3.12.7 venv with neutron-lib, neutron and neutron-vpnaas installed from the local clones and requirements stable/2026.2 @ 3b75407b upper-constraints (oslo.messaging 18.3.1, oslo.db 18.1.1, SQLAlchemy 2.0.52, ovsdbapp 2.19.0, WebOb 1.8.11). Capture profile: SQLite in memory, fake:// transport, auth_strategy = noauth with the identity injected by NeutronKeystoneContext, enforce_scope and enforce_new_defaults on. The OVN profile runs the OVN service plugin and driver on an ML2/openvswitch core so that no OVN database is needed: the VPN agent is an agents row instead of an AgentCache entry, and the RPC frames contain a few unrelated ML2/OVS notifications (neutron-vo-*, l3_agent routers_updated) that a real OVN deployment does not send.
2. HTTP behaviour that the contracts do not show
2.1 Pipeline
The VPN API is served by Neutron (neutron:etc/neutron/api-paste.ini): cors → http_proxy_to_wsgi → request_id → catch_errors → osprofiler → authtoken → keystonecontext → extensions (Routes) → base Controller. Consequences a rewrite must reproduce:
- authentication failures (401) come from keystonemiddleware before any VPN code;
- the body is validated against the attribute map before the policy check of the attributes and before the plugin; unknown keys → 400
Unrecognized attribute(s); read-only keys on PUT → 400Cannot update read-only attribute; GETof another project's resource → 404 with the plugin's…NotFoundtype (the plugin filters by project); areaderwithoutmembergets 404HTTPNotFoundonGETand 403PolicyNotAuthorizedon writes (observed);- uncaught exceptions → 500
HTTPInternalServerErrorwith the generic message.
2.2 Error body
{"NeutronError": {"type": "<ExceptionClass>", "message": "<text>", "detail": ""}}. Clients (openstacksdk) surface message; no client parses type for VPN [inferred from openstacksdk:openstack/network/v2/vpn_*.py, which declare no error mapping]. Keep the exact type names anyway: they are the only machine-readable code.
2.3 Other cross-cutting rules
- Responses carry
project_idandtenant_id(same value). Requests accept either. - Collections support
fields, filters on everyis_filterattribute (includingpsk, which openstacksdk exposes as a query parameter:openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:27-47), sorting and pagination. - Bulk create is emulated (
native_bulk: false):{"ikepolicies": [ … ]}creates one by one inside Neutron's controller [inferred from Neutron's base controller]. DELETEreturns 204 with an empty body;POST201.- The extension list (
GET /v2.0/extensions) advertisesvpnaas,vpn-endpoint-groups,vpn-flavors,vpn-aes-ccm-gcm,vpn-aes-ctr,vpn-no-sha1-3des, and on OVNvpn-agent-scheduler(route tables). Clients and Horizon gate features on these aliases.
3. Core algorithms: formal specification
3.1 Effective attribute validation (the extension merge)
Neutron applies extensions in order and replaces each extended attribute definition (neutron:neutron/api/extensions.py:353-364); every extension's own map is then overwritten with the merged result (neutron-lib:neutron_lib/api/extensions.py:146-149). Order = sorted file names in neutron_vpnaas/extensions/ (neutron:neutron/api/extensions.py:447). Result: vpn-no-sha1-3des (processed after vpn-aes-ctr) defines the final auth_algorithm and encryption_algorithm lists, computed from the base lists without CTR (neutron-lib:neutron_lib/api/definitions/vpn_no_sha1_3des.py:33-41). The effective lists are in 01 § Effective attribute validation and in the route tables.
Rewrite decision: reproducing the original means rejecting aes-*-ctr while advertising vpn-aes-ctr. Recommended: accept CTR (the DB enum, the strongSwan and Libreswan dialect maps already support it, neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:81-83, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:64-66) and record the deviation.
3.2 Connection validation (order matters for the error returned)
For POST /vpn/ipsec-site-connections (neutron_vpnaas/services/vpn/plugin.py:166-175, neutron_vpnaas/db/vpn/vpn_db.py:153-203):
driver_validator(conn) # legacy only: ah-esp → 400 IpsecValidationFailure
apply dpd defaults {hold, 30, 120}
404 if vpnservice, ikepolicy, ipsecpolicy missing # in this order
mode check (service.subnet_id set?) # MissingPeerCidrs | InvalidEndpointGroup | PeerCidrsInvalid | MissingRequiredEndpointGroup
if endpoint groups:
local group exists (404) / type == subnet (WrongEndpointGroupType)
each local subnet has a port on service.router (SubnetIsNotConnectedToRouter)
local subnets same IP version (MixedIPVersionsForIPSecEndpoints)
peer group exists / type == cidr; peer CIDRs same IP version
else:
peer_cidrs valid CIDRs (IPsecSiteConnectionPeerCidrError), same version (MixedIPVersionsForPeerCidrs)
local version = service.subnet.ip_version
local version == peer version (MixedIPVersionsForIPSecConnection)
dpd.timeout > dpd.interval (IPsecSiteConnectionDpdIntervalValueError)
mtu >= {4: 68, 6: 1280}[local version] (IPsecSiteConnectionMtuError)
peer_address: IP, else getaddrinfo()[0] replaces it (VPNPeerAddressNotResolved)
router gateway has a fixed IP of the peer's version (ExternalNetworkHasNoSubnet)
INSERT … status=PENDING_CREATE, route_mode=static, auth_mode=psk
PUT runs the same chain with previous values as defaults (neutron_vpnaas/db/vpn/vpn_validator.py:221-227), then the pending-state check, then a set-wise replacement of peer_cidrs; status is not changed.
3.3 Status reconciliation (agent ⇄ server)
- Server rule (
neutron_vpnaas/db/vpn/vpn_db.py:285-298,neutron_vpnaas/db/vpn/vpn_db.py:761-763):write(new) iff not pending(current) or report.updated_pending_status. - Agent rule (
neutron_vpnaas/services/vpn/device_drivers/ipsec.py:760-811): per router, one status capture; report iff pending flag, service status or connection-status map differs from the cache; first report after start forcesupdated_pending_statuson every connection; vanished connections reportedDOWN+ flag; cache updated before the RPC. - Status derivation: strongSwan
INSTALLED→ACTIVE,ROUTED|CONNECTING→DOWN; Libreswan "established … newest" →ACTIVE, other lines →DOWN; exit code 1|3 → no connection; service =ACTIVEiff ≥ 1 connection line. A Kubernetes-native agent (controller watching the VPN objects, or a DaemonSet per gateway node) can keep the same server rule and replace the polling by strongSwan'svicievents [suggestion].
3.4 Scheduling (OVN)
- Candidates: live agents of type
VPN Agent(neutron_vpnaas/scheduler/vpn_agent_scheduler.py:86-91); AZ variant: agents whose AZ is in the router'savailability_zone_hintsor[DEFAULT] default_availability_zones(neutron_vpnaas/scheduler/vpn_agent_scheduler.py:155-185). - Choice:
LeastRoutersScheduler— an agent with zero routers if any (arbitrary among them:set.pop()), else the one with the smallest binding count (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:367-382);ChanceScheduler—random.choice. Not deterministic: tests must compare the set of valid outcomes. - A router already bound keeps its agent; one agent per router (unique key).
- Auto-schedule of all unhosted routers to the calling agent on every
get_vpn_services_on_hostwhenvpn_auto_schedule(neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:99-103) — with the default scheduler this ignores load balancing entirely (whichever agent syncs first takes every unhosted router). - Failover: § 05 § 7.
3.5 OVN data-plane model
Per router with ≥ 1 VPN service:
- VPN gateway port on the external network (own public IP = tunnel endpoint,
vpnservice.external_v4_ip/v6_ip), bound to the agent host; - transit network/subnet
169.254.0.0/28attached to the router at169.254.0.1; namespace port169.254.0.2; - router extra routes: each peer CIDR →
169.254.0.2; protected against removal; - agent namespace
qvpn-<router_id>: external interfacevg…with the gateway IP and default route, internalvr…169.254.0.2/28, routes to each local CIDR via169.254.0.1, IP forwarding on; strongSwanauto=routeconnections between the gateway IP and the peer. On Kubernetes/OVN (or Kube-OVN) the same model maps to a pod (or a Multus-attached gateway) with two interfaces: one on the provider network with the public IP, one on a per-router transit segment, plus static routes on the logical router [suggestion].
3.6 Configuration rendering
Inputs: the get_vpn_services_on_host dict (03 § Sample frames). Translation tables: neutron_vpnaas/services/vpn/device_drivers/ipsec.py:137-166 (common), neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:76-123, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:61-92. Templates: neutron_vpnaas/services/vpn/device_drivers/template/strongswan/ipsec.conf.template:1-38, neutron_vpnaas/services/vpn/device_drivers/template/libreswan/ipsec.conf.template:1-91. Rules: ike=<enc>-<auth>-<dh>; ESP uses <enc>-<dh> (no auth) when the encryption is CCM/GCM ('cm' in encryption_algorithm); AH uses ah=<auth>-<dh>; ah-esp has no rendering (strongSwan would get an ESP line); lifetimes in seconds; local_id defaults to the tunnel IP; non-IP IDs prefixed with @; PSK as 0s<base64>. transform_protocol=ah-esp must be rejected by an OVN rewrite too (the original accepts it on OVN, § 8).
3.7 Test data
The project's tests are Python unit tests with mocks; there is no gabbi or Tempest-style suite in the repository, so no conformance_capture.py run was made. The language-independent conformance data are the two exchange files (193 exchanges), produced by the real server, and the observed-error table. Not encoded: concurrency (duplicate gateway creation, double scheduling), RPC timeouts, the agent's command execution (no namespaces on the capture host).
4. What the clients expect
| Expectation | Where the client relies on it |
|---|---|
resource keys and paths ipsec_site_connection(s) at /vpn/ipsec-site-connections, vpnservice(s), ikepolicy/ikepolicies, ipsecpolicy/ipsecpolicies, endpoint_group(s) at /vpn/endpoint-groups | openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:17-19 and the sibling vpn_*.py files |
query parameters accepted as filters (psk, peer_address, status, admin_state_up, external_v4_ip…) | openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:27-47, openstacksdk:openstack/network/v2/vpn_service.py:30-41 |
project_id with tenant_id alias; dpd as an object; peer_cidrs as a list | openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:62, openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:107-109 |
PUT with partial bodies (only changed attributes) | openstacksdk commit [inferred from allow_commit = True, openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:23] |
the agents (if kept unchanged) require the RPC contract of 03 § RabbitMQ: topics, method names, argument names, version 1.0, reply shapes including the ORM-shaped gw_port/transit_port dicts | neutron_vpnaas/services/vpn/device_drivers/ipsec.py:429-455, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:120-179 |
Neutron itself expects the callbacks (router interface/gateway/subnet/port/network delete protections) and the ovn_maintenance_periodics hook | 05 § 12 |
5. Database strategy
- Reuse the schema (it matches the migrations exactly). Keep the enum value lists and their order;
ipsecpeercidrs.cidr VARCHAR(32)should be widened to 43+ for IPv6 (or peer CIDRs stored only through endpoint groups). - Integrity is in FKs (unlike Keystone);
routervpnagentbindings.vpn_agent_idhas no FK because OVN agents are not rows. - No locking: concurrency relies on the unique keys
vpn_ext_gws.router_idandroutervpnagentbindings.router_id, and on retrying the whole transaction (RetryRequest). - Statuses are free strings (
VARCHAR(16)); values used:PENDING_CREATE,ACTIVE,DOWN,ERROR, andPENDING_DELETEfor gateways. - The neutron-vpnaas tables reference Neutron core tables (
routers,subnets,ports,networks,flavors): a separate OpenKeel VPN service must either share Neutron's database or replace these FKs by API lookups and clean-up hooks (router/subnet deletion protections are callbacks today).
6. Authentication, policies, configuration and CLI
- Policies: 20 rules,
ADMIN_OR_PROJECT_MEMBER, project scope (_rewrite/policies_stable-2026.2.yaml). The scheduler API has no rules: the rewrite should defineget_vpn-routers,create_vpn-router,delete_vpn-router,get_vpn-agentsas admin-only and return 403 (the original returns 500 to non-admins). - Configuration: server
[service_providers] service_provider = VPN:<name>:<driver>:default,vpn_scheduler_driver,vpn_auto_schedule,allow_automatic_vpnagent_failover,agent_down_time(Neutron); agent options in the two.inisamples. - Start-up guard: the server refuses to start if a VPN service references a removed provider (
neutron_vpnaas/services/vpn/plugin.py:87-111). - Root helper: the agents need root (rootwrap filters
etc/neutron/rootwrap.d/vpnaas.filters), namespace creation,mount --bindinside the namespace, and theipsecbinaries.
7. Conformance strategy
- Replay
exchanges-ovn_stable-2026.2.jsonagainst the rewrite in a fresh database, in order: for each step compare status,NeutronError.type, and the response body with UUIDs and timestamps abstracted (same approach astools/summarize.py); thesetup-*steps create Neutron networks/routers and need a Neutron (or equivalent) API. - Validate every success body against
openapi_stable-2026.2.jsoncomponent schemas. - RPC: if the agents are kept, compare the emitted frames of each step (
rpcarrays) by (mode, topic, server, method, args); if the agents are replaced, the frames define the information each node must receive. - Algorithms: scheduling outcomes must be in the valid set (§ 3.4); status reconciliation can be unit-tested from § 3.3. Original test fixtures for reference:
neutron_vpnaas/tests/unit/db/vpn/test_vpn_db.py,neutron_vpnaas/tests/unit/services/vpn/service_drivers/test_ovn_ipsec.py,neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py[not replayed].
8. Gaps found while building the pack
vpn-aes-ctradvertised but CTR rejected (§ 3.1; observed).- OVN:
ah-espaccepted (observed), no rendering for it. - OVN: router gateway can be cleared under an active VPN service (observed 200); legacy refuses (409).
- OVN: failed
POST vpnservicesleaves anERRORrow behind (observed). - Scheduler API: 500 for non-admins (missing rules), six routes without controller methods (500), duplicate
POSTbinding returns 201. ikepolicies/ipsecpoliciesroutes registered three times (byvpnaas,vpn-aes-ctr,vpn-no-sha1-3des,neutron_vpnaas/extensions/vpn_aes_ctr.py:26-37,neutron_vpnaas/extensions/vpn_no_sha1_3des.py:26-37); the first wins; harmless but 20 shadowed routes.- Dead code:
VPNStateInvalidToUpdatereference inset_vpnservice_status(neutron_vpnaas/db/vpn/vpn_db.py:522),RouterHasVPNExternal, Libreswan_get_nexthop,agent_updated(no handler),AZVPNAgentSchedulerDbMixin.get_router_availability_zonesusesrouter.vpn_agents, a relationship that does not exist (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:395-396). - api-ref outdated:
sha1/3desstill listed and used in samples; missing 409 on PUT policy, 400 on endpoint-group delete in use, scheduler API absent. - PSK readable by every project member through
GETand filterable.
9. Non-functional requirements
- API latency:
POST vpnserviceson OVN performs ~10 core/L3 plugin calls (ports, network, subnet, router interface) in one request;POST connectionwith an FQDN does a synchronous DNS lookup. vpnservice_added_to_agentblocks the API thread up to 2 ×rpc_response_timeout.- Agent polling: 3 processes per router per minute (
ip netns exec, wrapper,ipsec). - Failover detection: between
agent_down_timeandagent_down_time + agent_down_time // 2seconds after an agent stops (75–112 s with defaults), only withallow_automatic_vpnagent_failover. - Secrets: PSK in clear in DB, API, RPC; base64 (not encryption) in
ipsec.secrets(0600).
10. Rewrite checklist
- [ ] Serve the 25 VPN operations (+4 scheduler operations, admin-only) with the attribute maps of
routes-ovn_stable-2026.2.json; decide on CTR (§ 3.1). - [ ] Error bodies
NeutronError{type,message,detail}with the exact types and messages ofobserved-errors_stable-2026.2.md; 404 for foreign resources; 403 vs 404 per role. - [ ] Validation chain and order of § 3.2, including DNS resolution and IP-version checks; reject
ah-espon every driver. - [ ]
PENDING_CREATEon create; no status change on update; pending-state update refusal; server status rule § 3.3. - [ ] In-use protections: policies (409), endpoint groups (400), services (409); router interface / subnet / port / network / extra-route / gateway protections (decide whether the OVN gateway-clear protection should exist).
- [ ] OVN gateway model § 3.5 (gateway port, transit network, extra routes) with retry on concurrent creation and clean-up of the last service; delete the
ERRORrow on failed creation. - [ ] Agent registration and liveness (SB
Chassis_Privatekeys or a Kubernetes-native equivalent), scheduling § 3.4, failover § 05 § 7. - [ ] Agent data plane: namespace/pod per router, strongSwan configuration § 3.6, status reporting § 3.3.
- [ ] Policies file compatible with
policies_stable-2026.2.yaml(+ scheduler rules). - [ ] Replay
exchanges-ovn_stable-2026.2.json(§ 7) and validate againstopenapi_stable-2026.2.json.
Source: reverse-spec-neutron-vpnaas/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.