Rewrite pack — ovn-octavia-provider — branch stable/2026.2
Branch
stable/2026.2@c56940998594(11.0.0-2-gc569409) — repository https://opendev.org/openstack/ovn-octavia-provider — analysed on 2026-10-28
This pack is for re-implementing the OVN load-balancing back end of Octavia, for example in Go for OpenKeel, while keeping Octavia, Neutron (OVN mechanism driver) and existing OVN databases working unchanged. The provider has no REST API, database or policy of its own, so most of the usual extractors do not apply: the contracts here are the driver interface, the OVN NB row layout and the driver-agent socket messages.
References: octavia: Octavia stable/2026.2 @ 2900ed67cac2; octavia-lib: octavia-lib stable/2026.2 @ a46b0ff4fa95; neutron: Neutron stable/2026.2 @ eef1a0f7ee36.
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[ovn-nb-contract_stable-2026.2.json](_rewrite/ovn-nb-contract_stable-2026.2.json) | every external_ids key, column format, Neutron port naming/ownership, supported protocols/algorithms/HM types, request types and the event lock name; generated from ovn_octavia_provider/common/constants.py plus the formats sourced in 01 § 1.2 |
[cases-frame-vip-ips_stable-2026.2.json](_rewrite/cases-frame-vip-ips_stable-2026.2.json) | 57 recorded input/output vectors of the vips computation (§ 3.1), each with its source test |
[replay-report_stable-2026.2.json](_rewrite/replay-report_stable-2026.2.json) | proof that the vectors pass against the original code: 57/57 |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | oslo-config sample of the octavia.api.drivers.ovn namespace ([ovn], deprecated [neutron] options) |
[tools/ovn_octavia_provider.py](_rewrite/tools/ovn_octavia_provider.py) | conformance_capture.py adapter used to record and replay the vectors |
Extraction environment: Python 3.13 venv with the project and test-requirements.txt under the 2026.2 upper constraints: octavia-lib 3.12.0, ovsdbapp 2.19.0, ovs 3.7.1, neutron-lib 5.0.1, openstacksdk 4.20.0, futurist 3.5.0, tenacity 9.1.4. Not produced, because the project has none: routes, OpenAPI, request schemas, DDL, policies, gabbi conformance, HTTP exchanges.
2. Behaviour the contracts do not show
2.1 Where the code runs
| Role | Host process | Entry point | Must keep |
|---|---|---|---|
| provider driver | inside octavia-api (Python plugin) | octavia.api.drivers = ovn (setup.cfg:32-33) | the octavia-lib ProviderDriver method signatures; synchronous validation; fast return |
| provider agent | inside octavia-driver-agent | octavia.driver_agent.provider_agents = ovn (setup.cfg:38-39) | OVSDB event handling with a cluster-wide lock; maintenance tasks |
| sync tool | CLI or Neutron sync plugin | setup.cfg:35-36, setup.cfg:44-45 | repair from the Octavia DB |
Because the driver must be a Python class loaded by stevedore in octavia-api, a Go rewrite needs either a thin Python shim that forwards each driver call to a Go service (and maps its errors back to octavia-lib exceptions, § 2.3), or a re-implementation of the Octavia API itself. The asynchronous part (queue, OVSDB writes, events, status reports) can move entirely to a Go service that also serves as provider agent, provided it talks to the driver-agent sockets (§ 2.2) or to the Octavia DB directly.
2.2 Status and get messages
Framing, keys and statuses are in 03 § 2. Rules a rewrite must keep:
- report every object touched with its new provisioning status; Octavia leaves a parent in
PENDING_UPDATEuntil the LB is reportedACTIVE, so every child operation also reports the LBACTIVE(for exampleovn_octavia_provider/helper.py:2128-2130) [inferred: Octavia API locking]; DELETEDdeletes the Octavia record (octavia:octavia/api/drivers/driver_agent/driver_updater.py:99-113); report it even when the OVN side was already gone (ovn_octavia_provider/helper.py:1778-1781,ovn_octavia_provider/helper.py:2179-2181,ovn_octavia_provider/helper.py:4116-4122);- strip
:Dfrom every value before sending (ovn_octavia_provider/helper.py:100-107); - retry on failure (15 attempts, exponential ≤ 75 s,
ovn_octavia_provider/helper.py:448-451). A rewrite should not block the whole queue during those retries (current behaviour, § 8).
2.3 Errors returned synchronously
Octavia maps exception classes, not codes (02 § 1). A shim must raise: UnsupportedOptionError (→ 501) for every refusal listed in 02 § 2 with the same user_fault_string; Conflict (→ 409) for a VIP address in use; DriverError (→ 500) for back-end failures. Keep the messages: tempest and users match on them [inferred].
2.4 Ordering
Requests of one driver call are executed in order (FIFO queue, ovn_octavia_provider/helper.py:408-437), and some calls depend on it (pool delete = HM delete, member delete, DVR, pool delete, ovn_octavia_provider/driver.py:263-275). Different API calls run in different threads without ordering (§ 8).
3. Core algorithms
3.1 vips computation (_frame_vip_ips)
Input: E = the external_ids being written (copy of the row with the change applied), S = neutron:member_status of the row before the change (ovn_octavia_provider/helper.py:1377-1378, ovn_octavia_provider/helper.py:2608-2623).
if E['enabled'] == 'False': return {} # ovn_octavia_provider/helper.py:1383-1384
VIPS = [E['neutron:vip']] + split(E['neutron:additional_vips']) # ovn_octavia_provider/helper.py:1385-1390
FIP = E.get('neutron:vip_fip'); AFIPS = split(E.get('neutron:additional_vip_fips'))
result = {}
for key, value in E (map iteration order):
if not key contains 'listener_' or key ends with ':D': continue # ovn_octavia_provider/helper.py:1396-1399
(port, pool_key) = value.split(':') if exactly 2 parts else skip # ovn_octavia_provider/helper.py:1343-1348, 1401-1403
if pool_key empty or E[pool_key] missing or empty: continue # ovn_octavia_provider/helper.py:1405-1406
v4, v6 = [], []
for member 'member_<id>_<ip>:<mport>_<subnet>' in E[pool_key].split(','):
if S.get(id) == 'OFFLINE': continue # ovn_octavia_provider/helper.py:1412
append '[ip]:mport' to v6 if IPv6 else 'ip:mport' to v4 # ovn_octavia_provider/helper.py:1413-1417
for vip in VIPS:
if v4 and vip is IPv4: result[vip+':'+port] = ','.join(v4)
if v6 and vip is IPv6: result['['+vip+']:'+port] = ','.join(v6) # ovn_octavia_provider/helper.py:1419-1426
if v4 and FIP is IPv4: result[FIP+':'+port] = ','.join(v4) # ovn_octavia_provider/helper.py:1428-1430
for f in AFIPS: if v4 and f is IPv4: result[f+':'+port] = ','.join(v4) # ovn_octavia_provider/helper.py:1432-1437
return result
Rules that matter:
- the pool key referenced by a listener is always the enabled form
pool_<id>; a disabled pool is stored aspool_<id>:D, so it is not found and contributes nothing (ovn_octavia_provider/helper.py:1307-1311,ovn_octavia_provider/helper.py:2541-2556); - members with status
ERRORstay invips: OVN's own health check removes them from the data path; - member order inside a value is insertion order (append on create,
ovn_octavia_provider/helper.py:2751-2753); OVN does not care about it, so a comparison should be order-insensitive; - the column is cleared then set in the same transaction (
ovn_octavia_provider/helper.py:1444-1447); in sync mode it is written only when different (ovn_octavia_provider/helper.py:1442-1443).
Test cases: 57 vectors recorded from 563 unit tests of ovn_octavia_provider/tests/unit/test_helper.py and ovn_octavia_provider/tests/unit/test_driver.py; 25 with a non-empty result, 50 with a VIP FIP, 6 with additional VIPs, 3 with IPv6, 5 with disabled children, 1 with an OFFLINE member, 1 with a disabled LB. Format: input.row_external_ids (only neutron:member_status is read), input.lb_external_ids, expected. Replay: 57/57 pass against the original (replay-report_stable-2026.2.json). Not covered: additional FIPs, an IPv6 FIP (ignored by design).
3.2 OVN row selection per protocol (_get_or_create_ovn_lb)
One Octavia LB = 1..3 OVN rows with the same name (ovn_octavia_provider/helper.py:849-915):
- rows = all
Load_Balancerwithname = lb_id(ovn_octavia_provider/helper.py:831-832); - a row whose
protocolcontains the (lower-case) protocol → use it; - else a row with an empty
protocol→ set the protocol and use it; - else create a new row by copying
neutron:vip,neutron:vip_port_id,lr_ref,ls_refs, additional VIPs (re-read through the get socket), FIPs andadmin_state_upfrom the first row, and running the full LB creation (switch/router association) for it (ovn_octavia_provider/helper.py:879-912).
The reverse (_clean_lb_if_empty, ovn_octavia_provider/helper.py:223-242): a row without any listener*/pool* key loses its protocol if it is the only row, and is deleted otherwise.
3.3 Logical switch reference counting (ls_refs)
ls_refs counts why the LB is on a switch (ovn_octavia_provider/helper.py:980-1108):
- VIP association:
+1, plus+1per additional VIP (ovn_octavia_provider/helper.py:1062-1066); - each member:
+1on its subnet's switch (ovn_octavia_provider/helper.py:2773-2775); member delete:-1(ovn_octavia_provider/helper.py:2888-2890); - at
0(ref_ct == 1before decrement) the key is removed and the LB is removed from the switch (ovn_octavia_provider/helper.py:1078-1083); - provider-network switches are never associated (
ovn_octavia_provider/helper.py:1022-1030); - router-interface events add or remove switches without touching
ls_refs(update_ls_ref=False,ovn_octavia_provider/helper.py:330-334); - sync mode keeps the count when the LB is already on the switch (
ovn_octavia_provider/helper.py:1031-1051).
Router association adds the LB to the router and to every non-gateway switch of the router with the same IP family as the VIP; on removal, switches still reachable through another router of lr_ref are kept (ovn_octavia_provider/helper.py:1110-1202, ovn_octavia_provider/helper.py:1245-1258).
3.4 Operating-status derivation
_get_current_operating_statuses (ovn_octavia_provider/helper.py:4284-4407), formal rules:
- members: one entry per key of
neutron:member_status, provisioningACTIVE; - pool:
OFFLINEifadmin_state_upis false **orneutron:member_statusis empty**;ERRORif all its members areERROR;DEGRADEDif any isERROR; elseONLINE; members without a status count asONLINE(ovn_octavia_provider/helper.py:4337-4357); - listener:
OFFLINEif disabled;ERRORif any of its pools isERROR;DEGRADEDif any isDEGRADED; elseONLINE(ovn_octavia_provider/helper.py:4365-4385); - LB:
OFFLINEif disabled;ERROR/DEGRADEDif any listener is; elseONLINE(ovn_octavia_provider/helper.py:4393-4405); - admin states come from Octavia (get socket), not from the
:Dsuffixes.
Note that an empty neutron:member_status makes every pool of the row OFFLINE, even pools that have members (ovn_octavia_provider/helper.py:4348-4349).
3.5 Member operating status
| Event | New status | Where |
|---|---|---|
| create, disabled | OFFLINE | ovn_octavia_provider/helper.py:2836-2837 |
| create, row has health checks, mapping OK | ONLINE (else ERROR) | ovn_octavia_provider/helper.py:2838-2847 |
| create, no health check | NO_MONITOR | ovn_octavia_provider/helper.py:2834 |
| update, enabled, HM | ERROR if last was ERROR, else ONLINE | ovn_octavia_provider/helper.py:2983-2991 |
| HM create / HM delete | all pool members ONLINE / NO_MONITOR | ovn_octavia_provider/helper.py:3981-3985, ovn_octavia_provider/helper.py:4147-4149 |
Service_Monitor update offline / other / row deleted | ERROR / ONLINE / OFFLINE | ovn_octavia_provider/helper.py:4438-4443 |
3.6 Member batch diff
Key = member_<id>_<ip>:<port>_<subnet>; in the new list and in OVN → update; only in the new list → create; only in OVN → delete + DVR clean-up (ovn_octavia_provider/driver.py:551-591). The subnet of a member without one is the VIP subnet when the address fits (ovn_octavia_provider/driver.py:527-536).
4. What the clients expect
| Expectation | Where the client relies on it |
|---|---|
Neutron finds the LBs of a VIP port by external_ids['neutron:vip_port_id'] == <LSP name> when a FIP is set on the VIP, and walks pool_* values split on _ (4 parts: member, id, ip:port, subnet) to centralise member FIPs | neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:1137-1185 |
Neutron recognises VIP ports by the name prefix ovn-lb-vip-; for an unbound VIP port without device_owner it publishes no addresses on the LSP | neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:677-680, neutron:neutron/common/ovn/constants.py:313 |
Neutron treats ports with device_owner = ovn-lb-hm:distributed and device_id starting with ovn-lb-hm as HM ports (localport) | neutron:neutron/common/ovn/utils.py:831-833, neutron:neutron/common/ovn/constants.py:86 |
VIP ports must not end in :distributed (would become localport) | ovn_octavia_provider/common/constants.py:40-44 |
Neutron writes neutron:port_fip on the VIP LSP; the provider reacts to it | ovn_octavia_provider/event.py:62-76 |
Neutron port-forwarding LBs live in the same table, marked neutron:device_owner = port_forwarding_plugin; never touch them | ovn_octavia_provider/helper.py:921-924 |
| Octavia driver agent accepts the status keys and statuses of 03 § 2.2 | octavia:octavia/api/drivers/driver_agent/driver_updater.py:129 |
Octavia call_provider maps exception classes to 409/500/501 | octavia:octavia/api/drivers/utils.py:38-84 |
Since VIP ports now get device_owner = ovn-lb:vip, the Neutron "no addresses for unbound VIP ports" branch no longer applies to them (neutron:neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py:677-680) [inferred: effect on ARP for the VIP not checked].
5. Database strategy
There is no database to migrate: the OVN NB is the database. A rewrite that must take over existing deployments reads and writes the same rows and keys (ovn-nb-contract_stable-2026.2.json), including the legacy forms the maintenance tasks still repair (IPv6 without brackets, HM ports with network:distributed, VIP ports without owner, 05 § 4). There is no optimistic concurrency: OVSDB supports wait operations and the ovsdbapp Load_Balancer row _version, which a rewrite could use to make the read-modify-write of external_ids safe (03 § 1.3). The Octavia DB stays the source of truth for admin states and the object graph.
6. Authentication, configuration and CLI
- Configuration (
config_stable-2026.2.conf):[ovn] ovn_nb_connection,ovn_nb_private_key,ovn_nb_certificate,ovn_nb_ca_cert, the same for SB,ovsdb_connection_timeout(180),ovsdb_retry_max_interval(180),ovsdb_probe_interval(60000);[neutron]keystoneauth session/adapter/auth options (registered at runtime, not in the sample:ovn_octavia_provider/common/config.py:144-163), with deprecatedendpoint,endpoint_type,ca_certificates_file;[service_auth]from Octavia. Missing[neutron]auth options inherit[service_auth](ovn_octavia_provider/common/clients.py:49-102). Hard-coded: maintenance spacing 600 s, status retries, queue timeout 5 s, driver-agent socket paths (octavia-lib defaults). - CLI:
octavia-ovn-db-sync-util [--debug]reads Octavia's configuration (ovn_octavia_provider/cmd/octavia_ovn_db_sync_util.py:24-59); Neutron sync pluginoctavia_ovn_sync(05 § 5). - Policies: none; Octavia enforces RBAC before the driver is called.
7. Conformance strategy
- Vectors: run
cases-frame-vip-ips_stable-2026.2.jsonagainst the rewrite'svipsfunction, comparing the maps with order-insensitive member lists. - Functional tests of the original need a real OVN (
ovsdb-serverwith NB/SB schemas, ovn-northd):ovn_octavia_provider/tests/functional/base.pydrives the driver against it. They were not run (no OVN on the analysis host). They are the best source for a black-box suite: run them once with recording of every NB transaction and status message, then replay the same driver calls against the rewrite and compare the resulting NB rows (ovsdb-client dump) and status sequences. - Tempest:
octavia-tempest-pluginwithprovider=ovn(zuul.d/project.yaml) is the end-to-end suite. - Differential checks: for a given Octavia graph, the rewrite must produce the same
Load_Balancerrows (name, protocol,external_ids,vips,selection_fields,options,ip_port_mappings) andLoad_Balancer_Health_Checkrows, and the same status-socket messages.
8. Gaps, risks found while building the pack
- One driver instance, NB connection and thread per Octavia API call (
octavia:octavia/api/drivers/driver_factory.py:43,ovn_octavia_provider/helper.py:55-68): connection churn on ovsdb-server and unordered concurrent writers. A rewrite should keep one long-lived connection and a per-LB serial queue. - Status retries block the single queue for up to ~15 min (
ovn_octavia_provider/helper.py:448-451). pool_updateignoreslb_algorithmchanges (ovn_octavia_provider/driver.py:282-295);selection_fieldsandaffinity_timeoutare per OVN row, shared by all pools of a protocol (ovn_octavia_provider/helper.py:2364-2366).- Unsupported session-persistence types produce a 500 (
AttributeError) instead of a 501 (ovn_octavia_provider/driver.py:79-80). - Exceptions in
_get_or_create_ovn_lbleave listeners/pools inPENDING_CREATE(02 § 3). - Health-monitor
admin_state_up=falsedoes not disable OVN health checks (01 § 6.1). - The keystoneauth options of
[neutron]are missing from the generated sample (ovn_octavia_provider/common/config.py:186-190). - Not extracted:
_get_current_operating_statusesvectors (its inputs include Octavia objects read from mocks), NB transaction recordings (no OVN available).
9. Non-functional requirements
- Latency: driver methods must return quickly; only
create_vip_portand member subnet lookups make synchronous Neutron and socket calls. - HA: several agents may run; OVSDB events are processed by the lock holder only; the queue is per process and not persisted, so requests queued in a process that dies are lost and the objects stay
PENDING_*until a sync [inferred]. - Scale: every status computation costs 1–3 socket calls per affected OVN row;
_find_ovn_lb_with_pool_keyscans allLoad_Balancerrows (ovn_octavia_provider/helper.py:917-926);_find_lr_from_lspscans all routers (ovn_octavia_provider/helper.py:1285-1299).
10. Rewrite checklist
- [ ] Python shim implementing
ProviderDriver, with the same validations and exception classes/messages (§ 2.3, 02 § 2). - [ ] VIP port creation with names,
device_owner,device_id, conflict reuse and clean-up (01 § 2.1). - [ ] OVN NB writer reproducing every key of
ovn-nb-contract_stable-2026.2.json, one row per protocol (§ 3.2),ls_refs/lr_ref(§ 3.3). - [ ]
vipsfunction passing the 57 vectors (§ 3.1). - [ ] Health checks:
Load_Balancer_Health_Checkper VIP and FIP, HM source ports per subnet,ip_port_mappingswith IPv6 brackets. - [ ] Status reporting with the derivation rules (§ 3.4, § 3.5) and
DELETEDsemantics (§ 2.2). - [ ] Event handling with the OVSDB lock: router ports, VIP LSP FIP changes, SB
Service_Monitor. - [ ] Member FIP centralisation (
NATclear) and de-centralisation (empty FIP update) when the VIP has a FIP. - [ ] Maintenance: legacy data repair, or a one-off migration before switching.
- [ ] Sync/repair from the Octavia DB, including orphan health-check purge.
- [ ] Leave port-forwarding
Load_Balancerrows untouched.
Source: reverse-spec-ovn-octavia-provider/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.