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

Reverse-spec · Clients

python-heatclient — Rewrite pack

Client contract (rewrite pack) — python-heatclient — branch stable/2026.2

Branch stable/2026.2 @ 88120f11830d (5.3.0-2-g88120f1) — repository https://opendev.org/openstack/python-heatclient — analysed on 2026-11-01 Heat reference: [../reverse-spec-heat/](../reverse-spec-heat/00-summary_stable-2026.2.md), branch stable/2026.2 @ 5872396e5, read only. Versions: osc-lib 4.7.0, cliff 4.16.0, keystoneauth1 5.17.0, python-openstackclient 10.3.1.

The goal is to state **what the openstack orchestration commands require from Heat, so that a Go Heat keeps them working unchanged, and to provide data for testing it. The evidence comes from a local stub and from mocks. **No Python Heat and no rewrite were involved, so nothing here proves conformance of any service. See § 5.

1. Contents of _rewrite/

FileContentUse
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json)the 49 commands: class, purpose, each of the 162 arguments, client calls with file:line, error sitesgenerate a parser; check argument names
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json)per command: manager methods, static and observed requests, unit vector count, probe outcomesrequirements per command
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md)the 57 operations with source and usersgap analysis of a service
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json)230 vectors: argv → ordered manager calls with arguments ({uuid} normalised)test 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)per command and for 59 option variants: argv, exit, exception, requests with method, path, headers, bodywire reference; replay the argv against a service
[scenarios_stable-2026.2.json](_rewrite/scenarios_stable-2026.2.json)74 scenarios with hand-written answers: normal flows (G), defects (D), error bodies (E), legacy shell (L), templates (T); argv, exit, exception, stdout, requests with bodiesexamples of good sequences; list of shapes that break the client
extract_plugin_, probe_plugin_, capture_plugin_, repro_scenarios_, build_contract_stable-2026.2.py, inventory.py, check_refs.pythe scripts used, adapted for this packageregenerate everything

Adaptations: the probe's stub answers Heat's lookup 302 and Heat's fault body, and has fixture files, because otherwise half of the commands stop at the first answer. The capture runs from the repository root, where the tests expect their templates. All runs: empty HOME, no OS_*, stdin from /dev/null, 127.0.0.1 only.

2. What the commands require from Heat

Transport and routing

#RequirementIf absentSource
1Catalog entry orchestration whose URL already includes the tenant: <base>/v1/{tenant_id}no endpoint; there is no discovery and no fallbackheatclient/osc/plugin.py:42-43
2GET /stacks/{name_or_id} answers 302 with Location holding /stacks/{name}/{id}Location not returned with redirect, or list index out of range [observed, D11, D10]heatclient/v1/stacks.py:111-116
3The same 302 on POST|PUT|PATCH|DELETE /stacks/{x}, and on /stacks/{x}/resources|events|template|actions|environment|filesthose commands use the typed name directlyheatclient/v1/stacks.py:182-185, heatclient/v1/stacks.py:210, heatclient/v1/actions.py:39, heatclient/v1/events.py:61, heatclient/v1/resources.py:66
4Location keeps the query stringresolve_outputs=False, sort_dir, marker, limit are lost on the redirected request [observed: G01 with a fixed Location]heat:heat/api/openstack/v1/stacks.py:438-440
5Lookup works by ID as well as by namestack show <uuid>, and each stack create right after its POST, which looks up stack['id']heatclient/osc/v1/stack.py:213
6Absolute or relative Location: keystoneauth follows it; _resolve_stack_id needs only the /stacks/ markerheatclient/v1/stacks.py:116
7Content-Type: application/json on JSON answersthe body is ignored: None, then AttributeError or empty output [observed, D24]heatclient/common/utils.py:436-445
8Fault body with error.message on every 4xx and 5xxERROR: None [observed, E03], ERROR: b'…' [observed, E04]heatclient/exc.py:48-73
9error is an object, never a stringa logging traceback [observed, E05]heatclient/exc.py:67
10404 for "stack or child not found", not 400most commands' translated messages, the output fallback, and the end of stack delete --wait depend on itheatclient/osc/v1/stack.py:804-805
11403 is the only other status stack delete tolerates per stackany other stops the loopheatclient/osc/v1/stack.py:792-797
12Accept Content-Type: application/json on GET and DELETE, and bodies of JSON null on signalsent as is [observed, G30]heatclient/common/http.py:310-311, heatclient/v1/resources.py:115
13Accept tags as a comma-separated string, adopt_stack_data as a string, template: null on PATCH, files keys that are file:// URLs[observed, G01, D05, G06]heatclient/osc/v1/stack.py:177, heatclient/osc/v1/stack.py:883
14Ignore or tolerate the unknown key timeout on adoptstack adopt always sends it, null by default (defect 5)heatclient/osc/v1/stack.py:887

