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

Reverse-spec · Services

heat — REST API

REST API — heat — branch stable/2026.2

Branch stable/2026.2 @ 5872396e548c (27.0.1) — repository https://opendev.org/openstack/heat — analysed on 2026-10-29

Heat exposes two HTTP APIs, both thin RPC façades in front of heat-engine:

  • heat-api, the native Orchestration API v1 (heat/api/openstack/v1/__init__.py:27, 40 paths, 61 operations);
  • heat-api-cfn, an AWS CloudFormation-compatible query API (heat/api/cfn/v1/__init__.py:22): one path /v1/, the operation is chosen by the Action query parameter. It also serves the two signal endpoints that instances call back (/v1/waitcondition/{arn} and /v1/signal/{arn}).

Neither API process touches the database: every handler builds an RPC message to the engine topic (see 03 § RPC) and turns the reply or the remote exception into HTTP. The machine-readable route table, with the policy rule enforced by each handler, is [_rewrite/routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) (produced from the live routes.Mapper by _rewrite/tools/heat_routes.py).

1. Cross-cutting behaviour

1.1 Pipelines

etc/heat/api-paste.ini:15 (flavor default, chosen by [paste_deploy]/flavor, heat/api/__init__.py:28-37):

OrderFilterEffectSource
1corsCORS headersetc/heat/api-paste.ini:40
2request_idx-openstack-request-idetc/heat/api-paste.ini:85
3faultwrapturns every exception into the JSON error body (§ 1.3)heat/api/middleware/fault.py:52
4http_proxy_to_wsgihonours X-Forwarded-Protoetc/heat/api-paste.ini:61
5versionnegotiationstrips /v1, or answers 300 with the version listheat/api/middleware/version_negotiation.py:37
6authurlsets X-Auth-Urletc/heat/api-paste.ini:65
7authtokenkeystonemiddleware token validation (401)etc/heat/api-paste.ini:69
8contextbuilds heat.common.context.RequestContext from the identity headersetc/heat/api-paste.ini:55
9osprofilertracingetc/heat/api-paste.ini:88
10apiv1appheat.api.openstack.v1:APIetc/heat/api-paste.ini:25

Because faultwrap sits outside authtoken, a 401 from keystonemiddleware is returned as keystonemiddleware's own body, not Heat's error body. The CFN pipeline (etc/heat/api-paste.ini:22) has no faultwrap: CFN errors are XML ErrorResponse bodies built by heat/api/aws/exception.py:42-67, and ec2authtoken runs before authtoken.

/healthcheck is served by oslo.middleware on both APIs (etc/heat/api-paste.ini:5, :11).

1.2 Version negotiation, routing, 405 and OPTIONS

  • GET / or GET /versions → 300 Multiple Choices, body {"versions": [{"id": "v1.0", "status": "CURRENT", "links": [...]}]} (heat/api/versions.py:30-52, heat/api/middleware/version_negotiation.py:60-61).
  • /v1/... is stripped and routed; /v2/... (any other version in the path) → 300 (heat/api/middleware/version_negotiation.py:63-83).
  • Without a version in the path, an Accept: application/vnd.openstack.orchestration-v1.0 header is accepted; anything else → 404 (heat/api/middleware/version_negotiation.py:85-111).
  • A path that is not valid UTF-8 → 400 (heat/api/middleware/version_negotiation.py:46-49).
  • For every URL, the router registers a reject route for the methods it does not implement (405 with an Allow header) and an OPTIONS route (204 with Allow) (heat/api/openstack/v1/__init__.py:64-80, heat/common/wsgi.py:132-147).
  • Every path starts with /{tenant_id}: registered_policy_enforce returns 403 when tenant_id differs from the token's project, except for admins (is_admin context), whose tenant_id is replaced by their own project id (heat/api/openstack/v1/util.py:37-41).

1.3 Error body (native API)

heat/api/middleware/fault.py:152-164:

{"code": 400, "title": "Bad Request", "explanation": "The server could not comply with the request since it is either malformed or otherwise incorrect.",
 "error": {"type": "StackValidationFailed", "traceback": null, "message": "Property error: ..."}}
  • code, title, explanation come from the webob class chosen by FaultWrapper.error_map, looked up by the exception class name, then by its base classes, defaulting to 500 (heat/api/middleware/fault.py:55-107). Remote exceptions arrive as <Name>_Remote classes and the suffix is dropped (heat/api/middleware/fault.py:126-128).
  • error.message is present only when the exception has safe = True (every HeatException, heat/common/exception.py:54); UpdateInProgress, Python built-ins and oslo.messaging errors carry no message (heat/api/middleware/fault.py:161-162).
  • error.traceback is filled only with [DEFAULT]/debug = True (heat/api/middleware/fault.py:119-147): a debug-mode deployment leaks engine tracebacks.
  • The full table exception → status is computed in [_rewrite/errors_stable-2026.2.json](_rewrite/errors_stable-2026.2.json) and summarised in 02.

