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

Reverse-spec · Clients

python-masakariclient — Rewrite pack

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/

FileContentUse
[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 routesparser 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 commandsgap analysis of a service
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json)11 vectors: argv → ordered SDK proxy callsargument 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 therefirst 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 statusfull sequences, error texts, defects. Added to the requested list, because the probes stop at the first error
ScriptRole
[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

#RequirementWhy, if notEvidence
1catalog entry of type instance-ha (ha also works), with a public endpointService 'instance-ha' is disabled…[observed], openstacksdk:openstack/service_description.py:44
2GET /v1 answers {"version": {"id", "status", "version", "min_version", "links"}}; 401 without a token is fineotherwise the header goes unchecked[observed], masakari:masakari/api/openstack/ha/versions.py:33
3maximum at least 1.3, or every command fails by defaultsee scenario 4 of 04[observed], masakariclient/plugin.py:22
4minimum at most 1.0, and no header means 1.0: --os-ha-api-version 1 sends none on hosts, notifications and vmovesrequests fail for those users[observed]
5OpenStack-API-Version: instance-ha X.Y, with every value from 1.0 to 1.3[observed]
5bJSON answers to Accept: */*, which is what get_*, POST, PUT and DELETE sendthe SDK parses the body as JSON[observed]
6routes with and without /{project_id}, depending on the catalog[observed]
7POST /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
8POST …/hosts: key host with **uuid**same[observed], masakariclient/osc/v1/host.py:180
9POST /notifications: 202 or 2xx, key notification with **notification_uuid**same[observed], masakariclient/osc/v1/notification.py:136
10list responses always hold segments, hosts, notifications, vmoves, even emptyotherwise the user reads 'segments'[observed], openstacksdk:openstack/resource.py:2294
11each list item holds **name and uuid** for segments and hostsname lookupmasakariclient/common/utils.py:108, masakariclient/common/utils.py:110
12each list item holds an **id** usable as marker, and an empty page ends the listotherwise --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 segmentan 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 UUIDNotification not found: %s needs a 404[observed], masakariclient/osc/v1/notification.py:142
15reserved and on_maintenance accepted as the strings "True", "False", in bodies and querythe CLI only sends those[observed], masakari:masakari/api/validation/parameter_types.py:151
16enabled accepted from 1.2 as a JSON boolean, and returned from 1.2column is_enabled[observed], openstacksdk:openstack/instance_ha/v1/segment.py:55
17recovery_workflow_details from 1.1: list of {name, state, progress, progress_details}column shownopenstacksdk:openstack/instance_ha/v1/notification.py:27
18response field names as in the columns: for vmoves instance_uuid, instance_name, and failover_segment_id, source_host_uuid, generated_time, control_attributesabsent fields show None, silently, exit 0[observed], masakariclient/osc/v1/vmove.py:119
19query: 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
20error bodies {"<fault>": {"code", "message"}}: the SDK puts the message in the error textthe user reads no reason[observed for the stub's shape], masakari:masakari/api/openstack/wsgi.py:1003
21statuses 201, 202, 204 on writes as in reverse-spec-masakari; 409 for conflictsthe 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)

  • --marker expects, in the service, the integer id (masakari:masakari/db/sqlalchemy/api.py:200).
  • --limit 0 sends limit=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 name and control_attributes exist on the service (masakari:masakari/api/openstack/ha/hosts.py:60), but the SDK filters name on 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

TopicRuleSource
Namessegment_host_create → segment host create. Options keep their underscores: --is_enabled, --on_maintenance, --control_attributes, --recovery_method, --service_typesetup.cfg:33, masakariclient/osc/v1/segment.py:130
Positional ordersegment host create ends with the segment; the others start with itmasakariclient/osc/v1/host.py:121
Choicesthe case-sensitive True, Falsemasakariclient/osc/v1/host.py:144
Lookup01 § 1masakariclient/common/utils.py:90
Exit codes0, 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]
Columnsfixed lists, in ordermasakariclient/osc/v1/segment.py:283
Delete outputSegment deleted: <given value> and Host deleted: <given value>, the value as typedmasakariclient/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

EvidenceResultProvesDoes not prove
Parser dump15 of 15 entry points importedcatalogue complete
Unit tests22 of 22 pass, 11 vectors, on 3 of 15 classes; same test, call sequence and status in two runsargv → SDK call for those 3anything about a service: clients are mocks
Wire probes15 of 15 commands sent a request in each mode, on the real route; 200 {}: 3 exit 0routes up to the first errorlater steps
Stateful stub179 invocations, 239 requests; each command succeeded at least once; each defect reproducedfull sequences, exit codes, error texts against this stuba real service; the stub is my reading of the service, and may hide mismatches
Replaynoneno 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/v1 and announced 2.100. Fixed in my copy of the probe.
  • The extractor knew no ha proxy (with_rest=0) and no gate other than supports_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:12 and doc/source/cli/commands.rst:13 are out of date.

7. Non-functional requirements

  • Latency: each invocation authenticates and reads discovery. A name lookup lists a whole collection: GET /segments is on the hot path of 8 commands, and GET …/hosts on 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 payload is printed as is.

8. Checklist for an OpenKeel service

  • [ ] Catalog type instance-ha, with a public endpoint.
  • [ ] GET /v1: version 1.3, min_version 1.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_uuid and integer id in every representation.
  • [ ] "True", "False" accepted for host booleans.
  • [ ] limit and marker paginate to an empty page.
  • [ ] generated-since, repeated sort_key and sort_dir accepted.
  • [ ] 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.