Rewrite pack — zun — branch stable/2026.2
Branch
stable/2026.2@34135a563699— repository https://opendev.org/openstack/zun — analysed on 2026-11-01
Other repositories cited, with their commits: python-zunclient 1db4ba563a43, heat 5872396e548c, skyline-console 806d200392ee, gophercloud 4aae510cb4d2, oslo.messaging 9df3fda5aa64.
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) | route table derived from the controllers: method, path, handler, version range, schemas, content types, policies |
[versioned-methods_stable-2026.2.json](_rewrite/versioned-methods_stable-2026.2.json) | microversion ranges of the handlers that change |
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json), [schema-selection_stable-2026.2.json](_rewrite/schema-selection_stable-2026.2.json) | every JSON schema (Draft 4 with number-as-string minimum, maximum); which schema applies where |
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json) | OpenAPI 3.0.3 for stubs; x-handlers holds what OpenAPI cannot express; not the authority on behaviour |
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json), [api-ref-vs-code_stable-2026.2.json](_rewrite/api-ref-vs-code_stable-2026.2.json) | documented operations (46) and their gaps with the code |
[microversions_stable-2026.2.md](_rewrite/microversions_stable-2026.2.md) | 1.1 to 1.40 |
[schema-mariadb-migrated_stable-2026.2.sql](_rewrite/schema-mariadb-migrated_stable-2026.2.sql) | the schema to use: zun-db-manage upgrade on MariaDB 11.4 |
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql), [schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md) | DDL from the models; 4 differences with the migrations |
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml) | 82 default rules |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | all options |
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json) | 256 exchanges from 221 API tests; evidence, not replayable |
[response-schemas_stable-2026.2.json](_rewrite/response-schemas_stable-2026.2.json), [.md](_rewrite/response-schemas_stable-2026.2.md) | inferred response bodies per route and status |
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md) | (operation, status, code, message) seen |
[probe_stable-2026.2.json](_rewrite/probe_stable-2026.2.json) | 32 requests the tests do not send: routing, version negotiation, error bodies |
[vectors_stable-2026.2.json](_rewrite/vectors_stable-2026.2.json) | pure-function vectors (§ 3.5) |
[tools/](_rewrite/tools/zun_api.py) | zun_api.py, zun_probe.py, zun_adapter.py, schema_diff.py |
Extraction: Python 3.12 venv, requirements stable/2026.2 upper-constraints: pecan 1.8.0, WebOb 1.8.11, SQLAlchemy 2.0.52, alembic 1.19.1, oslo.db 18.1.1, oslo.messaging 18.3.1, oslo.policy 6.0.1, oslo.config 10.7.0, oslo.service 4.8.0, oslo.versionedobjects 3.12.0, keystonemiddleware 13.0.1, jsonschema 4.26.0, docker 7.2.0, grpcio 1.83.0, os-brick 7.1.1.
2. HTTP behaviour that the contracts do not show
2.1 Pipeline, outermost first
cors, request_id, osprofiler, authtoken, then the Pecan app wrapped in ParsableErrorMiddleware (etc/zun/api-paste.ini:7, zun/api/app.py:42). Pecan hooks: ContextHook, NoExceptionTracebackHook, RPCHook (zun/api/config.py:18). Then in _route of the v1 controller: version parse and range check, response headers (zun/api/controllers/v1/__init__.py:203). Then handler decorators in this order: content type, exception wrapper, query schema, body schema (zun/api/controllers/v1/containers.py:296).
So 401 wins over 406, version errors win over routing errors under /v1, and schema errors win over policy errors, because the policy is checked inside the handler (zun/api/controllers/v1/containers.py:313). Policy 403 also comes before state 409 where the handler loads the resource first (zun/api/controllers/v1/containers.py:853), and a missing resource gives 404 before the policy that needs its project.
2.2 Error body
Every non-2xx and non-3xx body (zun/api/middleware/parsable_error.py:25):
{"errors": [{"request_id": "", "code": "container.<kebab-case class>", "status": 404,
"title": "Container … could not be found.", "detail": "Container … could not be found.", "links": []}]}
- An exception that crossed RPC has a class name ending in
_Remote, so itscodegains-remote, for examplecontainer.conflict-remote, anddetailcarries the node's traceback (oslo.messaging:oslo_messaging/_drivers/common.py:205, 02 T3 and T4). A rewrite should not reproduce the traceback. codecomes fromconvert_excp_to_err_codeon the exception class name (zun/common/exception.py:132, vectors invectors_*.json). An exception of a renamed class changes the public code.request_idis always empty.- Server errors:
titleis the HTTP reason,detailisYour request could not be handled because of a problem in the server. Error Correlation id is: <uuid>,codeiscontainer.(zun/common/exception.py:90,zun/common/exception.py:141). - Pecan-generated 404, 405 and 406 are not JSON at that point, so
codeiscontainer., with emptytitleanddetail: observed in the probes. A rewrite is compatible as long as it returns the same shape; a filleddetailwould be an improvement no client breaks on [inferred from § 4].
2.3 Other cross-cutting rules
- Unknown query parameters: 400 on
GET /containersand/registries, ignored on other lists (zun/api/controllers/v1/containers.py:201,zun/api/controllers/v1/capsules.py:129). - Query schemas:
additionalProperties: false, so_methodand unknown parameters are refused on validated handlers. Probed. - Numbers may be strings in schemas:
_validate_minimumand_validate_maximumcoerce (zun/api/validation/validators.py:67). - Pecan merges query parameters into required handler arguments:
rename?name=,execute_resize?exec_id=work through this. Pecan also strips a.jsonsuffix:GET /v1/containers.jsonanswers 200. Probed. - Trailing slash accepted. Probed and in the exchanges.
Locationon 202 and 201 creations (zun/api/controllers/v1/containers.py:448), built fromzun/api/controllers/link.py.
3. Behaviour only the code defines
Zun has no heavy algorithm comparable to Placement's. What a rewrite would get wrong from the api-ref alone is the following.
3.1 Docker state to Zun status
zun/container/docker/driver.py:644. Input: Docker's State object or string, previous status, task_state, force.
if task_state set and not force: unchanged
if no state: Unknown, reason "container state is missing"
if state is a dict:
Error set: Stopped if Status in (exited, removing), else Status capitalised if known
detail "Exited(<code>) <age> ago (error)"
Paused: Paused, detail "Up <age> (paused)"
Restarting: Restarting
Running: Running, detail "Up <age>"
Dead: Dead
otherwise, by StartedAt and FinishedAt (zero time means empty):
never started and previous Creating: Created
never started and previous Created, Restarting, Error, Rebuilding: unchanged
started, not finished: Unknown, reason "unexpected container state"
started and finished: Stopped, detail "Exited(<code>) <age> ago "
if state is a string (list view), lower-cased: created, paused, running, dead, restarting, exited or removing; other gives Unknown
216 vectors: docker_state_mapping. A row in the list view gets a string state from Docker's list_containers; inspect gives a dict [inferred from zun/container/docker/driver.py:502, zun/container/docker/driver.py:601].
3.2 Placement request
zun/scheduler/utils.py:286: VCPU = max(1, ceil(cpu)), MEMORY_MB = max(1, memory), DISK_GB = disk when set and non-zero, required=!CUSTOM_ZUN_COMPUTE_STATUS_DISABLED, limit=max_placement_results (1000; zun/conf/scheduler.py:93), in_tree for a requested host. Microversion 1.31 (zun/scheduler/client/report.py:45). Consumer: the container UUID. Vectors: placement_query.
3.3 Scheduling
zun/scheduler/client/query.py:46, zun/scheduler/filter_scheduler.py:49:
- Allocation candidates; none or Placement unreachable gives
NoValidHost. compute_noderows withrp_uuidin the summaries, joined with live services.- Filters in order:
AvailabilityZoneFilter,ComputeFilter(not disabled, heartbeat fresh),RuntimeFilter(zun/conf/scheduler.py:64). Other filters are available, not enabled:CPUFilter,RamFilter,DiskFilter,CpuSetFilter,LabelFilter,PciPassthroughFilter(zun/scheduler/filters/). - Host order: the order in which the filters return the
compute_nodelist. There is no shuffling inzun/scheduler/filter_scheduler.py:122, so the first host that fits tends to be chosen again. - First host whose allocation claim succeeds wins.
- No weigher. Nothing keeps projects apart.
3.4 Claims on the node
Second admission on the node, under the compute_resources lock (zun/compute/compute_node_tracker.py:136, zun/compute/claims.py:120): memory, CPU, disk against the scheduler's limits, PCI, cpuset. dedicated CPU policy needs [compute]/enable_cpu_pinning and picks CPUs at random from the free ones (zun/compute/claims.py:111). Not reproducible, and need not be.
3.5 Vectors
vectors_stable-2026.2.json holds: VALID_STATES (24 actions), convert_excp_to_err_code (8), parse_image_name (30), should_pull_image (8), docker_state_mapping (216), placement_query (5), check_for_restart_policy (6), build_exposed_ports (6). Generated by calling the real functions. Replay: the file was generated twice from a clean process, byte-identical. status_detail holds an age, so only its first word is kept.
3.6 Image name rules
parse_image_name (zun/common/utils.py:169): Glance gives (name, ''). Otherwise tag defaults to latest; a name with no domain gets the registry's domain, else [docker]/default_registry. Invalid references raise ReferenceInvalidFormat. Grammar: zun/common/docker_image/reference.py:23.
3.7 Image pull decision
should_pull_image (zun/common/utils.py:258): never no; always yes; ifnotpresent when absent; unknown value no. Default always (zun/common/utils.py:252). The sandbox has its own ifnotpresent default (zun/conf/image_driver.py:47).
4. What the clients expect
The clients to keep working are named by the workspace: python-zunclient, with its openstack appcontainer plugin; skyline-console; heat; gophercloud. nova does not call Zun.
| Expectation | Where the client relies on it |
|---|---|
GET / returns versions with min_version, max_version: read for version discovery, after versions.get_current | python-zunclient:zunclient/api_versions.py:240, python-zunclient:zunclient/api_versions.py:250 |
Client maximum 1.40, default 1.latest, header OpenStack-API-Version | python-zunclient:zunclient/api_versions.py:31, python-zunclient:zunclient/api_versions.py:34, python-zunclient:zunclient/api_versions.py:35 |
Skyline pins container 1.40 | skyline-console:src/client/client/constants.js:111 |
Errors parsed as errors[0].title, plus detail for debug. A body without them yields a generic message | python-zunclient:zunclient/common/httpclient.py:55 |
DELETE /v1/services?host=&binary= (query form) | python-zunclient:zunclient/v1/services.py:71 |
PUT /v1/services/enable and others, parameters in the query | python-zunclient:zunclient/v1/services.py:75 |
GET /v1/images/{image}/search | python-zunclient:zunclient/v1/images.py:114 |
execute answer has exec_id, proxy_url, output, exit_code | python-zunclient:zunclient/osc/v1/containers.py:622 |
WebSocket URL with a working proxy; attach returns the URL as a JSON string; execute_resize by id | python-zunclient:zunclient/v1/containers.py:205, python-zunclient:zunclient/common/websocketclient/websocketclient.py:382, python-zunclient:zunclient/common/websocketclient/websocketclient.py:398 |
--wait polls status, with lower-case compare to created and running | python-zunclient:zunclient/osc/v1/containers.py:278, python-zunclient:zunclient/osc/v1/containers.py:984 |
Heat: statuses named exactly Creating, Created, Running, Stopped, Error, in the case shown | heat:heat/engine/resources/openstack/zun/container.py:375 |
Heat: addresses shape, network_list at 1.18, update, network_attach, network_detach | heat:heat/engine/resources/openstack/zun/container.py:500, heat:heat/engine/clients/os/zun.py:53, heat:heat/engine/clients/os/zun.py:84 |
Heat: 1.36 when tty is used | heat:heat/engine/resources/openstack/zun/container.py:330 |
Gophercloud capsules: POST must be 202, GET 200 or 203; two capsule shapes (before and after 1.32) told apart by a JSON type error | gophercloud:openstack/container/v1/capsules/requests.go:64, gophercloud:openstack/container/v1/capsules/requests.go:27, gophercloud:openstack/container/v1/capsules/results.go:27 |
Skyline menus appear only if container is in the catalogue | skyline-console:src/layouts/menu.jsx:775 |
Skyline: network_attach, execute, attach through its client | skyline-console:src/stores/zun/containers.js:97, skyline-console:src/stores/zun/containers.js:102, skyline-console:src/stores/zun/containers.js:107 |
Statuses that must not change: 202 on create, even for NoValidHost; 204 on delete; 200 on network_attach. Status strings in Title Case.
5. Database strategy
- Reuse or migrate: reuse the migrated schema to stay able to import an existing deployment. Differences with the models:
container.container_id255 against 36,hostname255 against 63,image_driverTEXTagainst 255,quota_usageshas no primary key in the migrations. - Integrity by code:
container.hostandcompute_node,volume_mapping.hosthave no foreign key; Placement allocations are external. - Type field:
containerholds four kinds,container_type0 to 3 (zun/common/consts.py:65), and capsule members point to the capsule throughcapsule_id(zun/db/sqlalchemy/models.py:173). - Locking:
SELECT … FOR UPDATEat 12 sites (zun/db/sqlalchemy/api.py:258),retry_on_deadlock, no optimistic generation. A rewrite on Galera must keep the deadlock retry: a certification failure surfaces as a deadlock. - Startup data: none. The
defaultquota class is read with config fallbacks (zun/common/consts.py:63). - Unused tables:
resource_provider,resource_class,inventory,allocation,quota_usageshave no live caller found [inferred]; keep for import, ignore otherwise. - Registry password: Fernet, key
[DEFAULT]/auth_encryption_key(zun/common/crypt.py:23,zun/conf/utils.py:21).
6. Authentication, policies, configuration and CLI
- Auth: keystonemiddleware
auth_tokenwith public routes/and/v1. A rewrite reads the same headers:X-User-Id,X-Project-Id,X-Roles,X-Auth-Token,X-User-Domain-Id,X-Project-Name, andkeystone.token_info(zun/api/hooks.py:50). - Admin:
is_admincomes from the context, which evaluatescontext_is_admin(zun/common/context.py:67);role:adminby default. - Policy language:
is_admin:True or project_id:%(project_id)s,rule:context_is_admin,!(zun/common/policies/base.py:15). Rules are not scoped: noscope_types, noreaderrole, soenforce_scopehas no effect here. Target:{project_id, user_id}from the context, or the resource (zun/common/policy.py:70). - Field filtering:
container:get_one:<field>,capsule:get:<field>,registry:get_one:<field>withmight_not_exist(zun/api/controllers/v1/views/containers_view.py:66): fields with no rule stay visible. - Filter gating:
container:get_one:<filter>on list filters (zun/api/controllers/v1/containers.py:192). - Configuration: oslo.config, 148 option sites (
_inventory/inventory_stable-2026.2.md). Deployment tools need[DEFAULT]/transport_url,host,my_ip,container_runtime,default_image_driver,[database],[keystone_auth],[keystone_authtoken],[docker],[network],[websocket_proxy]/base_url,[compute]/topic. - CLI:
zun-db-manage upgrade|version|stamp|revision(zun/cmd/db_manage.py:41),zun-status(zun/cmd/status.py:65).
7. Conformance strategy
- No black-box suite usable without Docker:
zun/tests/fullstack/needs a whole deployment, and the Tempest pluginzun-tempest-pluginis not in the workspace. - Recorded data: 256 exchanges from 221 tests, 0 failures (
http --wrap zun.api.app:setup_app,_rewrite/tools/zun_adapter.py). The tests build their data through the Python API and mock the scheduler and compute (zun/tests/unit/api/base.py:49), so these are evidence of the API layer. No seed replay was made: the exchanges cannot be replayed from an empty database as they stand. Use them for differential testing of status and body shape, and validate responses againstresponse-schemas_*.json. - Probes:
probe_*.jsonis replayable as is against a rewrite with an admin-free context, once the two known crashes are decided (§ 8). - Vectors: unit tests of the pure functions; replay proved byte-identical on the original.
- Original fixtures:
zun/tests/unit/api/base.py:36,zun/tests/unit/db/base.py:30,zun/tests/unit/db/utils.py:1. - What is missing, and most valuable: an end-to-end suite against a real runtime (create, start, stop, exec over WebSocket, delete, volume, security group, capsule), written as a black-box Python or Go suite, which runs against both the original and the rewrite. Fix this first, because the core of Zun is what the tests mock.
8. Gaps found while building the pack
rewrite_pack.pydoes not cover Pecan routes, lower-case schemas or this history format: replaced by_rewrite/tools/zun_api.py. Routes are **derived fromRestControllerrules**, then checked with exchanges and probes; 2 api-ref paths have no code route, 19 code routes are undocumented ([api-ref-vs-code](_rewrite/api-ref-vs-code_stable-2026.2.json)).- Crashes in the error paths (
zun/api/http_error.py:65,zun/api/utils.py:89). A rewrite should answer 406 and 415, a deliberate and harmless divergence.python-zunclientnegotiates up front, so it does not meet the first one [inferred frompython-zunclient:zunclient/api_versions.py:250]. - Other bugs a rewrite must not copy:
heal_with_rebuilding_containerargument (zun/container/docker/driver.py:518),except cinder_exceptionon a module (zun/volume/cinder_workflow.py:186),is_not_foundby substring (zun/container/docker/driver.py:47), deadcapsule_createandcapsule_delete(zun/compute/rpcapi.py:190). network_listwithout policy (zun/api/controllers/v1/containers.py:1305): decide to addcontainer:network_listor keep.- No data for SR-IOV,
cpuset, or the Kuryr driver's real behaviour. api-ref/source/images.incis empty in the api-ref, soimages,networks,registries,availability_zonesrest on the code alone.- No Go port exists yet in OpenKeel, and
docs,openkeel-chartandopenkeel-diagramdo not mention Zun.
9. Non-functional requirements
- Time scales: heartbeat at most 60 s, down after 180 s, state sync 60 s, volume wait 60 s, VIF wait 60 s, Docker client 60 s, exec 5 s, init containers 3600 s (
zun/conf/services.py:25,zun/conf/services.py:29,zun/conf/volume.py:49,zun/conf/cni_daemon.py:34,zun/conf/docker.py:24,zun/conf/docker.py:52,zun/container/cri/driver.py:212). - Scale: replace the full-table reads per node with a filter by host (
zun/compute/manager.py:1240,zun/compute/manager.py:1216). - Security: do not carry over plain TCP to Docker; give the console tokens an expiry; avoid passing user tokens in messages for long work.
- Isolation is a product decision. Zun's model has shared kernels with
runcand shared nodes across projects. A rewrite should state its target: Kata or gVisor byRuntimeClass, namespaces per project, andNetworkPolicyor OVN ACLs in place of Neutron security groups.
Design notes for OpenKeel [inferred, not specified by the code]
Zun's node layer is the part that does not transpose: it assumes a Docker daemon, kuryr-libnetwork, and a privileged per-node agent. On Kubernetes, a container or a capsule maps onto a Pod: zun-compute, Docker, Kuryr and CNI disappear in favour of a controller, the scheduler and Placement allocations become Kubernetes scheduling, exec and attach use the Kubernetes API, and Cinder volumes come from CSI PersistentVolumeClaims. This resembles how Nova runs on KubeVirt in OpenKeel, and those mappings would each need a design of their own.
10. Rewrite checklist
- [ ] Decide what to keep: full API at 1.40, or a subset (no
commit, noadd_security_group, norename). - [ ] Routes of
routes_*.json, with.jsonsuffix, trailing slash,/containerswithoutv1. - [ ] Version negotiation with the headers of § 1 in 01, and 406 fixed.
- [ ] JSON schemas per microversion, with string-to-number coercion.
- [ ] Error body and
codederivation of § 2.2. - [ ] Policy engine with field filtering and
might_not_exist. - [ ] Status strings and transitions of 01 § 7, and the mapping of § 3.1.
- [ ] 202 and
Errorfor no host; asynchronous delete with 204. - [ ] Container actions and events.
- [ ] Quotas, with real reservations if possible, and capsules included.
- [ ] Registries, with encrypted passwords and
***in views. - [ ] Console proxy compatible with
python-zunclient. - [ ] Heat and Skyline paths, per § 4.
- [ ] Vectors of
vectors_*.jsonas unit tests. - [ ] A new end-to-end suite, run on the original first (§ 7).
Source: reverse-spec-zun/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.