Client contract (rewrite pack) — python-masakariclient — branch stable/2026.2
Branch
stable/2026.2@073cd2cca082(8.9.0+ 2 commits) — repository https://opendev.org/openstack/python-masakariclient — analysed on 2026-11-01
This pack has two uses: keep these commands working against OpenKeel's Go Masakari, and allow a rewrite of the commands. Nothing here was run against a real Masakari: see § 5.
1. Contents of _rewrite/
| File | Content | Use |
|---|---|---|
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json) | the 15 commands, 55 arguments, client calls with file:line, gates, error sites, static routes | parser of a rewrite; argument compatibility |
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json) | per command: client methods, observed and static requests, gates, unit vectors, probe exit codes, stateful runs with (method, route, status) | requirements per command |
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md) | the 15 operations, with microversions and commands | gap analysis of a service |
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json) | 11 vectors: argv → ordered SDK proxy calls | argument mapping without Python |
[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) | each command with synthetic arguments, against a stub answering 404 or 200 {}: exit code and requests. The probe disables logging, so stderr is empty there | first request of each command; reaction to missing keys |
[wire-scenarios_stable-2026.2.json](_rewrite/wire-scenarios_stable-2026.2.json) | 19 scenarios, 179 invocations: argv, exit code, stdout, stderr, requests with headers and status | full sequences, error texts, defects. Added to the requested list, because the probes stop at the first error |
| Script | Role |
|---|---|
[extract_plugin_stable-2026.2.py](_rewrite/extract_plugin_stable-2026.2.py) | catalogue. Adapted: ha maps to the instance-ha proxy, and APIVersion("x.y") counts as a gate |
[probe_plugin_stable-2026.2.py](_rewrite/probe_plugin_stable-2026.2.py) | wire probes. Adapted: real discovery shape, a JSON payload, discovery logged separately |
[capture_plugin_stable-2026.2.py](_rewrite/capture_plugin_stable-2026.2.py) | vectors. Adapted: skips the mock's parent links, which recorded each call three times |
[scenarios_stub_stable-2026.2.py](_rewrite/scenarios_stub_stable-2026.2.py) | new: in-memory stub of Masakari with fault injection; runs the CLI in subprocesses |
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py) | merges everything; adapted from the OSC one |
Versions: python-masakariclient 8.9.1.dev2, openstacksdk 4.20.0, osc-lib 4.7.0, cliff 4.16.0, keystoneauth1 5.17.0, python-openstackclient 10.3.1, Python 3.13.
To regenerate, with HOME set to an empty directory and no OS_* variable:
python -I -B _rewrite/extract_plugin_stable-2026.2.py <repo> python-masakariclient masakariclient _rewrite
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-404> --mode 404
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-200> --mode 200
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json <repo>/masakariclient/tests/unit <repo>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json <repo>/masakariclient/tests/unit <repo>
python -I -B _rewrite/scenarios_stub_stable-2026.2.py <scenarios.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 <scenarios.json> <scratch-dir>
All of these bind or contact 127.0.0.1 only.
The generic rewrite_pack.py and conformance_capture.py were not used: there are no functional tests, and no service in this repository.
2. What the commands require from a service
| # | Requirement | Why, if not | Evidence |
|---|---|---|---|
| 1 | catalog entry of type instance-ha (ha also works), with a public endpoint | Service 'instance-ha' is disabled… | [observed], openstacksdk:openstack/service_description.py:44 |
| 2 | GET /v1 answers {"version": {"id", "status", "version", "min_version", "links"}}; 401 without a token is fine | otherwise the header goes unchecked | [observed], masakari:masakari/api/openstack/ha/versions.py:33 |
| 3 | maximum at least 1.3, or every command fails by default | see scenario 4 of 04 | [observed], masakariclient/plugin.py:22 |
| 4 | minimum at most 1.0, and no header means 1.0: --os-ha-api-version 1 sends none on hosts, notifications and vmoves | requests fail for those users | [observed] |
| 5 | OpenStack-API-Version: instance-ha X.Y, with every value from 1.0 to 1.3 | [observed] | |
| 5b | JSON answers to Accept: */*, which is what get_*, POST, PUT and DELETE send | the SDK parses the body as JSON | [observed] |
| 6 | routes with and without /{project_id}, depending on the catalog | [observed] | |
| 7 | POST /segments and GET /segments/{id} answer key segment with **uuid** | otherwise Request requires an ID but none was found after a real create | [observed], masakariclient/osc/v1/segment.py:169 |
| 8 | POST …/hosts: key host with **uuid** | same | [observed], masakariclient/osc/v1/host.py:180 |
| 9 | POST /notifications: 202 or 2xx, key notification with **notification_uuid** | same | [observed], masakariclient/osc/v1/notification.py:136 |
| 10 | list responses always hold segments, hosts, notifications, vmoves, even empty | otherwise the user reads 'segments' | [observed], openstacksdk:openstack/resource.py:2294 |
| 11 | each list item holds **name and uuid** for segments and hosts | name lookup | masakariclient/common/utils.py:108, masakariclient/common/utils.py:110 |
| 12 | each list item holds an **id** usable as marker, and an empty page ends the list | otherwise --limit loops or stops early | [observed], openstacksdk:openstack/resource.py:2325 |
| 13 | **GET /segments/{not-a-uuid} answers 404**, also PUT, DELETE, POST …/hosts, GET …/hosts under an unknown segment | an unknown name goes out as an ID; a 400 or 500 changes the message | [observed], masakariclient/common/utils.py:100 |
| 14 | **GET /notifications/{id} 404**, not 400, for a malformed UUID | Notification not found: %s needs a 404 | [observed], masakariclient/osc/v1/notification.py:142 |
| 15 | reserved and on_maintenance accepted as the strings "True", "False", in bodies and query | the CLI only sends those | [observed], masakari:masakari/api/validation/parameter_types.py:151 |
| 16 | enabled accepted from 1.2 as a JSON boolean, and returned from 1.2 | column is_enabled | [observed], openstacksdk:openstack/instance_ha/v1/segment.py:55 |
| 17 | recovery_workflow_details from 1.1: list of {name, state, progress, progress_details} | column shown | openstacksdk:openstack/instance_ha/v1/notification.py:27 |
| 18 | response field names as in the columns: for vmoves instance_uuid, instance_name, and failover_segment_id, source_host_uuid, generated_time, control_attributes | absent fields show None, silently, exit 0 | [observed], masakariclient/osc/v1/vmove.py:119 |
| 19 | query: limit, marker, repeated sort_key and sort_dir, generated-since with a hyphen, enabled, plus the mapped filters | [observed], openstacksdk:openstack/instance_ha/v1/notification.py:89 | |
| 20 | error bodies {"<fault>": {"code", "message"}}: the SDK puts the message in the error text | the user reads no reason | [observed for the stub's shape], masakari:masakari/api/openstack/wsgi.py:1003 |
| 21 | statuses 201, 202, 204 on writes as in reverse-spec-masakari; 409 for conflicts | the CLI does not check the exact 2xx [inferred] |
The client parses no message text and no error code. So any stable text will do, and only the statuses and keys above are contractual.
Where the client and the service could disagree (read, not run)
--markerexpects, in the service, the integerid(masakari:masakari/db/sqlalchemy/api.py:200).--limit 0sendslimit=0, since the string is truthy (masakariclient/common/utils.py:63). The service's answer was not tested.--filters failover_segment_id=is overwritten by the service (masakari:masakari/api/openstack/ha/hosts.py:59).--filters enabled=passes to the service (masakari:masakari/api/openstack/ha/segments.py:50) and is absent from the help.- Host filters
nameandcontrol_attributesexist on the service (masakari:masakari/api/openstack/ha/hosts.py:60), but the SDK filtersnameon the client side and does not forward it.
3. Operations
All 15 were observed at least once, listed in [api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md): /segments (5), /segments/{id}/hosts (5), /notifications (3), /notifications/{id}/vmoves (2). Service routes the CLI never calls: GET /, /v1/extensions, /v1/extensions/{id}. Static routes in the contract over-approximate: the extractor sees the hosts branch of get_uuid_by_name even in segment delete, which never takes it; the observed sequences are the reference.
4. For a rewrite of the commands
| Topic | Rule | Source |
|---|---|---|
| Names | segment_host_create → segment host create. Options keep their underscores: --is_enabled, --on_maintenance, --control_attributes, --recovery_method, --service_type | setup.cfg:33, masakariclient/osc/v1/segment.py:130 |
| Positional order | segment host create ends with the segment; the others start with it | masakariclient/osc/v1/host.py:121 |
| Choices | the case-sensitive True, False | masakariclient/osc/v1/host.py:144 |
| Lookup | 01 § 1 | masakariclient/common/utils.py:90 |
| Exit codes | 0, 1, 2; do not copy defects 1, 2, 4, 7, 9 of 02 § 4 without a decision: scripts may rely on exit 0 from segment delete [inferred] | |
| Columns | fixed lists, in order | masakariclient/osc/v1/segment.py:283 |
| Delete output | Segment deleted: <given value> and Host deleted: <given value>, the value as typed | masakariclient/osc/v1/segment.py:270, masakariclient/osc/v1/host.py:289 |
Recommendation: keep this Python plugin and make the service compatible. It is small, but it depends on openstacksdk for nearly all its behaviour, and it plugs into the Python openstack command.
5. Conformance: what was proved
| Evidence | Result | Proves | Does not prove |
|---|---|---|---|
| Parser dump | 15 of 15 entry points imported | catalogue complete | |
| Unit tests | 22 of 22 pass, 11 vectors, on 3 of 15 classes; same test, call sequence and status in two runs | argv → SDK call for those 3 | anything about a service: clients are mocks |
| Wire probes | 15 of 15 commands sent a request in each mode, on the real route; 200 {}: 3 exit 0 | routes up to the first error | later steps |
| Stateful stub | 179 invocations, 239 requests; each command succeeded at least once; each defect reproduced | full sequences, exit codes, error texts against this stub | a real service; the stub is my reading of the service, and may hide mismatches |
| Replay | none | no replay against Masakari or a rewrite was made, so the skill's "replay is mandatory" bar is not met |
Test coverage by command:
- with vectors, through the parser:
notification show,segment create,segment update; - tested without the parser, no vector:
notification vmove show,segment host show,segment host update,segment host delete,segment show,segment delete; - no test at all:
segment host create,segment host list,notification create,notification list,segment list,notification vmove list.
The delete test does not see defect 1: it only checks the mock call (masakariclient/tests/unit/osc/v1/test_segment.py:214).
To close the gap (for you to run, since this work had to stay on the stub): point the scenarios at a real Masakari on stable/2026.2 and at openkeel-masakari, by replacing the Stub with an endpoint and keeping SCENARIOS; the inject entries need the stub, so keep those aside. Comparing exit codes and sequences between the two gives a compatibility verdict.
6. Gaps found while building the pack
- The generic stub had no
GET /instance-ha/v1and announced 2.100. Fixed in my copy of the probe. - The extractor knew no
haproxy (with_rest=0) and no gate other thansupports_microversion(gates=0). Fixed: 36 of 36 calls resolved, 8 gates. - The capture tripled each call, because the tests set
app = mock.Mock(). Fixed. README.rst:12anddoc/source/cli/commands.rst:13are out of date.
7. Non-functional requirements
- Latency: each invocation authenticates and reads discovery. A name lookup lists a whole collection:
GET /segmentsis on the hot path of 8 commands, andGET …/hostson 3 more. - Timeouts: no project-level timeout is set in the plugin [inferred from
masakariclient/plugin.py:41]. - Secrets: no secret is specific to this plugin. The notification
payloadis printed as is.
8. Checklist for an OpenKeel service
- [ ] Catalog type
instance-ha, with apublicendpoint. - [ ]
GET /v1:version1.3,min_version1.0. - [ ] All values from 1.0 to 1.3 honoured; no header means 1.0; JSON answered to
Accept: */*. - [ ] Unknown ID and non-UUID both give 404 on every member route.
- [ ] Collection keys always present, also when empty.
- [ ]
uuid,notification_uuidand integeridin every representation. - [ ]
"True","False"accepted for host booleans. - [ ]
limitandmarkerpaginate to an empty page. - [ ]
generated-since, repeatedsort_keyandsort_diraccepted. - [ ] Scenario suite: same exit codes and sequences as a real Masakari.
Source: reverse-spec-python-masakariclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.