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

Reverse-spec · Services

zun — REST API

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: authtoken in 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). /healthcheck is a separate application (etc/zun/api-paste.ini:4).
  • Microversions: header OpenStack-API-Version: container <major.minor>, default 1.1, maximum 1.40, latest accepted (zun/api/controllers/versions.py:78, zun/api/controllers/versions.py:129, zun/api/controllers/versions.py:137). Every response under /v1 carries OpenStack-API-Minimum-Version, OpenStack-API-Maximum-Version, OpenStack-API-Version and Vary (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/hosts then answers 406, since hosts need 1.4 (probe "version: no header gives 1.1").
  • Version in the path: any first segment other than v1 is prefixed with v1, so GET /containers equals GET /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: limit capped by [api]/max_limit (default 1000), marker is a UUID, sort_key and sort_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, privileged and image_pull_policy disappear from a container view unless container: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 at zun/api/controllers/v1/views/containers_view.py:20.
  • Cost of a read: GET on 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.

MethodPathHandlerPolicyVersionSuccessErrors
GET/zun/api/controllers/root.py:81public—200—
GET/v1zun/api/controllers/v1/__init__.py:173public—200406
GET/v1/containerszun/api/controllers/v1/containers.py:164container:get_all owner; container:get_all_all_projects admin—200400, 403
POST/v1/containerszun/api/controllers/v1/containers.py:305container:create owner1.1 to 1.40, 5 schemas202400, 403, 404, 409, 415\*
GET/v1/containers/{ident}zun/api/controllers/v1/containers.py:220container:get_one owner—200403, 404, 409
PATCH/v1/containers/{ident}zun/api/controllers/v1/containers.py:691container:update owner—200400, 403, 404, 409
DELETE/v1/containers/{ident}zun/api/controllers/v1/containers.py:775container:delete owner; container:delete_force adminforce 1.7, stop 1.12204400, 403, 404, 409
POST/v1/containers/{ident}/startzun/api/controllers/v1/containers.py:848container:start owner—202403, 404, 409
POST/v1/containers/{ident}/stopzun/api/controllers/v1/containers.py:866container:stop owner—202400, 403, 404, 409
POST/v1/containers/{ident}/rebootzun/api/controllers/v1/containers.py:884container:reboot owner—202400, 403, 404, 409
POST/v1/containers/{ident}/rebuildzun/api/controllers/v1/containers.py:823container:rebuild owner—202400, 403, 404, 409
POST/v1/containers/{ident}/pausezun/api/controllers/v1/containers.py:903container:pause owner—202403, 404, 409
POST/v1/containers/{ident}/unpausezun/api/controllers/v1/containers.py:920container:unpause owner—202403, 404, 409
POST/v1/containers/{ident}/killzun/api/controllers/v1/containers.py:1024container:kill owner—202400, 403, 404, 409
GET/v1/containers/{ident}/logszun/api/controllers/v1/containers.py:938container:logs owner—200400, 403, 404, 409
POST/v1/containers/{ident}/executezun/api/controllers/v1/containers.py:973container:execute owner—200400, 403, 404, 409
POST/v1/containers/{ident}/execute_resizezun/api/controllers/v1/containers.py:1004container:execute_resize owner—200400, 403, 404, 409
GET/v1/containers/{ident}/attachzun/api/controllers/v1/containers.py:1043container:attach owner—200400, 403, 404, 409
POST/v1/containers/{ident}/resizezun/api/controllers/v1/containers.py:1064container:resize owner—200400, 403, 404, 409
POST/v1/containers/{ident}/resize_containerzun/api/controllers/v1/containers.py:751container:resize_container owner≥ 1.19202400, 403, 404, 406, 409
GET/v1/containers/{ident}/topzun/api/controllers/v1/containers.py:1081container:top owner—200400, 403, 404, 409
GET/v1/containers/{ident}/statszun/api/controllers/v1/containers.py:1175container:stats owner—200403, 404, 409
GET/v1/containers/{ident}/get_archivezun/api/controllers/v1/containers.py:1121container:get_archive ownerbase64 from 1.25200403, 404, 409
POST/v1/containers/{ident}/put_archivezun/api/controllers/v1/containers.py:1160container:put_archive ownerbase64 from 1.25200403, 404, 409
POST/v1/containers/{ident}/commitzun/api/controllers/v1/containers.py:1191container:commit owner—202400, 403, 404, 409
POST/v1/containers/{ident}/renamezun/api/controllers/v1/containers.py:730container:rename owner1.1 to 1.13200400, 403, 404, 406, 409
POST/v1/containers/{ident}/add_security_groupzun/api/controllers/v1/containers.py:638container:add_security_group owner1.1 to 1.14202400, 403, 404, 406, 409
POST/v1/containers/{ident}/remove_security_groupzun/api/controllers/v1/containers.py:666container:remove_security_group owner1.1 to 1.14202400, 403, 404, 406, 409
POST/v1/containers/{ident}/network_detachzun/api/controllers/v1/containers.py:1211container:network_detach owner≥ 1.6202400, 403, 404, 406
POST/v1/containers/{ident}/network_attachzun/api/controllers/v1/containers.py:1250container:network_attach owner≥ 1.8200400, 403, 404, 406
GET/v1/containers/{ident}/network_listzun/api/controllers/v1/containers.py:1280, zun/api/controllers/v1/containers.py:1305none≥ 1.13, shape changes at 1.18200404, 406
GET/v1/containers/{ident}/container_actionszun/api/controllers/v1/containers.py:89container:actions owner—200403, 404
GET/v1/containers/{ident}/container_actions/{request_ident}zun/api/controllers/v1/containers.py:103container:actions owner; traceback with container:action:events admin—200403, 404
GET/v1/capsuleszun/api/controllers/v1/capsules.py:129capsule:get_all ownershape changes at 1.32200400, 403
POST/v1/capsuleszun/api/controllers/v1/capsules.py:182capsule:create ownershape changes at 1.32202400, 403, 404, 409, 415\*
GET/v1/capsules/{ident}zun/api/controllers/v1/capsules.py:374capsule:get ownershape changes at 1.32200403, 404
DELETE/v1/capsules/{ident}zun/api/controllers/v1/capsules.py:383capsule:delete owner; capsule:delete_all_projects admin—204403, 404
GET/v1/imageszun/api/controllers/v1/images.py:88image:get_all admin—200400, 403
POST/v1/imageszun/api/controllers/v1/images.py:134image:pull admin—202400, 403, 409, 415\*
GET/v1/images/{image_id}zun/api/controllers/v1/images.py:97image:get_one admin—200403, 404
DELETE/v1/images/{image_id}zun/api/controllers/v1/images.py:80image:delete admin—200403, 404
GET/v1/images/{image}/searchzun/api/controllers/v1/images.py:160image:search owner—200400, 403, 500
GET/v1/hostszun/api/controllers/v1/hosts.py:58host:get_all admin≥ 1.4200400, 403, 406
GET/v1/hosts/{host_ident}zun/api/controllers/v1/hosts.py:92host:get admin≥ 1.4200403, 404, 406
GET/v1/serviceszun/api/controllers/v1/zun_services.py:132zun-service:get_all admin—200403
DELETE/v1/services or /v1/services/{host}/{binary}zun/api/controllers/v1/zun_services.py:149zun-service:delete admin—200403, 404
PUT/v1/services/enablezun/api/controllers/v1/zun_services.py:170zun-service:enable admin—200400, 403, 404
PUT/v1/services/disablezun/api/controllers/v1/zun_services.py:180zun-service:disable admin—200400, 403, 404
PUT/v1/services/force_downzun/api/controllers/v1/zun_services.py:194zun-service:force_down admin—200400, 403, 404
GET/v1/availability_zoneszun/api/controllers/v1/availability_zone.py:64availability_zones:get_all owner—200400, 403
POST/v1/networkszun/api/controllers/v1/networks.py:69network:create admin—200400, 403, 415\*
DELETE/v1/networks/{network_ident}zun/api/controllers/v1/networks.py:83network:delete admin≥ 1.27204403, 404, 406
GET/v1/quotas/{project_id}zun/api/controllers/v1/quotas.py:63quota:get owner—200403
PUT/v1/quotas/{project_id}zun/api/controllers/v1/quotas.py:46quota:update admin—200400, 403
DELETE/v1/quotas/{project_id}zun/api/controllers/v1/quotas.py:83quota:delete admin—200403
GET/v1/quotas/{project_id}/defaultszun/api/controllers/v1/quotas.py:73quota:get_default owner—200403
GET/v1/quota_classes/{name}zun/api/controllers/v1/quota_classes.py:53quota_class:get admin—200403
PUT/v1/quota_classes/{name}zun/api/controllers/v1/quota_classes.py:36quota_class:update admin—200400, 403
GET/v1/registrieszun/api/controllers/v1/registries.py:95registry:get_all owner—200400, 403
POST/v1/registrieszun/api/controllers/v1/registries.py:164registry:create owner—201400, 403, 415\*
GET/v1/registries/{ident}zun/api/controllers/v1/registries.py:147registry:get_one owner—200403, 404
PATCH/v1/registries/{ident}zun/api/controllers/v1/registries.py:182registry:update owner—200400, 403, 404
DELETE/v1/registries/{ident}zun/api/controllers/v1/registries.py:206registry:delete owner—204403, 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: disk is defaulted later.
    • runtime needs 1.5 and container:create:runtime, admin by default (zun/api/controllers/v1/containers.py:349).
    • privileged needs 1.21 and container:create:privileged, denied to everybody by default (zun/api/controllers/v1/containers.py:379, zun/common/policies/container.py:43).
    • image_pull_policy needs container:create:image_pull_policy, admin by default (zun/api/controllers/v1/containers.py:398).
    • host needs container: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 -container is 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: memory default_memory 512 MB, cpu default_cpu 1.0 (zun/api/controllers/v1/containers.py:495). disk stays 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 in zun/common/utils.py:508.
    • command is a string up to 1.19 and a list from 1.20 (zun/api/controllers/v1/containers.py:258). interactive also sets tty from 1.20 to 1.35 (zun/api/controllers/v1/containers.py:273). entrypoint appears at 1.40.
    • mounts: {type: volume, source} reuses a Cinder volume, {type: volume, size} creates one with auto_remove, {type: bind, source: <content>} writes a local file from content (zun/api/controllers/v1/containers.py:578, zun/volume/driver.py:93). A bind mount does not expose a host path: source is the file content.
    • exposed_ports keys are port or port/proto, proto in tcp and udp (zun/common/utils.py:478).
  • Flow:
    1. (synchronous) Row inserted with status Creating (zun/api/controllers/v1/containers.py:428, zun/api/controllers/v1/containers.py:436).
    2. (synchronous) Volume rows created; a Cinder volume may be created (zun/api/controllers/v1/containers.py:597).
    3. (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).
    4. (synchronous, off by default) Image check on the node if [api]/enable_image_validation (zun/compute/api.py:72).
    5. (synchronous) Action create recorded and RPC cast container_create to the chosen node (zun/compute/api.py:98, zun/compute/rpcapi.py:58). Response 202, Location header.
    6. (asynchronous) Volumes attached, image fetched, Docker container created, optionally started: 05 § container_create.
  • Outcome: Creating in the response, then Created or Running, or Error with status_reason. Observed with GET.
  • 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 Error with reason There 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 by containers.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 in response-schemas.
  • Effects: tables container, volume, container_actions; RPC; REST to Neutron, Cinder, Placement.
  • Spec/code mismatch: api-ref/source/images.inc in 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 to container:get_one:<filter> (zun/api/controllers/v1/containers.py:186). Any other parameter gives 400 Unknown parameters (zun/api/controllers/v1/containers.py:201). Filter on host is admin only.
  • Effect of a read: the single read refreshes state from Docker over RPC call container_show and 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, or cpu and memory live.
  • Rules: states Created, Running, Stopped, Paused (zun/common/utils.py:73). Schema container_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 RPC call container_update: a resource claim, then docker 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=true needs container:delete_force and 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).