Correction to the Heat reverse-spec: its 07 cites heatclient:heatclient/common/http.py:191-243 for "the client re-sends the same method on 302". That code belongs to the older HTTPClient. With OSC, the path is SessionClient, and keystoneauth re-sends the method (keystoneauth1:keystoneauth1/session.py:1246) [observed]. The requirement stands; the source differs.

Response keys the client reads

Missing keys cause the raw errors of 02 § 4, defect 12. Keys marked * have no fallback.

RouteKeysSource
POST /stacksstack\*, stack.id\*heatclient/osc/v1/stack.py:205, heatclient/osc/v1/stack.py:213
POST /stacks/previewstack\*; prints all keysheatclient/v1/stacks.py:165
GET /stacksstacks\*; id\*, stack_name, stack_status, creation_time, updated_time; optional stack_owner, parent, deletion_time, projectheatclient/common/base.py:118, heatclient/osc/v1/stack.py:658-712
GET /stacks/{n}/{i}stack\*, then id, stack_name, description, creation_time, updated_time, stack_status\*, stack_status_reason, parameters, outputs, links. stack_status must hold a _, or stack failures list failsheatclient/osc/v1/stack.py:455-475, heatclient/v1/stacks.py:87-90
PUT|PATCH …/previewresource_changesheatclient/osc/v1/stack.py:387
…/outputsoutputs\*; output_key, descriptionheatclient/osc/v1/stack.py:1089, heatclient/osc/v1/stack.py:1098
…/outputs/{k}output\*; output_error if present is an errorheatclient/osc/v1/stack.py:1046, heatclient/osc/v1/stack.py:1062
…/snapshotssnapshots\*; id, name, action, status, status_reason, creation_timeheatclient/osc/v1/snapshot.py:55-66
POST …/snapshotsid, name, action, status, status_reason, data, creation_timeheatclient/osc/v1/snapshot.py:175-184
…/resourcesresources\*; resource_name, physical_resource_id, resource_type, resource_status, updated_time, links (rel: self, stack, nested), required_by for -f dotheatclient/osc/v1/resource.py:133-146, heatclient/common/resource_formatter.py:22-27, heatclient/common/resource_formatter.py:125
…/resources/{r}resource\*; physical_resource_id for nested hooksheatclient/v1/resources.py:89, heatclient/common/hook_utils.py:47
…/metadatametadata\*, an objectheatclient/osc/v1/resource.py:186
…/eventsevents\*; id, resource_name\*, resource_status, resource_status_reason, physical_resource_id, event_time\*, links (rel: stack, root_stack, self)heatclient/common/utils.py:200-260, heatclient/common/event_utils.py:86-91, heatclient/common/event_utils.py:195-203
…/events/{e}event\*; prints all keysheatclient/v1/events.py:88
/software_configssoftware_configs\*; id, name, group, creation_timeheatclient/osc/v1/software_config.py:107
/software_configs/{c}, POSTsoftware_config\*; id\*, name, group, config, inputs, outputs, options, creation_timeheatclient/v1/software_configs.py:61, heatclient/osc/v1/software_config.py:228-238, heatclient/osc/v1/software_deployment.py:124
/software_deploymentssoftware_deployments\*; id, config_id, server_id, action, status, creation_time, status_reasonheatclient/osc/v1/software_deployment.py:210-213
/software_deployments/{d}, POSTsoftware_deployment\*; id, server_id, config_id\*, creation_time, updated_time, status, status_reason, input_values, action, output_values\* with deploy_stdout, deploy_stderr. Never empty on createheatclient/osc/v1/software_deployment.py:130, heatclient/osc/v1/software_deployment.py:168, heatclient/osc/v1/software_deployment.py:253-265, heatclient/osc/v1/software_deployment.py:339
/software_deployments/metadata/{s}metadataheatclient/v1/software_deployments.py:50
/resource_typesresource_types\*: strings, or objects with resource_type, description under with_description=Trueheatclient/v1/resource_types.py:31-36
/template_versionstemplate_versions\*, at least 2 entries; version, type, aliasesheatclient/osc/v1/template.py:44-54
/template_versions/{v}/functionstemplate_functions\*; functions, descriptionheatclient/osc/v1/template.py:95-98
/servicesservices\*; hostname, binary, engine_id, host, topic, updated_at, statusheatclient/osc/v1/service.py:35-36
/build_infoan object; api, engine formatted as JSONheatclient/osc/v1/build_info.py:39-45
POST /validateany objectheatclient/osc/v1/template.py:186-188

