Client contract (rewrite pack) — python-octaviaclient — branch stable/2026.2
Branch
stable/2026.2@b37975ec8f2d(tag3.15.0+ 2 commits) — repository https://opendev.org/openstack/python-octaviaclient — analysed on 2026-11-01
This pack states **what openstack loadbalancer … requires from a load-balancer service**, so that an OpenKeel replacement keeps working with the unmodified plugin. It is a client contract, not a plan to rewrite the plugin.
No real service was contacted. The CLI ran against a local stub on 127.0.0.1. Octavia's code and OpenAPI were read, not run. See § 5.
1. Contents of _rewrite/
| File | Content | Use |
|---|---|---|
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json) | the 82 commands: class, purpose, each of the 491 arguments (names, type, default, choices, required, exclusive groups, help), client calls with file:line, error sites | check argument names; generate a parser |
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json) | per command: client methods, observed requests, typed sequence with bodies, the effect of each option on the wire, vectors, probe exits; the method-to-route table | requirements per command |
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md) | 73 Octavia, 5 Neutron and 2 Keystone operations, with client method, presence in Octavia's OpenAPI, commands | gap analysis of an OpenKeel service |
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json) | 287 vectors: argv → ordered API method calls, normalised ({uuid}) | test argument mapping without Python |
[wire-probe-404_stable-2026.2.json](_rewrite/wire-probe-404_stable-2026.2.json) | stub answering 404 with a fault body | reaction to absence |
[wire-probe-200_stable-2026.2.json](_rewrite/wire-probe-200_stable-2026.2.json) | stub answering 200 {} | reaction to an empty object |
[wire-probe-typed_stable-2026.2.json](_rewrite/wire-probe-typed_stable-2026.2.json) | stub answering shaped resources, with required arguments | full request sequences; added because modes 404 and 200 stop at the first lookup |
[wire-probe-options_stable-2026.2.json](_rewrite/wire-probe-options_stable-2026.2.json) | one run per optional argument: 475 runs, all exit 0 | the wire effect of each option |
[defect-repro_stable-2026.2.json](_rewrite/defect-repro_stable-2026.2.json) | scenarios of 02 § 5, and the sweep with no network in the catalog | evidence for defects |
| Script | Role | Change from the shared tool |
|---|---|---|
[extract_plugin_stable-2026.2.py](_rewrite/extract_plugin_stable-2026.2.py) | imports the entry points, dumps parsers, walks code | unchanged |
[probe_plugin_stable-2026.2.py](_rewrite/probe_plugin_stable-2026.2.py) | stub and shell runner | Octavia fault body; typed mode; --options; logging kept, so that error text is recorded; ClientCache reset; status and headers recorded |
[repro_defects_stable-2026.2.py](_rewrite/repro_defects_stable-2026.2.py) | defect scenarios | new |
[capture_plugin_stable-2026.2.py](_rewrite/capture_plugin_stable-2026.2.py) | unit tests → vectors | test-ID prefix |
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py) | merges everything | rewritten for this plugin |
Versions: python-octaviaclient 3.15.1.dev2 (installed files identical to the repository), osc-lib 4.7.0, cliff 4.16.0, python-openstackclient 10.3.1, openstacksdk 4.20.0, keystoneauth1 5.17.0, Python 3.13.
To regenerate, with HOME set to an empty directory and OS_* unset:
python -I -B _rewrite/extract_plugin_stable-2026.2.py <repo> python-octaviaclient octaviaclient _rewrite
for m in 404 200 typed; do python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-$m> --mode $m; done
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-options> --mode typed --options
python -I -B _rewrite/repro_defects_stable-2026.2.py <repro.json>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json <repo>/octaviaclient/tests/unit
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json <repo>/octaviaclient/tests/unit
python -I -B _rewrite/build_contract_stable-2026.2.py . <repo> <dir-404>/wire-probe_stable-2026.2.json <dir-200>/wire-probe_stable-2026.2.json <dir-typed>/wire-probe_stable-2026.2.json <dir-options>/wire-probe_stable-2026.2.json <repro.json> <run-1>/c.json <run-2>/c.json <octavia-openapi.json> <tables-dir>
The generic rewrite_pack.py and conformance_capture.py were not used, because the plugin has no functional tests to record (octaviaclient/tests/functional/base.py:45).
2. What the CLI requires from a service
"Hard" means that the command fails without it. Items marked [observed] were seen on the stub, which shows the client's demand, not a service's answer.
Catalog and URL
| # | Requirement | Hard | Source |
|---|---|---|---|
| 1 | a public endpoint of type load-balancer | yes | octaviaclient/osc/plugin.py:32-36 [observed] |
| 2 | also an endpoint of type network, or 13 commands fail before any request | yes | octaviaclient/osc/v2/utils.py:273 [observed] |
| 3 | the catalog URL must not end in /v2; bare or /v2.0 are fine | yes | octaviaclient/api/v2/octavia.py:104-106 [observed] |
| 4 | routes live under /v2.0/lbaas/… and /v2.0/octavia/amphorae…; /v2 alone is not used | yes | octaviaclient/api/constants.py:32-33 [observed] |
| 5 | no version document or microversion is needed | n/a | octaviaclient/osc/plugin.py:37-41 [observed] |
Lookup
| # | Requirement | Hard | Source |
|---|---|---|---|
| 6 | list routes honour ?id= and ?name=, including amphorae, members and rules, and return only matches. An ignored filter gives found with name or ID as soon as 2 rows exist | yes | octaviaclient/osc/v2/utils.py:108-145 |
| 7 | availability zones honour ?name=, and their rows have name; no id is required | yes | octaviaclient/osc/v2/utils.py:110, octaviaclient/osc/v2/utils.py:168-169 |
| 8 | GET /resource/{id} for an unknown ID answers 404 (400 is treated alike) | yes, for the fallback | osc-lib:osc_lib/api/api.py:411-425 [observed] |
| 9 | list rows have id, and name where looked up | yes | octaviaclient/osc/v2/utils.py:193 |
Response shape
| # | Requirement | Hard | Source |
|---|---|---|---|
| 10 | lists always hold the collection key, even when empty; otherwise a bare key name and exit 1 | yes | octaviaclient/api/v2/octavia.py:92 [observed] |
| 11 | a single-resource object has its wrapper as the first JSON key. Check the encoder of a Go service: Go map encoding sorts keys, so any added key must sort after the wrapper; a struct keeps field order | yes | osc-lib:osc_lib/api/api.py:409 [observed] |
| 12 | POST bodies are {"<wrapper>": {...}}, valid JSON | yes | octaviaclient/osc/v2/load_balancer.py:178 [observed] |
| 13 | all bodies decoded by the client are valid JSON; a proxy page gives raw Python errors | yes | 02 § 5 items 5, 6 [observed] |
| 14 | wrappers and keys as in octaviaclient/api/constants.py:14-30: loadbalancer, listener, pool, member, healthmonitor, l7policy, rule, amphora, flavor, flavorprofile, availability_zone, availability_zone_profile, quota | yes | octaviaclient/osc/v2/l7rule.py:111, octaviaclient/osc/v2/availabilityzone.py:71 |
| 15 | stats holds active_connections, bytes_in, bytes_out, request_errors, total_connections; amphora_stats is a list of rows with these as numbers and listener_id | missing: blank; non-number: failure | octaviaclient/osc/v2/constants.py:50-55, octaviaclient/osc/v2/amphora.py:253-256 |
| 16 | tags is present on every taggable resource, or set --tag and unset --tag give 'tags' | yes for those options | octaviaclient/osc/v2/utils.py:827, octaviaclient/osc/v2/utils.py:839 [observed] |
| 17 | other fields are read by column list; a missing one shows blank. Lists: octaviaclient/osc/v2/constants.py:15-380. Each listed field exists in Octavia's observed response schemas (checked against reverse-spec-octavia/_rewrite/response-schemas_stable-2026.2.json, read only) | no | osc-lib:osc_lib/utils/__init__.py:513 |
| 18 | flavor_capabilities and availability_zone_capabilities are lists of name, description | key: yes | octaviaclient/osc/v2/provider.py:84, octaviaclient/osc/v2/provider.py:92, octaviaclient/osc/v2/constants.py:321-325 |
| 19 | GET /lbaas/quotas/defaults answers {"quota": {...}}, keys load_balancer, health_monitor, etc. | yes | octaviaclient/osc/v2/quota.py:95, octaviaclient/osc/v2/constants.py:251-259 |
Status, for --wait
| # | Requirement | Hard | Source |
|---|---|---|---|
| 20 | provisioning_status on load balancers, listeners, pools, members, health monitors, policies and rules | yes with --wait | octaviaclient/osc/v2/utils.py:792 [observed] |
| 21 | the status leaves ACTIVE before the answer to a write, or --wait returns at once | yes for correctness | osc-lib:osc_lib/utils/__init__.py:806-810 [observed on a stub that stays ACTIVE] |
| 22 | failure ends in ERROR, otherwise the wait for ACTIVE never ends | yes | osc-lib:osc_lib/utils/__init__.py:811-813 |
| 23 | a finished deletion answers 404 on GET | yes with delete --wait | octaviaclient/osc/v2/utils.py:808-821 |
| 24 | parent references: listener.loadbalancers[0].id, pool.loadbalancers[0].id, healthmonitor.pools[0].id, l7policy.listener_id, amphora.loadbalancer_id (may be null) | yes with --wait | octaviaclient/osc/v2/listener.py:253, octaviaclient/osc/v2/pool.py:176, octaviaclient/osc/v2/health_monitor.py:159, octaviaclient/osc/v2/l7policy.py:133, octaviaclient/osc/v2/amphora.py:161 |
What the service must accept
| # | Requirement | Source |
|---|---|---|
| 25 | connection_limit as a JSON string on listener create and set. A strict Go decoder into int rejects this. wsme converts it [inferred, octavia:octavia/api/v2/types/listener.py:130] | octaviaclient/osc/v2/utils.py:337 [observed] |
| 26 | query booleans True and False, capitalised: admin_state_up, enabled, cascade. Octavia parses cascade with bool_from_string (octavia:octavia/api/v2/controllers/load_balancer.py:880) | octaviaclient/osc/v2/utils.py:291-293 [observed] |
| 27 | repeated query parameters: tags=a&tags=b, and tags-any, not-tags, not-tags-any | octaviaclient/osc/v2/utils.py:200-207 [observed] |
| 28 | null on PUT means back to default, for all fields of unset commands | octaviaclient/osc/v2/utils.py:774-781 [observed] |
| 29 | an empty object on PUT: {"loadbalancer": {}} | octaviaclient/osc/v2/load_balancer.py:486-489 [observed] |
| 30 | quota PUT with both spellings: load_balancer, health_monitor (from set) and loadbalancer, healthmonitor (from unset) | octaviaclient/osc/v2/utils.py:589-591, octaviaclient/osc/v2/quota.py:262 [observed]; octavia:octavia/api/v2/types/quotas.py:61-64 |
| 31 | flavor_data and availability_zone_data as strings holding JSON; http_version as a number | octaviaclient/osc/v2/utils.py:674, octaviaclient/osc/v2/utils.py:716, octaviaclient/osc/v2/utils.py:576 [observed] |
| 32 | a tags array in any order, treated as a set | octaviaclient/osc/v2/utils.py:827-829 [observed] |
| 33 | failover and configure PUT with no body | octaviaclient/api/v2/octavia.py:221, octaviaclient/api/v2/octavia.py:817 [observed] |
Errors and paging
| # | Requirement | Source |
|---|---|---|
| 34 | error bodies hold faultstring, or the user sees canned text; faultcode and debuginfo are not read | octaviaclient/api/v2/octavia.py:44-46 [observed] |
| 35 | x-openstack-request-id is shown in errors | octaviaclient/api/exceptions.py:24-27 [observed] |
| 36 | <key>_links is optional. Its next link must stop at the end, since the client has no page limit. Its query replaces all parameters, so a compatible service should put the filters in the link; this improves on Octavia, which does not (octavia:octavia/api/common/pagination.py:151-172) | octaviaclient/api/v2/octavia.py:94-101 [observed] |
3. Coverage of OpenKeel services
| Service type | Operations | Note |
|---|---|---|
| load-balancer | 73 | Octavia replacement; see reverse-spec-octavia/ |
| network | 5, read only | subnets, ports, networks, qos/policies, security-groups, each filtered by id or name |
| identity | 2, read only | projects/{value}, projects?name= |
No OpenKeel service was probed: the request allowed only the local stub.
4. Rewriting the plugin itself
Not recommended now. The plugin is thin, and scripts depend on command and argument names, which cli-commands_stable-2026.2.json holds. A short list to keep, and to fix:
| Topic | Rule | Source |
|---|---|---|
| Names | loadbalancer_listener_create → loadbalancer listener create | setup.cfg:33-115 |
| Exit codes | 0, 1, 2, as in 02 § 1 | cliff:cliff/app.py:428 |
| Fix | filters kept across pages; lazy network client; a wait timeout; error text instead of raw Python | 02 § 5 |
| Risk | BaseAPI, wait_for_status, wait_for_delete and ClientCache are deprecated in osc-lib, and a removal breaks the plugin | osc-lib:osc_lib/api/api.py:69 |
5. Conformance: what was proved
| Evidence | Result | What it proves | What it does not prove |
|---|---|---|---|
| Parser dump | 82 of 82 classes imported, 0 failures | the catalogue is complete for the declared entry points | nothing beyond them |
| Unit tests with recorder | 421 of 421 pass, in two runs; 287 vectors; 82 of 82 classes | argv → API method calls | anything about a service: the API is mocked |
| Comparison of runs | 287 of 287 vectors identical over 4 runs, sequences and arguments, with Python's default hash randomisation | deterministic in these tests | real order of tags, which does vary (below) |
| Wire probe 404 | 72 commands sent a request, 75 requests; exits: 72 with 1, 10 with 0 | lookup behaviour, fault handling | anything past the first 404 |
Wire probe 200 {} | 72 commands sent a request, 78 requests; exits: 71 with 1, 11 with 0 | reaction to an empty object | |
| Wire probe typed | 72 commands sent a request, 139 requests; all 82 exit 0 | every default sequence reaches its real write route | correctness on a real service; the stub is built from the client's own columns, so the exit 0 is partly circular |
| Option sweep | 475 runs, all exit 0, 82 of 82 commands sent requests, 983 requests | each option's wire form; all 73 Octavia routes seen | combinations of options |
| Defect scenarios | 66 scenarios and an 82-command sweep | 02 § 5 | that a real service triggers them, unless stated |
| Route cross-check | 72 of 73 routes in Octavia's OpenAPI; the other is quotas/defaults | routes exist on paper | behaviour; read only |
Replay against the original service: not done, and not possible under this request's rules. The usual "replay is mandatory" bar is not met. The vectors were not replayed against any rewrite either, since none exists.
Tag order: the PUT body varies from run to run (["t3","t4","t2","t1"], then ["t2","t4","t1","t3"]) [observed], so wire files are not byte-stable across runs on set --tag and unset --tag. The unit vectors were identical over 4 runs, but this does not show that the order is stable: their merged tag lists hold only 2 items.
To close the gap: point probe_plugin at a devstack on stable/2026.2, or at the OpenKeel lab, with the stub removed, and compare request sequences and exit codes with wire-probe-typed.
6. Gaps found while building the pack
- The shared probe's modes stop at the first lookup, since
{}has no collection key and 404 ends the command. Hencetypedmode. Its 10unsetcommands send nothing with no option; they were reached through--options. - The shared probe disabled logging, which hid every error text; this copy keeps it.
ClientCacheholds clients on the class (osc-lib:osc_lib/clientmanager.py:47), so in one process later commands kept the first endpoint. This made two catalog scenarios pass falsely until the probe reset it. Results before the reset were discarded.- The extractor resolves SDK calls to routes only:
with_restis 3 of 197 calls. Octavia routes come from theOctaviaAPItable thatbuild_contractreads, and from the wire. - Client calls in
cli-commandsare static and over-approximate:amphora configurelistsload_balancer_show, reached through astatus_fargument. For routes, the wire wins. - No functional tests exist in the repository, so no suite could be replayed.
7. Non-functional requirements
- Latency: each invocation authenticates, and each name adds one list request, or two for a child with a parent [observed]. A script of N commands sends N token requests.
- Polling: one
GETper 3 s per--wait(octaviaclient/osc/v2/utils.py:793). - Paging: the client requests no page size, so the service's limit decides. Its next page is requested at once.
8. Checklist for an OpenKeel service
- [ ] Types
load-balancerandnetworkare in the catalog; the URL does not end in/v2. - [ ] Routes of
api-surface_stable-2026.2.mdexist under/v2.0, includingGET /lbaas/quotas/defaults. - [ ] Lists keep their key when empty; filters
idandnamework on every list. - [ ] The wrapper is the first JSON key of a single-resource body.
- [ ] Decoding accepts
connection_limitas a string,True/Falsein queries, repeated tag parameters,nullas reset, and emptyPUTobjects. - [ ] Both quota spellings are accepted.
- [ ] Writes leave
ACTIVEbefore answering; failures end inERROR; finished deletes give 404. - [ ] Errors hold
faultstring; responses carryx-openstack-request-id. - [ ]
nextlinks carry the filters and stop at the end. - [ ] Run
probe_pluginagainst the service and compare withwire-probe-typed.
Source: reverse-spec-python-octaviaclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.