ActionAllowed states (zun/common/utils.py:54)ParametersSet before the cast
startCreated, Stopped, Error——
stopRunningtimeout—
rebootCreated, Running, Stopped, ErrortimeoutRestarting (zun/api/controllers/v1/containers.py:895)
rebuildCreated, Running, Stopped, Errorimage, image_driverRebuilding (zun/api/controllers/v1/containers.py:840)
pauseRunning——
unpausePaused——
killRunningsignal—

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).

ActionPurposeRule
logsstdout, stderr, timestamps, tail, sincereturned as a string (zun/container/docker/driver.py:834)
executerun 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 WebSocketzun/compute/manager.py:826, zun/compute/api.py:175, zun/container/docker/driver.py:868
attachreturns a WebSocket URL with a single token stored on the row; needs interactivezun/api/controllers/v1/containers.py:1052, zun/compute/manager.py:928, zun/compute/api.py:196
resize, execute_resizeresize a TTY (h, w)zun/container/docker/driver.py:936, zun/container/docker/driver.py:879
top, statsprocess list; one stats snapshotzun/container/docker/driver.py:945, zun/container/docker/driver.py:982
get_archive, put_archivecopy a tar; base64 from 1.25zun/compute/manager.py:973, zun/compute/manager.py:991
commitGlance only: creates the Glance image at once, returns its UUID, then pauses, commits and uploads in the backgroundzun/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 through get_resource.
  • network_attach: synchronous checks, then cast. Status 200, unlike the other casts (zun/api/controllers/v1/containers.py:1250). 400 if the network is already attached.
  • network_detach: by network or port (≥ 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; kind must be capsule or Capsule, spec.containers is required, metadata, nets and availabilityZone are 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 with always become on-failure with 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_quotas in zun/api/controllers/v1/capsules.py].
    • image_pull_policy without the policy is silently removed, not refused (zun/api/controllers/v1/capsules.py:264).
    • Sandbox image sandbox_image, kubernetes/pause by 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).
  • Flow: rows created, volumes built and rolled back on error, then the same compute_api.container_create as containers, with run=False (zun/api/controllers/v1/capsules.py:344). The driver differs on the node: 05.
  • Outcome: 202, Location, Creating status. No start, stop or 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. POST needs host and repo, stores the row, and casts image_pull to that node (zun/api/controllers/v1/images.py:142, zun/compute/manager.py:1119). Errors in the pull are only logged. DELETE is a cast and returns 200 (zun/api/controllers/v1/images.py:80, zun/compute/manager.py:1108). search is a call with 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: state is computed with service_is_up: forced_down false and last heartbeat within service_down_time (180 s) (zun/api/servicegroup.py:26, zun/conf/services.py:25). enable, disable, force_down read host and binary from the query, not the body (zun/api/controllers/v1/zun_services.py:170). disable stops scheduling through ComputeFilter and through a Placement trait [inferred from zun/compute/compute_node_tracker.py:341].
  • Availability zones: distinct availability_zone values of services (zun/api/controllers/v1/availability_zone.py:64). Filter AvailabilityZoneFilter is on by default (zun/conf/scheduler.py:66).