1.4 Request bodies

  • A body is deserialised only when Content-Length > 0 and the content type is JSON (or missing, or text/plain, bug 1887882) and the body starts with { (heat/common/wsgi.py:236-266). Otherwise the handler is called without body; a handler that requires one raises TypeError, turned into 400 "The server could not comply with the request since it is either malformed or otherwise incorrect." (heat/common/wsgi.py:345-354).
  • Bodies larger than [DEFAULT]/max_json_body_size (1048576) → RequestLimitExceeded → 400 (heat/common/wsgi.py:268-278).
  • Templates are limited to [DEFAULT]/max_template_size (524288 bytes) when inline (heat/api/openstack/v1/stacks.py:102-112).
  • Unknown query parameters are silently ignored (heat/api/openstack/v1/util.py:114-145), except on the resource list, which rejects them with 400 (heat/api/openstack/v1/resources.py:108-113).
  • Handlers that return nothing produce 200 with the JSON body null (heat/common/serializers.py, JSONResponseSerializer.default): this is the case of POST .../actions and POST .../resources/{name}/signal.

1.5 Identity of a stack

  • A stack is addressed by name and id: /{tenant_id}/stacks/{stack_name}/{stack_id}. _get_stack rejects a mismatched name with 404 (heat/engine/service.py:578-580).
  • /{tenant_id}/stacks/{stack_name_or_id}[/<subpath>] (any method) answers 302 Found to the canonical URL, keeping the query string (heat/api/openstack/v1/stacks.py:426-442); clients rely on following it (heatclient, gabbi redirects: True). An ARN (arn:openstack:heat::<tenant>:stacks/<name>/<id>, heat/common/identifier.py:92-103) is also accepted.
  • The links[].href of a stack is req.relative_url(identity.url_path()) (heat/api/openstack/v1/util.py:91-99), i.e. it reflects the host the client used.

1.6 Policies

Each handler enforces <REQUEST_SCOPE>:<handler name> (heat/api/openstack/v1/util.py:42-49). On this branch the defaults are the "secure RBAC" ones: reader for reads, member for writes, heat_stack_user (the role of in-instance credentials) allowed only on stacks:lookup, resource:signal, resource:metadata, software_deployments:metadata and cloudformation:DescribeStackResource, and denied everywhere else through rule:deny_stack_user; note that resource:signal needs only reader, so any project reader can signal a resource; stacks:global_index and software_configs:global_index are ! (deny everybody). The full list (100 rules, with scope_types and deprecated rules) is [_rewrite/policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json). Resource-type policies (resource_types:<type>, for example resource_types:OS::Nova::Flavor = rule:project_admin) are enforced by the engine on every stack create/update/delete and on resource_types/{type} (heat/engine/service.py:813, heat/engine/service.py:1751).

Exceptions to the pattern:

  • PATCH /stacks/{name}/{id} is decorated with no_policy_enforce and enforces stacks:update_patch, or stacks:update_no_change when the body changes nothing (heat/api/openstack/v1/stacks.py:507-524, heat/api/openstack/v1/stacks.py:169-178).
  • POST .../actions enforces nothing itself; the sub-action handler enforces actions:<action> (heat/api/openstack/v1/actions.py:46-69). The rule actions:action exists in the defaults but is never checked.
  • GET /stacks?global_tenant=true calls global_index, which enforces stacks:global_index (deny by default) and then lists with an admin context (heat/api/openstack/v1/stacks.py:344-360).

2. Native API — summary

Paths are relative to /v1. "Mode" is the engine RPC; every one of them is a call (blocking, [DEFAULT]/rpc_response_timeout), see 03.

MethodPathHandlerPolicyEngine RPCSuccessTypical errors
POST/{tenant_id}/validateheat/api/openstack/v1/stacks.py:617stacks:validate_templatevalidate_template200400
GET/{tenant_id}/resource_typesheat/api/openstack/v1/stacks.py:656stacks:list_resource_typeslist_resource_types200—
GET/{tenant_id}/resource_types/{type_name}heat/api/openstack/v1/stacks.py:704stacks:resource_schemaresource_schema200400, 403, 404
GET/{tenant_id}/resource_types/{type_name}/templateheat/api/openstack/v1/stacks.py:711stacks:generate_templategenerate_template200400, 500
GET/{tenant_id}/template_versionsheat/api/openstack/v1/stacks.py:679stacks:list_template_versionslist_template_versions200—
GET/{tenant_id}/template_versions/{template_version}/functionsheat/api/openstack/v1/stacks.py:687stacks:list_template_functionslist_template_functions200404
GET/{tenant_id}/stacksheat/api/openstack/v1/stacks.py:348stacks:index (stacks:global_index)list_stacks [+ count_stacks]200400, 403
POST/{tenant_id}/stacksheat/api/openstack/v1/stacks.py:404stacks:createcreate_stack201400, 409
POST/{tenant_id}/stacks/previewheat/api/openstack/v1/stacks.py:370stacks:previewpreview_stack200400, 409
GET/{tenant_id}/stacks/detailheat/api/openstack/v1/stacks.py:363stacks:detaillist_stacks200—
any/{tenant_id}/stacks/{stack_name}[/resources|events|template|actions|environment|files]heat/api/openstack/v1/stacks.py:426stacks:lookupidentify_stack302404
GET/{tenant_id}/stacks/{stack_name}/{stack_id}heat/api/openstack/v1/stacks.py:445stacks:showshow_stack200404
PUT/{tenant_id}/stacks/{stack_name}/{stack_id}heat/api/openstack/v1/stacks.py:488stacks:updateupdate_stack202400, 404, 409
PATCH/{tenant_id}/stacks/{stack_name}/{stack_id}heat/api/openstack/v1/stacks.py:507stacks:update_patch / update_no_changeupdate_stack (existing=True)202400, 404, 409
DELETE/{tenant_id}/stacks/{stack_name}/{stack_id}heat/api/openstack/v1/stacks.py:590stacks:deletedelete_stack204404, 409
GET.../{stack_id}/templateheat/api/openstack/v1/stacks.py:466stacks:templateget_template200404
GET.../{stack_id}/environmentheat/api/openstack/v1/stacks.py:476stacks:environmentget_environment200404
GET.../{stack_id}/filesheat/api/openstack/v1/stacks.py:483stacks:filesget_files200404
PUT.../{stack_id}/previewheat/api/openstack/v1/stacks.py:548stacks:preview_updatepreview_update_stack200400, 404
PATCH.../{stack_id}/previewheat/api/openstack/v1/stacks.py:569stacks:preview_update_patchpreview_update_stack200400, 404
DELETE.../{stack_id}/abandonheat/api/openstack/v1/stacks.py:599stacks:abandonabandon_stack200400 (disabled), 404
GET.../{stack_id}/exportheat/api/openstack/v1/stacks.py:609stacks:exportexport_stack200400 (disabled), 404
POST.../{stack_id}/snapshotsheat/api/openstack/v1/stacks.py:727stacks:snapshotstack_snapshot200400, 404, 409
GET.../{stack_id}/snapshotsheat/api/openstack/v1/stacks.py:743stacks:list_snapshotsstack_list_snapshots200404
GET.../{stack_id}/snapshots/{snapshot_id}heat/api/openstack/v1/stacks.py:732stacks:show_snapshotshow_snapshot200404
DELETE.../{stack_id}/snapshots/{snapshot_id}heat/api/openstack/v1/stacks.py:738stacks:delete_snapshotdelete_snapshot204400, 404
POST.../{stack_id}/snapshots/{snapshot_id}/restoreheat/api/openstack/v1/stacks.py:750stacks:restore_snapshotstack_restore202400, 404
GET.../{stack_id}/outputsheat/api/openstack/v1/stacks.py:755stacks:list_outputslist_outputs200404
GET.../{stack_id}/outputs/{output_key}heat/api/openstack/v1/stacks.py:762stacks:show_outputshow_output200404
GET.../{stack_id}/resourcesheat/api/openstack/v1/resources.py:96resource:indexlist_stack_resources200400, 404
GET.../{stack_id}/resources/{resource_name}heat/api/openstack/v1/resources.py:135resource:showdescribe_stack_resource200403, 404
PATCH.../{stack_id}/resources/{resource_name}heat/api/openstack/v1/resources.py:167resource:mark_unhealthyresource_mark_unhealthy200400, 404, 409
GET.../{stack_id}/resources/{resource_name}/metadataheat/api/openstack/v1/resources.py:150resource:metadatadescribe_stack_resource200403, 404
POST.../{stack_id}/resources/{resource_name}/signalheat/api/openstack/v1/resources.py:160resource:signalresource_signal200 (null)400, 404
GET.../{stack_id}/eventsheat/api/openstack/v1/events.py:110events:indexlist_events200400, 404
GET.../{stack_id}/resources/{resource_name}/eventsheat/api/openstack/v1/events.py:110events:indexlist_events200404
GET.../{stack_id}/resources/{resource_name}/events/{event_id}heat/api/openstack/v1/events.py:153events:showlist_events (uuid filter)200404
POST.../{stack_id}/actionsheat/api/openstack/v1/actions.py:46actions:{suspend|resume|check|cancel_update|cancel_without_rollback}stack_suspend / stack_resume / stack_check / stack_cancel_update200 (null)400, 404
GET/{tenant_id}/build_infoheat/api/openstack/v1/build_info.py:36build_info:build_infoget_revision200—
GET/{tenant_id}/software_configsheat/api/openstack/v1/software_configs.py:78software_configs:index (global_index)list_software_configs200400, 403
POST/{tenant_id}/software_configsheat/api/openstack/v1/software_configs.py:100software_configs:createcreate_software_config200400
GET/{tenant_id}/software_configs/{config_id}heat/api/openstack/v1/software_configs.py:93software_configs:showshow_software_config200404
DELETE/{tenant_id}/software_configs/{config_id}heat/api/openstack/v1/software_configs.py:115software_configs:deletedelete_software_config204400, 404
GET/{tenant_id}/software_deploymentsheat/api/openstack/v1/software_deployments.py:38software_deployments:indexlist_software_deployments200—
POST/{tenant_id}/software_deploymentsheat/api/openstack/v1/software_deployments.py:65software_deployments:createcreate_software_deployment200400, 404
GET/{tenant_id}/software_deployments/metadata/{server_id}heat/api/openstack/v1/software_deployments.py:48software_deployments:metadatametadata_software_deployments200—
GET/{tenant_id}/software_deployments/{deployment_id}heat/api/openstack/v1/software_deployments.py:58software_deployments:showshow_software_deployment200404
PUT/{tenant_id}/software_deployments/{deployment_id}heat/api/openstack/v1/software_deployments.py:76software_deployments:updateupdate_software_deployment200400, 404
DELETE/{tenant_id}/software_deployments/{deployment_id}heat/api/openstack/v1/software_deployments.py:88software_deployments:deletedelete_software_deployment204404
GET/{tenant_id}/servicesheat/api/openstack/v1/services.py:37service:indexlist_services200403, 503

The stack_lookup routes accept GET, POST, PUT, PATCH and DELETE (heat/api/openstack/v1/__init__.py:155-183); a DELETE /stacks/{name} is therefore answered by a 302 to the canonical URL, and the client must re-send the DELETE there (the integration tests do; see exchanges in _rewrite/exchanges_stable-2026.2.json).

3. Stacks

POST /{tenant_id}/stacks — create a stack

Functional description

  • Purpose: instantiate a template (HOT or CFN) as a new stack and provision its resources asynchronously.
  • Actor: project member (stacks:create); every resource type of the template must also pass resource_types:<type> (heat/engine/service.py:813).
  • Preconditions:
    • no live stack of the same name in the project (StackExists → 409, checked early at heat/engine/service.py:739-740 and again atomically at insert time, heat/db/api.py:982-998);
    • fewer than [DEFAULT]/max_stacks_per_tenant (512) stacks in the project, unless admin (heat/engine/service.py:743-749, RequestLimitExceeded → 400);
    • at most [DEFAULT]/max_resources_per_stack (1000) resources at the top level (heat/engine/service.py:760-766);
    • with adopt_stack_data, [DEFAULT]/enable_stack_adopt must be true (heat/engine/service.py:780-782, NotSupported → 400).
  • Business rules:
    • the template is inline (template, JSON object or YAML/JSON string), fetched from template_url by heat-api (heat/api/openstack/v1/stacks.py:115-122, heat/common/urlfetch.py), or taken from adopt_stack_data.template;
    • environment (string or object) and parameters are merged; when environment_files is given, environment is ignored and the files listed (looked up in files) are merged in order (heat/api/openstack/v1/stacks.py:139-153, heat/engine/service.py:799-801);
    • files_container names a Swift container whose objects become files (heat/engine/service.py:795-797);
    • timeout_mins must be an integer (heat/api/openstack/v1/stacks.py:390-392); tags a comma-separated string or list; converge is refused on create (heat/api/openstack/v1/stacks.py:396-400);
    • every other body key is passed to the engine as args (heat/api/openstack/v1/stacks.py:164-167), notably disable_rollback (default true in heatclient, false in the engine), timeout_mins, tags, adopt_stack_data;
    • the full template validation runs synchronously: parameters and their constraints, resource types (unknown type → 400 "The Resource Type (X) could not be found."), properties schemas, intrinsic functions, dependency cycles, outputs (heat/engine/service.py:768-820).
  • Flow:
    1. (synchronous, heat-api) policy, body parsing, template_url download.
    2. (synchronous, engine) parse and validate, then stack.store(): raw_template and stack rows; with deferred_auth_method = trusts (default), a Keystone trust from the user to the Heat service user is created and stored encrypted in user_creds (heat/engine/stack.py:809-820).
    3. (synchronous, engine) a project is created in the stack-user domain for in-instance credentials (stack_user_project_id, heat/engine/service.py:891-901, heat/engine/clients/os/keystone/heat_keystoneclient.py:437-451). A failure there marks the stack CREATE_FAILED but still returns 201.
    4. (synchronous, engine) convergence (default, [DEFAULT]/convergence_engine = True): a new traversal id, resource rows in state INIT_COMPLETE, the dependency graph stored in stack.current_deps, one sync_point row per graph node, and one check_resource cast per leaf (heat/engine/stack.py:1445-1566). The legacy engine instead takes the stack lock and runs the whole create in a thread of this engine (heat/engine/service.py:929-937).
    5. (asynchronous, workers) each check_resource creates its resource and propagates to its dependants; the last root marks the stack CREATE_COMPLETE (05 § check_resource).
  • Outcome: 201 with Location = the canonical stack URL and body {"stack": {"id": "<uuid>", "links": [{"href": "...", "rel": "self"}]}} (heat/api/openstack/v1/stacks.py:419-423, heat/api/openstack/v1/stacks.py:777-782). The stack is CREATE_IN_PROGRESS; the client polls GET or the events.
  • Effects visible elsewhere: a Keystone trust and a project in the heat domain; notification orchestration.stack.create.start (heat/engine/notification/stack.py:18-39); one event row per state change.
  • Limitations: template_url is fetched with the API's network access (SSRF surface, schemes restricted by heat/common/urlfetch.py); the 201 means "accepted", not "valid at runtime" — runtime failures (quotas of other services, bad image) only show up as CREATE_FAILED.

Technical sheet

  • Request: {"stack_name": "s", "template": {...} | "template_url": "...", "parameters": {...}, "environment": {...}|"...", "environment_files": [...], "files": {"name": "content"}, "files_container": "c", "timeout_mins": 60, "disable_rollback": true, "tags": "a,b", "adopt_stack_data": "<json>"}. Missing stack_name → 400 "No stack name specified"; no template → 400 "No template specified" (heat/api/openstack/v1/stacks.py:85-128).
  • Response: 201, Location, {"stack": {"id", "links"}}.
  • Errors: 400 (validation, limits, parse error "Template not in valid format: ..."), 403 (policy, tenant mismatch, resource_types: policy), 409 (StackExists), 500 (Keystone trust or domain failures, AuthorizationFailure).

GET /{tenant_id}/stacks — list stacks

  • Purpose: list the project's stacks, with filtering, sorting, pagination and optional count.
  • Parameters (heat/api/openstack/v1/stacks.py:217-341): limit, marker (stack id), sort_keys (repeatable; mapped through heat/db/api.py:846-857), sort_dir, show_deleted, show_nested, show_hidden (stacks tagged with [DEFAULT]/hidden_stack_tags are hidden by default), tags, tags_any, not_tags, not_tags_any, with_count, global_tenant, and filters on id, status, name, action, tenant, username, owner_id, plus stack_name, stack_status… (rpc_api.STACK_KEYS minus the unsupported ones). Repeated filter values are OR-ed.
  • Response: {"stacks": [...], "links": [{"rel": "next", "href": ...}]?, "count": N?}; each stack has id, stack_name, description (always "" in lists, heat/engine/api.py:259), stack_status (<ACTION>_<STATUS>), stack_status_reason, creation_time, updated_time, deletion_time, stack_owner, parent, stack_user_project_id, tags, links, plus project for admins (heat/api/openstack/v1/views/stacks_view.py:38-77).
  • Errors: 400 for a non-boolean flag or a non-integer limit; 403 for global_tenant=true under default policy.

GET /{tenant_id}/stacks/{stack_name}/{stack_id} — show a stack

  • Returns {"stack": {...}} with, in addition to the list fields, parameters (all values as strings, hidden ones as ******), outputs (resolved, unless resolve_outputs=false), template_description, disable_rollback, timeout_mins, capabilities: [], notification_topics: [] (heat/engine/api.py:197-241).
  • Resolving outputs may call other services (attributes of resources) and stores the resolved attributes for convergence stacks (heat/engine/service.py:600-619). It also works on a soft-deleted stack (show_deleted=True, heat/engine/service.py:595).

PUT /{tenant_id}/stacks/{stack_name}/{stack_id} — update (replace template)

  • Purpose: move the stack to a new template/environment; resources are updated in place, replaced, added or removed.
  • Preconditions: the stack is not SUSPEND_* or DELETE_* (heat/engine/service.py:1098-1104, NotSupported → 400); immutable parameters unchanged (ImmutableParameterModified → 400, heat/engine/service.py:1050-1053); with the legacy engine, no other action in progress (stack lock → ActionInProgress 409). With convergence, an update while another update is in progress is allowed: it starts a new traversal and the previous one stops propagating (05).
  • Rules: timeout_mins, disable_rollback and converge default to the current stack's values (heat/engine/service.py:1035-1041); converge=true forces every resource to be checked against reality (observe_on_update).
  • Outcome: 202 Accepted with an empty body (heat/api/openstack/v1/stacks.py:503); the stack goes to UPDATE_IN_PROGRESS, then UPDATE_COMPLETE, or UPDATE_FAILED and, when rollback is enabled, ROLLBACK_IN_PROGRESS → ROLLBACK_COMPLETE|FAILED.

PATCH /{tenant_id}/stacks/{stack_name}/{stack_id} — update (patch)

  • Same as PUT with existing=true (heat/api/openstack/v1/stacks.py:72-73): a missing template reuses the current one (or, for a FAILED legacy stack, the previous template; otherwise NotSupported "PATCH update to non-COMPLETE stack", heat/engine/service.py:963-990); parameters are merged into the existing ones; clear_parameters removes some; files are merged; tags are kept unless given (heat/engine/service.py:1043-1045).

PUT|PATCH .../preview — preview an update

  • Returns {"resource_changes": {"unchanged": [...], "updated": [...], "replaced": [...], "added": [...], "deleted": [...]}}, each entry a resource as in the resource list; show_nested=true descends into nested stacks (heat/engine/service.py:1131-1238). The preview is advisory: plugins can decide on replacement at runtime (heat/engine/service.py:1141-1143).

DELETE /{tenant_id}/stacks/{stack_name}/{stack_id} — delete

  • Flow (convergence): if a traversal is in progress it is stopped (stop_traversal: new traversal id, <ACTION>_FAILED "Stack <ACTION> cancelled", its sync points deleted); the in-flight workers are cancelled through the cancel_check_resource cast to each engine that holds a resource lock, waiting up to 3 × 5 s (heat/engine/worker.py:378-442); then a convergence traversal to an empty template with action DELETE (heat/engine/service.py:1506-1520). Snapshots of the stack are deleted as independent graph nodes (heat/engine/stack.py:1656-1664).
  • Outcome: 204 (the RPC is a call, heat/api/openstack/v1/stacks.py:593-596), stack DELETE_IN_PROGRESS; at DELETE_COMPLETE the trust, the stack-domain project and the stack row (soft delete: deleted_at) are removed (heat/engine/stack.py:2429-2442).
  • A stack already DELETE_COMPLETE → 404 (heat/engine/service.py:1500-1504).
  • Legacy engine: stack lock; if another engine holds it, a send(THREAD_CANCEL) call to its listener, then a wait of up to error_wait_time + 10 s before stop_stack (heat/engine/service.py:1522-1603).

DELETE .../abandon and GET .../export

  • Both need [DEFAULT]/enable_stack_abandon = True (default false → 400 "Stack Abandon is not supported.", heat/engine/service.py:1625-1626).
  • export returns the abandon data (template, environment, resources with their resource_id, resource_data, metadata, status) without changing anything (heat/engine/service.py:1606-1615).
  • abandon returns the same data and deletes the stack from Heat only: the physical resources are left in place (heat/engine/service.py:1641-1661, convergence traversal with abandon=True).

Snapshots

  • POST .../snapshots {"name": ...}: refused while the stack is *_IN_PROGRESS (409 ActionInProgress) or above [DEFAULT]/max_snapshots_per_stack (32) (heat/engine/service.py:2194-2211). Convergence: a snapshot row CREATE_IN_PROGRESS whose id becomes the traversal id of a SNAPSHOT traversal (heat/engine/service.py:2213-2229); each resource stores its snapshot data in resource_snapshot; at the end stack.update_snapshot writes data and COMPLETE/FAILED (heat/engine/stack.py:2382-2390). Response 200 with the snapshot (id, name, status, status_reason, data, creation_time).
  • DELETE .../snapshots/{id}: refused while CREATE_IN_PROGRESS (400, heat/engine/service.py:2273-2276); sets DELETE_IN_PROGRESS, then deletes each resource's backup (for example Cinder backups) and the rows; 204.
  • POST .../snapshots/{id}/restore: the snapshot must be COMPLETE (400 "Restoring incomplete snapshot is not supported.", heat/engine/service.py:2332-2334); the stack is converged back to the snapshot's template and resource data with action RESTORE; 202.

Outputs, template, environment, files

  • GET .../outputs lists {"output_key", "description"} without resolving values (heat/engine/service.py:1443-1453); GET .../outputs/{key} resolves one (output_value, or output_error when resolution fails) (heat/engine/service.py:1456-1475); unknown key → 404.
  • GET .../template returns the stored template as JSON (the raw dict, no wrapper); GET .../environment the stored environment with hidden parameters masked as ****** (heat/engine/service.py:1410-1427); GET .../files the files dict.

Template and type introspection

  • POST /validate: same parsing as create, then a stack validation with strict_validate=False (heat/engine/service.py:1315-1388). Returns {"Description", "Parameters": {name: {"Type", "Default", "NoEcho", "Description", "Label", "AllowedValues"?, ...}}, "ParameterGroups"?, "Environment"}; with show_nested=true the nested templates' parameters under NestedParameters. ignore_errors=99001,... tolerates the listed error codes (only 99001, service unavailable, exists: heat/common/exception.py:33-36). Any validation error is returned by the engine as {"Error": "..."} and turned into **400 with the message as explanation** by the API (heat/api/openstack/v1/stacks.py:650-651), not through the fault map.
  • GET /resource_types?support_status=SUPPORTED&name=<regex>&version=<heat version>&with_description=true lists the types of the global environment that the caller may use and whose service is in the catalog ([_rewrite/resource-types_stable-2026.2.json](_rewrite/resource-types_stable-2026.2.json)).
  • GET /resource_types/{type} returns {"resource_type", "properties": {name: schema}, "attributes": {...}, "support_status": {...}, "description"?}; a type whose service is not in the catalog → 400 ResourceTypeUnavailable; a hidden type → 400 NotSupported; an unknown type → 404 "The Resource Type (X) could not be found." (EntityNotFound from heat/engine/environment.py:534, which is not a NotFound subclass and so escapes the except exception.NotFound meant to turn it into InvalidGlobalResource, heat/engine/service.py:1753-1759), see 02.
  • GET /resource_types/{type}/template?template_type=hot|cfn (default cfn) generates a skeleton template with one parameter per property (heat/engine/resource.py:2560-2612).
  • GET /template_versions → [{"version": "heat_template_version.2021-04-16", "type": "hot", "aliases": ["heat_template_version.wallaby"]}, ...]; GET /template_versions/{v}/functions?with_condition_func=true → [{"functions": "get_attr", "description": "..."}] (heat/engine/service.py:1685-1742); the full tables are [_rewrite/template-versions_stable-2026.2.json](_rewrite/template-versions_stable-2026.2.json).

4. Resources and events

GET .../resources

  • nested_depth (capped at [DEFAULT]/max_nested_stack_depth, 5), with_detail, filters type (substring match on the type, heat/engine/service.py:2136-2140), status, name, action, id, physical_resource_id; any other parameter → 400 "Invalid filter parameters" (heat/api/openstack/v1/resources.py:99-113).
  • Each item: resource_name, logical_resource_id (same value, CFN legacy), physical_resource_id, resource_type, resource_status (<ACTION>_<STATUS>), resource_status_reason, updated_time, creation_time, required_by, links (self, stack, nested for nested stacks), parent_resource, plus attributes, description, metadata (removed from the list), properties with with_detail (heat/api/openstack/v1/resources.py:28-69).

GET .../resources/{name} and .../metadata

  • with_attr=<name> (repeatable) forces the resolution of extra attributes. For callers that hold the heat_stack_user role (in-instance agents), access is limited to the resources their user is allowed to see through OS::Heat::AccessPolicy / the stack-user resource (heat/engine/service.py:1912-1950, Forbidden → 403).
  • .../metadata returns {"metadata": {...}}: the resource metadata, including deployments and os-collect-config keys for servers (the agents' poll target with POLL_SERVER_HEAT).

POST .../resources/{name}/signal

  • Purpose: deliver an external event to a resource: a wait-condition handle, a software deployment, a scaling policy, an alarm, or a hook clear ({"unset_hook": "pre-create"}).
  • Rules: the resource must exist (404) and be stored (ResourceNotAvailable 404); _signal_check_action refuses signals while the resource is *_IN_PROGRESS for some types or during DELETE/SUSPEND (heat/engine/resource.py:2457-2486, ResourceActionNotSupported → 400 with the reason); a type without handle_signal → 400 "Signal resource during … is not supported" (heat/engine/resource.py:2527-2548).
  • Flow: the engine handles it in a thread and returns immediately unless the resource has no handle_signal (heat/engine/service.py:1995-2004); the API answers 200 null.
  • Credentials: callers are usually stack-domain users (heat_stack_user role) with a token scoped to the stack-user project; resource:signal allows them.

PATCH .../resources/{name} — mark unhealthy

  • Body {"mark_unhealthy": true|false, "resource_status_reason": "..."}; other keys → 400; mark_unhealthy must be boolean-like (heat/api/openstack/v1/resources.py:167-194).
  • true sets CHECK_FAILED (except for a resource being deleted); false sets CHECK_COMPLETE only from CHECK_FAILED (heat/engine/service.py:2040-2047). The next update replaces a CHECK_FAILED resource (needs_replace_failed). The resource is locked for the change (convergence: resource.engine_id; legacy: stack lock); a busy resource → 409 ActionInProgress (heat/engine/service.py:2048-2050). The resource may be named by logical name, physical id or reference id (heat/engine/service.py:2053-2078).

Events

  • GET .../events and .../resources/{name}/events: limit, marker (event uuid), sort_keys, sort_dir, nested_depth (adds root_stack_id), filters resource_status, resource_action, resource_name, resource_type, physical_resource_id… (heat/api/openstack/v1/events.py:110-150). A named resource with no events → 404 "No events found for resource" (heat/api/openstack/v1/events.py:146-148).
  • GET .../events/{event_id} returns one event with resource_properties (heat/api/openstack/v1/events.py:153-161); events are pruned beyond [DEFAULT]/max_events_per_stack (1000) in batches of event_purge_batch_size (heat/db/api.py:1467-1500).

POST .../actions

  • Body with exactly one key: suspend, resume, check, cancel_update, cancel_without_rollback (heat/api/openstack/v1/actions.py:52-69); otherwise 400 "No action specified" / "Multiple actions specified" / "Invalid action X specified".
  • suspend, resume, check start a convergence traversal with that action (heat/engine/service.py:2149-2188, heat/engine/service.py:2307-2322); an already SUSPEND_COMPLETE stack is left alone (heat/engine/service.py:2155-2158).
  • cancel_update needs UPDATE_IN_PROGRESS and triggers a rollback; cancel_without_rollback accepts UPDATE_IN_PROGRESS or CREATE_IN_PROGRESS and only stops the traversal (heat/engine/service.py:1254-1274); other states → 400 "Cancelling update when stack is X is not supported."

5. Software configs and deployments

These resources back OS::Heat::SoftwareConfig / OS::Heat::SoftwareDeployment and are also used directly by in-instance agents (os-collect-config, heat-config).

  • POST /software_configs {"name", "group", "config", "inputs": [...], "outputs": [...], "options": {...}}: the inputs and outputs are validated against the I/O schema (heat/engine/service_software_config.py:61-79, heat/engine/software_config_io.py); limited to max_software_configs_per_tenant (4096) for non-admins (heat/engine/service.py:2378-2385). Returns 200 (not 201) {"software_config": {"id", "name", "group", "config", "inputs", "outputs", "options", "creation_time"}}. Software configs are immutable: no update route.
  • GET /software_configs?limit&marker&global_tenant, GET/DELETE /software_configs/{id}.
  • POST /software_deployments {"config_id", "server_id", "input_values", "action", "status", "status_reason", "stack_user_project_id"} (heat/engine/service_software_config.py:277-300): stack_user_project_id longer than 64 → 400. A config_id that does not exist or belongs to another project (non-admin caller) → 404 Software config with id <id> not found, on create and on update (heat/db/api.py:1515-1525, heat/db/api.py:1561-1567, heat/db/api.py:1636-1643); before the security fix of this branch, any project could deploy another project's config (06 § 3). Creating, updating or deleting a deployment rewrites the server resource's metadata (deployments list) and pushes it to the server's Swift TempURL or Zaqar queue when configured (heat/engine/service_software_config.py:109-162).
  • PUT /software_deployments/{id} updates only the keys that are present and not null (heat/api/openstack/v1/software_deployments.py:76-85).
  • GET /software_deployments/{id}: for a deployment IN_PROGRESS whose signal transport is TEMP_URL_SIGNAL or ZAQAR_SIGNAL, reading it polls Swift or Zaqar and applies a pending signal before answering (heat/engine/service_software_config.py:257-271).
  • GET /software_deployments/metadata/{server_id} returns the configs of all the server's deployments, sorted by config name — the document in-instance agents consume (heat/engine/service_software_config.py:94-106).

6. Services and build info

  • GET /services (admin) lists the service rows with status up when the last report is younger than 2 × report_interval (heat/common/service_utils.py:49-72).
  • GET /build_info returns {"api": {"revision"}, "engine": {"revision"}} from [revision]/heat_revision (heat/api/openstack/v1/build_info.py:36-43).

7. CloudFormation-compatible API (heat-api-cfn)

7.1 Authentication

ec2authtoken (heat/api/aws/ec2token.py:135-256) reads Signature and AWSAccessKeyId from the query or the Authorization header, sends the signed request description to Keystone POST /v3/ec2tokens, and on success injects X-Auth-Token, X-Tenant-Id, X-Roles… for authtoken and context. Without a signature but with X-Auth-User, the request goes on to normal token auth; otherwise IncompleteSignature (400) or MissingAuthenticationToken (403). Keystone's failure messages are parsed literally: "EC2 access key not found." → InvalidClientTokenId (403), "EC2 signature not supplied." → SignatureDoesNotMatch (403) (heat/api/aws/ec2token.py:226-241). [ec2authtoken]/multi_cloud tries each configured Keystone in turn.

7.2 Actions

Routing: GET|POST /v1/?Action=<Name>&... (heat/api/cfn/v1/__init__.py:26-65). Responses are XML by default, JSON with ContentType=JSON (heat/common/wsgi.py:322-329, heat/common/wsgi.py:379-389). Each action enforces cloudformation:<Action> (heat/api/cfn/v1/stacks.py:51-64; failure → AccessDenied 403).

ActionHandlerEngine RPCNotes
ListStacksheat/api/cfn/v1/stacks.py:106list_stacksStackSummaries
CreateStackheat/api/cfn/v1/stacks.py:262create_stackParameters.member.N.ParameterKey/Value, TemplateBody/TemplateUrl, TimeoutInMinutes, DisableRollback
UpdateStackheat/api/cfn/v1/stacks.py:266update_stack
CancelUpdateStackheat/api/cfn/v1/stacks.py:361stack_cancel_update
DescribeStacksheat/api/cfn/v1/stacks.py:149show_stackoutputs as OutputKey/OutputValue
DeleteStackheat/api/cfn/v1/stacks.py:452delete_stack
DescribeStackEventsheat/api/cfn/v1/stacks.py:472list_events
ValidateTemplateheat/api/cfn/v1/stacks.py:405validate_template
GetTemplateheat/api/cfn/v1/stacks.py:375get_template
EstimateTemplateCostheat/api/cfn/v1/stacks.py:392—returns a fixed dummy URL
DescribeStackResourceheat/api/cfn/v1/stacks.py:521describe_stack_resource**the poll target of POLL_SERVER_CFN agents** (metadata as JSON text)
DescribeStackResourcesheat/api/cfn/v1/stacks.py:565describe_stack_resources / find_physical_resourcePhysicalResourceId lookup
ListStackResourcesheat/api/cfn/v1/stacks.py:630list_stack_resources

An unknown Action, or none, reaches index/default → InvalidAction (400) (heat/api/cfn/v1/stacks.py:48-49). Engine errors are mapped by map_remote_error (heat/api/aws/exception.py:285-338) to InvalidParameterValue, AccessDenied, AlreadyExists, InvalidAction (in progress), RequestLimitExceeded or InternalFailure.

Hidden routes: mapper.resource("stack", "stacks") (heat/api/cfn/v1/__init__.py:48-49) also creates REST-style routes /v1/stacks[.:format], /v1/stacks/{id}… bound to the same CFN controller; POST /v1/stacks reaches create, PUT /v1/stacks/{id} update, DELETE /v1/stacks/{id} delete; index, show, new, edit and detail fall to default → InvalidAction. They are undocumented and not used by any known client.

7.3 Signal endpoints

  • PUT /v1/waitcondition/{arn} (heat/api/cfn/v1/signal.py:25-38): body {"Status": "SUCCESS"|"FAILURE", "Reason", "UniqueId", "Data"} from cfn-signal/curl; the engine is called with sync_call=True (the signal is processed before the answer); response {"resource": name, "metadata": {...}}.
  • POST /v1/signal/{arn} (heat/api/cfn/v1/signal.py:40-50): generic signal (scaling policies, alarms, OS::Heat::SoftwareDeployment with CFN_SIGNAL), asynchronous.
  • Both URLs are pre-signed EC2 URLs generated by the engine for a stack-domain user's EC2 keypair (heat/engine/resources/signal_responder.py:119-182): SignatureMethod=HmacSHA256, SignatureVersion=2, AWSAccessKeyId, Timestamp (unless never_expire), Signature; the base URL is [DEFAULT]/heat_waitcondition_server_url with /waitcondition replaced by /signal for signals, and lower-cased. No policy is enforced on these two routes; authorisation is the EC2 signature plus the stack-domain user's scope.

Lifecycle: stack

status is <ACTION>_<STATUS>; actions INIT, CREATE, DELETE, UPDATE, ROLLBACK, SUSPEND, RESUME, ADOPT, SNAPSHOT, CHECK, RESTORE, statuses IN_PROGRESS, FAILED, COMPLETE (heat/engine/stack.py, Stack.ACTIONS/STATUSES).

stateDiagram-v2
  [*] --> CREATE_IN_PROGRESS: POST /stacks (API)
  CREATE_IN_PROGRESS --> CREATE_COMPLETE: last root check_resource (worker)
  CREATE_IN_PROGRESS --> CREATE_FAILED: resource failure / timeout (worker)
  CREATE_FAILED --> ROLLBACK_IN_PROGRESS: disable_rollback=false (mark_failed)
  ROLLBACK_IN_PROGRESS --> ROLLBACK_COMPLETE: empty-template traversal done
  ROLLBACK_IN_PROGRESS --> ROLLBACK_FAILED: failure
  CREATE_COMPLETE --> UPDATE_IN_PROGRESS: PUT/PATCH (API)
  UPDATE_IN_PROGRESS --> UPDATE_IN_PROGRESS: new PUT/PATCH (new traversal)
  UPDATE_IN_PROGRESS --> UPDATE_COMPLETE: traversal done
  UPDATE_IN_PROGRESS --> UPDATE_FAILED: failure / cancel_without_rollback
  UPDATE_IN_PROGRESS --> ROLLBACK_IN_PROGRESS: cancel_update / failure with rollback
  UPDATE_COMPLETE --> SUSPEND_IN_PROGRESS: actions suspend
  SUSPEND_IN_PROGRESS --> SUSPEND_COMPLETE: traversal done
  SUSPEND_COMPLETE --> RESUME_IN_PROGRESS: actions resume
  RESUME_IN_PROGRESS --> RESUME_COMPLETE: traversal done
  UPDATE_COMPLETE --> CHECK_IN_PROGRESS: actions check
  CHECK_IN_PROGRESS --> CHECK_COMPLETE: all resources checked
  CHECK_IN_PROGRESS --> CHECK_FAILED: "Resource CHECK failed: ..."
  UPDATE_COMPLETE --> SNAPSHOT_IN_PROGRESS: POST snapshots
  SNAPSHOT_IN_PROGRESS --> SNAPSHOT_COMPLETE: traversal done
  SNAPSHOT_COMPLETE --> RESTORE_IN_PROGRESS: POST restore
  RESTORE_IN_PROGRESS --> RESTORE_COMPLETE: traversal done
  UPDATE_COMPLETE --> DELETE_IN_PROGRESS: DELETE (API)
  CREATE_FAILED --> DELETE_IN_PROGRESS: DELETE (API)
  DELETE_IN_PROGRESS --> DELETE_COMPLETE: traversal done, row soft-deleted
  DELETE_IN_PROGRESS --> DELETE_FAILED: failure
  CREATE_IN_PROGRESS --> CREATE_FAILED: "Engine went down" (legacy reset at startup) / reset_stack_status
  DELETE_COMPLETE --> [*]

Every *_COMPLETE state can start any other action (only the main paths are drawn); DELETE is always allowed, and stops an in-progress traversal first.

Lifecycle: resource

Resource states use the same <ACTION>_<STATUS> scheme, plus INIT_COMPLETE (stored but not created). In convergence, a resource row is locked while engine_id is set (05 § locks).

stateDiagram-v2
  [*] --> INIT_COMPLETE: stack create/update stores the row
  INIT_COMPLETE --> CREATE_IN_PROGRESS: check_resource (is_update)
  CREATE_IN_PROGRESS --> CREATE_COMPLETE: handle_create + check_create_complete
  CREATE_IN_PROGRESS --> CREATE_FAILED: plugin error / timeout / "Worker went down"
  CREATE_COMPLETE --> UPDATE_IN_PROGRESS: check_resource with new template
  UPDATE_IN_PROGRESS --> UPDATE_COMPLETE: in-place update
  UPDATE_IN_PROGRESS --> UPDATE_FAILED: failure
  CREATE_COMPLETE --> CHECK_FAILED: PATCH mark_unhealthy=true
  CHECK_FAILED --> CHECK_COMPLETE: PATCH mark_unhealthy=false
  CREATE_COMPLETE --> DELETE_IN_PROGRESS: cleanup node (not in new template / replaced)
  DELETE_IN_PROGRESS --> DELETE_COMPLETE: handle_delete done; row purged at stack completion
  DELETE_IN_PROGRESS --> DELETE_FAILED: failure
  CREATE_COMPLETE --> SUSPEND_COMPLETE: suspend traversal
  SUSPEND_COMPLETE --> RESUME_COMPLETE: resume traversal

A replacement creates a new resource row (replaces = old id, INIT_COMPLETE) and the old row gets replaced_by; the old physical resource is deleted in the cleanup phase (heat/engine/resource.py:385-437).

Source: reverse-spec-heat/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.