Rewrite pack — swift — branch stable/2026.2
Branch
stable/2026.2@a1bdaeba21bd— repository https://opendev.org/openstack/swift — analysed on 2026-10-29 Client repositories cited:python-swiftclientstable/2026.2 @dae63b4621d3,python-openstackclient@8bb083b83829,glance_store@3bced266129d,cinder@c117f9cf08b4,skyline-console@806d200392ee,skyline-apiserver@4af72bfd4577,liberasurecodetag 1.8.0 @d971d8554e05.
Target: OpenKeel Swift in Go, keeping Swift's own model (rings, replication or EC, SQLite listings), devices on block-CSI PVCs (Longhorn by default) in one StatefulSet of storage pods. The design must allow a rolling migration: Go proxies and Go storage nodes join a running Python cluster, share its rings, disks, peers and clients, and replace the Python processes one by one. Sections 3 and 4 are therefore the core of this file: they state every wire and disk contract that a Go node must honour byte for byte, and how to read Swift's pickles without executing them.
1. Contents of _rewrite/
| File | Content | Use in the rewrite |
|---|---|---|
routes_stable-2026.2.json | 79 routes: proxy (Swift API and middleware endpoints), s3api, account/container/object servers (incl. REPLICATE, SSYNC, UPDATE), with handler and file:line | router tables of the Go proxy and storage servers |
openapi_stable-2026.2.json | OpenAPI 3.0.3 of the proxy API: 8 paths, 25 operations, listing/query/header parameters, response schemas observed | API skeleton, client doc; COPY/UPDATE under x-extra-methods; object names x-multi-segment |
pipeline_stable-2026.2.json | pipelines of every sample config, filter options, paste factories, required filters | middleware chain of the Go proxy |
config_stable-2026.2.conf, config-options_stable-2026.2.json | 1009 options in 105 groups from etc/*.conf-sample, oslo-sample format; group <file>__<section> (mapping in the JSON) | oslo_conf_to_yaml.py input (checked: it parses), YAML config |
info_stable-2026.2.json | GET /info of the test cluster (16 sections) | capabilities document clients rely on |
schema-sqlite-account_stable-2026.2.sql, schema-sqlite-container_stable-2026.2.sql | DDL (tables, view, triggers, index) written by the brokers of this branch on first PUT, dumped from sqlite_master | create/accept listing DBs identical to Python's |
vectors-ring_stable-2026.2.json + 4 *.ring.gz | rings v1/v2 (3 replicas, 1.5 replicas, EC 4+2), full tables, 32 names × (hash, partition, primaries, 6 handoffs) | ring reader + placement unit tests |
vectors-ondisk_stable-2026.2.json | 19 file sets → chosen/obsolete/unexpected files (replication and EC 4+2), parsed names, 7 suffix hashes on real directories | diskfile selection, hashes.pkl values |
vectors-pickle_stable-2026.2.json | xattr metadata (py3, py2-style, multi-xattr 70 KB, forbidden global), hashes.pkl (repl/EC/empty), REPLICATE bodies, .pending records (account/container), async pending; opcodes and globals used | restricted pickle codec tests (both directions) |
vectors-timestamp_stable-2026.2.json | Timestamp normal/internal/short/raw/isoformat, encode_timestamps / decode_timestamps | timestamp type |
vectors-replication_stable-2026.2.json | chexor, SSYNC missing-check and wanted lines | DB hash, SSYNC codec |
spec-reference_stable-2026.2.py | stdlib-only reimplementation written from this spec (ring v1/v2 reader, placement, handoffs, restricted unpickler, metadata decode, chexor, suffix hashes). 153/153 vector checks pass | executable spec to port function by function |
exchanges-client_stable-2026.2.json.gz | 15 981 client-side exchanges (15 069 Swift API, 912 S3) of 1080 functional tests, tagged by test | differential testing of the Go proxy |
exchanges-object-server_…, exchanges-container-server_…, exchanges-account-server_….json.gz | 30 015 / 24 866 / 5 068 internal requests as received by Python storage servers during the same tests | wire contract of Go storage servers, replay against them |
backend-headers_stable-2026.2.json | per storage-server operation: request headers seen and counts | header checklist of each backend verb |
response-schemas_stable-2026.2.{json,md}, observed-errors_stable-2026.2.md | inferred response bodies per route and status (Swift + S3), error combinations | response validation |
Library versions used for extraction: CPython 3.13.0, eventlet/greenlet/lxml/boto3 per requirements/upper-constraints.txt (stable/2026.2); PyECLib not installable on the build host (no liberasurecode): a placement-only stub was used, so EC encode/decode was not exercised (§ 8).
2. HTTP behaviour the contracts do not show
2.1 Middleware pipeline (outermost first) and which error wins
catch_errors → gatekeeper → healthcheck → proxy-logging → cache → listing_formats → container_sync → bulk → tempurl → ratelimit → (authtoken → s3api → s3token →) tempauth/keystoneauth → copy → container-quotas → account-quotas → slo → dlo → versioned_writes → symlink → proxy-logging → proxy-server (etc/proxy-server.conf-sample:160). Required filters are auto-inserted (swift/proxy/server.py:75, swift/proxy/server.py:785).
Consequences to keep:
catch_errorsturns any exception below into 500 and setsX-Trans-Id/X-Openstack-Request-Idon every response, errors included (swift/common/middleware/catch_errors.py:150). Trans-id formattx<21 hex>-<10 hex epoch>[<suffix>](swift/common/utils/__init__.py:453).gatekeeperstripsX-Backend-*and sysmeta before anything else sees them (swift/common/middleware/gatekeeper.py:87).tempurlandformpostrun before auth: they setswift.authorizethemselves;bulkandslosubrequests go through the rest of the pipeline again with the caller's identity.- Authorization is not a middleware response but a hook (
swift.authorize) called by the proxy before the handler (swift/proxy/server.py:601): a request for a missing container returns 401/403 before 404 for an unauthorised user, and 404 for an authorised one. Withdelay_denial, ACL reads (container info) happen first. - Quotas (413) are checked before SLO/DLO/versioning see the request;
copyturns COPY into GET+PUT above them.
2.2 Error body
No JSON error body anywhere in the Swift API: HTML <html><h1>…</h1><p>…</p></html> from RESPONSE_REASONS (swift/common/swob.py:59, swift/common/swob.py:1469) or short plain text. HEAD has none. Clients parse the status only (§ 5). S3: XML <Error> with Code, Message, RequestId (swift/common/middleware/s3api/s3response.py). Storage servers put a Python traceback in 500 bodies (swift/obj/server.py:1452): a Go node may return any text there.
2.3 Other cross-cutting rules
- Metadata limits and name limits of 01 § 1.3 (
swift/common/constraints.py:48), overridable inswift.conf [swift-constraints]and published in/info. - Header names are case-insensitive, returned title-cased (
X-Object-Meta-Color); values are WSGI latin-1 strings carrying UTF-8 bytes. - ETags are returned unquoted for plain objects, quoted for SLO/DLO manifests;
etag_quotercan quote all (swift/common/middleware/etag_quoter.py:55). Cinder compares the PUT ETag with its own MD5 after swiftclient strips quotes (§ 5). Last-Modifiedis theX-Timestamprounded up to the second; listings use ISO 8601 with microseconds.- Unknown query parameters are ignored;
limit > 10000→ 412. Expect: 100-continueis honoured;Transfer-Encoding: chunkedbeatsContent-Length(swift/proxy/server.py:526).
3. Core algorithms: formal specification
3.1 Ring placement and handoffs
Input: ring (§ 3.2), swift.conf [swift-hash] prefix/suffix (latin-1 bytes), names (UTF-8).
hash = md5(prefix + "/" + "/".join(names) + suffix) # swift/common/utils/__init__.py:1193
part = be_uint32(hash[0:4]) >> part_shift # swift/common/ring/ring.py:498
primaries = dedup([devs[row[part]] for row in replica2part2dev if part < len(row)]), index = position
# swift/common/ring/ring.py:487
handoffs: start = be_uint32(md5(decimal(part))[0:4]) >> part_shift; inc = max(1, parts // 65536)
seq = range(start, parts, inc) ++ range(inc - ((parts - start) % inc), start, inc)
for tier in (region, zone, ip, device): # swift/common/ring/ring.py:561
if every value of that tier among assigned devices is used: next tier
for hp in seq: for row in rows (in order) where hp < len(row):
d = devs[row[hp]]; if d unused and d's tier value unused: yield d (handoff_index++),
mark d's region, zone, ip and id used; stop the tier when all values are used
Deterministic, no randomness; only devices with assigned partitions count (swift/common/ring/ring.py:364). Proof: spec-reference reproduces all 128 lookups of vectors-ring (4 rings × 32 names). The ring is reloaded when its mtime changes, at most every 15 s (swift/common/ring/ring.py:33).
Not to reimplement as-is: the builder's rebalance. RingBuilder.rebalance(seed) (swift/common/ring/builder.py:541) uses Python's random (MT19937 with Python's seeding) and dict ordering; a Go builder cannot produce the same assignment from the same seed. It does not need to: any valid assignment works, as long as the file is in Swift's format and partition moves respect min_part_hours (data moves are what cost). Keep swift-ring-builder usable on the produced rings (v2 is readable by Swift ≥ 2.36.0, the version that introduced it, commit ae062f8b0; use v1 while any Python node older than 2.36 remains).
3.2 Ring file bytes
See 03 § 1.3 for the full layout. Summary to implement:
- gzip; first 6 decompressed bytes
R1NG+ uint16 BE version (swift/common/ring/io.py:270). - v1: uint32 BE JSON length, JSON (
devs,part_shift,replica_count= row count,byteorder,version,next_part_power), rows of uint16 inbyteorder(swift/common/ring/ring.py:117). - v2: Z_FULL_FLUSH-separated sections, each
uint64 BE length + data:swift/ring/metadata(JSON withdev_id_bytes, floatreplica_count),swift/ring/devices,swift/ring/assignments(rows in network order, 2 or 4 bytes, last row may be short), then the JSON index{name: [cstart, ustart, cend, uend, "sha256", hex]}, then two uncompressed uint64 (index offsets) so thatseek(-31, END)finds the compressed index offset (swift/common/ring/io.py:301,swift/common/ring/io.py:627). Digest covers length prefix + data (swift/common/ring/io.py:365). - Writers: fixed gzip mtime 1300507380, JSON
sort_keys,ensure_ascii, atomic rename (swift/common/ring/ring.py:282,swift/common/ring/io.py:400). Device id0xFFFF(or0xFFFFFFFF) = none (swift/common/ring/utils.py:41). - Go readers must accept both versions and both byte orders in v1; Go writers should produce v2 only when every reader is ≥ 2.36.
3.3 On-disk object state
Rules of 03 § 4.2 (swift/obj/diskfile.py:971): file-name grammar (<ts>.data, <ts>#<f>[#d].data, <ts>.meta, <ts>[+-]<hexdelta>.meta, <ts>.ts, legacy <ts>.durable), selection of the newest tombstone/data/meta/ctype-meta, obsolete and reclaimable files, reclaim_age (7 days) and commit_window (60 s) (swift/obj/diskfile.py:82). Timestamps: 10 µs units + 64-bit offset; text %016.05f[_%016x] (swift/common/utils/timestamp.py:24). Vectors: vectors-ondisk (19 sets), vectors-timestamp.
Write protocol (crash consistency, swift/obj/diskfile.py:1958): temp file in tmp[-N] on the same filesystem → write → xattrs → fsync → rename into <hash>/ → fsync dir → append suffix to hashes.invalid under the partition lock (swift/obj/diskfile.py:477) → cleanup. A Go writer must never leave a .data without its xattrs visible under the final name, and must invalidate the suffix, otherwise the replicators of other nodes never notice the change.
3.4 Suffix hashes (hashes.pkl, REPLICATE)
- Replication:
md5over every hash directory of the suffix in sorted order, feeding<ts>.meta,<ts>.ts,<ts>.dataof the chosen files then<ctype ts>_ctype(swift/obj/diskfile.py:1190,swift/obj/diskfile.py:3210). - EC: per fragment index
md5of the timestamps of its fragment files; keyNonegets meta/ts,<ts>.durableof the newest durable set and ctype (swift/obj/diskfile.py:3898). - Cleanup runs during hashing (obsolete files deleted, empty dirs removed → suffix dropped,
swift/obj/diskfile.py:1282). hashes.pklconcurrency: read, compute, re-read under lock, write only if unchanged else restart (swift/obj/diskfile.py:1393).- Proof:
spec-referencereproduces the 7 suffix hashes (4 replication, 3 EC).
3.5 Quorum and best response
best_response (swift/proxy/controllers/base.py:2225): group statuses by 2xx/3xx/4xx, first class with ≥ quorum statuses wins, highest status of that class returned, else 503 (swift/proxy/controllers/base.py:2272). Quorums: n//2+1 (accounts, containers, replication, swift/common/utils/__init__.py:2550); EC PUT ec_ndata + min_parity_fragments_needed() (swift/common/storage_policy.py:637). Container updates spread by num_container_updates (swift/proxy/controllers/obj.py:90). Error limiting: 10 errors / 60 s (swift/proxy/server.py:229). Read path: try nodes in sorting_method order, newest X-Backend-Timestamp wins between 404s and data.
3.6 Pickled metadata: format and safe reading
Every pickle Swift writes, where, and the opcodes it uses are listed in 03 § 4.7–§ 4.8 and in vectors-pickle. Requirements for the Go codec:
- Never run a pickle VM. Implement a decoder for the closed opcode set:
PROTO, EMPTY_DICT, EMPTY_LIST, EMPTY_TUPLE, MARK, STOP, BINUNICODE, SHORT_BINUNICODE, SHORT_BINSTRING, BINSTRING, SHORT_BINBYTES, BINBYTES, BININT1, BININT2, BININT, LONG1, BINFLOAT, NONE, NEWTRUE, NEWFALSE, TUPLE, TUPLE1, TUPLE2, TUPLE3, SETITEM, SETITEMS, APPEND, APPENDS, BINPUT, LONG_BINPUT, BINGET, LONG_BINGET, GLOBAL, REDUCE, BUILD; refuse everything else (spec-reference_stable-2026.2.pyunpickleis a 120-line model). GLOBALonly for the allow-list ofswift/common/utils/pickle.py:49;REDUCEonly for_codecs.encode(str, "latin1")(→ bytes) anddict/bytes/copyreg._reconstructor(→ map). A forbidden global (vectoros.system) must be an error, not a skip.- Bounds: input ≤ 1 MiB (metadata is at most a few KB plus system keys; split xattrs reassembled first), depth ≤ 32, memo indices checked.
- Metadata semantics: reassemble
user.swift.metadata,…1,…2… untilENODATA; verifyuser.swift.metadata_checksum(MD5 hex) when present, else optionally add it (swift/obj/diskfile.py:218); decode keys/values as UTF-8 keeping invalid bytes (surrogateescape); Python-2 pickles (no_codecs\nencodein the first 32 bytes) need the latin-1 round trip except forname(swift/obj/diskfile.py:164). - Writing: emit what CPython 3
pickle.dumps(dict_of_bytes, protocol=2)emits (each bytes value as_codecs.encode(<latin-1 text>, 'latin1')with memoisedGLOBAL), split into 64 KiB xattrs, then the checksum xattr (swift/obj/diskfile.py:272). Thevectors-pickleblobs are byte-exact targets.hashes.pkl,.pendingtuples, async pendings and REPLICATE bodies use plain protocol-2 types (no globals). - Error mapping: decode failure on xattrs → quarantine the hash dir (as
DiskFileQuarantined); onhashes.pkl→ treat as invalid and rehash (swift/obj/diskfile.py:404); on a.pendingrecord → log and drop that record (swift/common/db.py:849). - Ring builder files are unrestricted pickles of rich objects: do not read them in Go servers (§ 3.1).
3.7 Listings: row merge and DB hash
chexor(old, name, created_at)XOR-accumulatesmd5("<name>-<created_at>")(swift/common/db.py:176); must be registered as SQL functionchexor/3on every connection because the triggers call it (swift/common/db.py:224). Vectors: 3 steps invectors-replication.- Row merge
update_new_item_from_existing(swift/container/backend.py:180): data, content-type and metadata timestamps merged independently, stored increated_atasencode_timestamps(swift/common/utils/timestamp.py:490);swift_bytesfollows data. Rows are DELETE+INSERT, never UPDATE (trigger). Listing order: byname(binary UTF-8),deleted = 0filter viaix_object_deleted_name. - Metadata column JSON
{key: [value, ts]}merge by timestamp (swift/common/db.py:1015).
3.8 Erasure coding
- Policy options:
ec_type,ec_num_data_fragments,ec_num_parity_fragments,ec_object_segment_size(default 1 MiB,swift/common/storage_policy.py:35),ec_duplication_factor(swift/common/storage_policy.py:463); ring replicas =(k+m)·duplication; node index → fragment indexindex % (k+m). - PUT: the client body is cut into segments of
segment_size; each segment is encoded by PyECLib into k+m fragments; fragment i of each segment is appended to fragment archive i (swift/proxy/controllers/obj.py:2131, encode atswift/proxy/controllers/obj.py:2165, last short segmentswift/proxy/controllers/obj.py:2189). The archive stored as<ts>#<i>[#d].datais the concatenation of those fragments. - Each fragment = 80-byte liberasurecode header + payload:
fragment_metadata(packed:idxu32,sizeu32,frag_backend_metadata_sizeu32,orig_data_sizeu64,chksum_typeu8,chksum8×u32,chksum_mismatchu8,backend_idu8,backend_versionu32 = 59 bytes) +magicu320xb0c5ecc+libec_versionu32 +metadata_chksumu32 (crc32 of the 59 bytes) + 9 padding bytes (liberasurecode:include/erasurecode/erasurecode.h:264,liberasurecode:include/erasurecode/erasurecode.h:326). Fields are host-endian (little-endian in practice); readers byte-swap when the magic is swapped (liberasurecode:src/erasurecode.c:1135); the metadata checksum has a legacy alternative crc (liberasurecode:src/erasurecode.c:1162). Backend ids:liberasurecode:include/erasurecode/erasurecode.h:43. fragment_size(full fragment incl. header) comes fromget_segment_info(segment_size, segment_size)(swift/common/storage_policy.py:586); ranged GETs map client ranges to segment-aligned fragment ranges (swift/proxy/controllers/obj.py:1688,swift/proxy/controllers/obj.py:1746) and decode with any k fragments of the same timestamp (swift/proxy/controllers/obj.py:1667).- Recommendation: do not reimplement Reed-Solomon in pure Go for compatibility. Python nodes, the reconstructor and the proxy must be able to decode fragments a Go proxy wrote and vice versa; the matrices and padding of each
ec_type(Jerasure, ISA-L,liberasurecode_rs_vand) are defined by those C libraries. Link liberasurecode via cgo (same version family as the cluster's PyECLib 1.8.0 / liberasurecode 1.8.x) and treat fragment archives as opaque on storage nodes (they never decode: only the proxy and reconstructor do). A pure-Go codec is acceptable only for a Go-only cluster and a new policy index [inferred]. - Not verified here: no EC encode/decode ran (no liberasurecode on the build host); EC placement, file naming, suffix hashing and the MIME/commit protocol were exercised.
4. Mixed-cluster compatibility contract (Go nodes beside Python nodes)
A migration step is "replace one pod's processes by Go processes on the same PVCs". It is safe only if all of the following hold. Each item is testable with the pack.
| # | Contract | Spec | Test data |
|---|---|---|---|
| C1 | Same swift.conf (hash prefix/suffix, policies with the same indexes and types, constraints) and same ring files; reload on mtime change | 03 § 1, § 3.1 | vectors-ring, ring files |
| C2 | Identical placement and handoff order in proxies and replicators | § 3.1 | 128 lookups |
| C3 | Storage-server HTTP verbs, paths, statuses and headers of 03 § 2 (incl. X-Backend-Timestamp on 404/409, X-Backend-Durable-Timestamp, X-Backend-Fragments, X-Backend-Etag-Is-At, X-Backend-Fragment-Preferences, override headers, X-Backend-Next-Part-Power) | 03 § 2, 02 § 4 | exchanges-*-server, backend-headers |
| C4 | Object PUT: Expect: 100-continue; MIME footers and multiphase commit with the exact document names and the two 100-continue rounds; container update fan-out and async pendings | 03 § 2.3–2.4 | object-server exchanges (headers), § 3.3 |
| C5 | On-disk layout, file names, xattr metadata pickles and checksum, hashes.pkl/hashes.invalid, partition .lock (flock), quarantine paths, async pending pickles; Python auditor/replicator/expirer must keep working on Go-written files and vice versa | 03 § 4, § 3.3–3.6 | vectors-ondisk, vectors-pickle |
| C6 | Identical suffix hashes, pickled REPLICATE responses (protocol 2, int and None keys for EC) | § 3.4 | 7 suffix vectors, REPLICATE vectors |
| C7 | SSYNC protocol both as sender and receiver, including X-Backend-Accept-No-Commit, durable:False, :ERROR: lines | 03 § 2.7 | vectors-replication |
| C8 | rsync daemon with modules account, container, object (--xattrs writes must land on the PVC) while Python replicators exist; or switch objects to sync_method = ssync everywhere first | 03 § 3 | — |
| C9 | SQLite listing DBs: same schema (accept legacy ones and migrate lazily as Python does), chexor, rollback journal, .pending records with the directory lock, sharded DB file names <hash>_<epoch>.db, shard ranges | 03 § 5, § 3.7 | schema-sqlite-*, vectors-pickle pending, vectors-replication chexor |
| C10 | DB REPLICATE JSON RPC (sync, merge_items, merge_syncs, complete_rsync, rsync_then_merge, shard-range ops) and the in-sync / usync / rsync decision | 03 § 2.8 | container/account exchanges |
| C11 | Hidden accounts and their naming: .expiring_objects (task container and object names), .misplaced_objects, .shards_<account> | 05 | — |
| C12 | memcache keys and JSON values if Go and Python proxies share memcached (or separate memcached) | 03 § 6 | — |
| C13 | EC fragments bit-compatible (liberasurecode) | § 3.8 | not covered (gap) |
Recommended order: (1) Go proxies (stateless; verify C1–C4, C12 with differential replay of exchanges-client against a mixed cluster); (2) Go object servers + Python daemons on the same pods (C3–C6); (3) Go object daemons (C6–C8, C11); (4) container/account servers and daemons (C9–C10); (5) remove rsync once no Python replicator remains; (6) EC policies last (C13).
5. What the clients expect
| Client | Relies on | Source |
|---|---|---|
| python-swiftclient (CLI, SDK of Glance and Cinder) | v1 auth headers X-Storage-Url, X-Auth-Token/X-Storage-Token; Keystone catalog type object-store; JSON listings with name/subdir and marker paging; retry on 5xx, 408, 499 and 498/429, one re-auth on 401; ETag compared with the downloaded MD5 unless X-Object-Manifest, X-Static-Large-Object or Content-Range is present; /info to decide SLO for segmented uploads; ETag returned by PUT with quotes stripped | python-swiftclient:swiftclient/client.py:493, python-swiftclient:swiftclient/client.py:573, python-swiftclient:swiftclient/client.py:924, python-swiftclient:swiftclient/client.py:1773, python-swiftclient:swiftclient/client.py:1434, python-swiftclient:swiftclient/service.py:451, python-swiftclient:swiftclient/service.py:1519 |
OSC (openstack container/object …) | own client: JSON listings with limit/marker/end_marker paging on name; X-Container-Read: .r:*,.rlistings for public; X-Storage-Policy; x-container-object-count, x-container-bytes-used, x-storage-policy, ACL and sync headers on HEAD; Etag, X-Trans-Id | python-openstackclient:openstackclient/api/object_store_v1.py:38, python-openstackclient:openstackclient/api/object_store_v1.py:88, python-openstackclient:openstackclient/api/object_store_v1.py:176, python-openstackclient:openstackclient/api/object_store_v1.py:440 |
| Glance swift store | DLO uploads: segments <image>-%05d, manifest X-Object-Manifest: <container>/<image>-; reads SLO and DLO on delete (?multipart-manifest=delete for SLO, list+delete for DLO); 409 = image exists, 413 = storage full, 404 = not found; container ACLs X-Container-Read/Write (multi-tenant) | glance_store:glance_store/_drivers/swift/store.py:1010, glance_store:glance_store/_drivers/swift/store.py:1090, glance_store:glance_store/_drivers/swift/store.py:1142, glance_store:glance_store/_drivers/swift/store.py:1130, glance_store:glance_store/_drivers/swift/store.py:1678 |
| Cinder backup (swift driver) | HEAD container → 404 → PUT with X-Storage-Policy; objects volume_<id>/<timestamp>/az_<az>_backup_<id>-<n> plus _metadata and _sha256file JSON objects; PUT ETag must equal the MD5 of the body sent (else InvalidBackup): the ETag of a plain object must stay the plaintext MD5, also with encryption | cinder:cinder/backup/drivers/swift.py:341, cinder:cinder/backup/drivers/swift.py:347, cinder:cinder/backup/drivers/swift.py:378, cinder:cinder/backup/drivers/swift.py:453 |
Skyline (console through skyline-apiserver's nginx proxy, service object-store → swift) | GET ?format=json&delimiter=/&prefix= listings; folders as zero-byte objects name/; **COPY verb with Destination** for copy/rename/cut; X-Container-Read: .r:*,.rlistings for public containers; HEAD headers x-container-bytes-used, x-container-object-count, x-storage-policy, x-timestamp; same-origin, so no CORS | skyline-console:src/stores/swift/object.js:83, skyline-console:src/client/swift/index.js:91, skyline-console:src/stores/swift/container.js:114, skyline-apiserver:skyline_apiserver/config/openstack.py:141 |
| S3 clients (boto3, aws-cli, s3cmd) | SigV2/SigV4 (header and presigned), x-amz-content-sha256 incl. streaming chunked, multipart upload on SLO, S3 XML errors and codes, ListObjectsV2, path and virtual-host style; Keystone /v3/s3tokens for EC2 credentials | swift/common/middleware/s3api/s3api.py:212, swift/common/middleware/s3api/s3token.py:154; 912 recorded S3 exchanges |
Statuses that must not change: 404 (not 403) for a missing object to an authorised user; 202 for container PUT of an existing container; 204 for POST/DELETE; 201 for object PUT; 409 for non-empty container DELETE (Glance, Cinder and S3 BucketNotEmpty rely on it); 412 for If-None-Match: *.
6. Database strategy (SQLite listings)
- Reuse, do not migrate. Python and Go nodes must read each other's DB files through replication (
rsyncof whole DB files). Use the exact DDL ofschema-sqlite-*, the samePRAGMAs, rollback journal mode (swift/common/db.py:220), and registerchexor. Accept old schemas (nostorage_policy_index, noreported/tombstones) and migrate them lazily with the sameALTER TABLEs (swift/container/backend.py:1561–swift/container/backend.py:1662). - Integrity is enforced by triggers (counts, hash, no UPDATE of rows), not foreign keys.
- Concurrency: one writer per DB through the
.pendingfile andlock_parent_directory(flock on the DB directory),BEGIN IMMEDIATEin merges (swift/container/backend.py:1331); readers commit pendings first (swift/common/db.py:822). - Quarantine malformed DBs (
swift/common/db.py:502). - No startup data; hidden accounts are created on demand (
auto_create_account_prefix.).
7. Authentication, configuration and CLI
- Auth: re-implement
keystoneauthsemantics (operator roles, reseller admin, service tokens, ACL v1/v2, system reader,swift_owner) on top of the OpenKeel Keystone token validation (instead of keystonemiddleware), andtempauthonly for tests. Keep the account namingAUTH_<project_id>(swift/common/middleware/keystoneauth.py:277). - Middleware-provided authorisation (tempurl HMACs, formpost, container-sync realms, s3 signatures) must be bit-exact: they are verified against keys stored in metadata by users.
- Encryption: if used, keep the key derivation (HMAC-SHA256 of path under the root secret) and crypto-meta header formats; otherwise existing encrypted objects become unreadable (
swift/common/middleware/crypto/keymaster.py:307,swift/common/middleware/crypto/crypto_utils.py:205). - Configuration:
config_stable-2026.2.conffeedsoslo_conf_to_yaml.py; keepswift.confitself as a file shared with Python nodes during migration (it is a cluster contract, not a per-service option file). - CLI tools operators use against the cluster and that must keep working on Go-written data:
swift-ring-builder,swift-get-nodes,swift-object-info,swift-account-info/-container-info,swift-recon,swift-manage-shard-ranges,swift-dispersion-*(setup.cfg:55).swift-reconneeds/recon/*on storage servers with the recon cache JSON shapes (swift/common/middleware/recon.py:424).
8. Conformance strategy
- Unit vectors (no Python needed): port
spec-reference_stable-2026.2.pyand run it on the vectors (153 checks pass on the reference). Add EC fragment vectors once liberasurecode is available. - Client-level differential tests: replay
exchanges-clienttest by test (each test builds its own data through the API, starting from an empty accountAUTH_test), compare status, key headers (etag,content-length,x-*-count,content-type) and JSON bodies; normalise timestamps, trans ids and S3 signatures. 1080 tests ran (225 skipped by configuration); 5 failures/errors in the recorded run are recorder artefacts: the recorder buffers bodies, which breaks SLO heartbeating, upload timeouts and the file-size-limit test. All 15 distinct tests failing under any of the four recordings pass when re-run without the recorder. - Backend wire tests: replay
exchanges-object-server/-container-server/-account-serverrequests against a Go storage server on an empty device in order (chunked PUT bodies were not captured: replay with a synthetic body of the recordedContent-Length/ETag), compare statuses andX-Backend-*response headers. - Run Swift's functional tests against the Go proxy:
test/functionalaccepts an external cluster throughSWIFT_TEST_CONFIG_FILE(test/__init__.py:46); in-process mode is how this pack was recorded (test/functional/__init__.py:476). - Probe tests (
test/probe/) need a multi-node SAIO with daemons; run them in the mixed-cluster stage (C5–C11). Not run here. - Adapter used:
~/.claude/skills/generate-reverse-spec/scripts/adapters/swift.pywithconformance_capture.py http --wrap test.functional:loadapp(proxy) and--wrap swift.obj.server:ObjectControlleretc. (class wrap patches__call__in place).
9. Gaps found while building the pack
- EC encode/decode, fragment archive bytes and the reconstructor's rebuild were not exercised (no liberasurecode on the host). Fragment format is documented from liberasurecode 1.8.0 headers only.
- Chunked request bodies (object data to object servers, EC MIME documents, SSYNC streams) are not in the backend exchanges; their framing is specified from code (03 § 2.3, § 2.7).
- Daemons (replicators, auditors, updaters, sharder, reconciler, expirer, reaper) produce no recorded exchanges: probe tests need a real multi-node setup.
keystoneauthands3tokenwere not exercised (the functional cluster usestempauth).- The api-ref does not cover the internal API,
UPDATE, or private methods. - Pickle writer byte-exactness is checked against CPython 3.13 output only [inferred stable across 3.x for these simple types].
10. Non-functional requirements
- Streaming everywhere: objects up to 5 GiB per PUT (SLO beyond), constant memory; 64 KiB chunks (
object_chunk_size,client_chunk_size). - Timeouts:
conn_timeout0.5 s,node_timeout10 s,client_timeout60 s,container_update_timeout1 s. fsyncdiscipline of § 3.3;fallocate_reserveto keep space for metadata.- Each device is an independent failure domain (
mount_check→ 507); in Kubernetes a PVC not mounted must answer 507, not write to the container's root filesystem. - Background daemons rate-limited (files/s, bytes/s, objects/s) as in 05.
11. Rewrite checklist
- [ ] Ring reader v1/v2, placement and handoffs pass
vectors-ring; reload on mtime. - [ ] Ring writer produces files
swift-ring-builderand Swift ≥ 2.36 load (v1 while older nodes exist). - [ ] Timestamp type passes
vectors-timestamp. - [ ] Restricted pickle codec passes
vectors-pickle(decode all, refuse the forbidden global, re-encode byte-exact). - [ ] Diskfile selection and suffix hashes pass
vectors-ondisk; write protocol with fsync and invalidation. - [ ] Object server verbs and headers (C3, C4) pass the object-server replay; REPLICATE pickle; SSYNC sender/receiver.
- [ ] rsyncd sidecar with the three modules until all replicators are Go/ssync.
- [ ] SQLite brokers with the exact DDL,
chexor,.pending, merge rules, sharding states; DB REPLICATE RPC. - [ ] Proxy: pipeline order, quorum/best_response, error limiting, account autocreate, listings formats, sharded listings.
- [ ] Middlewares: gatekeeper, tempurl, formpost, slo, dlo, symlink, versioning (both), copy, bulk, quotas, staticweb, container_sync, ratelimit, encryption (if used), s3api (+ Keystone s3tokens).
- [ ]
/infosections and limits as ininfo_stable-2026.2.json. - [ ] Client expectations of § 5, statuses that must not change.
- [ ] Daemons of 05 with the same queues (
.expiring_objects,.misplaced_objects,.shards_*) and recon cache files. - [ ] EC via liberasurecode (cgo), bit-compatible with PyECLib 1.8.0; add EC vectors before enabling EC policies.
- [ ] Mixed-cluster stages of § 4 run with Swift's functional and probe tests.
Source: reverse-spec-swift/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.