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/
| File | Use 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:
- Same handlers under
/v2,/v2.0, with and without/lbaas. Trailing slash accepted. - 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*,limitandsortdirection errors, routing errors, 406. Their order relative to policy matters: hooks and_lookuprun first, so a member URL of an unknown pool gives 404 even to someone unauthorised. Unsetvsnullin Go: decode to a presence-tracking type; a plain*stringis enough for fields only if the decoder sets "present" separately.limit=0and negative mean unlimited;fields=a,bselects nothing.DELETEon quotas andPUTon quotas answer 202 with a body.- Response key order is irrelevant;
tenant_idalways present; emptyname/descriptionas"", unset optional attributes asnull;nullattributes are kept in the body (WSME renders them). providerread asoctaviawhen the column is empty (octavia/api/v2/types/load_balancer.py:92-93).- 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]:
| Topic | Upstream | Go API |
|---|---|---|
| driver call | Python method in-process | HTTP 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 mapping | call_provider (5 classes, 02 § 2) | map the provider's fault kinds to these 5, with user_fault_string |
enabled_provider_drivers | stevedore | a static map: {"ovn": "<description>"}; default_provider_driver = ovn |
GET /providers | from config | one entry; description text is a deployment choice |
| capabilities | get_supported_*_metadata | the provider inherits the base class: 501 Provider 'ovn' does not support a requested action: …; flavors with flavor_data → validate_flavor 501 |
| status, stats, get | octavia-driver-agent | the 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 |
| VIP | create_vip_port then loadbalancer_create | keep 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_owned | set 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 DELETED | the 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 |
| TLS | Barbican fetch, inline in driver objects | the 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 network | test double | the 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
| Expectation | Where 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 faultstring | openstacksdk:openstack/exceptions.py:200-201 |
polling provisioning_status until ACTIVE or ERROR: a 409 on a child write is retried by waiting | cloud-provider-openstack:pkg/util/openstack/loadbalancer.go:55-56, cloud-provider-openstack:pkg/util/openstack/loadbalancer.go:184-188 |
cascade=true on delete | gophercloud: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, Magnum | octavia/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 loadlookup-tables: every status enum is an FK, theDELETED-PLACEHOLDERrows exist with NIL ids. Keepalembic_version=fabf4983846bif 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_pathis 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_headersis 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. ScalarListTypecolumns are comma-joined text; empty list semantic followstls_versions,alpn_protocolsdefaults.- Integrity kept by code, not FKs:
listener_statisticshas none;vip_security_group,tagsby cascades. - Locks: 03 § 1.3. Galera:
SELECT FOR UPDATEis local to a node and conflicts surface as deadlocks at commit (certification failures):DBDeadlockon the quota row → 503Project busy, on the LB row → unhandled 500 in the Python code [inferred]. Retry or map it in Go. - Isolation: flavor and AZ delete use
SERIALIZABLEto race with LB creates; an FK onload_balancer.flavor_idprotects them anyway. updated_at: SQLAlchemyonupdateon 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:
keystoneby keystonemiddleware semantic (keystonemiddleware's reverse-spec), skip list/,/load-balancer/,/healthcheck,/load-balancer/healthcheck.noauthis needed for conformance. - Policies: reuse the OpenKeel Go policy engine with
policies_stable-2026.2.json. Evaluateis_admin:Trueascontext_is_adminor noauth. Supportenforce_new_defaults=falseand file overrides, for the five modes of 01 § 6; the matrix is a direct test table. Targets contain onlyproject_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 andminimum_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(defaultSINGLE, shown nowhere but in the IP count and thetopologycolumn; the API response has notopology);[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-statusis 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:
| Field | Meaning | Count |
|---|---|---|
db_delta | rows 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, admin | 146 |
harness.config | the configuration overrides when they changed since the seed (full set) | 215 |
harness.driver_calls | calls received by the provider: {method, provider, args, kwargs[, mocked: true]} with octavia-lib dictionaries; compare them with what the Go API sends | 3 831 |
harness.network_calls | calls received by the noop network driver and their results: a tape for a fake Neutron | 1 123 |
harness.patches | every other active mock.patch: {target, tape: [{mock, result|raises}]} or {target, new}; tells what failure the test injected | 1 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:
- fresh base test (
create_all+ lookup rows); load the seed rows by natural key; apply seed config; - for each exchange in order: apply
db_delta, identity, config; patch in the tapes (noop network answers come fromnetwork_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; - 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:
| Test | Why |
|---|---|
test_healthcheck_get_html_detailed, …_json_detailed, …_html_detailed_failed, …_json_detailed_failed | detailed healthcheck bodies hold the hostname, GC counters and stack frames of the process |
test_create_neutron_failure | exception 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_sec | amphora 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):
- Service in test mode (
auth_strategy=testing): a test-only way to set identity fromharness.identity(e.g. headers accepted only in this mode), config fromseed.config+harness.config, DB loaded withlookup-tablesand seeds. Decodeinsert_headers. - Provider double named
noop_driver,noop_driver-alt,amphora,octavia, recording calls, with injectable faults fromharness.patchestargets…call_provider,…get_driver,…NoopManager.*. - Neutron double answering from
harness.network_calls. - 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 byharness.patches[].target. - provider
amphoraoroctaviain 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/amphoraewith 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 onFlavorRepository.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, innoauth, after creating the LB namedprobe.amphora-empty: run as is.cases-driver-seam: ordered; scenario builds the graph, then sends the messages through the sockets; compareresponseanddb_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
| Gap | Consequence |
|---|---|
The tests use SQLite and models DDL (create_all), never the migrations | MariaDB 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 run | 401 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 exchanges | statuses are set by DB deltas; an e2e test Go API + Go provider + OVN is needed |
| No Neutron contract test: the noop driver ignores ownership | the 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 references | not 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
Serverfaults 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_idalias. - [ ] 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
DELETEDload balancers. - [ ] Conformance runner: target 8 683 / 8 691 as the original, minus declared differences.
- [ ] E2E with the Go provider on OVN:
PENDING_*toACTIVE, 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.