Texts and values the client parses

A rewrite must keep these strings exactly.

ValueCompared withPurposeClientHeat
Stack CREATE started, Stack UPDATE started, Stack DELETE startedevent resource_status_reasonstart of the current actionheatclient/common/event_utils.py:27, heatclient/common/event_utils.py:31, heatclient/common/event_utils.py:35heat:heat/engine/stack.py:1282
CREATE paused until Hook pre-create is cleared, and the UPDATE and DELETE formssamea hook is pendingheatclient/common/event_utils.py:28, heatclient/common/event_utils.py:32, heatclient/common/event_utils.py:36heat:heat/engine/resource.py:538
Hook pre-create is cleared, and the other typessamea hook was releasedheatclient/common/event_utils.py:29, heatclient/common/event_utils.py:33, heatclient/common/event_utils.py:37heat:heat/engine/resource.py:2486
<ACTION>_IN_PROGRESS, <ACTION>_COMPLETE, <ACTION>_FAILED, with actions CREATE, UPDATE, DELETE, SUSPEND, RESUME, CHECK, ADOPTstack status and event statusend of --wait, cancel and hook rulesheatclient/common/event_utils.py:183-187, heatclient/common/hook_utils.py:65-74, heatclient/osc/v1/stack.py:1307-1311
stack event: resource_name = stack name, physical_resource_id = stack ID = last segment of the stack linkevent fieldsspot the event that ends the waitheatclient/common/event_utils.py:195-203
root_stack link on eventsevent linksthe server honoured nested_depth; without it the client walks nested stacksheatclient/common/event_utils.py:86-92
nested link on resources; the last 2 segments of its href are name/idresource linksnested stacksheatclient/common/utils.py:138-144, heatclient/osc/v1/stack_failures.py:71-72
stack link: …/stacks/{name}/{id}event and resource linksstack name of an eventheatclient/common/event_utils.py:154-160, heatclient/v1/resources.py:39-45
resource status ends with FAILED; resource_type is OS::Heat::StructuredDeployment or OS::Heat::SoftwareDeploymentresource fieldsfailure reportheatclient/osc/v1/stack_failures.py:68, heatclient/osc/v1/stack_failures.py:99-100

stack_status for cancellation is lowered and compared with create_in_progress and update_in_progress (heatclient/osc/v1/stack.py:1307-1318).

What the commands never call

GET /stacks/detail, GET / (versions), CFN routes, PUT /software_deployments/{id} from OSC (heatclient/v1/software_deployments.py:70 serves the legacy shell), and the actions:* other than the 5 listed. A Heat rewrite must validate them by other means: see the Heat pack.

3. Interaction sequences worth testing

Scenarios G01 to G31 give good sequences. Those a service most easily breaks:

  1. Name lookup with each method (G05, G06, G08, G27).
  2. Event paging: limit=1&sort_dir=desc to get a marker, then marker=<id>&sort_dir=asc until the stack event (G02, G05).
  3. 404 on events once a stack is gone (G08).
  4. Nested events: nested_depth with a root_stack link (G13); without it, the fallback multiplies requests (G14).
  5. Hook cycle: event texts in order (G17), unset_hook signal on name/id (G18).
  6. Client pagination: limit returning fewer than asked, then marker=<last id> (G10).

4. For a rewrite of the CLI itself

TopicRuleSource
Namesstack_resource_mark_unhealthy → stack resource mark unhealthy; scripts depend on these and on argument names, including the odd --with_conditionspyproject.toml:45-93, heatclient/osc/v1/template.py:74
Exit codes0, 1, 2 as in 02; do not copy its § 4cliff:cliff/command.py:196
Formatslog default for events; dot for resources; JSON or YAML defaults on RawFormat commandspyproject.toml:95-109, heatclient/common/format_utils.py:20-55
Client-side template handlingthe largest part: get_file, type resolution, environment merge rules, file keys as absolute URLs, base64 for non UTF-8heatclient/common/template_utils.py:110-178, heatclient/common/template_utils.py:196-213, heatclient/common/utils.py:410-421
--parameter; split when given once; repeated key gives a listheatclient/common/utils.py:322-349
Event log line<time> <id> [<resource path>]: <status> <reason>, with paths built across nested stacksheatclient/common/utils.py:178-266
PluginPython entry points; a non-Python rewrite cannot be loaded by openstackpyproject.toml:42-43

