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 theActionquery 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):
| Order | Filter | Effect | Source |
|---|---|---|---|
| 1 | cors | CORS headers | etc/heat/api-paste.ini:40 |
| 2 | request_id | x-openstack-request-id | etc/heat/api-paste.ini:85 |
| 3 | faultwrap | turns every exception into the JSON error body (§ 1.3) | heat/api/middleware/fault.py:52 |
| 4 | http_proxy_to_wsgi | honours X-Forwarded-Proto | etc/heat/api-paste.ini:61 |
| 5 | versionnegotiation | strips /v1, or answers 300 with the version list | heat/api/middleware/version_negotiation.py:37 |
| 6 | authurl | sets X-Auth-Url | etc/heat/api-paste.ini:65 |
| 7 | authtoken | keystonemiddleware token validation (401) | etc/heat/api-paste.ini:69 |
| 8 | context | builds heat.common.context.RequestContext from the identity headers | etc/heat/api-paste.ini:55 |
| 9 | osprofiler | tracing | etc/heat/api-paste.ini:88 |
| 10 | apiv1app | heat.api.openstack.v1:API | etc/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 /orGET /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.0header 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
rejectroute for the methods it does not implement (405 with anAllowheader) and anOPTIONSroute (204 withAllow) (heat/api/openstack/v1/__init__.py:64-80,heat/common/wsgi.py:132-147). - Every path starts with
/{tenant_id}:registered_policy_enforcereturns 403 whentenant_iddiffers from the token's project, except for admins (is_admincontext), whosetenant_idis 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,explanationcome from the webob class chosen byFaultWrapper.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>_Remoteclasses and the suffix is dropped (heat/api/middleware/fault.py:126-128).error.messageis present only when the exception hassafe = True(everyHeatException,heat/common/exception.py:54);UpdateInProgress, Python built-ins and oslo.messaging errors carry no message (heat/api/middleware/fault.py:161-162).error.tracebackis 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 > 0and the content type is JSON (or missing, ortext/plain, bug 1887882) and the body starts with{(heat/common/wsgi.py:236-266). Otherwise the handler is called withoutbody; a handler that requires one raisesTypeError, 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 ofPOST .../actionsandPOST .../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_stackrejects 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, gabbiredirects: True). An ARN (arn:openstack:heat::<tenant>:stacks/<name>/<id>,heat/common/identifier.py:92-103) is also accepted.- The
links[].hrefof a stack isreq.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 withno_policy_enforceand enforcesstacks:update_patch, orstacks:update_no_changewhen the body changes nothing (heat/api/openstack/v1/stacks.py:507-524,heat/api/openstack/v1/stacks.py:169-178).POST .../actionsenforces nothing itself; the sub-action handler enforcesactions:<action>(heat/api/openstack/v1/actions.py:46-69). The ruleactions:actionexists in the defaults but is never checked.GET /stacks?global_tenant=truecallsglobal_index, which enforcesstacks: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.
| Method | Path | Handler | Policy | Engine RPC | Success | Typical errors |
|---|---|---|---|---|---|---|
| POST | /{tenant_id}/validate | heat/api/openstack/v1/stacks.py:617 | stacks:validate_template | validate_template | 200 | 400 |
| GET | /{tenant_id}/resource_types | heat/api/openstack/v1/stacks.py:656 | stacks:list_resource_types | list_resource_types | 200 | — |
| GET | /{tenant_id}/resource_types/{type_name} | heat/api/openstack/v1/stacks.py:704 | stacks:resource_schema | resource_schema | 200 | 400, 403, 404 |
| GET | /{tenant_id}/resource_types/{type_name}/template | heat/api/openstack/v1/stacks.py:711 | stacks:generate_template | generate_template | 200 | 400, 500 |
| GET | /{tenant_id}/template_versions | heat/api/openstack/v1/stacks.py:679 | stacks:list_template_versions | list_template_versions | 200 | — |
| GET | /{tenant_id}/template_versions/{template_version}/functions | heat/api/openstack/v1/stacks.py:687 | stacks:list_template_functions | list_template_functions | 200 | 404 |
| GET | /{tenant_id}/stacks | heat/api/openstack/v1/stacks.py:348 | stacks:index (stacks:global_index) | list_stacks [+ count_stacks] | 200 | 400, 403 |
| POST | /{tenant_id}/stacks | heat/api/openstack/v1/stacks.py:404 | stacks:create | create_stack | 201 | 400, 409 |
| POST | /{tenant_id}/stacks/preview | heat/api/openstack/v1/stacks.py:370 | stacks:preview | preview_stack | 200 | 400, 409 |
| GET | /{tenant_id}/stacks/detail | heat/api/openstack/v1/stacks.py:363 | stacks:detail | list_stacks | 200 | — |
| any | /{tenant_id}/stacks/{stack_name}[/resources|events|template|actions|environment|files] | heat/api/openstack/v1/stacks.py:426 | stacks:lookup | identify_stack | 302 | 404 |
| GET | /{tenant_id}/stacks/{stack_name}/{stack_id} | heat/api/openstack/v1/stacks.py:445 | stacks:show | show_stack | 200 | 404 |
| PUT | /{tenant_id}/stacks/{stack_name}/{stack_id} | heat/api/openstack/v1/stacks.py:488 | stacks:update | update_stack | 202 | 400, 404, 409 |
| PATCH | /{tenant_id}/stacks/{stack_name}/{stack_id} | heat/api/openstack/v1/stacks.py:507 | stacks:update_patch / update_no_change | update_stack (existing=True) | 202 | 400, 404, 409 |
| DELETE | /{tenant_id}/stacks/{stack_name}/{stack_id} | heat/api/openstack/v1/stacks.py:590 | stacks:delete | delete_stack | 204 | 404, 409 |
| GET | .../{stack_id}/template | heat/api/openstack/v1/stacks.py:466 | stacks:template | get_template | 200 | 404 |
| GET | .../{stack_id}/environment | heat/api/openstack/v1/stacks.py:476 | stacks:environment | get_environment | 200 | 404 |
| GET | .../{stack_id}/files | heat/api/openstack/v1/stacks.py:483 | stacks:files | get_files | 200 | 404 |
| PUT | .../{stack_id}/preview | heat/api/openstack/v1/stacks.py:548 | stacks:preview_update | preview_update_stack | 200 | 400, 404 |
| PATCH | .../{stack_id}/preview | heat/api/openstack/v1/stacks.py:569 | stacks:preview_update_patch | preview_update_stack | 200 | 400, 404 |
| DELETE | .../{stack_id}/abandon | heat/api/openstack/v1/stacks.py:599 | stacks:abandon | abandon_stack | 200 | 400 (disabled), 404 |
| GET | .../{stack_id}/export | heat/api/openstack/v1/stacks.py:609 | stacks:export | export_stack | 200 | 400 (disabled), 404 |
| POST | .../{stack_id}/snapshots | heat/api/openstack/v1/stacks.py:727 | stacks:snapshot | stack_snapshot | 200 | 400, 404, 409 |
| GET | .../{stack_id}/snapshots | heat/api/openstack/v1/stacks.py:743 | stacks:list_snapshots | stack_list_snapshots | 200 | 404 |
| GET | .../{stack_id}/snapshots/{snapshot_id} | heat/api/openstack/v1/stacks.py:732 | stacks:show_snapshot | show_snapshot | 200 | 404 |
| DELETE | .../{stack_id}/snapshots/{snapshot_id} | heat/api/openstack/v1/stacks.py:738 | stacks:delete_snapshot | delete_snapshot | 204 | 400, 404 |
| POST | .../{stack_id}/snapshots/{snapshot_id}/restore | heat/api/openstack/v1/stacks.py:750 | stacks:restore_snapshot | stack_restore | 202 | 400, 404 |
| GET | .../{stack_id}/outputs | heat/api/openstack/v1/stacks.py:755 | stacks:list_outputs | list_outputs | 200 | 404 |
| GET | .../{stack_id}/outputs/{output_key} | heat/api/openstack/v1/stacks.py:762 | stacks:show_output | show_output | 200 | 404 |
| GET | .../{stack_id}/resources | heat/api/openstack/v1/resources.py:96 | resource:index | list_stack_resources | 200 | 400, 404 |
| GET | .../{stack_id}/resources/{resource_name} | heat/api/openstack/v1/resources.py:135 | resource:show | describe_stack_resource | 200 | 403, 404 |
| PATCH | .../{stack_id}/resources/{resource_name} | heat/api/openstack/v1/resources.py:167 | resource:mark_unhealthy | resource_mark_unhealthy | 200 | 400, 404, 409 |
| GET | .../{stack_id}/resources/{resource_name}/metadata | heat/api/openstack/v1/resources.py:150 | resource:metadata | describe_stack_resource | 200 | 403, 404 |
| POST | .../{stack_id}/resources/{resource_name}/signal | heat/api/openstack/v1/resources.py:160 | resource:signal | resource_signal | 200 (null) | 400, 404 |
| GET | .../{stack_id}/events | heat/api/openstack/v1/events.py:110 | events:index | list_events | 200 | 400, 404 |
| GET | .../{stack_id}/resources/{resource_name}/events | heat/api/openstack/v1/events.py:110 | events:index | list_events | 200 | 404 |
| GET | .../{stack_id}/resources/{resource_name}/events/{event_id} | heat/api/openstack/v1/events.py:153 | events:show | list_events (uuid filter) | 200 | 404 |
| POST | .../{stack_id}/actions | heat/api/openstack/v1/actions.py:46 | actions:{suspend|resume|check|cancel_update|cancel_without_rollback} | stack_suspend / stack_resume / stack_check / stack_cancel_update | 200 (null) | 400, 404 |
| GET | /{tenant_id}/build_info | heat/api/openstack/v1/build_info.py:36 | build_info:build_info | get_revision | 200 | — |
| GET | /{tenant_id}/software_configs | heat/api/openstack/v1/software_configs.py:78 | software_configs:index (global_index) | list_software_configs | 200 | 400, 403 |
| POST | /{tenant_id}/software_configs | heat/api/openstack/v1/software_configs.py:100 | software_configs:create | create_software_config | 200 | 400 |
| GET | /{tenant_id}/software_configs/{config_id} | heat/api/openstack/v1/software_configs.py:93 | software_configs:show | show_software_config | 200 | 404 |
| DELETE | /{tenant_id}/software_configs/{config_id} | heat/api/openstack/v1/software_configs.py:115 | software_configs:delete | delete_software_config | 204 | 400, 404 |
| GET | /{tenant_id}/software_deployments | heat/api/openstack/v1/software_deployments.py:38 | software_deployments:index | list_software_deployments | 200 | — |
| POST | /{tenant_id}/software_deployments | heat/api/openstack/v1/software_deployments.py:65 | software_deployments:create | create_software_deployment | 200 | 400, 404 |
| GET | /{tenant_id}/software_deployments/metadata/{server_id} | heat/api/openstack/v1/software_deployments.py:48 | software_deployments:metadata | metadata_software_deployments | 200 | — |
| GET | /{tenant_id}/software_deployments/{deployment_id} | heat/api/openstack/v1/software_deployments.py:58 | software_deployments:show | show_software_deployment | 200 | 404 |
| PUT | /{tenant_id}/software_deployments/{deployment_id} | heat/api/openstack/v1/software_deployments.py:76 | software_deployments:update | update_software_deployment | 200 | 400, 404 |
| DELETE | /{tenant_id}/software_deployments/{deployment_id} | heat/api/openstack/v1/software_deployments.py:88 | software_deployments:delete | delete_software_deployment | 204 | 404 |
| GET | /{tenant_id}/services | heat/api/openstack/v1/services.py:37 | service:index | list_services | 200 | 403, 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 passresource_types:<type>(heat/engine/service.py:813). - Preconditions:
- no live stack of the same name in the project (
StackExists→ 409, checked early atheat/engine/service.py:739-740and 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_adoptmust be true (heat/engine/service.py:780-782,NotSupported→ 400).
- no live stack of the same name in the project (
- Business rules:
- the template is inline (
template, JSON object or YAML/JSON string), fetched fromtemplate_urlby heat-api (heat/api/openstack/v1/stacks.py:115-122,heat/common/urlfetch.py), or taken fromadopt_stack_data.template; environment(string or object) andparametersare merged; whenenvironment_filesis given,environmentis ignored and the files listed (looked up infiles) are merged in order (heat/api/openstack/v1/stacks.py:139-153,heat/engine/service.py:799-801);files_containernames a Swift container whose objects becomefiles(heat/engine/service.py:795-797);timeout_minsmust be an integer (heat/api/openstack/v1/stacks.py:390-392);tagsa comma-separated string or list;convergeis 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), notablydisable_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).
- the template is inline (
- Flow:
- (synchronous, heat-api) policy, body parsing,
template_urldownload. - (synchronous, engine) parse and validate, then
stack.store():raw_templateandstackrows; withdeferred_auth_method = trusts(default), a Keystone trust from the user to the Heat service user is created and stored encrypted inuser_creds(heat/engine/stack.py:809-820). - (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 stackCREATE_FAILEDbut still returns 201. - (synchronous, engine) convergence (default,
[DEFAULT]/convergence_engine = True): a new traversal id,resourcerows in stateINIT_COMPLETE, the dependency graph stored instack.current_deps, onesync_pointrow per graph node, and onecheck_resourcecast 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). - (asynchronous, workers) each
check_resourcecreates its resource and propagates to its dependants; the last root marks the stackCREATE_COMPLETE(05 § check_resource).
- (synchronous, heat-api) policy, body parsing,
- 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 isCREATE_IN_PROGRESS; the client pollsGETor the events. - Effects visible elsewhere: a Keystone trust and a project in the
heatdomain; notificationorchestration.stack.create.start(heat/engine/notification/stack.py:18-39); oneeventrow per state change. - Limitations:
template_urlis fetched with the API's network access (SSRF surface, schemes restricted byheat/common/urlfetch.py); the 201 means "accepted", not "valid at runtime" — runtime failures (quotas of other services, bad image) only show up asCREATE_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>"}. Missingstack_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 throughheat/db/api.py:846-857),sort_dir,show_deleted,show_nested,show_hidden(stacks tagged with[DEFAULT]/hidden_stack_tagsare hidden by default),tags,tags_any,not_tags,not_tags_any,with_count,global_tenant, and filters onid,status,name,action,tenant,username,owner_id, plusstack_name,stack_status… (rpc_api.STACK_KEYSminus the unsupported ones). Repeated filter values are OR-ed. - Response:
{"stacks": [...], "links": [{"rel": "next", "href": ...}]?, "count": N?}; each stack hasid,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, plusprojectfor admins (heat/api/openstack/v1/views/stacks_view.py:38-77). - Errors: 400 for a non-boolean flag or a non-integer
limit; 403 forglobal_tenant=trueunder 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, unlessresolve_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_*orDELETE_*(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 →ActionInProgress409). 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_rollbackandconvergedefault to the current stack's values (heat/engine/service.py:1035-1041);converge=trueforces every resource to be checked against reality (observe_on_update). - Outcome:
202 Acceptedwith an empty body (heat/api/openstack/v1/stacks.py:503); the stack goes toUPDATE_IN_PROGRESS, thenUPDATE_COMPLETE, orUPDATE_FAILEDand, 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 aFAILEDlegacy stack, the previous template; otherwiseNotSupported"PATCH update to non-COMPLETE stack",heat/engine/service.py:963-990); parameters are merged into the existing ones;clear_parametersremoves some;filesare 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=truedescends 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 thecancel_check_resourcecast 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 actionDELETE(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), stackDELETE_IN_PROGRESS; atDELETE_COMPLETEthe trust, the stack-domain project and thestackrow (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 toerror_wait_time + 10s beforestop_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). exportreturns the abandon data (template, environment, resources with theirresource_id,resource_data, metadata, status) without changing anything (heat/engine/service.py:1606-1615).abandonreturns 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 withabandon=True).
Snapshots
POST .../snapshots {"name": ...}: refused while the stack is*_IN_PROGRESS(409ActionInProgress) or above[DEFAULT]/max_snapshots_per_stack(32) (heat/engine/service.py:2194-2211). Convergence: asnapshotrowCREATE_IN_PROGRESSwhose id becomes the traversal id of aSNAPSHOTtraversal (heat/engine/service.py:2213-2229); each resource stores its snapshot data inresource_snapshot; at the endstack.update_snapshotwritesdataandCOMPLETE/FAILED(heat/engine/stack.py:2382-2390). Response 200 with the snapshot (id,name,status,status_reason,data,creation_time).DELETE .../snapshots/{id}: refused whileCREATE_IN_PROGRESS(400,heat/engine/service.py:2273-2276); setsDELETE_IN_PROGRESS, then deletes each resource's backup (for example Cinder backups) and the rows; 204.POST .../snapshots/{id}/restore: the snapshot must beCOMPLETE(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 actionRESTORE; 202.
Outputs, template, environment, files
GET .../outputslists{"output_key", "description"}without resolving values (heat/engine/service.py:1443-1453);GET .../outputs/{key}resolves one (output_value, oroutput_errorwhen resolution fails) (heat/engine/service.py:1456-1475); unknown key → 404.GET .../templatereturns the stored template as JSON (the raw dict, no wrapper);GET .../environmentthe stored environment with hidden parameters masked as******(heat/engine/service.py:1410-1427);GET .../filesthe files dict.
Template and type introspection
POST /validate: same parsing as create, then a stack validation withstrict_validate=False(heat/engine/service.py:1315-1388). Returns{"Description", "Parameters": {name: {"Type", "Default", "NoEcho", "Description", "Label", "AllowedValues"?, ...}}, "ParameterGroups"?, "Environment"}; withshow_nested=truethe nested templates' parameters underNestedParameters.ignore_errors=99001,...tolerates the listed error codes (only99001, 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 asexplanation** 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=truelists 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 → 400ResourceTypeUnavailable; a hidden type → 400NotSupported; an unknown type → 404 "The Resource Type (X) could not be found." (EntityNotFoundfromheat/engine/environment.py:534, which is not aNotFoundsubclass and so escapes theexcept exception.NotFoundmeant to turn it intoInvalidGlobalResource,heat/engine/service.py:1753-1759), see 02.GET /resource_types/{type}/template?template_type=hot|cfn(defaultcfn) 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, filterstype(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,nestedfor nested stacks),parent_resource, plusattributes,description,metadata(removed from the list),propertieswithwith_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 theheat_stack_userrole (in-instance agents), access is limited to the resources their user is allowed to see throughOS::Heat::AccessPolicy/ the stack-user resource (heat/engine/service.py:1912-1950,Forbidden→ 403)..../metadatareturns{"metadata": {...}}: the resource metadata, includingdeploymentsandos-collect-configkeys for servers (the agents' poll target withPOLL_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 (
ResourceNotAvailable404);_signal_check_actionrefuses signals while the resource is*_IN_PROGRESSfor some types or duringDELETE/SUSPEND(heat/engine/resource.py:2457-2486,ResourceActionNotSupported→ 400 with the reason); a type withouthandle_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 200null. - Credentials: callers are usually stack-domain users (
heat_stack_userrole) with a token scoped to the stack-user project;resource:signalallows them.
PATCH .../resources/{name} — mark unhealthy
- Body
{"mark_unhealthy": true|false, "resource_status_reason": "..."}; other keys → 400;mark_unhealthymust be boolean-like (heat/api/openstack/v1/resources.py:167-194). truesetsCHECK_FAILED(except for a resource being deleted);falsesetsCHECK_COMPLETEonly fromCHECK_FAILED(heat/engine/service.py:2040-2047). The next update replaces aCHECK_FAILEDresource (needs_replace_failed). The resource is locked for the change (convergence:resource.engine_id; legacy: stack lock); a busy resource → 409ActionInProgress(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 .../eventsand.../resources/{name}/events:limit,marker(event uuid),sort_keys,sort_dir,nested_depth(addsroot_stack_id), filtersresource_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 withresource_properties(heat/api/openstack/v1/events.py:153-161); events are pruned beyond[DEFAULT]/max_events_per_stack(1000) in batches ofevent_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,checkstart a convergence traversal with that action (heat/engine/service.py:2149-2188,heat/engine/service.py:2307-2322); an alreadySUSPEND_COMPLETEstack is left alone (heat/engine/service.py:2155-2158).cancel_updateneedsUPDATE_IN_PROGRESSand triggers a rollback;cancel_without_rollbackacceptsUPDATE_IN_PROGRESSorCREATE_IN_PROGRESSand 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 tomax_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_idlonger than 64 → 400. Aconfig_idthat does not exist or belongs to another project (non-admin caller) → 404Software 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 (deploymentslist) 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 deploymentIN_PROGRESSwhose signal transport isTEMP_URL_SIGNALorZAQAR_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 theservicerows withstatusupwhen the last report is younger than2 × report_interval(heat/common/service_utils.py:49-72).GET /build_inforeturns{"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).
| Action | Handler | Engine RPC | Notes |
|---|---|---|---|
| ListStacks | heat/api/cfn/v1/stacks.py:106 | list_stacks | StackSummaries |
| CreateStack | heat/api/cfn/v1/stacks.py:262 | create_stack | Parameters.member.N.ParameterKey/Value, TemplateBody/TemplateUrl, TimeoutInMinutes, DisableRollback |
| UpdateStack | heat/api/cfn/v1/stacks.py:266 | update_stack | |
| CancelUpdateStack | heat/api/cfn/v1/stacks.py:361 | stack_cancel_update | |
| DescribeStacks | heat/api/cfn/v1/stacks.py:149 | show_stack | outputs as OutputKey/OutputValue |
| DeleteStack | heat/api/cfn/v1/stacks.py:452 | delete_stack | |
| DescribeStackEvents | heat/api/cfn/v1/stacks.py:472 | list_events | |
| ValidateTemplate | heat/api/cfn/v1/stacks.py:405 | validate_template | |
| GetTemplate | heat/api/cfn/v1/stacks.py:375 | get_template | |
| EstimateTemplateCost | heat/api/cfn/v1/stacks.py:392 | — | returns a fixed dummy URL |
| DescribeStackResource | heat/api/cfn/v1/stacks.py:521 | describe_stack_resource | **the poll target of POLL_SERVER_CFN agents** (metadata as JSON text) |
| DescribeStackResources | heat/api/cfn/v1/stacks.py:565 | describe_stack_resources / find_physical_resource | PhysicalResourceId lookup |
| ListStackResources | heat/api/cfn/v1/stacks.py:630 | list_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"}fromcfn-signal/curl; the engine is called withsync_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::SoftwareDeploymentwithCFN_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(unlessnever_expire),Signature; the base URL is[DEFAULT]/heat_waitcondition_server_urlwith/waitconditionreplaced by/signalfor 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.