Client contract (rewrite pack) — python-openstackclient — branch stable/2026.2
Branch
stable/2026.2@8bb083b83829— repository https://opendev.org/openstack/python-openstackclient — analysed on 2026-11-01
This pack serves two purposes:
- **Keep
openstackworking against OpenKeel's Go services**, by stating what the CLI requires from them. - Re-implement the CLI, thanks to the machine-readable catalogue of its commands and arguments.
It differs from the Placement and osc-placement packs on one point: no replay against the original Python services was possible. The CLI ran against a stub and against the OpenKeel lab. See § 5 for what was proved and what was not.
1. Contents of _rewrite/
| File | Content | Use |
|---|---|---|
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json) | the 799 commands: group, class, purpose, each of the 3,471 arguments (names, type, default, choices, required, exclusive groups, help), client calls with file:line, microversion gates, error sites | generate the parser of a rewrite; check compatibility of argument names |
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json) | per command: client methods, observed requests, static requests, gates, number of unit vectors, probe exit codes | requirements per command |
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md) | the 815 operations per service, with source, microversions and commands | gap analysis of an OpenKeel service |
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json) | 3,109 vectors: argv → ordered client calls with their arguments, normalised ({uuid}) | test argument mapping without Python |
[lab-compat_stable-2026.2.md](_rewrite/lab-compat_stable-2026.2.md), [wire-lab_stable-2026.2.json](_rewrite/wire-lab_stable-2026.2.json) | outcome per command on the OpenKeel lab, and the 730 recorded runs | compatibility status |
[wire-probe-404_stable-2026.2.json](_rewrite/wire-probe-404_stable-2026.2.json), [wire-probe-200_stable-2026.2.json](_rewrite/wire-probe-200_stable-2026.2.json) | for each of the 661 commands: argv, exit code, up to 12 requests with path, microversion, body | wire reference |
| Script | Role |
|---|---|
[extract_cli_stable-2026.2.py](_rewrite/extract_cli_stable-2026.2.py) | imports the entry points, dumps parsers, walks the code with ast, resolves SDK methods to routes |
[probe_wire_stable-2026.2.py](_rewrite/probe_wire_stable-2026.2.py) | local stub of Keystone and the version documents; runs each command through the real shell |
[probe_lab_stable-2026.2.py](_rewrite/probe_lab_stable-2026.2.py) | same commands against a lab on 127.0.0.1, recording every exchange; masks secrets, stores neither tokens nor stdout |
[capture_unit_stable-2026.2.py](_rewrite/capture_unit_stable-2026.2.py) | runs the unit tests with a recorder |
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py) | merges everything |
The generic rewrite_pack.py and conformance_capture.py were not used: they expect a service or functional tests running in-process, while the OSC functional tests need a live cloud (openstackclient/tests/functional/base.py:1).
Versions: python-openstackclient 10.3.1, osc-lib 4.7.0, cliff 4.16.0, openstacksdk 4.20.0, keystoneauth1 5.17.0, Python 3.13.
To regenerate:
python -I -B _rewrite/extract_cli_stable-2026.2.py <repo> _rewrite
python -I _rewrite/probe_wire_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-404> --mode 404
python -I _rewrite/probe_wire_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-200> --mode 200
python -I -B _rewrite/capture_unit_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json
python -I -B _rewrite/capture_unit_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json
python -I -B _rewrite/build_contract_stable-2026.2.py . <dir-404>/wire-probe_stable-2026.2.json <dir-200>/wire-probe_stable-2026.2.json <run-1>/c.json <run-2>/c.json
Set HOME to an empty directory and unset OS_*, so that no real clouds.yaml is read. The probe targets 127.0.0.1 only and never runs server ssh.
2. What the CLI requires from a service
These apply to every OpenKeel service that must work with an unmodified openstack command.
| # | Requirement | Why | Source |
|---|---|---|---|
| 1 | the Keystone token holds a catalog with public endpoints, under the service types the CLI asks for | otherwise public endpoint for … not found | [observed], openstackclient/object/client.py:32 |
| 2 | block storage may register as block-storage, volume, volumev3, volumev2 or block-store | availability checks try all five | openstackclient/common/clientmanager.py:171 |
| 3 | compute and volume version documents hold version, min_version, status, links | the SDK reads the range before any call | [observed] with the stub |
| 4 | network and image answer version discovery at the catalog URL; image and identity may use 300 | discovery | [observed] |
| 5 | OpenStack-API-Version: compute <v> and volume <v> are honoured from the minimum up to the announced maximum | the SDK sends its own maximum per resource, and users can pin 2.1 | [observed], 03 |
| 6 | announcing a microversion means implementing it | gates call supports_microversion and then send the matching fields; 55 compute and 29 volume values are checked | openstacksdk:openstack/utils.py:242 |
| 7 | GET /resource/{name} answers 404 for an unknown ID or a name, never 400 or 500 | this triggers the name lookup | [observed], osc-lib:osc_lib/utils/__init__.py:204 |
| 8 | the filtered list works: ?name= on the list of network and identity, and on /detail for compute and block storage | second step of the lookup | [observed] |
| 9 | list responses always hold the collection key, even when empty | otherwise the SDK raises KeyError | [observed] |
| 10 | POST /servers/{id}/action and POST /volumes/{id}/action accept the action in the body key | server and volume actions | [observed] for the route, unit vectors for the arguments |
| 11 | Nova keeps os-agents, os-hosts, os-security-groups, os-networks and os-floating-ips at microversion 2.1, or the commands that use them fail | legacy calls pinned to 2.1 | openstackclient/api/compute_v2.py:68 |
| 12 | Neutron exposes GET /extensions and GET /extensions/{alias} | feature detection, for example external-gateway-multihoming | [observed] |
| 13 | Swift keeps AUTH_<id> as the last URL segment, and sends Etag and X-Trans-Id | the CLI displays them | openstackclient/api/object_store_v1.py:532 |
| 14 | error bodies keep the shape of the original service | the SDK extracts the text shown to the user | [inferred] |
| 15 | GET / of each service answers version data | versions show | [observed], openstackclient/common/versions.py:82 |
Which requests a command sends
Read observed_requests first, then static_requests. Static routes follow the SDK source, and are accurate for the method and base path, but:
- the fallback of
find_*is written?name=, while compute and block storage use/detail?name=; - lists on compute, block storage and flavors really go to
/detail; - 1,562 of the 1,948 observed requests match a static route exactly; the other 386 are those variants, discovery of extensions, and SDK-internal calls such as
project cleanup.
3. Coverage of OpenKeel services
Operations used by the CLI, from [api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md):
| Service type | OpenKeel service | Operations | Note |
|---|---|---|---|
| network | openkeel-neutron | 293 | including the 100 extension commands |
| block-storage | openkeel-cinder | 151 | |
| identity | openkeel-keystone | 150 | includes keystoneclient calls |
| compute | openkeel-nova | 136 | |
| image | openkeel-glance | 68 | |
| object-store | Swift replacement | 13 |
Result on the OpenKeel lab
Run on 2026-11-01 against the core profile of openkeel-lab, with images built from the local working trees, the compute agent enabled by labelling the node (openkeel-lab:components/nova/kustomization.yaml:3), fake compute and Cinder drivers, and no object store. 657 commands ran; project cleanup, user password set and server ssh were excluded. Details are in the compatibility table.
| Outcome of the last run | Commands | Reading |
|---|---|---|
| Succeeded | 219 | the CLI works end to end on these |
| 501 from the service | 28 | declared gaps: subnet pool, address scope, network qos policy, network trunk, network segment, host, usage, console access, server add port and server add security group, volume group types, consistency groups |
| HTML 404 from Keystone | 27 | routes absent, not declared: federation, endpoint groups, limits, policies, mappings |
| 404 from a service | 173 | inconclusive; includes the 97 Neutron extension commands (bgpvpn, fwaas, vpnaas, taas, dynamic routing) |
| Other | 210 | mostly the probe: missing parent resources, resource state, or no object store |
Requirements of § 2 confirmed on the lab: discovery of every deployed service (1, 3, 4), microversions up to compute 2.100 and volume 3.71 (5), 404 then name lookup, including /detail (7, 8), list keys (9), and server actions (10). A real cycle server create --wait reached ACTIVE, then server delete --wait succeeded. No 5xx other than 501, and no pod restart.
Findings for OpenKeel:
- Keystone is inconsistent with the other services: an unimplemented operation gives an HTML 404 there (
openkeel-keystone:internal/api/web.go:243), where Nova, Neutron and Cinder give an explicit 501. The user reads "not found" rather than "not implemented", andidentity provider createfails the same way. - **Neutron extensions answer
The resource could not be found**, which is what Neutron does for an extension that is not loaded [inferred], so the absence is visible only throughGET /extensions. - **
server add portandserver add security groupare 501**, although they are common day-to-day operations. - **
GET /v3/on Cinder answers 401 without a token** [lab]; the CLI copes with it. Whether the Python Cinder does the same was not checked.
This covers the lab's fake drivers only, and says nothing about real VMs or volumes.
4. For a rewrite of the CLI itself
| Topic | Rule | Source |
|---|---|---|
| Names | server_add_fixed_ip → server add fixed ip; scripts depend on these and on argument names | pyproject.toml:61 |
| Exit codes | 0, 1, 2 as in 02; do not copy the defects in its § 4 | cliff:cliff/command.py:196 |
| Unknown command | message on stdout, exit 2, with fuzzy suggestions | cliff:cliff/app.py:363 |
| Version selection | commands depend on the major API version | openstackclient/shell.py:150 |
| Plugins | external Python plugins cannot be loaded by a non-Python rewrite; about 20 known plugins would need replacing | openstackclient/common/clientmanager.py:273 |
| Formatters | table, json, yaml, csv, value, -c, --sort-column | cliff |
| Configuration | clouds.yaml, secure.yaml, OS_*, with the precedence of openstack.config | osc-lib:osc_lib/shell.py:217 |
| Authentication | every keystoneauth plugin (password, v3applicationcredential, v3oidc…, admin_token) | openstackclient/shell.py:81 |
| Prompts | --password-prompt, project cleanup confirmation | openstackclient/identity/v3/user.py:303, openstackclient/common/project_cleanup.py:33 |
Recommendation: keep the Python CLI and make the services compatible, rather than rewriting it. The surface is large (799 commands), most behaviour sits in openstacksdk, and plugins are Python.
5. Conformance: what was proved
| Evidence | Result | What it proves | What it does not prove |
|---|---|---|---|
| Parser dump | 799 of 799 classes imported, 0 failures | the catalogue is complete for the declared entry points | runtime plugin commands |
| Unit tests with recorder | 3,250 of 3,250 pass, 3,109 vectors, in two independent runs | argv → client calls, for 765 of 799 classes | anything about a service, since the clients are mocks |
| Comparison of the two runs | 3,109 of 3,109 vectors have the same test, call sequence and status | method sequences are deterministic | argument values holding random IDs were not compared |
| Lab probe | 657 commands, 730 runs, 2,795 requests; 219 succeed | the CLI works against the Go services on those commands | conformance with the Python services; anything needing real drivers |
| Wire probe on the stub | 661 commands run; 615 sent a request; 1,948 requests recorded across both modes; 441 exit 0 in {} mode | real routes, headers and negotiation up to the first error | later steps; behaviour with real data |
The unit vectors were not replayed against a rewrite of the CLI, since none exists, and nothing was compared with a Python OpenStack. The usual "replay is mandatory" bar is therefore not met for service behaviour, and the lab run shows compatibility, not identical behaviour. The mocks also hide real defects: the first 4 defects of 02 § 4 all pass their unit tests. This is the main limit of the pack.
To close the gap: run probe_lab_stable-2026.2.py against a devstack on stable/2026.2 and compare it with wire-lab_stable-2026.2.json, or run openstackclient/tests/functional against both.
Classes without a unit vector (34)
EC2 credentials (8, v2 and v3); dynamic routing bgp_dragent, bgp_peer, bgp_speaker (8); object and container set, unset, save and account (9, of which the API wrapper has its own tests); BGPVPN router associations (3); ShowVersions, SetServerVolume, ShowEndpointGroup, ShowVolumeAttachment, ShowVolumeGroup, ShowVolumeGroupSnapshot. The list is printed by build_contract_stable-2026.2.py.
6. Gaps found while building the pack
- The stub stops multi-step commands early; 337 of the 815 operations are static only.
- 46 commands sent nothing, because of synthetic arguments, or because they make no calls (
token issue,catalog list). - Non-default groups (
identity.v2,image.v1,volume.v2) were not probed. share.v2code exists, is unit-tested, and is never loaded (openstackclient/shell.py:43). It should not be counted as user-facing surface.- Identity is in transition: 144 calls still go through keystoneclient, against 562 through the SDK.
7. Non-functional requirements
- Latency: each invocation authenticates and reads discovery documents, so Keystone and the version endpoints are on the hot path. A script of N commands sends N token requests.
- Payload size:
object createandimage createstream; services must not buffer the body in memory. - Listing cost:
server listalso lists images and flavors unless told otherwise (openstackclient/compute/v2/server.py:3142).
8. Checklist for an OpenKeel service
- [ ] The service is in the Keystone catalog under the expected type, with a
publicendpoint. - [ ] Version discovery answers at the catalog URL, with
min_versionandversionwhere microversions apply. - [ ] Every announced microversion is implemented; the oldest pinned ones (2.1) still work.
- [ ]
GET /resource/{name}gives 404; the name filter works, on/detailwhere applicable. - [ ] Empty lists keep their collection key.
- [ ] Error bodies match the original.
- [ ] Each operation of
api-surface_stable-2026.2.mdfor the service is implemented, or its absence is documented. - [ ] An unimplemented operation answers 501 with the service's error body, not a 404.
- [ ] The probe, pointed at the lab, gives the same routes and an exit code of 0 with real resources.
Source: reverse-spec-python-openstackclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.