Recommendation: keep the Python client and make the service compatible. The commands are thin, the behaviour worth keeping sits in template handling and event logic, and the plugin only loads in Python. If the plugin is carried forward, fix defects 1, 2, 3 and 5 first. Those are small changes, but none was made here, because the repository is read-only for this work.

5. Conformance: what was proved

EvidenceResultWhat it provesWhat it does not prove
Parser dump49 of 49 classes imported, 0 failuresthe catalogue is complete for the declared entry points
OSC unit tests with recorder239 of 239 pass, 230 vectors with a parsed command, for 48 of 49 classesargv → manager callsanything about a service: managers are mocks. defects 1 and 5 pass these tests
Second run of the capture230 of 230 vectors with the same test, call sequence and statusmethod sequences are deterministicargument values were not compared
Whole unit suite913 run, 893 pass; 17 failures and 3 errors, none in heatclient/tests/unit/oscthe managers build the expected URLs from mockssee Blind spots in 00 for the cause of the failures
Probe 40449 commands and 59 variants; 45 + 47 runs sent a request; 151 requests; all 49 default runs exit 1real routes, headers and bodies up to the first errorwhat a service answers
Probe 200 ({})45 + 47 runs sent a request; 164 requests; 17 + 12 runs exit 0which commands tolerate an empty answerthat those commands work: {} is not a Heat answer
Scenarios74 runs, each with scripted answersthe client's handling of those bytes, including every defect listedthat a real Heat answers like this; the bodies come from reading Heat and its reverse-spec, not from a capture

The usual "replay is mandatory" bar is not met. No vector was replayed against a Python Heat or a rewrite, because the rules of this work exclude any real service. To close the gap, on a devstack stable/2026.2:

  1. run heatclient/tests/functional/osc;
  2. run the probe's argv and the scenario argv with a real template, and compare routes and statuses with the JSON files here;
  3. then do the same against the Go Heat.

Also compare the Heat pack's exchanges_stable-2026.2.json, which recorded this client's requests on the real Python Heat through heat-tempest-plugin: a useful cross-check, not done here.

Classes without a unit vector (1)

heatclient.osc.v1.stack:ListFileStack (stack file list, heatclient/osc/v1/stack.py:628): no test. It was seen on the stub only.

Vectors without calls: 13 tests stop on the client before any call, on purpose (bad arguments, a declined prompt).

6. Gaps found while building the pack

  • 3 operations were never sent by the probes, only by scenarios: POST /software_deployments, DELETE /software_deployments/{id}, GET …/events/{id}.
  • 4 commands send nothing with minimal arguments: software deployment create (defect 1), software deployment output show, stack output show (both need an optional positional or --all), stack update (needs -t or --existing). Their variants do.
  • The 200 probe stops at the first {} that the client cannot use, so only the scenarios reach later steps.
  • --wait paths ran with time.sleep replaced, so real timing was not tested.
  • Swift calls were seen only with the legacy shell, on a stub answering 201 to everything.
  • The extractor's REST resolution targets openstacksdk, so it found almost nothing here (with_rest=2). The static route table in build_contract_stable-2026.2.py was written by hand from heatclient/v1/*.py. A route added to a manager would not appear by itself.

7. Checklist for a Go Heat

  • [ ] 302 lookup with every method, sub-paths, the query kept (§ 2 #2–5).
  • [ ] Fault body with error.message, JSON content type (§ 2 #7–9).
  • [ ] 404 semantics, including events of a deleted stack (§ 2 #10).
  • [ ] Response keys of § 2, collection keys always present, even when empty.
  • [ ] Event texts and links of § 2, unchanged.
  • [ ] events honouring marker, limit, sort_dir, sort_keys, nested_depth, with a root_stack link.
  • [ ] At least 2 template versions with aliases.
  • [ ] Decide global_tenant policy knowing that stack list --long sends it (defect 2).
  • [ ] Run the probes' argv with software deployment create --signal-transport NO_SIGNAL, since the default fails on the client.

Source: reverse-spec-python-heatclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.