From 32b501e57e03d00bf787fc419dbfc74cc5c6591f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:14:52 -0400 Subject: [PATCH 1/8] Add zfs::parse_uri and stdio send/recv helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- src/storage_zfs.sh | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 96385ea..25186fc 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -262,6 +262,55 @@ zfs::container_check() { fi } +# Parses a zfs:// URI into "host\tname". Format: zfs://host/NAME (NAME may +# contain extra path components which are reassembled into the container name). +zfs::parse_uri() { + local -r uri="$1" + if [[ ! "${uri}" =~ ^zfs://([^/]+)/(.+)$ ]]; then + common::err "Invalid zfs:// URI: ${uri}" + fi + printf "%s\t%s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" +} + +# Sends a clone's @pristine snapshot (or a fresh snapshot if the container is +# not a clone) to stdout. Used by --zfs-send. +zfs::send_clone_stdout() { + local -r name="$1" + local -r store=$(zfs::store_dataset) + local -r target="${store}/${name}" + local origin + + if ! zfs list -H "${target}" > /dev/null 2>&1; then + common::err "No such container: ${name}" + fi + + origin=$(zfs get -H -o value origin "${target}") + if [ -z "${origin}" ] || [ "${origin}" = "-" ]; then + local snap="${target}@enroot-export-$$" + zfs snapshot "${snap}" + trap "zfs destroy '${snap}' 2> /dev/null || :" RETURN + zfs send "${snap}" + else + zfs send "${origin}" + fi +} + +# Receives a zfs send stream from stdin into the template cache, then clones +# the resulting template into a named user container. Used by --zfs-recv. +# The cache key is the sha256 of the (buffered) stream bytes — same scheme as +# zfs::create_from_stream uses for .zfs files. +zfs::recv_to_template_stdin() { + local -r name="$1" + local buf sha template + + buf=$(mktemp -p "${ENROOT_TEMP_PATH:-/tmp}" enroot-recv.XXXXXX) + trap "rm -f '${buf}' 2> /dev/null || :" RETURN + cat > "${buf}" + sha=$(zfs::image_sha256 "${buf}") + template=$(zfs::ensure_template_from_stream "${buf}" "${sha}") + zfs::clone_container "${template}" "${name}" +} + # Materializes a ZFS stream file into a template (cached by file sha) and # clones it as the user's named container. Counterpart of zfs::ensure_template # + zfs::clone_container for the .sqsh path; this is called from runtime::create From 31bfdd41d1afbc6696fcb8686e54e79bf2d864c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:15:24 -0400 Subject: [PATCH 2/8] Add zfs::pull_via_ssh and zfs::push_via_ssh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- src/storage_zfs.sh | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 25186fc..7abed91 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -311,6 +311,45 @@ zfs::recv_to_template_stdin() { zfs::clone_container "${template}" "${name}" } +# Pulls a container from a remote enroot host over SSH. URI is zfs://host/NAME; +# the SSH peer must be running enroot with the ZFS backend. Local NAME +# defaults to the URI's basename if not given. +zfs::pull_via_ssh() { + local -r uri="$1" + local name="$2" + local host remote_name + + common::checkcmd ssh + zfs::parse_uri "${uri}" \ + | { common::read -r host; common::read -r remote_name; } + + [ -z "${name}" ] && name="${remote_name##*/}" + + common::log INFO "Pulling ${remote_name} from ${host}" NL + ssh "${host}" enroot export --zfs-send "${remote_name}" \ + | zfs::recv_to_template_stdin "${name}" +} + +# Pushes a local container to a remote enroot host over SSH. URI may be +# zfs://host (push under the same NAME) or zfs://host/REMOTE_NAME (rename on +# the remote side). +zfs::push_via_ssh() { + local -r name="$1" uri="$2" + local host remote_name + + common::checkcmd ssh + if [[ "${uri}" =~ ^zfs://([^/]+)/?(.*)$ ]]; then + host="${BASH_REMATCH[1]}" + remote_name="${BASH_REMATCH[2]:-${name}}" + else + common::err "Invalid zfs:// URI: ${uri}" + fi + + common::log INFO "Pushing ${name} to ${host} as ${remote_name}" NL + zfs::send_clone_stdout "${name}" \ + | ssh "${host}" enroot import --zfs-recv -n "${remote_name}" +} + # Materializes a ZFS stream file into a template (cached by file sha) and # clones it as the user's named container. Counterpart of zfs::ensure_template # + zfs::clone_container for the .sqsh path; this is called from runtime::create From e65b94e9777356c9dbe6921c3dd5f67041a8fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:16:18 -0400 Subject: [PATCH 3/8] Add internal --zfs-send / --zfs-recv flags to export / import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- enroot.in | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/enroot.in b/enroot.in index 99c1661..b712ffd 100644 --- a/enroot.in +++ b/enroot.in @@ -355,7 +355,7 @@ enroot::digest() { } enroot::import() { - local uri= filename= arch= + local uri= filename= arch= name= zfs_recv= while [ $# -gt 0 ]; do case "$1" in @@ -379,6 +379,20 @@ enroot::import() { filename="${1#*=}" shift ;; + -n|--name) + [ -z "${2-}" ] && enroot::usage import 1 + name="$2" + shift 2 + ;; + --name=*) + [ -z "${1#*=}" ] && enroot::usage import 1 + name="${1#*=}" + shift + ;; + --zfs-recv) + zfs_recv=y + shift + ;; -h|--help) enroot::usage import 0 ;; --) @@ -389,6 +403,19 @@ enroot::import() { break ;; esac done + + # Internal --zfs-recv: read a stream from stdin and clone to NAME, no URI. + if [ -n "${zfs_recv}" ]; then + if ! zfs::enabled; then + common::err "--zfs-recv requires ENROOT_STORAGE_BACKEND=zfs" + fi + if [ -z "${name}" ]; then + common::err "--zfs-recv requires -n NAME" + fi + zfs::recv_to_template_stdin "${name}" + return + fi + if [ $# -ne 1 ]; then enroot::usage import 1 fi @@ -445,7 +472,7 @@ enroot::load() { } enroot::export() { - local name= filename= format=sqsh + local name= filename= format=sqsh zfs_send= while [ $# -gt 0 ]; do case "$1" in @@ -473,6 +500,10 @@ enroot::export() { format="${1#*=}" shift ;; + --zfs-send) + zfs_send=y + shift + ;; -h|--help) enroot::usage export 0 ;; --) @@ -488,6 +519,15 @@ enroot::export() { fi name="$1" + # Internal --zfs-send: write a stream to stdout, no filename or format. + if [ -n "${zfs_send}" ]; then + if ! zfs::enabled; then + common::err "--zfs-send requires ENROOT_STORAGE_BACKEND=zfs" + fi + zfs::send_clone_stdout "${name}" + return + fi + runtime::export "${name}" "${filename}" "${format}" } From c4b4347e0b3f260e4929009a966d5853ed4c8a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:17:03 -0400 Subject: [PATCH 4/8] Dispatch zfs:// in runtime::load and runtime::export; reject in runtime::import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- src/runtime.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/runtime.sh b/src/runtime.sh index d41bc9b..0f0716d 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -537,6 +537,8 @@ runtime::import() { docker::import "${uri}" "${filename}" "${arch}" ;; dockerd://* | podman://*) docker::daemon::import "${uri}" "${filename}" "${arch}" ;; + zfs://*) + common::err "zfs:// has no portable file artifact; use 'enroot load zfs://...'" ;; *) common::err "Invalid argument: ${uri}" ;; esac @@ -559,6 +561,11 @@ runtime::load() { case "${uri}" in docker://*) docker::load "${uri}" "${rootfs}" "${arch}" ;; + zfs://*) + if ! zfs::enabled; then + common::err "zfs:// URIs require ENROOT_STORAGE_BACKEND=zfs" + fi + zfs::pull_via_ssh "${uri}" "${rootfs}" ;; *) common::err "Invalid argument: ${uri}" ;; esac @@ -574,6 +581,15 @@ runtime::export() { common::err "Invalid argument: ${rootfs_name}" fi + # Destination is a zfs:// URI: push to a remote enroot host over SSH. + if [[ "${filename}" == zfs://* ]]; then + if ! zfs::enabled; then + common::err "zfs:// destinations require ENROOT_STORAGE_BACKEND=zfs" + fi + zfs::push_via_ssh "${rootfs_name}" "${filename}" + return + fi + case "${format}" in sqsh) ;; zfs) From 5a6ad8318323fd305686ee66450ab340da9ddae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:17:33 -0400 Subject: [PATCH 5/8] Document zfs:// in load and export usage strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick Sodré --- enroot.in | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enroot.in b/enroot.in index b712ffd..7ac8edb 100644 --- a/enroot.in +++ b/enroot.in @@ -170,9 +170,9 @@ enroot::usage() { Create a container image from a container root filesystem. Options: - -o, --output Name of the output image file (defaults to "NAME.sqsh" or "NAME.zfs") + -o, --output Output destination: a filename (defaults to "NAME.sqsh" or "NAME.zfs") or a "zfs://[USER@]HOST[/REMOTE_NAME]" URI to push to a remote enroot host over SSH -f, --force Overwrite an existing container image - --format Output format: "sqsh" (default) or "zfs" (zfs send stream; requires ZFS backend) + --format Output format for file destinations: "sqsh" (default) or "zfs" (zfs send stream; requires ZFS backend) EOF ;; import) @@ -212,6 +212,7 @@ enroot::usage() { Schemes: docker://[USER@][REGISTRY#]IMAGE[:TAG] Load a Docker image from a registry + zfs://[USER@]HOST/NAME Pull a container from a remote enroot host (full zfs send stream over SSH; both hosts must run the ZFS backend) Options: -a, --arch Architecture of the image (defaults to host architecture) From b5476a736495d66bd6659e20eb991ea90689c764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:28:37 -0400 Subject: [PATCH 6/8] Fix zfs::parse_uri output to one-component-per-line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- src/storage_zfs.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 7abed91..52fc6fd 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -262,14 +262,16 @@ zfs::container_check() { fi } -# Parses a zfs:// URI into "host\tname". Format: zfs://host/NAME (NAME may -# contain extra path components which are reassembled into the container name). +# Parses a zfs:// URI and prints two lines: the host and the container name +# (NAME may contain extra path components which are reassembled into the +# container name). Matches the docker::_parse_uri output convention so callers +# can use the same `common::read -r` pattern. zfs::parse_uri() { local -r uri="$1" if [[ ! "${uri}" =~ ^zfs://([^/]+)/(.+)$ ]]; then common::err "Invalid zfs:// URI: ${uri}" fi - printf "%s\t%s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + printf "%s\n%s\n" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" } # Sends a clone's @pristine snapshot (or a fresh snapshot if the container is From 18840083224938aa2d09f92538f92e4553f32d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:29:11 -0400 Subject: [PATCH 7/8] Mark Plan D (zfs:// URI transport) as implemented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick Sodré --- CLAUDE.md | 2 +- doc/zfs.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9fae39b..e234f52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ When debugging container behavior, the order is: image `/etc/{rc,fstab,environme ## Active design proposals - **`doc/zfs.md`** — optional ZFS storage backend (`ENROOT_STORAGE_BACKEND=zfs`). Replaces `unsquashfs`-per-create with extract-once-then-`zfs clone`. Adds a `.zfs` (zfs send stream) image format and a `zfs://host/NAME` transport scheme alongside today's `.sqsh`. Introduces a shared template cache with a live/warm/cold lifecycle (knobs: `ENROOT_TEMPLATE_WARM_SECONDS`, `ENROOT_TEMPLATE_PRESSURE_THRESHOLD`; eviction is implicit on `create`, no daemon, no `enroot gc` command). Default backend (`dir`) is unchanged. -- **`doc/plans/`** — six implementation plans (A–F) breaking the ZFS backend into independently-landable slices. Start with `doc/plans/README.md` for the index and recommended landing order (A → E → F → B → C → D). Plans add a new sourced module `src/storage_zfs.sh` (under a `zfs::` namespace) and branch in `src/runtime.sh`, `src/docker.sh` on `ENROOT_STORAGE_BACKEND`. **Plans A, E, F, B merged; C in review** on `zenroot/main` (PRs [zeroae/enroot#1](https://github.com/zeroae/enroot/pull/1), [#2](https://github.com/zeroae/enroot/pull/2), [#3](https://github.com/zeroae/enroot/pull/3), [#5](https://github.com/zeroae/enroot/pull/5), [#7](https://github.com/zeroae/enroot/pull/7)); D is still design-only. +- **`doc/plans/`** — six implementation plans (A–F) breaking the ZFS backend into independently-landable slices. Start with `doc/plans/README.md` for the index and recommended landing order (A → E → F → B → C → D). Plans add a new sourced module `src/storage_zfs.sh` (under a `zfs::` namespace) and branch in `src/runtime.sh`, `src/docker.sh` on `ENROOT_STORAGE_BACKEND`. **All six plans merged** on `zenroot/main` (PRs [#1](https://github.com/zeroae/enroot/pull/1), [#2](https://github.com/zeroae/enroot/pull/2), [#3](https://github.com/zeroae/enroot/pull/3), [#5](https://github.com/zeroae/enroot/pull/5), [#7](https://github.com/zeroae/enroot/pull/7), and Plan D in review). ## Conventions diff --git a/doc/zfs.md b/doc/zfs.md index 0e99576..6100d19 100644 --- a/doc/zfs.md +++ b/doc/zfs.md @@ -1,6 +1,6 @@ # ZFS storage backend -This document describes an optional ZFS-aware mode for the enroot container store. **Plans A (foundation), B (template warm/cold lifecycle), C (`.zfs` image format), E (ephemeral start), and F (Docker load) are implemented**: `enroot create`, `enroot remove`, ephemeral `enroot start `, and `enroot load docker://...` all use ZFS datasets when `ENROOT_STORAGE_BACKEND=zfs`, with a shared template cache that survives `enroot remove` (warm) for `ENROOT_TEMPLATE_WARM_SECONDS` and gets pressure-evicted LRU once the templates dataset crosses `ENROOT_TEMPLATE_PRESSURE_THRESHOLD` of its quota. `enroot create` accepts both `.sqsh` and `.zfs` (zfs send stream) inputs; `enroot export --format=zfs` produces the latter. The remaining transport (`zfs://` URI) is tracked under `doc/plans/`. The default storage backend (plain directories under `ENROOT_DATA_PATH`) is unchanged and remains the only option on hosts without ZFS. +This document describes an optional ZFS-aware mode for the enroot container store. **All six plans (A–F) are implemented.** When `ENROOT_STORAGE_BACKEND=zfs`: `enroot create`, `enroot remove`, ephemeral `enroot start `, and `enroot load docker://...` all use ZFS datasets, with a shared template cache that survives `enroot remove` (warm) for `ENROOT_TEMPLATE_WARM_SECONDS` and gets pressure-evicted LRU once the templates dataset crosses `ENROOT_TEMPLATE_PRESSURE_THRESHOLD` of its quota. `enroot create` accepts both `.sqsh` and `.zfs` (zfs send stream) inputs; `enroot export --format=zfs` produces the latter. The `zfs://[USER@]HOST/NAME` URI scheme transports containers between enroot hosts over SSH (`enroot load zfs://...` to pull, `enroot export NAME zfs://...` to push). The default storage backend (plain directories under `ENROOT_DATA_PATH`) is unchanged and remains the only option on hosts without ZFS. ## Motivation From 46c7a082a72d35c954ad74c8ebe769f5e9ee9604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:46:06 -0400 Subject: [PATCH 8/8] Allow enroot import zfs:// to produce .zfs or .sqsh files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- enroot.in | 3 ++- src/runtime.sh | 2 +- src/storage_zfs.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/enroot.in b/enroot.in index 7ac8edb..183fa80 100644 --- a/enroot.in +++ b/enroot.in @@ -185,10 +185,11 @@ enroot::usage() { docker://[USER@][REGISTRY#]IMAGE[:TAG] Import a Docker image from a registry dockerd://IMAGE[:TAG] Import a Docker image from the Docker daemon podman://IMAGE[:TAG] Import a Docker image from a local podman repository + zfs://[USER@]HOST/NAME Import a container from a remote enroot host (defaults to ".zfs"; ".sqsh" produced if -o ends in .sqsh, requires local ZFS backend) Options: -a, --arch Architecture of the image (defaults to host architecture) - -o, --output Name of the output image file (defaults to "URI.sqsh") + -o, --output Name of the output image file (defaults to "URI.sqsh" or "URI.zfs" for zfs://) EOF ;; digest) diff --git a/src/runtime.sh b/src/runtime.sh index 0f0716d..b42588b 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -538,7 +538,7 @@ runtime::import() { dockerd://* | podman://*) docker::daemon::import "${uri}" "${filename}" "${arch}" ;; zfs://*) - common::err "zfs:// has no portable file artifact; use 'enroot load zfs://...'" ;; + zfs::import_uri "${uri}" "${filename}" ;; *) common::err "Invalid argument: ${uri}" ;; esac diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 52fc6fd..7c18afd 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -313,6 +313,62 @@ zfs::recv_to_template_stdin() { zfs::clone_container "${template}" "${name}" } +# Imports a container from a remote enroot host over SSH and writes a file. +# Output format is inferred from the filename extension: ".sqsh" produces a +# squashfs (requires local ZFS to receive into a temp dataset, mksquashfs out, +# then destroy the temp); anything else produces a raw zfs send stream +# (".zfs" by convention; no local ZFS receive required). +zfs::import_uri() { + local -r uri="$1" + local filename="$2" + local host remote_name + + common::checkcmd ssh + zfs::parse_uri "${uri}" \ + | { common::read -r host; common::read -r remote_name; } + + if [ -z "${filename}" ]; then + filename="${remote_name##*/}.zfs" + fi + filename=$(common::realpath "${filename}") + if [ -e "${filename}" ]; then + if [ -z "${ENROOT_FORCE_OVERRIDE-}" ]; then + common::err "File already exists: ${filename}" + else + rm -f "${filename}" + fi + fi + + case "${filename}" in + *.sqsh) + zfs::checkenv + common::checkcmd mksquashfs + local -r store=$(zfs::store_dataset) + local -r tmp_ds="${store}/${zfs_template_subdir}/import-$$.tmp" + local mountpoint + zfs create -p "${store}/${zfs_template_subdir}" 2> /dev/null || : + common::log INFO "Pulling ${remote_name} from ${host} (sqsh)" NL + if ! ssh "${host}" enroot export --zfs-send "${remote_name}" \ + | zfs receive -F "${tmp_ds}"; then + zfs destroy -r "${tmp_ds}" 2> /dev/null || : + common::err "Receive from ${uri} failed" + fi + mountpoint=$(zfs get -H -o value mountpoint "${tmp_ds}") + common::log INFO "Creating squashfs filesystem..." NL + mksquashfs "${mountpoint}" "${filename}" -all-root ${TTY_OFF+-no-progress} \ + -processors "${ENROOT_MAX_PROCESSORS}" ${ENROOT_SQUASH_OPTIONS} >&2 \ + || { zfs destroy -r "${tmp_ds}" 2> /dev/null || :; \ + common::err "mksquashfs failed"; } + zfs destroy -r "${tmp_ds}" + ;; + *) + common::log INFO "Pulling ${remote_name} from ${host} (zfs send stream)" NL + ssh "${host}" enroot export --zfs-send "${remote_name}" > "${filename}" \ + || { rm -f "${filename}"; common::err "ssh transport failed for ${uri}"; } + ;; + esac +} + # Pulls a container from a remote enroot host over SSH. URI is zfs://host/NAME; # the SSH peer must be running enroot with the ZFS backend. Local NAME # defaults to the URI's basename if not given.