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

Reverse-spec · Services

zun — Rewrite pack

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/

FileUse 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 its code gains -remote, for example container.conflict-remote, and detail carries the node's traceback (oslo.messaging:oslo_messaging/_drivers/common.py:205, 02 T3 and T4). A rewrite should not reproduce the traceback.
  • code comes from convert_excp_to_err_code on the exception class name (zun/common/exception.py:132, vectors in vectors_*.json). An exception of a renamed class changes the public code.
  • request_id is always empty.
  • Server errors: title is the HTTP reason, detail is Your request could not be handled because of a problem in the server. Error Correlation id is: <uuid>, code is container. (zun/common/exception.py:90, zun/common/exception.py:141).
  • Pecan-generated 404, 405 and 406 are not JSON at that point, so code is container., with empty title and detail: observed in the probes. A rewrite is compatible as long as it returns the same shape; a filled detail would be an improvement no client breaks on [inferred from § 4].

2.3 Other cross-cutting rules

  • Unknown query parameters: 400 on GET /containers and /registries, ignored on other lists (zun/api/controllers/v1/containers.py:201, zun/api/controllers/v1/capsules.py:129).
  • Query schemas: additionalProperties: false, so _method and unknown parameters are refused on validated handlers. Probed.
  • Numbers may be strings in schemas: _validate_minimum and _validate_maximum coerce (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 .json suffix: GET /v1/containers.json answers 200. Probed.
  • Trailing slash accepted. Probed and in the exchanges.
  • Location on 202 and 201 creations (zun/api/controllers/v1/containers.py:448), built from zun/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:

  1. Allocation candidates; none or Placement unreachable gives NoValidHost.
  2. compute_node rows with rp_uuid in the summaries, joined with live services.
  3. 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/).
  4. Host order: the order in which the filters return the compute_node list. There is no shuffling in zun/scheduler/filter_scheduler.py:122, so the first host that fits tends to be chosen again.
  5. First host whose allocation claim succeeds wins.
  6. 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.

ExpectationWhere the client relies on it
GET / returns versions with min_version, max_version: read for version discovery, after versions.get_currentpython-zunclient:zunclient/api_versions.py:240, python-zunclient:zunclient/api_versions.py:250
Client maximum 1.40, default 1.latest, header OpenStack-API-Versionpython-zunclient:zunclient/api_versions.py:31, python-zunclient:zunclient/api_versions.py:34, python-zunclient:zunclient/api_versions.py:35
Skyline pins container 1.40skyline-console:src/client/client/constants.js:111
Errors parsed as errors[0].title, plus detail for debug. A body without them yields a generic messagepython-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 querypython-zunclient:zunclient/v1/services.py:75
GET /v1/images/{image}/searchpython-zunclient:zunclient/v1/images.py:114
execute answer has exec_id, proxy_url, output, exit_codepython-zunclient:zunclient/osc/v1/containers.py:622
WebSocket URL with a working proxy; attach returns the URL as a JSON string; execute_resize by idpython-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 runningpython-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 shownheat:heat/engine/resources/openstack/zun/container.py:375
Heat: addresses shape, network_list at 1.18, update, network_attach, network_detachheat: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 usedheat: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 errorgophercloud: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 catalogueskyline-console:src/layouts/menu.jsx:775
Skyline: network_attach, execute, attach through its clientskyline-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_id 255 against 36, hostname 255 against 63, image_driver TEXT against 255, quota_usages has no primary key in the migrations.
  • Integrity by code: container.host and compute_node, volume_mapping.host have no foreign key; Placement allocations are external.
  • Type field: container holds four kinds, container_type 0 to 3 (zun/common/consts.py:65), and capsule members point to the capsule through capsule_id (zun/db/sqlalchemy/models.py:173).
  • Locking: SELECT … FOR UPDATE at 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 default quota class is read with config fallbacks (zun/common/consts.py:63).
  • Unused tables: resource_provider, resource_class, inventory, allocation, quota_usages have 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_token with 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, and keystone.token_info (zun/api/hooks.py:50).
  • Admin: is_admin comes from the context, which evaluates context_is_admin (zun/common/context.py:67); role:admin by 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: no scope_types, no reader role, so enforce_scope has 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> with might_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 plugin zun-tempest-plugin is 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 against response-schemas_*.json.
  • Probes: probe_*.json is 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.py does not cover Pecan routes, lower-case schemas or this history format: replaced by _rewrite/tools/zun_api.py. Routes are **derived from RestController rules**, 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-zunclient negotiates up front, so it does not meet the first one [inferred from python-zunclient:zunclient/api_versions.py:250].
  • Other bugs a rewrite must not copy: heal_with_rebuilding_container argument (zun/container/docker/driver.py:518), except cinder_exception on a module (zun/volume/cinder_workflow.py:186), is_not_found by substring (zun/container/docker/driver.py:47), dead capsule_create and capsule_delete (zun/compute/rpcapi.py:190).
  • network_list without policy (zun/api/controllers/v1/containers.py:1305): decide to add container:network_list or keep.
  • No data for SR-IOV, cpuset, or the Kuryr driver's real behaviour.
  • api-ref/source/images.inc is empty in the api-ref, so images, networks, registries, availability_zones rest on the code alone.
  • No Go port exists yet in OpenKeel, and docs, openkeel-chart and openkeel-diagram do not mention Zun.

9. Non-functional requirements

  1. 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).
  2. Scale: replace the full-table reads per node with a filter by host (zun/compute/manager.py:1240, zun/compute/manager.py:1216).
  3. Security: do not carry over plain TCP to Docker; give the console tokens an expiry; avoid passing user tokens in messages for long work.
  4. Isolation is a product decision. Zun's model has shared kernels with runc and shared nodes across projects. A rewrite should state its target: Kata or gVisor by RuntimeClass, namespaces per project, and NetworkPolicy or 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, no add_security_group, no rename).
  • [ ] Routes of routes_*.json, with .json suffix, trailing slash, /containers without v1.
  • [ ] Version negotiation with the headers of § 1 in 01, and 406 fixed.
  • [ ] JSON schemas per microversion, with string-to-number coercion.
  • [ ] Error body and code derivation 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 Error for 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_*.json as 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.