REST API — zun — branch stable/2026.2
Branch
stable/2026.2@34135a563699— repository https://opendev.org/openstack/zun — analysed on 2026-11-01
Routing is Pecan RestController object dispatch, so no route table exists in the code. The table below comes from [_rewrite/routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) (_rewrite/tools/zun_api.py), checked against 256 recorded exchanges and 32 probes of the real application ([_rewrite/probe_stable-2026.2.json](_rewrite/probe_stable-2026.2.json)). Errors are described in 02-errors, messages in 03-interactions, and asynchronous work in 05-async-processing.
1. Cross-cutting rules
- Listening port: 9517 by default (
zun/conf/api.py:17). Service type in the catalogue:container(zun/common/clients.py:43). - Authentication:
authtokenin the paste pipeline, except/and/v1(etc/zun/api-paste.ini:7,etc/zun/api-paste.ini:13,zun/api/middleware/auth_token.py:27)./healthcheckis a separate application (etc/zun/api-paste.ini:4). - Microversions: header
OpenStack-API-Version: container <major.minor>, default 1.1, maximum 1.40,latestaccepted (zun/api/controllers/versions.py:78,zun/api/controllers/versions.py:129,zun/api/controllers/versions.py:137). Every response under/v1carriesOpenStack-API-Minimum-Version,OpenStack-API-Maximum-Version,OpenStack-API-VersionandVary(zun/api/controllers/v1/__init__.py:207). History: [_rewrite/microversions_stable-2026.2.md](_rewrite/microversions_stable-2026.2.md). - Default version trap: a client that sends no header gets 1.1, and
/v1/hoststhen answers 406, since hosts need 1.4 (probe "version: no header gives 1.1"). - Version in the path: any first segment other than
v1is prefixed withv1, soGET /containersequalsGET /v1/containers(zun/api/controllers/root.py:88, probe "routing: no version in the path goes to v1"). - Identifiers:
{container_ident},{capsule_ident}and others accept a UUID or a name. An admin context searches all projects (zun/api/utils.py:60,zun/api/utils.py:71). - Pagination:
limitcapped by[api]/max_limit(default 1000),markeris a UUID,sort_keyandsort_dir(zun/api/utils.py:39,zun/api/utils.py:52,zun/conf/api.py:36). - Project scope: all queries filter by project unless admin with
all_projects(zun/db/sqlalchemy/api.py:111). A resource of another project is therefore a 404, not a 403 [inferred from the filter]. - Field filtering by policy:
host,runtime,privilegedandimage_pull_policydisappear from a container view unlesscontainer:get_one:<field>allows them (zun/api/controllers/v1/views/containers_view.py:64). Those four rules are admin only ([_rewrite/policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json)). The field list is atzun/api/controllers/v1/views/containers_view.py:20. - Cost of a read:
GETon one container makes a synchronous RPC call to its host when it has one (zun/api/controllers/v1/containers.py:232).
2. Summary table
Version — means all versions. Policy owner means is_admin:True or project_id:%(project_id)s, admin means rule:context_is_admin.
| Method | Path | Handler | Policy | Version | Success | Errors |
|---|---|---|---|---|---|---|
| GET | / | zun/api/controllers/root.py:81 | public | — | 200 | — |
| GET | /v1 | zun/api/controllers/v1/__init__.py:173 | public | — | 200 | 406 |
| GET | /v1/containers | zun/api/controllers/v1/containers.py:164 | container:get_all owner; container:get_all_all_projects admin | — | 200 | 400, 403 |
| POST | /v1/containers | zun/api/controllers/v1/containers.py:305 | container:create owner | 1.1 to 1.40, 5 schemas | 202 | 400, 403, 404, 409, 415\* |
| GET | /v1/containers/{ident} | zun/api/controllers/v1/containers.py:220 | container:get_one owner | — | 200 | 403, 404, 409 |
| PATCH | /v1/containers/{ident} | zun/api/controllers/v1/containers.py:691 | container:update owner | — | 200 | 400, 403, 404, 409 |
| DELETE | /v1/containers/{ident} | zun/api/controllers/v1/containers.py:775 | container:delete owner; container:delete_force admin | force 1.7, stop 1.12 | 204 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/start | zun/api/controllers/v1/containers.py:848 | container:start owner | — | 202 | 403, 404, 409 |
| POST | /v1/containers/{ident}/stop | zun/api/controllers/v1/containers.py:866 | container:stop owner | — | 202 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/reboot | zun/api/controllers/v1/containers.py:884 | container:reboot owner | — | 202 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/rebuild | zun/api/controllers/v1/containers.py:823 | container:rebuild owner | — | 202 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/pause | zun/api/controllers/v1/containers.py:903 | container:pause owner | — | 202 | 403, 404, 409 |
| POST | /v1/containers/{ident}/unpause | zun/api/controllers/v1/containers.py:920 | container:unpause owner | — | 202 | 403, 404, 409 |
| POST | /v1/containers/{ident}/kill | zun/api/controllers/v1/containers.py:1024 | container:kill owner | — | 202 | 400, 403, 404, 409 |
| GET | /v1/containers/{ident}/logs | zun/api/controllers/v1/containers.py:938 | container:logs owner | — | 200 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/execute | zun/api/controllers/v1/containers.py:973 | container:execute owner | — | 200 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/execute_resize | zun/api/controllers/v1/containers.py:1004 | container:execute_resize owner | — | 200 | 400, 403, 404, 409 |
| GET | /v1/containers/{ident}/attach | zun/api/controllers/v1/containers.py:1043 | container:attach owner | — | 200 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/resize | zun/api/controllers/v1/containers.py:1064 | container:resize owner | — | 200 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/resize_container | zun/api/controllers/v1/containers.py:751 | container:resize_container owner | ≥ 1.19 | 202 | 400, 403, 404, 406, 409 |
| GET | /v1/containers/{ident}/top | zun/api/controllers/v1/containers.py:1081 | container:top owner | — | 200 | 400, 403, 404, 409 |
| GET | /v1/containers/{ident}/stats | zun/api/controllers/v1/containers.py:1175 | container:stats owner | — | 200 | 403, 404, 409 |
| GET | /v1/containers/{ident}/get_archive | zun/api/controllers/v1/containers.py:1121 | container:get_archive owner | base64 from 1.25 | 200 | 403, 404, 409 |
| POST | /v1/containers/{ident}/put_archive | zun/api/controllers/v1/containers.py:1160 | container:put_archive owner | base64 from 1.25 | 200 | 403, 404, 409 |
| POST | /v1/containers/{ident}/commit | zun/api/controllers/v1/containers.py:1191 | container:commit owner | — | 202 | 400, 403, 404, 409 |
| POST | /v1/containers/{ident}/rename | zun/api/controllers/v1/containers.py:730 | container:rename owner | 1.1 to 1.13 | 200 | 400, 403, 404, 406, 409 |
| POST | /v1/containers/{ident}/add_security_group | zun/api/controllers/v1/containers.py:638 | container:add_security_group owner | 1.1 to 1.14 | 202 | 400, 403, 404, 406, 409 |
| POST | /v1/containers/{ident}/remove_security_group | zun/api/controllers/v1/containers.py:666 | container:remove_security_group owner | 1.1 to 1.14 | 202 | 400, 403, 404, 406, 409 |
| POST | /v1/containers/{ident}/network_detach | zun/api/controllers/v1/containers.py:1211 | container:network_detach owner | ≥ 1.6 | 202 | 400, 403, 404, 406 |
| POST | /v1/containers/{ident}/network_attach | zun/api/controllers/v1/containers.py:1250 | container:network_attach owner | ≥ 1.8 | 200 | 400, 403, 404, 406 |
| GET | /v1/containers/{ident}/network_list | zun/api/controllers/v1/containers.py:1280, zun/api/controllers/v1/containers.py:1305 | none | ≥ 1.13, shape changes at 1.18 | 200 | 404, 406 |
| GET | /v1/containers/{ident}/container_actions | zun/api/controllers/v1/containers.py:89 | container:actions owner | — | 200 | 403, 404 |
| GET | /v1/containers/{ident}/container_actions/{request_ident} | zun/api/controllers/v1/containers.py:103 | container:actions owner; traceback with container:action:events admin | — | 200 | 403, 404 |
| GET | /v1/capsules | zun/api/controllers/v1/capsules.py:129 | capsule:get_all owner | shape changes at 1.32 | 200 | 400, 403 |
| POST | /v1/capsules | zun/api/controllers/v1/capsules.py:182 | capsule:create owner | shape changes at 1.32 | 202 | 400, 403, 404, 409, 415\* |
| GET | /v1/capsules/{ident} | zun/api/controllers/v1/capsules.py:374 | capsule:get owner | shape changes at 1.32 | 200 | 403, 404 |
| DELETE | /v1/capsules/{ident} | zun/api/controllers/v1/capsules.py:383 | capsule:delete owner; capsule:delete_all_projects admin | — | 204 | 403, 404 |
| GET | /v1/images | zun/api/controllers/v1/images.py:88 | image:get_all admin | — | 200 | 400, 403 |
| POST | /v1/images | zun/api/controllers/v1/images.py:134 | image:pull admin | — | 202 | 400, 403, 409, 415\* |
| GET | /v1/images/{image_id} | zun/api/controllers/v1/images.py:97 | image:get_one admin | — | 200 | 403, 404 |
| DELETE | /v1/images/{image_id} | zun/api/controllers/v1/images.py:80 | image:delete admin | — | 200 | 403, 404 |
| GET | /v1/images/{image}/search | zun/api/controllers/v1/images.py:160 | image:search owner | — | 200 | 400, 403, 500 |
| GET | /v1/hosts | zun/api/controllers/v1/hosts.py:58 | host:get_all admin | ≥ 1.4 | 200 | 400, 403, 406 |
| GET | /v1/hosts/{host_ident} | zun/api/controllers/v1/hosts.py:92 | host:get admin | ≥ 1.4 | 200 | 403, 404, 406 |
| GET | /v1/services | zun/api/controllers/v1/zun_services.py:132 | zun-service:get_all admin | — | 200 | 403 |
| DELETE | /v1/services or /v1/services/{host}/{binary} | zun/api/controllers/v1/zun_services.py:149 | zun-service:delete admin | — | 200 | 403, 404 |
| PUT | /v1/services/enable | zun/api/controllers/v1/zun_services.py:170 | zun-service:enable admin | — | 200 | 400, 403, 404 |
| PUT | /v1/services/disable | zun/api/controllers/v1/zun_services.py:180 | zun-service:disable admin | — | 200 | 400, 403, 404 |
| PUT | /v1/services/force_down | zun/api/controllers/v1/zun_services.py:194 | zun-service:force_down admin | — | 200 | 400, 403, 404 |
| GET | /v1/availability_zones | zun/api/controllers/v1/availability_zone.py:64 | availability_zones:get_all owner | — | 200 | 400, 403 |
| POST | /v1/networks | zun/api/controllers/v1/networks.py:69 | network:create admin | — | 200 | 400, 403, 415\* |
| DELETE | /v1/networks/{network_ident} | zun/api/controllers/v1/networks.py:83 | network:delete admin | ≥ 1.27 | 204 | 403, 404, 406 |
| GET | /v1/quotas/{project_id} | zun/api/controllers/v1/quotas.py:63 | quota:get owner | — | 200 | 403 |
| PUT | /v1/quotas/{project_id} | zun/api/controllers/v1/quotas.py:46 | quota:update admin | — | 200 | 400, 403 |
| DELETE | /v1/quotas/{project_id} | zun/api/controllers/v1/quotas.py:83 | quota:delete admin | — | 200 | 403 |
| GET | /v1/quotas/{project_id}/defaults | zun/api/controllers/v1/quotas.py:73 | quota:get_default owner | — | 200 | 403 |
| GET | /v1/quota_classes/{name} | zun/api/controllers/v1/quota_classes.py:53 | quota_class:get admin | — | 200 | 403 |
| PUT | /v1/quota_classes/{name} | zun/api/controllers/v1/quota_classes.py:36 | quota_class:update admin | — | 200 | 400, 403 |
| GET | /v1/registries | zun/api/controllers/v1/registries.py:95 | registry:get_all owner | — | 200 | 400, 403 |
| POST | /v1/registries | zun/api/controllers/v1/registries.py:164 | registry:create owner | — | 201 | 400, 403, 415\* |
| GET | /v1/registries/{ident} | zun/api/controllers/v1/registries.py:147 | registry:get_one owner | — | 200 | 403, 404 |
| PATCH | /v1/registries/{ident} | zun/api/controllers/v1/registries.py:182 | registry:update owner | — | 200 | 400, 403, 404 |
| DELETE | /v1/registries/{ident} | zun/api/controllers/v1/registries.py:206 | registry:delete owner | — | 204 | 403, 404 |
\* A wrong Content-Type is meant to give 415, but the code crashes first: see 02-errors and § 8.
The Errors column lists what the handler and its validators can raise after reading the call chain. The statuses actually observed per route are in [_rewrite/observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md). Any call guarded by check_container_host answers 500 with an obfuscated message when the node is down, because ContainerHostNotUp has no explicit status (zun/compute/rpcapi.py:25, zun/common/exception.py:371). Over quota is 403 (zun/common/exception.py:710).
3. Containers
POST /v1/containers
Functional description
- Purpose: create a container on a compute node, and optionally start it (
?run=true). - Actor: project member. Admins can also pick the host and the runtime.
- Preconditions: quota available; requested networks, ports, security groups, volumes and registry visible to the caller; at least one compute node with capacity and a live
zun-compute. - Business rules:
- Security groups are de-duplicated and resolved to IDs; a name matching several groups gives 409, an unknown name 400 (
zun/api/controllers/v1/containers.py:316,zun/api/controllers/v1/containers.py:616). - Quotas are checked on containers, CPU, memory and disk before anything is created (
zun/api/controllers/v1/containers.py:335,zun/common/quota.py:166). Defaults: 40 containers, 51200 MB, 20 CPUs, 100 GB (zun/conf/quota.py:26,zun/conf/quota.py:38,zun/conf/quota.py:51,zun/conf/quota.py:64). The disk delta counts only what the request names:diskis defaulted later. runtimeneeds 1.5 andcontainer:create:runtime, admin by default (zun/api/controllers/v1/containers.py:349).privilegedneeds 1.21 andcontainer:create:privileged, denied to everybody by default (zun/api/controllers/v1/containers.py:379,zun/common/policies/container.py:43).image_pull_policyneedscontainer:create:image_pull_policy, admin by default (zun/api/controllers/v1/containers.py:398).hostneedscontainer:create:requested_destination, admin by default, and 1.39 through the schema (zun/api/controllers/v1/containers.py:423).- Without
name, a random name ending in-containeris generated (zun/api/controllers/v1/containers.py:242). Uniqueness depends on[compute]/unique_container_name_scope, empty by default, so names may repeat (zun/db/sqlalchemy/api.py:165). - Defaults:
memorydefault_memory512 MB,cpudefault_cpu1.0 (zun/api/controllers/v1/containers.py:495).diskstays unset here and is set on the node (zun/compute/manager.py:252). - Without
nets, Zun takes the project's oldest non-shared network, else the oldest shared one, else 409 (zun/network/neutron.py:274). The call to it is inzun/common/utils.py:508. commandis a string up to 1.19 and a list from 1.20 (zun/api/controllers/v1/containers.py:258).interactivealso setsttyfrom 1.20 to 1.35 (zun/api/controllers/v1/containers.py:273).entrypointappears at 1.40.mounts:{type: volume, source}reuses a Cinder volume,{type: volume, size}creates one withauto_remove,{type: bind, source: <content>}writes a local file from content (zun/api/controllers/v1/containers.py:578,zun/volume/driver.py:93). Abindmount does not expose a host path:sourceis the file content.exposed_portskeys areportorport/proto,protoin tcp and udp (zun/common/utils.py:478).
- Security groups are de-duplicated and resolved to IDs; a name matching several groups gives 409, an unknown name 400 (
- Flow:
- (synchronous) Row inserted with status
Creating(zun/api/controllers/v1/containers.py:428,zun/api/controllers/v1/containers.py:436). - (synchronous) Volume rows created; a Cinder volume may be created (
zun/api/controllers/v1/containers.py:597). - (synchronous) Scheduling in the API process: Placement allocation candidates, filters, allocation claim (
zun/compute/api.py:55,zun/scheduler/client/query.py:46,zun/scheduler/filter_scheduler.py:49). - (synchronous, off by default) Image check on the node if
[api]/enable_image_validation(zun/compute/api.py:72). - (synchronous) Action
createrecorded and RPCcast container_createto the chosen node (zun/compute/api.py:98,zun/compute/rpcapi.py:58). Response 202,Locationheader. - (asynchronous) Volumes attached, image fetched, Docker container created, optionally started: 05 § container_create.
- (synchronous) Row inserted with status
- Outcome:
Creatingin the response, thenCreatedorRunning, orErrorwithstatus_reason. Observed withGET. - Effects visible elsewhere: Placement allocation, Neutron port, optional Neutron security group
zun-<uuid>, Cinder volume in use. - Limitations: when no host fits, the API still answers 202, the container is stored in
Errorwith reasonThere are not enough hosts available.(zun/compute/api.py:57). When a requested host has no compute node, the row is deleted and 4xx is returned (zun/compute/api.py:119).
Technical sheet
- Request: query
run; body validated bycontainers.legacy_container_create(1.1 to 1.19),container_create(1.20 to 1.38),container_create_v139,container_create_v140(zun/api/controllers/v1/schemas/containers.py:48,zun/api/controllers/v1/schemas/containers.py:58,zun/api/controllers/v1/schemas/containers.py:73,zun/api/controllers/v1/schemas/containers.py:77). Dumps in [_rewrite/request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json). - Example:
{"image": "cirros", "command": ["ping", "8.8.8.8"], "nets": [{"network": "<uuid>"}], "memory": "512", "cpu": 1.0}. - Response: 202,
Location: /v1/containers/<uuid>(zun/api/controllers/v1/containers.py:448); body as inresponse-schemas. - Effects: tables
container,volume,container_actions; RPC; REST to Neutron, Cinder, Placement. - Spec/code mismatch:
api-ref/source/images.incin the api-ref is empty; see § 8.
GET /v1/containers and GET /v1/containers/{ident}
- Purpose: list or read containers.
- Rules: filters
name,image,project_id,user_id,memory,host,task_state,status,auto_remove, each subject tocontainer:get_one:<filter>(zun/api/controllers/v1/containers.py:186). Any other parameter gives 400Unknown parameters(zun/api/controllers/v1/containers.py:201). Filter onhostis admin only. - Effect of a read: the single read refreshes state from Docker over RPC
call container_showand saves the row if changed (zun/api/controllers/v1/containers.py:235,zun/compute/manager.py:618). If the node is down, the DB row is returned and the error logged (zun/api/controllers/v1/containers.py:236). The list reads only the database.
PATCH /v1/containers/{ident}
- Purpose: change
name, orcpuandmemorylive. - Rules: states
Created,Running,Stopped,Paused(zun/common/utils.py:73). Schemacontainer_update. - Flow: name only is a database write (
zun/api/controllers/v1/containers.py:712). CPU or memory checks the quota on the delta, then RPCcall container_update: a resource claim, thendocker update(zun/api/controllers/v1/containers.py:719,zun/compute/manager.py:891,zun/container/docker/driver.py:905). - Limitations: synchronous;
resize_container(≥ 1.19) does the same asynchronously without the quota check (zun/api/controllers/v1/containers.py:751,zun/compute/manager.py:1290).
DELETE /v1/containers/{ident}
- Purpose: delete a container.
- Rules: allowed states without flags:
Created,Error,Stopped,Deleted,Dead(zun/common/utils.py:56).force=trueneedscontainer:delete_forceand widens the states (zun/api/controllers/v1/containers.py:799).stop=true(≥ 1.12) sends a stop first when running (zun/api/controllers/v1/containers.py:804). - Flow: with a host, RPC
cast container_delete, 204 (zun/api/controllers/v1/containers.py:815); without one, the row is removed immediately (zun/api/controllers/v1/containers.py:818). - Outcome: 204 means accepted, not deleted. The row disappears once Docker, Neutron, Cinder and Placement cleanups have succeeded: 05 § container_delete.
Lifecycle actions
start, stop, reboot, pause, unpause, kill, rebuild follow the same pattern: find, policy, state check, action record, cast, 202 (zun/api/controllers/v1/containers.py:848, zun/compute/api.py:157).
| Action | Allowed states (zun/common/utils.py:54) | Parameters | Set before the cast |
|---|---|---|---|
| start | Created, Stopped, Error | — | — |
| stop | Running | timeout | — |
| reboot | Created, Running, Stopped, Error | timeout | Restarting (zun/api/controllers/v1/containers.py:895) |
| rebuild | Created, Running, Stopped, Error | image, image_driver | Rebuilding (zun/api/controllers/v1/containers.py:840) |
| pause | Running | — | — |
| unpause | Paused | — | — |
| kill | Running | signal | — |
Rebuild keeps the UUID and its ports, removes the Docker container and creates it again, then starts it if it was running (zun/api/controllers/v1/containers.py:838, zun/compute/manager.py:684).
Runtime inspection
Synchronous RPC call, so MessagingTimeout applies, and check_container_host refuses a dead node (zun/compute/rpcapi.py:25).
| Action | Purpose | Rule |
|---|---|---|
logs | stdout, stderr, timestamps, tail, since | returned as a string (zun/container/docker/driver.py:834) |
execute | run a command. With run=true, wait up to [docker]/execute_timeout (5 s) and return output and exit_code; with run=false, return exec_id and a proxy_url for a WebSocket | zun/compute/manager.py:826, zun/compute/api.py:175, zun/container/docker/driver.py:868 |
attach | returns a WebSocket URL with a single token stored on the row; needs interactive | zun/api/controllers/v1/containers.py:1052, zun/compute/manager.py:928, zun/compute/api.py:196 |
resize, execute_resize | resize a TTY (h, w) | zun/container/docker/driver.py:936, zun/container/docker/driver.py:879 |
top, stats | process list; one stats snapshot | zun/container/docker/driver.py:945, zun/container/docker/driver.py:982 |
get_archive, put_archive | copy a tar; base64 from 1.25 | zun/compute/manager.py:973, zun/compute/manager.py:991 |
commit | Glance only: creates the Glance image at once, returns its UUID, then pauses, commits and uploads in the background | zun/compute/manager.py:1023, zun/compute/manager.py:1064 |
Networks of a container
network_list: no policy check (zun/api/controllers/v1/containers.py:1305). The database project filter still applies throughget_resource.network_attach: synchronous checks, thencast. Status 200, unlike the other casts (zun/api/controllers/v1/containers.py:1250). 400 if the network is already attached.network_detach: bynetworkorport(≥ 1.17); 202 (zun/api/controllers/v1/containers.py:1211).
Container actions
GET …/container_actions lists the audit trail, and …/{request_ident} adds events (zun/api/controllers/v1/containers.py:89, zun/api/controllers/v1/containers.py:103). The traceback of an event is shown only with container:action:events.
4. Capsules
POST /v1/capsules
- Purpose: create a pod-like group of containers sharing network and IPC.
- Template: Kubernetes-like;
kindmust becapsuleorCapsule,spec.containersis required,metadata,netsandavailabilityZoneare optional (zun/api/controllers/v1/capsules.py:48). - Rules:
- CPU and memory: sum of the normal containers, raised to the largest init container (
zun/api/controllers/v1/capsules.py:243,zun/api/controllers/v1/capsules.py:252). - Restart policy: from the spec, else
always; init containers withalwaysbecomeon-failurewith 10 retries (zun/api/controllers/v1/capsules.py:209,zun/api/controllers/v1/capsules.py:311). - Quotas are not checked, unlike containers [inferred: no
_check_container_quotasinzun/api/controllers/v1/capsules.py]. image_pull_policywithout the policy is silently removed, not refused (zun/api/controllers/v1/capsules.py:264).- Sandbox image
sandbox_image,kubernetes/pauseby default (zun/api/controllers/v1/capsules.py:238,zun/conf/image_driver.py:39). - Volumes are Cinder only (
zun/api/controllers/v1/capsules.py:428).
- CPU and memory: sum of the normal containers, raised to the largest init container (
- Flow: rows created, volumes built and rolled back on error, then the same
compute_api.container_createas containers, withrun=False(zun/api/controllers/v1/capsules.py:344). The driver differs on the node: 05. - Outcome: 202,
Location,Creatingstatus. Nostart,stopor other action exists for capsules.
Others
GET list and read, shape changes at 1.32 through legacy_api_version (zun/api/controllers/v1/capsules.py:101). DELETE (zun/api/controllers/v1/capsules.py:383): cast when placed, else rows removed; 204.
Listing has a quirk: all_projects=true sets the flag without checking capsule:get_all_all_projects (zun/api/controllers/v1/capsules.py:133). The database filter still requires context.is_admin (zun/db/sqlalchemy/api.py:112), so a non-admin simply stays in their project.
5. Images, hosts, services, zones
- Images track what is on a node.
POSTneedshostandrepo, stores the row, and castsimage_pullto that node (zun/api/controllers/v1/images.py:142,zun/compute/manager.py:1119). Errors in the pull are only logged.DELETEis a cast and returns 200 (zun/api/controllers/v1/images.py:80,zun/compute/manager.py:1108).searchis acallwith no named server, so it runs on any compute node (zun/compute/rpcapi.py:183), and the Docker driver searches Docker Hub only (zun/image/docker/driver.py:119). - Hosts read
compute_node(zun/api/controllers/v1/hosts.py:77). - Services:
stateis computed withservice_is_up:forced_downfalse and last heartbeat withinservice_down_time(180 s) (zun/api/servicegroup.py:26,zun/conf/services.py:25).enable,disable,force_downreadhostandbinaryfrom the query, not the body (zun/api/controllers/v1/zun_services.py:170).disablestops scheduling throughComputeFilterand through a Placement trait [inferred fromzun/compute/compute_node_tracker.py:341]. - Availability zones: distinct
availability_zonevalues of services (zun/api/controllers/v1/availability_zone.py:64). FilterAvailabilityZoneFilteris on by default (zun/conf/scheduler.py:66).
6. Networks, quotas, registries
- Networks (
admin):POSTwithneutron_net_idis a **callwith no named server**, so one arbitrary node creates a Docker network (zun/compute/rpcapi.py:211).DELETEis equally unaddressed (zun/compute/rpcapi.py:216), which is a problem since a Docker network is per node: see § 8. - Quotas:
PUTcreates or updates limits (zun/api/controllers/v1/quotas.py:46).GET ?usages=returns limit, in use (zun/common/quota.py:108).quota:getlets a project read itself. - Quota classes:
PUTupdate or create,GET(zun/api/controllers/v1/quota_classes.py:36). NoDELETE. - Registries store credentials for private image registries.
POSTbody{"registry": {name, domain, username, password}}(zun/api/controllers/v1/registries.py:164). The view always showspasswordas***(zun/api/controllers/v1/views/registries_view.py:48). The password is encrypted in the database and decrypted when read: see 03 § Database. A container references one withregistry(≥ 1.31) (zun/api/controllers/v1/containers.py:417).
7. Lifecycle: container
Status values: zun/common/consts.py:14. Transitions driven by the API, by zun-compute and by the Docker state mapping (zun/container/docker/driver.py:644).
stateDiagram-v2 [*] --> Creating: POST (API) Creating --> Error: NoValidHost in the API, or create failure (zun-compute) Creating --> Created: docker create and state refresh (zun-compute) Created --> Running: start, or run=true (cast) Running --> Stopped: stop, kill (cast), or Docker state exited (sync) Stopped --> Running: start (cast) Running --> Paused: pause (cast) Paused --> Running: unpause (cast) Running --> Restarting: reboot (API sets it) Restarting --> Running: docker restart (zun-compute) Created --> Rebuilding: rebuild (API sets it) Running --> Rebuilding: rebuild Stopped --> Rebuilding: rebuild Error --> Rebuilding: rebuild Rebuilding --> Created: re-created (zun-compute) Rebuilding --> Error: failure Running --> Dead: Docker state dead (sync) Running --> Unknown: unexpected Docker state (sync) Running --> Error: container missing in Docker, no auto_remove (show or sync) Running --> Deleted: container missing in Docker, auto_remove (sync) Deleted --> [*]: delete_unused_containers (timer) Created --> Deleting: DELETE (API) Stopped --> Deleting: DELETE Error --> Deleting: DELETE Deleting --> [*]: cleanups succeed (zun-compute) Deleting --> Error: Docker failure without force
A separate field, task_state, marks the operation in progress (image_pulling, container_creating, container_stopping…; zun/common/consts.py:28, zun/compute/manager.py:309). While it is set, status refreshes from Docker are skipped unless forced (zun/container/docker/driver.py:645).
Lifecycle: capsule
zun/common/consts.py:21 defines Pending, Running, Succeeded, Failed, Unknown, but the API writes Creating (zun/api/controllers/v1/capsules.py:202) and refreshes use the same mapping as containers (zun/compute/manager.py:1242). Pending, Succeeded and Failed are not produced by the code read [inferred].
stateDiagram-v2 [*] --> Creating: POST (API) Creating --> Error: NoValidHost, or create failure Creating --> Running: sandbox and containers started Running --> Stopped: sandbox exited (sync) Running --> [*]: DELETE, cleanups succeed Error --> [*]: DELETE
8. Spec/code mismatches and traps
| Finding | Evidence |
|---|---|
A microversion out of range should give 406 with zun.microversion-unsupported; HTTPNotAcceptableAPIVersion.__call__ calls .encode on bytes and the exception escapes the application. Observed in-process for 1.0, 1.41 and 2.1; a WSGI server answers a bare 500 [inferred] | zun/api/http_error.py:65, probe "version: above maximum" |
A wrong Content-Type should give 415; content_type.decode on a str raises first, outside the exception wrapper, and escapes the application. Same consequence | zun/api/utils.py:89, probe "errors: content type" |
Generic 404, 405 and 406 bodies lose their message: code is container. with empty title and detail, because they are not JSON when they reach the middleware | zun/api/middleware/parsable_error.py:69, probes "routing: unknown resource", "version: wrong service" |
api-ref spells add_securtiy_group; the code route is add_security_group. The typo answers 404 | api-ref/source/containers.inc:866, zun/api/controllers/v1/containers.py:152 |
api-ref DELETE /v1/services with query parameters works, as does the undocumented /v1/services/{host}/{binary} | probes "routing: api-ref form of service delete" and "routing: code form of service delete" |
api-ref/source/images.inc is empty: images, networks, registries and availability zones are undocumented | api-ref/source/index.rst:12 |
network_attach answers 200 for an asynchronous operation | zun/api/controllers/v1/containers.py:1275 |
network_list has no policy check | zun/api/controllers/v1/containers.py:1305 |
| Network create and delete are addressed to no server, while Docker networks are per node, so the delete can run on a node that lacks the network | zun/compute/rpcapi.py:211, zun/compute/rpcapi.py:216 |
NoValidHost gives 202 and status Error, not a 4xx | zun/compute/api.py:57 |
A % in an unknown query parameter gives 500, not 400: the message is formatted twice | zun/api/controllers/v1/containers.py:204, probe "errors: percent sign in an unknown parameter" |
GET /v1/images/{not-a-uuid} gives 500: Image.get_by_name does not exist | zun/api/utils.py:75, probe "errors: image name instead of a UUID" |
?_method= is refused with a schema error on validated handlers, so Pecan's method override does not widen the API there | probe "routing: POST with _method override" |
Source: reverse-spec-zun/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.