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

Reverse-spec · Services

octavia — Rewrite pack

Rewrite pack — octavia — branch stable/2026.2

Branch stable/2026.2 @ 2900ed67cac2 (19.0.0-19-g2900ed67) — repository https://opendev.org/openstack/octavia — analysed on 2026-11-01

This pack is for serving the Octavia v2 API from Go in OpenKeel: behind the Neutron endpoint, with openkeel-ovn-octavia-provider as only back end, Python Octavia not running. Files 01 to 05 describe the behaviour; this one says how to use the contracts, and what only the code tells.

References: openstacksdk: fbdc40381, gophercloud: 4aae510, cloud-provider-openstack: b210c18, openkeel-ovn-octavia-provider: working copy (not a git repository), all in the workspace; reverse-spec-ovn-octavia-provider/ is the provider's specification.

1. Contents of _rewrite/

FileUse in a rewrite
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json)all 297 route entries; canonical, alias_of, handler, policies, success status, request and response types
[route-probe_stable-2026.2.json](_rewrite/route-probe_stable-2026.2.json)each entry requested on the real application, plus paths and methods that are not routes
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json), [openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json)136 WSME types as JSON-schema-like objects with x-source, x-type-to-db-map; OpenAPI 3.0.3 of the canonical routes. Permissive: most rules are in the controllers
[response-schemas_stable-2026.2.json](_rewrite/response-schemas_stable-2026.2.json), [response-schemas_stable-2026.2.md](_rewrite/response-schemas_stable-2026.2.md)response bodies inferred from the run, per operation and status
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json)the conformance suite: 8 691 HTTP exchanges with database deltas, identity, mock tapes, provider and network calls (§ 7)
[fixture-seeds_stable-2026.2.json](_rewrite/fixture-seeds_stable-2026.2.json)per test: rows and configuration at its first request
[replay-report_stable-2026.2.json](_rewrite/replay-report_stable-2026.2.json)proof of the suite on the original: counts and each difference
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md)every (operation, status, code, message), templated
[http-probes_stable-2026.2.json](_rewrite/http-probes_stable-2026.2.json)cross-cutting behaviour with bodies: parsing, negotiation, pagination, filters, fields
[amphora-empty_stable-2026.2.json](_rewrite/amphora-empty_stable-2026.2.json)what the amphora routes answer with no amphora
[cases-driver-seam_stable-2026.2.json](_rewrite/cases-driver-seam_stable-2026.2.json)40 ordered vectors of status, statistics and get messages, with the row changes
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml), [rbac-matrix_stable-2026.2.json](_rewrite/rbac-matrix_stable-2026.2.json)97 rules; decisions of 81 rules × 9 personas × own/other project × 5 modes
[schema-mysql-migrated_stable-2026.2.sql](_rewrite/schema-mysql-migrated_stable-2026.2.sql)the schema to create: octavia-db-manage upgrade heads on MariaDB 11.4, revision fabf4983846b
[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 models; 27 differences
[lookup-tables_stable-2026.2.json](_rewrite/lookup-tables_stable-2026.2.json)rows of the migrated empty database: enum tables and DELETED-PLACEHOLDER rows
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json)74 documented operations with their parameters
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf)oslo-config sample: pick [api_settings], [networking], [quotas], [driver_agent], [oslo_policy], [database]
[tools/](_rewrite/tools)adapter, probes, generators, to re-extract on a later commit

Extraction environment: Python 3.13.0 venv, upper constraints of requirements stable/2026.2 @ 3b75407b: WSME 0.13.0, Pecan 1.8.0, SQLAlchemy 2.0.52, oslo.db 18.1.1, octavia-lib 3.12.0, WebTest 3.0.7, against the repository checkout. Tests ran with PYTHONDONTWRITEBYTECODE=1; the source tree is untouched.

2. HTTP behaviour that the contracts do not show

01 § 1 and § 3 have the rules, 02 § 1 the bodies. The ones a Go router will not reproduce by accident:

  1. Same handlers under /v2, /v2.0, with and without /lbaas. Trailing slash accepted.
  2. Three error bodies, chosen by where the error is raised (02 § 1). Reproduce format A everywhere except: parent-not-found on /pools/{id}/members* and /l7policies/{id}/rules*, limit and sort direction errors, routing errors, 406. Their order relative to policy matters: hooks and _lookup run first, so a member URL of an unknown pool gives 404 even to someone unauthorised.
  3. Unset vs null in Go: decode to a presence-tracking type; a plain *string is enough for fields only if the decoder sets "present" separately.
  4. limit=0 and negative mean unlimited; fields=a,b selects nothing.
  5. DELETE on quotas and PUT on quotas answer 202 with a body.
  6. Response key order is irrelevant; tenant_id always present; empty name/description as "", unset optional attributes as null; null attributes are kept in the body (WSME renders them).
  7. provider read as octavia when the column is empty (octavia/api/v2/types/load_balancer.py:92-93).
  8. Text echo: Value: '<python repr>', <list repr>, set order. Implement stable forms, and let the conformance comparison normalise them (§ 7).