6. Networks, quotas, registries

  • Networks (admin): POST with neutron_net_id is a **call with no named server**, so one arbitrary node creates a Docker network (zun/compute/rpcapi.py:211). DELETE is equally unaddressed (zun/compute/rpcapi.py:216), which is a problem since a Docker network is per node: see § 8.
  • Quotas: PUT creates or updates limits (zun/api/controllers/v1/quotas.py:46). GET ?usages= returns limit, in use (zun/common/quota.py:108). quota:get lets a project read itself.
  • Quota classes: PUT update or create, GET (zun/api/controllers/v1/quota_classes.py:36). No DELETE.
  • Registries store credentials for private image registries. POST body {"registry": {name, domain, username, password}} (zun/api/controllers/v1/registries.py:164). The view always shows password as *** (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 with registry (≥ 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

FindingEvidence
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 consequencezun/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 middlewarezun/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 404api-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 undocumentedapi-ref/source/index.rst:12
network_attach answers 200 for an asynchronous operationzun/api/controllers/v1/containers.py:1275
network_list has no policy checkzun/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 networkzun/compute/rpcapi.py:211, zun/compute/rpcapi.py:216
NoValidHost gives 202 and status Error, not a 4xxzun/compute/api.py:57
A % in an unknown query parameter gives 500, not 400: the message is formatted twicezun/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 existzun/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 thereprobe "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.