From baf35ef33432b0cafe0dedb2fe5a27016a186884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 10:43:39 -0400 Subject: [PATCH 1/6] Add zfs::ensure_template_from_stream and zfs::send_stream helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensure_template_from_stream is the recv-side sibling of ensure_template: same atomic .tmp lock pattern, sweep + touch integration with Plan B's warm/cold lifecycle, but uses 'zfs receive' instead of 'unsquashfs' to materialize the template. The received dataset's snapshot may not be named @pristine (depends on what the sender sent), so we look it up and rename if needed. send_stream is the export-side helper: zfs send the clone's origin @pristine snapshot to a file. For non-clones (containers created by some other means), takes a fresh snapshot, sends, then destroys it. zfs receive does not auto-create parents, so the templates parent is created via 'zfs create -p' before the recv attempt. Signed-off-by: Patrick Sodré --- src/storage_zfs.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 79e777b..75140ac 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -262,6 +262,87 @@ zfs::container_check() { fi } +# Materializes a template from a zfs send stream file. The cache key is the +# sha256 of the stream file (same scheme as the .sqsh path). Atomic via a +# .tmp dataset; integrates with the same eviction sweep as ensure_template. +zfs::ensure_template_from_stream() { + local -r stream="$1" sha="$2" + local -r store=$(zfs::store_dataset) + local -r template="${store}/${zfs_template_subdir}/${sha}" + local -r tmp="${template}.tmp" + local -r snap="${template}@${zfs_pristine_snap}" + local i timeout=600 + + zfs::sweep_templates + + # Fast path: already cached. + if zfs list -H -t snapshot "${snap}" > /dev/null 2>&1; then + zfs::touch_template "${template}" + printf "%s" "${template}" + return + fi + + # Ensure the templates parent exists before receive (zfs receive does not + # auto-create parents). + zfs create -p "${store}/${zfs_template_subdir}" 2> /dev/null || : + + if zfs receive -F "${tmp}" < "${stream}" 2> /dev/null; then + # The received dataset brings its own snapshot. Rename the dataset to + # the final template name; if the recv'd snapshot wasn't already named + # @pristine, alias it. + zfs rename "${tmp}" "${template}" + if ! zfs list -H -t snapshot "${snap}" > /dev/null 2>&1; then + local recvd_snap + recvd_snap=$(zfs list -H -t snapshot -o name -r -d 1 "${template}" | head -1) + [ -n "${recvd_snap}" ] && zfs rename "${recvd_snap}" "${snap}" + fi + zfs set readonly=on "${template}" + zfs::touch_template "${template}" + printf "%s" "${template}" + return + fi + + # Receive failed. Clean our orphan .tmp (if any) and wait for another + # writer's @pristine. + zfs destroy -r "${tmp}" 2> /dev/null || : + for ((i = 0; i < timeout; i++)); do + if zfs list -H -t snapshot "${snap}" > /dev/null 2>&1; then + printf "%s" "${template}" + return + fi + sleep 1 + done + common::err "Timed out waiting for stream receive: ${template}" +} + +# Sends a clone's @pristine snapshot (or a fresh snapshot if the container is +# not a clone) to stdout. Used by --format=zfs export. +zfs::send_stream() { + local -r name="$1" filename="$2" + 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 + # Not a clone — must take a fresh snapshot of the live dataset. + local snap="${target}@enroot-export-$$" + zfs snapshot "${snap}" + if ! zfs send "${snap}" > "${filename}"; then + zfs destroy "${snap}" 2> /dev/null || : + common::err "Failed to send stream for ${name}" + fi + zfs destroy "${snap}" 2> /dev/null || : + else + zfs send "${origin}" > "${filename}" \ + || common::err "Failed to send stream for ${name}" + fi +} + # Materializes the merged Docker rootfs into a ZFS template (cached by # cache_key) and clones it as the user's named container. Designed to be # called from docker::load AFTER docker::_prepare_layers has populated the From 9f8d8fe980ee4d36513aacc6c27f492643d793b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 10:44:24 -0400 Subject: [PATCH 2/6] Dispatch runtime::create on .sqsh vs .zfs extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .zfs is a zfs send stream; only valid with the ZFS backend. .sqsh keeps existing two-axis dispatch (extension first, then backend). Signed-off-by: Patrick Sodré --- src/runtime.sh | 54 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/runtime.sh b/src/runtime.sh index 7dfded9..801da18 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -437,23 +437,47 @@ runtime::create() { if [ ! -f "${image}" ]; then common::err "No such file or directory: ${image}" fi - if ! unsquashfs -s "${image}" > /dev/null 2>&1; then - common::err "Invalid image format: ${image}" - fi - # Resolve the container rootfs name. - if [ -z "${rootfs}" ]; then - rootfs=$(basename "${image%.sqsh}") - fi - if [[ "${rootfs}" == */* ]]; then - common::err "Invalid argument: ${rootfs}" - fi + case "${image}" in + *.zfs) + if ! zfs::enabled; then + common::err ".zfs images require ENROOT_STORAGE_BACKEND=zfs" + fi + if [ -z "${rootfs}" ]; then + rootfs=$(basename "${image%.zfs}") + fi + if [[ "${rootfs}" == */* ]]; then + common::err "Invalid argument: ${rootfs}" + fi + runtime::_create_zfs_from_stream "${image}" "${rootfs}" + ;; + *) + if ! unsquashfs -s "${image}" > /dev/null 2>&1; then + common::err "Invalid image format: ${image} (expected .sqsh or .zfs)" + fi + if [ -z "${rootfs}" ]; then + rootfs=$(basename "${image%.sqsh}") + fi + if [[ "${rootfs}" == */* ]]; then + common::err "Invalid argument: ${rootfs}" + fi + if zfs::enabled; then + runtime::_create_zfs "${image}" "${rootfs}" + else + runtime::_create_dir "${image}" "${rootfs}" + fi + ;; + esac +} - if zfs::enabled; then - runtime::_create_zfs "${image}" "${rootfs}" - else - runtime::_create_dir "${image}" "${rootfs}" - fi +runtime::_create_zfs_from_stream() { + local -r image="$1" rootfs_name="$2" + local sha template + + zfs::checkenv + sha=$(zfs::image_sha256 "${image}") + template=$(zfs::ensure_template_from_stream "${image}" "${sha}") + zfs::clone_container "${template}" "${rootfs_name}" } runtime::_create_dir() { From aeff6d0d43c05a1784bcdcf27372136d51370e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 10:46:02 -0400 Subject: [PATCH 3/6] Add --format=zfs to enroot export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI gains a --format flag (default sqsh; zfs requires the ZFS backend). runtime::export refactors into format-specific helpers: _export_sqsh keeps the existing mksquashfs path byte-for-byte; _export_zfs writes a zfs send stream of the clone's @pristine snapshot via zfs::send_stream. Behavior change: the function signature now takes an optional third format argument from enroot::export. Old callers that didn't pass a third arg still work — sqsh is the default. Signed-off-by: Patrick Sodré --- enroot.in | 17 +++++++++++--- src/runtime.sh | 61 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/enroot.in b/enroot.in index 3fe7672..99c1661 100644 --- a/enroot.in +++ b/enroot.in @@ -170,8 +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") + -o, --output Name of the output image file (defaults to "NAME.sqsh" or "NAME.zfs") -f, --force Overwrite an existing container image + --format Output format: "sqsh" (default) or "zfs" (zfs send stream; requires ZFS backend) EOF ;; import) @@ -444,7 +445,7 @@ enroot::load() { } enroot::export() { - local name= filename= + local name= filename= format=sqsh while [ $# -gt 0 ]; do case "$1" in @@ -462,6 +463,16 @@ enroot::export() { filename="${1#*=}" shift ;; + --format) + [ -z "${2-}" ] && enroot::usage export 1 + format="$2" + shift 2 + ;; + --format=*) + [ -z "${1#*=}" ] && enroot::usage export 1 + format="${1#*=}" + shift + ;; -h|--help) enroot::usage export 0 ;; --) @@ -477,7 +488,7 @@ enroot::export() { fi name="$1" - runtime::export "${name}" "${filename}" + runtime::export "${name}" "${filename}" "${format}" } enroot::create() { diff --git a/src/runtime.sh b/src/runtime.sh index 801da18..58152fd 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -573,24 +573,44 @@ runtime::load() { } runtime::export() { - local rootfs="$1" filename="$2" - local exclude=() + local rootfs_name="$1" filename="$2" format="${3:-sqsh}" - common::checkcmd mksquashfs - - # Resolve the container rootfs path. - if [ -z "${rootfs}" ]; then + if [ -z "${rootfs_name}" ]; then common::err "Invalid argument" fi - if [[ "${rootfs}" == */* ]]; then - common::err "Invalid argument: ${rootfs}" + if [[ "${rootfs_name}" == */* ]]; then + common::err "Invalid argument: ${rootfs_name}" + fi + + case "${format}" in + sqsh) ;; + zfs) + if ! zfs::enabled; then + common::err "--format=zfs requires ENROOT_STORAGE_BACKEND=zfs" + fi + ;; + *) common::err "Invalid format: ${format}" ;; + esac + + if [ "${format}" = "sqsh" ]; then + runtime::_export_sqsh "${rootfs_name}" "${filename}" + else + runtime::_export_zfs "${rootfs_name}" "${filename}" fi - rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs}") +} + +runtime::_export_sqsh() { + local -r rootfs_name="$1" + local filename="$2" + local rootfs exclude=() + + common::checkcmd mksquashfs + + rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs_name}") if [ ! -d "${rootfs}" ]; then common::err "No such file or directory: ${rootfs}" fi - # Generate an absolute filename if none was specified. if [ -z "${filename}" ]; then filename="$(basename "${rootfs}").sqsh" fi @@ -612,12 +632,31 @@ runtime::export() { exclude+=("${rootfs}${lock_file}") fi - # Export a container image from the rootfs specified. common::log INFO "Creating squashfs filesystem..." NL mksquashfs "${rootfs}" "${filename}" -all-root ${TTY_OFF+-no-progress} -processors "${ENROOT_MAX_PROCESSORS}" \ ${ENROOT_SQUASH_OPTIONS} ${exclude[@]+-e "${exclude[@]}"} >&2 } +runtime::_export_zfs() { + local -r rootfs_name="$1" + local filename="$2" + + if [ -z "${filename}" ]; then + filename="${rootfs_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 + + common::log INFO "Creating zfs send stream..." NL + zfs::send_stream "${rootfs_name}" "${filename}" +} + runtime::list() { local -r fancy="$1" local cwd= name= pid= entry=() From 502531f0c38d6aa9bf611a177bbb9c1de01c8578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 10:47:01 -0400 Subject: [PATCH 4/6] Mark Plan C (.zfs image format) 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 b9dbc7f..9fae39b 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 merged; B 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)); C, D are 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`. **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. ## Conventions diff --git a/doc/zfs.md b/doc/zfs.md index d8a297a..0e99576 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), 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. The remaining transports (`.zfs` file format, `zfs://` URI) are 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. **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. ## Motivation From ced1c56ecd912b4d057664c881dec6d80c4a3b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:00:10 -0400 Subject: [PATCH 5/6] Restore upstream comments dropped by earlier ZFS refactors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of the function refactors landed across Plans A and C found seven upstream comments that were dropped during the function splits. Restoring them brings the helpers back to byte-for-byte parity with upstream's documentation. In runtime::_create_dir (Plan A): restore # Resolve the container rootfs path. # Extract the container rootfs from the image. In runtime::remove and the new _remove_dir / _remove_zfs (Plan A): restore # Resolve the container rootfs path. # Remove the rootfs specified after asking for confirmation. In runtime::_export_sqsh (Plan C): restore # Resolve the container rootfs path. # Generate an absolute filename if none was specified. # Export a container image from the rootfs specified. Plan E's runtime::start and Plan F's docker::load were verified to have preserved their upstream comments in full. Signed-off-by: Patrick Sodré --- src/runtime.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/runtime.sh b/src/runtime.sh index 58152fd..03d8dd1 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -486,6 +486,7 @@ runtime::_create_dir() { common::checkcmd unsquashfs find + # Resolve the container rootfs path. rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs_name}") if [ -e "${rootfs}" ]; then if [ -z "${ENROOT_FORCE_OVERRIDE-}" ]; then @@ -495,6 +496,7 @@ runtime::_create_dir() { fi fi + # Extract the container rootfs from the image. common::log INFO "Extracting squashfs filesystem..." NL # XXX: https://github.com/NVIDIA/enroot/issues/90 [ $(ulimit -n) -gt $((2**26)) ] && ulimit -n $((2**26)) @@ -606,11 +608,13 @@ runtime::_export_sqsh() { common::checkcmd mksquashfs + # Resolve the container rootfs path. rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs_name}") if [ ! -d "${rootfs}" ]; then common::err "No such file or directory: ${rootfs}" fi + # Generate an absolute filename if none was specified. if [ -z "${filename}" ]; then filename="$(basename "${rootfs}").sqsh" fi @@ -632,6 +636,7 @@ runtime::_export_sqsh() { exclude+=("${rootfs}${lock_file}") fi + # Export a container image from the rootfs specified. common::log INFO "Creating squashfs filesystem..." NL mksquashfs "${rootfs}" "${filename}" -all-root ${TTY_OFF+-no-progress} -processors "${ENROOT_MAX_PROCESSORS}" \ ${ENROOT_SQUASH_OPTIONS} ${exclude[@]+-e "${exclude[@]}"} >&2 @@ -720,6 +725,7 @@ runtime::list() { runtime::remove() { local rootfs_name="$1" + # Resolve the container rootfs path. if [ -z "${rootfs_name}" ]; then common::err "Invalid argument" fi @@ -741,6 +747,8 @@ runtime::_remove_dir() { if [ ! -d "${rootfs}" ]; then common::err "No such file or directory: ${rootfs}" fi + + # Remove the rootfs specified after asking for confirmation. if [ -z "${ENROOT_FORCE_OVERRIDE-}" ]; then read -r -e -p "Do you really want to delete ${rootfs}? [y/N] " fi @@ -753,6 +761,8 @@ runtime::_remove_zfs() { local -r rootfs_name="$1" local rootfs rootfs="${ENROOT_DATA_PATH}/${rootfs_name}" + + # Remove the rootfs specified after asking for confirmation. if [ -z "${ENROOT_FORCE_OVERRIDE-}" ]; then read -r -e -p "Do you really want to delete ${rootfs}? [y/N] " fi From 4775c8d92a5fafe1a2de90bb8c347f4605902d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 29 Apr 2026 11:07:20 -0400 Subject: [PATCH 6/6] Move Plan C ZFS-side helpers into storage_zfs.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the same principle that drove the Plan F refactor, keep ZFS code together so the delta to existing files (here, src/runtime.sh) stays minimal. The previous commit had two private runtime helpers (runtime::_create_zfs_from_stream and runtime::_export_zfs) that were thin wrappers over zfs:: calls; consolidating them into the ZFS module and dispatching directly from runtime::create / runtime::export is cleaner. New helpers in src/storage_zfs.sh: - zfs::create_from_stream IMAGE NAME Counterpart of zfs::ensure_template + zfs::clone_container for the .sqsh path; called from runtime::create when the input image has a .zfs extension. - zfs::export_to_file NAME FILENAME Owns filename defaulting and the file-already-exists guard so runtime::export's ZFS branch is a single dispatch call. src/runtime.sh now calls the new helpers directly; the deleted private wrappers were boilerplate. Signed-off-by: Patrick Sodré --- src/runtime.sh | 34 ++-------------------------------- src/storage_zfs.sh | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/runtime.sh b/src/runtime.sh index 03d8dd1..d41bc9b 100644 --- a/src/runtime.sh +++ b/src/runtime.sh @@ -449,7 +449,7 @@ runtime::create() { if [[ "${rootfs}" == */* ]]; then common::err "Invalid argument: ${rootfs}" fi - runtime::_create_zfs_from_stream "${image}" "${rootfs}" + zfs::create_from_stream "${image}" "${rootfs}" ;; *) if ! unsquashfs -s "${image}" > /dev/null 2>&1; then @@ -470,16 +470,6 @@ runtime::create() { esac } -runtime::_create_zfs_from_stream() { - local -r image="$1" rootfs_name="$2" - local sha template - - zfs::checkenv - sha=$(zfs::image_sha256 "${image}") - template=$(zfs::ensure_template_from_stream "${image}" "${sha}") - zfs::clone_container "${template}" "${rootfs_name}" -} - runtime::_create_dir() { local -r image="$1" rootfs_name="$2" local rootfs @@ -597,7 +587,7 @@ runtime::export() { if [ "${format}" = "sqsh" ]; then runtime::_export_sqsh "${rootfs_name}" "${filename}" else - runtime::_export_zfs "${rootfs_name}" "${filename}" + zfs::export_to_file "${rootfs_name}" "${filename}" fi } @@ -642,26 +632,6 @@ runtime::_export_sqsh() { ${ENROOT_SQUASH_OPTIONS} ${exclude[@]+-e "${exclude[@]}"} >&2 } -runtime::_export_zfs() { - local -r rootfs_name="$1" - local filename="$2" - - if [ -z "${filename}" ]; then - filename="${rootfs_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 - - common::log INFO "Creating zfs send stream..." NL - zfs::send_stream "${rootfs_name}" "${filename}" -} - runtime::list() { local -r fancy="$1" local cwd= name= pid= entry=() diff --git a/src/storage_zfs.sh b/src/storage_zfs.sh index 75140ac..96385ea 100644 --- a/src/storage_zfs.sh +++ b/src/storage_zfs.sh @@ -262,6 +262,43 @@ zfs::container_check() { fi } +# 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 +# when the input image has a .zfs extension. +zfs::create_from_stream() { + local -r image="$1" name="$2" + local sha template + + zfs::checkenv + sha=$(zfs::image_sha256 "${image}") + template=$(zfs::ensure_template_from_stream "${image}" "${sha}") + zfs::clone_container "${template}" "${name}" +} + +# Exports a clone's @pristine snapshot as a zfs send stream file. Owns +# filename defaulting and the file-already-exists guard so runtime::export's +# ZFS branch is a single dispatch call. +zfs::export_to_file() { + local -r name="$1" + local filename="$2" + + if [ -z "${filename}" ]; then + filename="${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 + + common::log INFO "Creating zfs send stream..." NL + zfs::send_stream "${name}" "${filename}" +} + # Materializes a template from a zfs send stream file. The cache key is the # sha256 of the stream file (same scheme as the .sqsh path). Atomic via a # .tmp dataset; integrates with the same eviction sweep as ensure_template.