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

Reverse-spec · Services

ovn-octavia-provider — Rewrite pack

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/

FileUse 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

RoleHost processEntry pointMust keep
provider driverinside octavia-api (Python plugin)octavia.api.drivers = ovn (setup.cfg:32-33)the octavia-lib ProviderDriver method signatures; synchronous validation; fast return
provider agentinside octavia-driver-agentoctavia.driver_agent.provider_agents = ovn (setup.cfg:38-39)OVSDB event handling with a cluster-wide lock; maintenance tasks
sync toolCLI or Neutron sync pluginsetup.cfg:35-36, setup.cfg:44-45repair 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_UPDATE until the LB is reported ACTIVE, so every child operation also reports the LB ACTIVE (for example ovn_octavia_provider/helper.py:2128-2130) [inferred: Octavia API locking];
  • DELETED deletes 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 :D from 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 as pool_<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 ERROR stay in vips: 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):

  1. rows = all Load_Balancer with name = lb_id (ovn_octavia_provider/helper.py:831-832);
  2. a row whose protocol contains the (lower-case) protocol → use it;
  3. else a row with an empty protocol → set the protocol and use it;
  4. 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 and admin_state_up from 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 +1 per additional VIP (ovn_octavia_provider/helper.py:1062-1066);
  • each member: +1 on 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 == 1 before 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, provisioning ACTIVE;
  • pool: OFFLINE if admin_state_up is false **or neutron:member_status is empty**; ERROR if all its members are ERROR; DEGRADED if any is ERROR; else ONLINE; members without a status count as ONLINE (ovn_octavia_provider/helper.py:4337-4357);
  • listener: OFFLINE if disabled; ERROR if any of its pools is ERROR; DEGRADED if any is DEGRADED; else ONLINE (ovn_octavia_provider/helper.py:4365-4385);
  • LB: OFFLINE if disabled; ERROR/DEGRADED if any listener is; else ONLINE (ovn_octavia_provider/helper.py:4393-4405);
  • admin states come from Octavia (get socket), not from the :D suffixes.

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

EventNew statusWhere
create, disabledOFFLINEovn_octavia_provider/helper.py:2836-2837
create, row has health checks, mapping OKONLINE (else ERROR)ovn_octavia_provider/helper.py:2838-2847
create, no health checkNO_MONITORovn_octavia_provider/helper.py:2834
update, enabled, HMERROR if last was ERROR, else ONLINEovn_octavia_provider/helper.py:2983-2991
HM create / HM deleteall pool members ONLINE / NO_MONITORovn_octavia_provider/helper.py:3981-3985, ovn_octavia_provider/helper.py:4147-4149
Service_Monitor update offline / other / row deletedERROR / ONLINE / OFFLINEovn_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

ExpectationWhere 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 FIPsneutron: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 LSPneutron: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 itovn_octavia_provider/event.py:62-76
Neutron port-forwarding LBs live in the same table, marked neutron:device_owner = port_forwarding_plugin; never touch themovn_octavia_provider/helper.py:921-924
Octavia driver agent accepts the status keys and statuses of 03 § 2.2octavia:octavia/api/drivers/driver_agent/driver_updater.py:129
Octavia call_provider maps exception classes to 409/500/501octavia: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 deprecated endpoint, 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 plugin octavia_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.json against the rewrite's vips function, comparing the maps with order-insensitive member lists.
  • Functional tests of the original need a real OVN (ovsdb-server with NB/SB schemas, ovn-northd): ovn_octavia_provider/tests/functional/base.py drives 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-plugin with provider=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_Balancer rows (name, protocol, external_ids, vips, selection_fields, options, ip_port_mappings) and Load_Balancer_Health_Check rows, 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_update ignores lb_algorithm changes (ovn_octavia_provider/driver.py:282-295); selection_fields and affinity_timeout are 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_lb leave listeners/pools in PENDING_CREATE (02 § 3).
  • Health-monitor admin_state_up=false does 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_statuses vectors (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_port and 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_key scans all Load_Balancer rows (ovn_octavia_provider/helper.py:917-926); _find_lr_from_lsp scans 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).
  • [ ] vips function passing the 57 vectors (§ 3.1).
  • [ ] Health checks: Load_Balancer_Health_Check per VIP and FIP, HM source ports per subnet, ip_port_mappings with IPv6 brackets.
  • [ ] Status reporting with the derivation rules (§ 3.4, § 3.5) and DELETED semantics (§ 2.2).
  • [ ] Event handling with the OVSDB lock: router ports, VIP LSP FIP changes, SB Service_Monitor.
  • [ ] Member FIP centralisation (NAT clear) 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_Balancer rows 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.