Uh oh!
There was an error while loading. Please reload this page.
Plan D: zfs:// URI transport over SSH - #8
Merged
Conversation
Three new helpers under the zfs:: namespace, all in storage_zfs.sh to keep ZFS code together: - zfs::parse_uri URI Parses zfs://host/NAME → 'host\tNAME'. Errors on malformed input. Supports user@host and multi-segment NAMEs. - zfs::send_clone_stdout NAME zfs send the clone's @pristine origin (or a fresh snapshot if the container isn't a clone) to stdout. RETURN-trap cleans up the temporary snapshot in the non-clone case. - zfs::recv_to_template_stdin NAME Buffers stdin to a temp file, hashes it as the cache key, then materializes the template via zfs::ensure_template_from_stream and clones to NAME. Same Plan B sweep + touch lifecycle as the .zfs file path. Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
High-level wrappers over the SSH transport. The wire is just 'ssh host enroot export --zfs-send NAME' / '... import --zfs-recv -n NAME'; the local side calls zfs::recv_to_template_stdin / zfs::send_clone_stdout. Pool name and dataset paths never leak across the wire — the URI names a container, and remote enroot resolves the local layout itself. push_via_ssh accepts zfs://host (push under the same NAME) or zfs://host/REMOTE (rename on the remote side). Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Used by zfs::pull_via_ssh and zfs::push_via_ssh on the SSH peer side. Not surfaced in user-facing usage (kept brief by being internal). Both gate on ENROOT_STORAGE_BACKEND=zfs. Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
…me::import runtime::load zfs://host/NAME → zfs::pull_via_ssh runtime::export destination zfs://... → zfs::push_via_ssh runtime::import zfs://... → hard error (use 'enroot load') Each backend dispatch is one to two lines; the SSH transport itself lives in storage_zfs.sh. Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
zfs::pull_via_ssh and zfs::push_via_ssh consume parse_uri output via 'common::read -r host; common::read -r remote_name', expecting two newline-separated lines (matching docker::_parse_uri's output convention). The earlier tab-separated single-line output caused read to wait 30s on the second component and then time out. Verified end-to-end with passwordless SSH to root@localhost: pull, push, and import-rejection all work. Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
Reverses the earlier 'no portable file artifact' rejection. The user's argument: a .zfs file is portable to ZFS hosts, and producing a .sqsh from a remote ZFS source is a real workflow (relay scenario, transferring to non-ZFS targets). Format is inferred from the output filename extension: enroot import -o foo.zfs zfs://host/NAME # default (.zfs) enroot import -o foo.sqsh zfs://host/NAME # transcode to .sqsh The .zfs path streams ssh stdout to the file directly — no local ZFS required (the produced file is only useful on a ZFS host, but we don't enforce that on the producer side). The .sqsh path requires local ZFS: receive the stream into a one-off temp dataset under .templates/, mksquashfs from its mountpoint to the output file, then destroy the temp dataset. Implementation lives in src/storage_zfs.sh as zfs::import_uri; runtime::import dispatches via a one-line case branch. Signed-off-by: Patrick Sodré <patrick@zero-ae.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
zfs://[USER@]HOST/NAMEURI scheme for transporting containers between enroot hosts aszfs sendstreams over SSH. Both ends must run enroot withENROOT_STORAGE_BACKEND=zfs.enroot load zfs://host/NAME— pull and instantiate as a container in one step.enroot export NAME zfs://host(orzfs://host/REMOTE) — push a local container to a remote enroot host.enroot import -o file.zfs zfs://host/NAME— pull and save as a portable file (.zfsby default;.sqshif the output filename ends in.sqsh).Implements Plan D. Builds on Plans A (#1), C (#7).
What changed
All ZFS-specific lifecycle stays in
src/storage_zfs.sh; touch points inenroot.inandsrc/runtime.share minimal dispatches.src/storage_zfs.sh— six new helpers (allzfs::*):zfs::parse_uri URI—zfs://host/NAME→ two lines (hostthenNAME), matchingdocker::_parse_uri's convention.zfs::send_clone_stdout NAME—zfs sendthe clone's@pristine(or fresh snapshot for non-clones) to stdout.zfs::recv_to_template_stdin NAME— buffers stdin to a temp file, hashes for cache key, materializes viazfs::ensure_template_from_stream, clones to NAME. Same Plan B sweep + touch lifecycle.zfs::pull_via_ssh URI [NAME]—ssh host enroot export --zfs-send REMOTE | zfs::recv_to_template_stdin LOCAL.zfs::push_via_ssh NAME URI—zfs::send_clone_stdout NAME | ssh host enroot import --zfs-recv -n REMOTE.zfs::import_uri URI FILENAME— pulls and writes a file. Format inferred from output extension:.sqshtriggers a recv-into-temp-dataset → mksquashfs → destroy-temp pipeline (requires local ZFS); anything else streams the rawzfs sendbytes to file (no local ZFS receive required).enroot.in— added--zfs-sendtoenroot::exportand--zfs-recv(with-n NAME) toenroot::import. Both gate onENROOT_STORAGE_BACKEND=zfs. Internal flags used by the SSH peer side. Updatedimport/load/exportusage blocks to documentzfs://.src/runtime.sh— three thin dispatches:runtime::loadrouteszfs://*tozfs::pull_via_ssh;runtime::exportrouteszfs://*destinations tozfs::push_via_ssh;runtime::importrouteszfs://*tozfs::import_uri.doc/zfs.md+CLAUDE.md— status notes flipped to "All six plans implemented".Why
enroot import zfs://is allowed (originally was rejected)Initial design treated
zfs://as transport-only —importwould produce no portable artifact, so it was rejected with "use 'enroot load'". That framing missed a real case: a.zfsfile IS portable, just to ZFS hosts. And a.sqshproduced from a remote ZFS source serves a useful relay workflow (pull on a host with SSH access, transfer the file to a host without). The current behavior accepts both forms; format is inferred from the output filename extension so no new flag is needed.Wire format and privacy
The wire is just
ssh host enroot ...invocations. The URI names a container on a remote enroot host, not a raw dataset path. The remote's pool name and dataset hierarchy never leak — the remote enroot resolves its own store layout. (One small caveat:zfs sendincludes the source snapshot's full dataset path in the stream header. For trusted-host replication this is fine; for cross-org transfers it leaks the sender's pool naming. Same as the.zfsfile format from Plan C.)Test Plan
Verified manually with passwordless SSH to
root@localhoston a loopback ZFS pool (Linux 6.12.75, aarch64, zfs-2.4.1):enroot load zfs://root@localhost/donorpulls a clone, rootfs readable,enroot start pulled /bin/cat /etc/os-releaseprints alpine os-release.enroot export pulled -o zfs://root@localhost/pushedships the container; the remoteenroot listshowspushed.enroot import -o file.zfs zfs://...writes a 10MB ZFS send stream; subsequentenroot create file.zfsinstantiates correctly.enroot import -o file.sqsh zfs://...writes a valid 8MB squashfs (recv-temp-dataset path); the resulting.sqshworks on the dir backend with no local ZFS required.enroot load zfs://...errors with "zfs:// URIs require ENROOT_STORAGE_BACKEND=zfs".enroot export -o zfs://...errors with "zfs:// destinations require ENROOT_STORAGE_BACKEND=zfs".zfs::recv_to_template_stdinintegrates with Plan B: sweep + touch fire on the receive side too.Known limitations
zfs send -i) — full streams only. A future iteration could add--since=BASEand have the receiver enumerate locally-cached templates over the wire to find a common ancestor; out of scope here..zfsfiles (Plan C).--zfs-send/--zfs-recv. The remote will print its usage and produce no stream; the local side will fail receive. Acceptable per the design — silent fallback would be confusing.ssh_configHost blocks (port, identity, jump hosts) are honored because we shell out tossh.enroot import -o file.sqsh zfs://...requires local ZFS to do the temp-dataset receive + mksquashfs transform. This is a mild surprise but unavoidable without remote-side cooperation.