3. The seam with the Go provider

3.1 Current shape of the provider

openkeel-ovn-octavia-provider exposes POST /v1/driver/{method} with a bearer token on port 9877 (openkeel-ovn-octavia-provider:internal/api/api.go:137), for the 19 methods it implements (openkeel-ovn-octavia-provider:internal/api/api.go:37), with bodies {object, old, new, cascade, pool_id, members, lb_id, project_id, vip, additional_vips} where objects are to_dict(recurse=True, render_unsets=False) (openkeel-ovn-octavia-provider:internal/api/api.go:21-33). It sends status and reads objects on Unix sockets with the driver-agent framing (openkeel-ovn-octavia-provider:internal/driverlib/driverlib.go:123-124). The Python shim exists only for upstream octavia-api and disappears.

Decisions for the Go API [inferred, to confirm when building it]:

TopicUpstreamGo API
driver callPython method in-processHTTP to :9877 or a Go interface if linked in. Same bodies; each call runs inside the open DB transaction, so bound the timeout (the shim's default is 60 s)
fault mappingcall_provider (5 classes, 02 § 2)map the provider's fault kinds to these 5, with user_fault_string
enabled_provider_driversstevedorea static map: {"ovn": "<description>"}; default_provider_driver = ovn
GET /providersfrom configone entry; description text is a deployment choice
capabilitiesget_supported_*_metadatathe provider inherits the base class: 501 Provider 'ovn' does not support a requested action: …; flavors with flavor_data → validate_flavor 501
status, stats, getoctavia-driver-agentthe Go API (or a sidecar with its DB credentials) must listen on the three sockets with the same framing; or the provider gets a direct interface. Vectors: cases-driver-seam
VIPcreate_vip_port then loadbalancer_createkeep both calls and the cleanup on failure (octavia/api/v2/controllers/load_balancer.py:582-585, octavia/api/v2/controllers/load_balancer.py:671-678)
octavia_ownedset by the API when it made the port, or from the driver's dict (octavia/api/drivers/utils.py:615-616); drives deallocate_vip on DELETEDthe OVN provider does not set it and deletes its own port in lb_delete (ovn-octavia-provider:ovn_octavia_provider/helper.py:1788-1798), so the status path makes no Neutron call. A failed creation is still cleaned up by the API (delete_port): it needs Neutron access
TLSBarbican fetch, inline in driver objectsthe OVN provider rejects TERMINATED_HTTPS and TLS pools; API validation still runs first (04 § 5). A first version can return the provider's 501 after the non-Barbican checks and skip the Barbican path; list this as a known difference: the 400 Could not retrieve certificate becomes a 501 for bad refs
noop provider and networktest doublethe conformance suite needs a test mode with **noop_driver, noop_driver-alt, amphora, octavia as provider names** that accept everything (§ 7), plus a scriptable Neutron

3.2 What the provider supports vs what the API accepts

From the provider's specification (its 01): pool protocols TCP, UDP, SCTP; algorithms SOURCE_IP_PORT, SOURCE_IP; no L7, no TLS; failover → UnsupportedOptionError 501 Provider 'ovn' does not support a requested option: OVN provider does not support loadbalancer failover; L7 and flavors → NotImplementedError 501. These answers come after the API-level validation, the row locks and PENDING_*, and the rollback restores everything.

3.3 Status duty

The provider must report every object the API put in PENDING_*, parents included (03 § 2.3). It does (reverse-spec-ovn-octavia-provider/07-rewrite-pack_stable-2026.2.md § 2.2). Test both sides together with the end-to-end suite: nothing in exchanges exercises this, since the noop driver never reports and tests set statuses by DB delta.

4. What the clients expect

ExpectationWhere the client relies on it
paths with /lbaas/ and /octavia/amphorae, without version prefix in the code: the catalog URL gives /v2.0 or /v2…openstacksdk:openstack/load_balancer/v2/load_balancer.py:26, openstacksdk:openstack/load_balancer/v2/amphora.py:27, gophercloud:openstack/loadbalancer/v2/listeners/urls.go:6
**GET /lbaas/quotas/defaults** for default quotas (a route that does not exist as such, it reads project defaults)openstacksdk:openstack/load_balancer/v2/quota.py:71
error text under faultstringopenstacksdk:openstack/exceptions.py:200-201
polling provisioning_status until ACTIVE or ERROR: a 409 on a child write is retried by waitingcloud-provider-openstack:pkg/util/openstack/loadbalancer.go:55-56, cloud-provider-openstack:pkg/util/openstack/loadbalancer.go:184-188
cascade=true on deletegophercloud:openstack/loadbalancer/v2/loadbalancers/requests.go:234
GET / version list, highest = v2.30 CURRENT: feature gates on the version in cloud-provider-openstack [inferred], Heat, Magnumoctavia/api/root_controller.py:158

The catalog entry for service type load-balancer is not defined by this repository. Serving behind the Neutron endpoint gives a catalog URL such as <neutron>/load-balancer, or a path /v2.0/lbaas on Neutron's own port, like the old neutron-lbaas. Note that /v2.0/lbaas/… then coincides with Octavia's alias, and /v2.0/flavors collides with Neutron's own flavors API: the Go router should dispatch only /v2.0/lbaas/ and /v2/lbaas/-family paths from a shared listener, and the unprefixed family only on a dedicated path [inferred]. Also /load-balancer/ is known to the auth skip list (octavia/common/keystone.py:25-26): the upstream deployment under Apache uses this prefix; GET /load-balancer/ must work without a token.

5. Database strategy

  • Use the migrated schema (schema-mysql-migrated) and load lookup-tables: every status enum is an FK, the DELETED-PLACEHOLDER rows exist with NIL ids. Keep alembic_version = fabf4983846b if upgrades from Python are in scope; OpenKeel has none, so a Go migration tool may own the schema. Differences with the models: _rewrite/schema-diff_stable-2026.2.md, e.g. health_monitor.url_path is 255 in the database and 2048 in the model; the API validates only format, so a long path gets a DB error (500, APIException).
  • **listener.insert_headers is a Python pickle** (octavia/db/models.py:601); there is no production data in OpenKeel, so store JSON in a Go-owned schema. In the conformance seeds it is decoded to a JSON object.
  • ScalarListType columns are comma-joined text; empty list semantic follows tls_versions, alpn_protocols defaults.
  • Integrity kept by code, not FKs: listener_statistics has none; vip_security_group, tags by cascades.
  • Locks: 03 § 1.3. Galera: SELECT FOR UPDATE is local to a node and conflicts surface as deadlocks at commit (certification failures): DBDeadlock on the quota row → 503 Project busy, on the LB row → unhandled 500 in the Python code [inferred]. Retry or map it in Go.
  • Isolation: flavor and AZ delete use SERIALIZABLE to race with LB creates; an FK on load_balancer.flavor_id protects them anyway.
  • updated_at: SQLAlchemy onupdate on every status change; tests template timestamps.
  • in_use_* quota counters are stored, recomputed by COUNT when zero or NULL (octavia/db/repositories.py:481-488).

6. Authentication, policies, configuration

  • Auth: keystone by keystonemiddleware semantic (keystonemiddleware's reverse-spec), skip list /, /load-balancer/, /healthcheck, /load-balancer/healthcheck. noauth is needed for conformance.
  • Policies: reuse the OpenKeel Go policy engine with policies_stable-2026.2.json. Evaluate is_admin:True as context_is_admin or noauth. Support enforce_new_defaults=false and file overrides, for the five modes of 01 § 6; the matrix is a direct test table. Targets contain only project_id.
  • Configuration to keep: [api_settings]: auth_strategy, allow_pagination, allow_sorting, allow_filtering, allow_field_selection, pagination_max_limit, api_base_uri, allow_tls_terminated_listeners, allow_ping_health_monitors, allow_prometheus_listeners, enabled_provider_drivers, default_provider_driver, udp_connect_min_interval_health_monitor, healthcheck_enabled, healthcheck_refresh_interval, TLS defaults and minimum_tls_version, tls_cipher_prohibit_list; [networking]: allow_vip_*, valid_vip_networks, denylist_vip_networks, reserved_ips, allow_invisible_resource_usage; [quotas]: default_*_quota; [haproxy_amphora]: timeout_* (defaults only: they appear in the API); [controller_worker] loadbalancer_topology (default SINGLE, shown nowhere but in the IP count and the topology column; the API response has no topology); [driver_agent]: socket paths, timeouts; [audit] optional.
  • Start-up checks: the default ciphers versus the prohibit list, and TLS defaults versus the minimum, abort start-up: octavia/common/validate.py:496-509, octavia/common/validate.py:551-563.
  • No CLI other than DB management; octavia-status is out of scope.

7. Conformance strategy

7.1 Data

Capture: conformance_capture.py http --wrap octavia.api.app:setup_app --seed-per-test with adapter [tools/adapter_octavia.py](_rewrite/tools/adapter_octavia.py), over octavia/tests/functional/api/test_root_controller.py, octavia/tests/functional/api/test_content_types.py, octavia/tests/functional/api/test_healthcheck.py, and all of octavia/tests/functional/api/v2/. Result: 991 tests, 0 failures, 1 skipped by the project (test_get_all_limited), 8 691 exchanges on 78 of the 79 canonical routes (GET /v2 is in the probes), status mix 200: 4 266, 201: 3 663, 400: 346, 403: 96, 404: 87, 409: 67, 202: 60, 204: 49, 500: 34, 501: 9, 503: 8, 405: 4, 406: 2.

Each exchange has: test, method, path, query, request_headers, request_body, status, response_headers, response_body, and optionally:

FieldMeaningCount
db_deltarows changed by the test outside HTTP before this request (set_lb_status, amphora rows…), {table: {insert, update, delete}}3 403
harness.identity{context: {project_id, is_admin}, policy_values: {roles, project_id, …}}: the identity the test forced; absent → noauth, admin146
harness.configthe configuration overrides when they changed since the seed (full set)215
harness.driver_callscalls received by the provider: {method, provider, args, kwargs[, mocked: true]} with octavia-lib dictionaries; compare them with what the Go API sends3 831
harness.network_callscalls received by the noop network driver and their results: a tape for a fake Neutron1 123
harness.patchesevery other active mock.patch: {target, tape: [{mock, result|raises}]} or {target, new}; tells what failure the test injected1 339

Value encoding in tapes: {"$obj": class, attrs}, {"$exc": class, bases, args, attrs, str}, {"$mock": true}, {"$bytes": b64}, {"$any_subnet": true} (the noop network's "contains every subnet"), {"$unset": true}.

7.2 Replay on the original (the proof)

conformance_capture.py replay --seeds re-runs each test without its Python code in a fresh database, from the JSON only:

  1. fresh base test (create_all + lookup rows); load the seed rows by natural key; apply seed config;
  2. for each exchange in order: apply db_delta, identity, config; patch in the tapes (noop network answers come from network_calls, because its results depend on a process-global left by earlier tests: octavia/network/drivers/noop_driver/driver.py:40-46); send the recorded request with recorded ids replaced by the ids this run generated;
  3. compare the status and the normalised body; compare the provider calls.

Normalisation: timestamps and request ids templated; host of URLs templated; UUIDs mapped through the pairing of recorded and replayed bodies (lists aligned on content); messages with Python-set or dict repr sorted; debuginfo ignored.

Result (_rewrite/replay-report_stable-2026.2.json): 990 tests, 0 errors; 8 683 / 8 691 exchanges identical (8 679 exact, 4 order-insensitive: same-second rows ordered by a random id); provider calls 3 827 / 3 831 identical. The 8 not identical:

TestWhy
test_healthcheck_get_html_detailed, …_json_detailed, …_html_detailed_failed, …_json_detailed_faileddetailed healthcheck bodies hold the hostname, GC counters and stack frames of the process
test_create_neutron_failureexception local to the test with a custom message attribute: the tape rebuilds the class, not its message (neutron_msg)
test_create_provider_octavia_no_port_secamphora provider inspecting a MagicMock's attribute; out of scope
test_create_over_quota_sanity_check, test_with_one_of_everything (graph)two pools in one request, with the same second created_at: their order follows random ids and the pairing mismatches pools [inferred]

The 4 provider-call differences are of the same family: in test_create_over_quota_sanity_check, test_with_one_of_everything, test_with_one_listener_one_hm_udp and test_with_one_listener_one_member, the nested default_pool (healthmonitor, members) of the object given to loadbalancer_create differs between runs, with the same HTTP answer. The cause was not established; do not treat this nested view as stable.

7.3 Running it against the Go API

Runner to build, in Go, from the same algorithm (Go repo, test/e2e):

  1. Service in test mode (auth_strategy=testing): a test-only way to set identity from harness.identity (e.g. headers accepted only in this mode), config from seed.config + harness.config, DB loaded with lookup-tables and seeds. Decode insert_headers.
  2. Provider double named noop_driver, noop_driver-alt, amphora, octavia, recording calls, with injectable faults from harness.patches targets …call_provider, …get_driver, …NoopManager.*.
  3. Neutron double answering from harness.network_calls.
  4. Compare as § 7.2. Provider call comparison is what proves the seam.

Which exchanges are not applicable or need classification:

  • targets load_certificates_data, BarbicanCertManager (1 277 exchange-patches): TLS. Applicable only if the Go API keeps the Barbican path; else expect 501 (§ 3.1). Select by harness.patches[].target.
  • provider amphora or octavia in the body: test the API's own behaviour with a name; keep the double.
  • oslo_messaging.*, amphora routes with rows: out of scope (46 exchanges touch /amphorae with data).
  • targets on Python internals (octavia.db.repositories.*, octavia.db.api.get_session, octavia.common.validate.*, ….controllers.…._apply_flavor_to_lb_dict): failures injected inside Octavia's own code (for instance a duplicate on FlavorRepository.create). The status and message are the contract; reproduce them by a test hook or skip them, 41 exchange-patches.

Other data:

  • route-probe: routing table (structure, not bodies).
  • http-probes: run as is, in noauth, after creating the LB named probe.
  • amphora-empty: run as is.
  • cases-driver-seam: ordered; scenario builds the graph, then sends the messages through the sockets; compare response and db_change (ids templated). 40/40 stable on two runs of the original.
  • rbac-matrix: unit test of the policy engine with the same credentials.

7.4 Original fixtures, for reference

BaseAPITest config: octavia/tests/functional/api/v2/base.py:88-108; DB seeding and placeholders: octavia/tests/functional/db/base.py:90-147; RBAC test pattern: octavia/tests/functional/api/v2/test_flavors.py:201-228; set_lb_status: octavia/tests/functional/api/v2/base.py:460.

8. Gaps found while building the pack

GapConsequence
The tests use SQLite and models DDL (create_all), never the migrationsMariaDB behaviour is unverified: type sizes (§ 5), FOR UPDATE, deadlocks. An e2e run against Galera is needed; the migration itself was exercised on MariaDB 11.4. SQLite migration fails (unsupported ALTER), so no schema-sqlite-migrated
No real auth_strategy=keystone run401 bodies, token reading: from code and the keystonemiddleware spec
56 route entries answer 500 for {}listed in route-probe; decide 400 in Go
Provider's real behaviour isn't in exchangesstatuses are set by DB deltas; an e2e test Go API + Go provider + OVN is needed
No Neutron contract test: the noop driver ignores ownershipthe user-token visibility (§ 3 of 03) is by reading only
API-ref vs code/quotas/defaults; /v2.0 vs /v2 prefixes of flavors and AZ; statuses; see 01 § 2
octavia-lib, WSME and Pecan cited without line referencesnot in the workspace

9. Non-functional requirements

  • Driver call timeout lower than the DB lock timeout; DB transaction per request.
  • Quota lock per project serialises creations of a project.
  • Lists: default limit 1000 (pagination_max_limit).
  • Body size limit 114688 bytes by default ([oslo_middleware] max_request_body_size) [inferred from oslo.middleware defaults]: graph creates can hit this.
  • Secrets: the driver objects carry TLS secrets; do not log them. Do not expose Server faults as upstream does with Python internals.

10. Rewrite checklist

  • [ ] Router: all 297 entries, 3 error formats, 406, 415, Accept, .json.
  • [ ] Presence-tracking decoder; WSME messages; unknown attribute rejection; tenant_id alias.
  • [ ] Pagination, sort, filters, tags, fields with http-probes.
  • [ ] Policy engine and 5 modes with rbac-matrix.
  • [ ] Create LB order of checks (01 § 4.1); VIP validation with user token; cleanup of created ports.
  • [ ] Graph create, one transaction.
  • [ ] Locks: test-and-set, statuses, rollback on driver error; transaction spanning the driver call with a timeout.
  • [ ] Member batch diff.
  • [ ] Quotas: lazily created row, in-use counters, decrement on DELETED.
  • [ ] Flavor, AZ, profiles with hidden placeholders.
  • [ ] Amphora routes as amphora-empty.
  • [ ] Status, stats, get sockets with cases-driver-seam.
  • [ ] Purge of DELETED load balancers.
  • [ ] Conformance runner: target 8 683 / 8 691 as the original, minus declared differences.
  • [ ] E2E with the Go provider on OVN: PENDING_* to ACTIVE, cascade delete, 501s.

Source: reverse-spec-octavia/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.