changeset 520:7d08fd78775c

fzfs: implement "fzfs clone-tree". Clone a ZFS dataset tree and preserve locally set properties.
author Franz Glasner <fzglas.hg@dom66.de>
date Sun, 01 Sep 2024 21:34:27 +0200
parents e26183b3bac9
children c05ef1c86c9c
files docs/conf.py docs/man/index.rst docs/man/man8/fzfs-clone-tree.rst docs/man/man8/fzfs.rst docs/man/man8/local-bsdtools.rst pkg-plist sbin/fzfs
diffstat 7 files changed, 279 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/docs/conf.py	Sun Sep 01 19:46:15 2024 +0200
+++ b/docs/conf.py	Sun Sep 01 21:34:27 2024 +0200
@@ -104,6 +104,7 @@
     ("man/man8/ftjail-umount-tmpl", "ftjail-umount-tmpl", "Unmount mounted Thin Jail template datasets", [author], 8),
     ("man/man8/fwireguard", "fwireguard", "Manage Wireguard interfaces", [author], 8),
     ("man/man8/fzfs", "fzfs", "A ZFS management helper tool", [author], 8),
+    ("man/man8/fzfs-clone-tree", "fzfs-clone-tree", "Clone a ZFS dataset tree", [author], 8),    
     ("man/man8/fzfs-copy-tree", "fzfs-copy-tree", "Copy a ZFS dataset tree based on an existing tree", [author], 8),
     ("man/man8/fzfs-create-tree", "fzfs-create-tree", "Create a ZFS dataset tree structure based on an existing tree", [author], 8),
     ("man/man8/fzfs-mount", "fzfs-mount", "Recursively mount a ZFS dataset and its children", [author], 8),
--- a/docs/man/index.rst	Sun Sep 01 19:46:15 2024 +0200
+++ b/docs/man/index.rst	Sun Sep 01 21:34:27 2024 +0200
@@ -29,6 +29,7 @@
    man8/ftjail-snapshot-tmpl
    man8/ftjail-umount-tmpl
    man8/fzfs
+   man8/fzfs-clone-tree   
    man8/fzfs-copy-tree
    man8/fzfs-create-tree
    man8/fzfs-mount
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/man/man8/fzfs-clone-tree.rst	Sun Sep 01 21:34:27 2024 +0200
@@ -0,0 +1,53 @@
+.. -*- coding: utf-8; indent-tabs-mode: nil; -*-
+
+fzfs-clone-tree
+===============
+
+.. program:: fzfs clone-tree
+
+
+Synopsis
+--------
+
+**fzfs clone-tree** [**-n**] `source-dataset` `dest-dataset`
+
+
+Description
+-----------
+
+Clone the ZFS snapshot that is rooted at `source-dataset` and all
+descendent snapshots into the destination dataset rooted at
+`dest-dataset`.
+
+`source-dataset` must be a snapshot. All of its children must have a
+snapshot with the very same snapshot.
+
+`dest-dataset` must not exist already.
+
+All properties that are of type ``local`` or ``received``are copied to the
+destination. This is also true for ``canmount`` and ``mountpoint``.
+
+The cloned datasets will are *not* mounted automatically.
+
+If something fails it is tried to delete the intermediately created
+datasets.
+
+
+Options
+-------
+
+.. option:: -n
+
+   Dry-run. Do not really clone datasets but show what would be done.
+
+
+Environment
+-----------
+
+All environment variables that affect :command:`zfs` are effective also.
+
+
+See Also
+--------
+
+:manpage:`fzfs(8)`
--- a/docs/man/man8/fzfs.rst	Sun Sep 01 19:46:15 2024 +0200
+++ b/docs/man/man8/fzfs.rst	Sun Sep 01 21:34:27 2024 +0200
@@ -35,6 +35,10 @@
 Subcommands
 -----------
 
+:manpage:`fzfs-clone-tree(8)`
+
+    Recursively clone a ZFS dataset tree         
+
 :manpage:`fzfs-copy-tree(8)`
 
     Recursively copy a ZFS dataset tree based while copying some
--- a/docs/man/man8/local-bsdtools.rst	Sun Sep 01 19:46:15 2024 +0200
+++ b/docs/man/man8/local-bsdtools.rst	Sun Sep 01 21:34:27 2024 +0200
@@ -72,6 +72,7 @@
 
 - :manpage:`fzfs(8)`
 
+  * :manpage:`fzfs-clone-tree(8)`
   * :manpage:`fzfs-copy-tree(8)`
   * :manpage:`fzfs-create-tree(8)`
   * :manpage:`fzfs-mount(8)`
--- a/pkg-plist	Sun Sep 01 19:46:15 2024 +0200
+++ b/pkg-plist	Sun Sep 01 21:34:27 2024 +0200
@@ -37,6 +37,7 @@
 %%DOCS%%share/man/man8/ftjail-umount-tmpl.8.gz
 %%DOCS%%share/man/man8/fwireguard.8.gz
 %%DOCS%%share/man/man8/fzfs.8.gz
+%%DOCS%%share/man/man8/fzfs-clone-tree.8.gz
 %%DOCS%%share/man/man8/fzfs-copy-tree.8.gz
 %%DOCS%%share/man/man8/fzfs-create-tree.8.gz
 %%DOCS%%share/man/man8/fzfs-mount.8.gz
--- a/sbin/fzfs	Sun Sep 01 19:46:15 2024 +0200
+++ b/sbin/fzfs	Sun Sep 01 21:34:27 2024 +0200
@@ -28,6 +28,8 @@
 
 COMMANDS:
 
+  clone-tree [-n] SOUECE-DATASET DEST-DATASET
+
   copy-tree [-A] [-M MOUNTPOINT] [-n] [-u] SOURCE-DATASET DEST-DATASET
 
   create-tree [-A] [-M MOUNTPOINT] [-n] [-p] [-u] SOURCE-DATASET DEST-DATASET
@@ -430,6 +432,190 @@
 
 
 #:
+#: Implementation of "clone-tree"
+#:
+command_clone_tree() {
+    local _ds_source _ds_dest
+    local _opt_dry_run
+
+    local _ds snapshot_name _ds_source_base _ds_relname
+    local _ds_canmount _ds_mountpoint
+    local _clone_props _arg_canmount _arg_other_clone_props
+    local _opt _idx _idx_lp _prop _propval
+
+    _opt_dry_run=""
+
+    while getopts "n" _opt ; do
+        case ${_opt} in
+            n)
+                _opt_dry_run="yes"
+                ;;
+            \?|:)
+                return 2;
+                ;;
+        esac
+    done
+    shift $((OPTIND-1))
+    OPTIND=1
+
+    _ds_source="${1-}"
+    _ds_dest="${2-}"
+
+    [ -z "${_ds_source}" ] && { err "no source dataset/snapshot given"; return 2; }
+    [ -z "${_ds_dest}" ] && { err "no destination name given"; return 2; }
+
+    if ! zfs get -H name "${_ds_source}" >/dev/null 2>&1; then
+        err "source dataset does not exist: ${_ds_source}"
+        return 1
+    fi
+    if zfs get -H name "${_ds_dest}" >/dev/null 2>&1; then
+        err "destination dataset already exists: ${_ds_dest}"
+        return 1
+    fi
+
+    _ds_source_base="${_ds_source%@*}"
+    _snapshot_name="${_ds_source##*@}"
+
+    if [ "${_ds_source_base}" = "${_snapshot_name}" ]; then
+        err "no snapshot given"
+        return 1
+    fi
+
+    array_create _ds_tree
+
+    while IFS=$'\n' read -r _ds; do
+        array_append _ds_tree "${_ds}"
+    done <<EOF_9ef07253679011efa78174d435fd3892
+$(zfs list -H -r -t filesystem -o name -s name "${_ds_source_base}")
+EOF_9ef07253679011efa78174d435fd3892
+
+    # Check the existence of all intermediate datasets and their shapshots
+    _idx=1
+    while array_tryget _ds _ds_tree ${_idx}; do
+        if  ! zfs get -H name "${_ds}@${_snapshot_name}" >/dev/null 2>&1; then
+            err "child dataset does not exist: ${_ds}@${_snapshot_name}" 1>&2
+            array_destroy _ds_tree
+            return 1
+        fi
+        _idx=$((${_idx} + 1))
+    done
+
+    array_create _cloned_datasets
+    alist_create _local_props
+
+    #
+    # 1. Clone with "safe" canmount settings
+    #
+
+    _idx=1
+    while array_tryget _ds _ds_tree ${_idx}; do
+        # Determine the relative name of the dataset
+        _ds_relname="${_ds#${_ds_source_base}}"
+
+        # Need to determine in *every* case (local, default, received, ...)
+        _ds_canmount="$(zfs get -H -o value canmount "${_ds}")"
+
+        alist_clear _local_props
+        while IFS=$'\t' read -r _prop _propval ; do
+            alist_set _local_props "${_prop}" "${_propval}"
+        done <<EOF_ce8c76187f33471f8e8c1607ed09c42e
+$(zfs get -H -o property,value -s local,received all "${_ds}")
+EOF_ce8c76187f33471f8e8c1607ed09c42e
+
+        #
+        # - "zfs clone" does NOT copy/clone properties.
+        #
+        #   Clones inherit their properties from the target filesystem
+        #   context or are set back to their ZFS default.
+        #
+        if [ "${_ds_canmount}" = "off" ]; then
+            _arg_canmount="-o canmount=off"
+        else
+            _arg_canmount="-o canmount=noauto"
+        fi
+
+        # Copy all local props with the exception of canmount and mountpoint
+        _arg_other_clone_props=""
+        _idx_lp=1
+        while alist_tryget_key_at_index _prop _local_props ${_idx_lp}; do
+            if [ "${_prop}" = "mountpoint" ]; then
+                _idx_lp=$((${_idx_lp} + 1))
+                continue
+            fi
+            if [ "${_prop}" = "canmount" ]; then
+                _idx_lp=$((${_idx_lp} + 1))
+                continue
+            fi
+            alist_tryget_value_at_index _propvalue _local_props ${_idx_lp}
+            _arg_other_clone_props="${_arg_other_clone_props} -o ${_prop}=${_propvalue}"
+            _idx_lp=$((${_idx_lp} + 1))
+        done
+
+        if ! checkyes _opt_dry_run; then
+            echo "Cloning ${_ds}@${_snapshot_name} into ${_ds_dest}${_ds_relname} with ${_arg_canmount} ${_arg_other_clone_props}"
+            if zfs clone ${_arg_canmount} ${_arg_other_clone_props} "${_ds}@${_snapshot_name}" "${_ds_dest}${_ds_relname}"; then
+                array_append _cloned_datasets "${_ds_dest}${_ds_relname}"
+            else
+                _destroy_datasets _cloned_datasets  || true
+                return 1
+            fi
+        else
+            echo "Would execute: zfs clone ${_arg_canmount} ${_arg_other_clone_props} '${_ds}@${_snapshot_name}' '${_ds_dest}${_ds_relname}'"
+        fi
+        _idx=$((${_idx} + 1))
+    done
+
+    #
+    # 2. Copy property mountpoint for root and inherit for all children;
+    #    also handle canmount.
+    #
+    _idx=1
+    while array_tryget _ds _ds_tree ${_idx}; do
+        # Determine the relative name of the dataset
+        _ds_relname="${_ds#${_ds_source_base}}"
+
+        # Need to determine in *every* case (default, local, received, ...)
+        _ds_canmount="$(zfs get -H -o value canmount "${_ds}")"
+        # Local mountpoint
+        _ds_mountpoint="$(zfs get -H -o value -s local,received mountpoint "${_ds}")"
+
+        if [ \( "${_ds_canmount}" = "off" \) -o \( "${_ds_canmount}" = "noauto" \) ]; then
+            #
+            # Already handled above because "nomount" is the default if not
+            # already set to "off".
+            #
+            _arg_canmount=""
+        else
+            _arg_canmount="-o canmount=${_ds_canmount}"
+        fi
+
+        if [ \( -n "${_arg_canmount}" \) -o \( -n "${_ds_mountpoint}" \) ]; then
+            if ! checkyes _opt_dry_run; then
+                # If a local or received mountpoint is given set it here
+                if [ -n "${_ds_mountpoint}" ]; then
+                    echo "Correcting properties for ${_ds_dest}${_ds_relname}: ${_arg_canmount} mountpoint=\"${_ds_mountpoint}\""
+                    zfs set -u ${_arg_canmount} mountpoint="${_ds_mountpoint}" "${_ds_dest}${_ds_relname}" || true
+                else
+                    echo "Correcting properties for ${_ds_dest}${_ds_relname}: ${_arg_canmount}"
+                    zfs set -u ${_arg_canmount} "${_ds_dest}${_ds_relname}" || true
+                fi
+            else
+                # If a local or received mountpoint is given set it here
+                if [ -n "${_ds_mountpoint}" ]; then
+                    echo "Would execute: zfs set -u ${_arg_canmount} mountpoint='${_ds_mountpoint}' '${_ds_dest}${_ds_relname}'"
+                else
+                    echo "Would execute: zfs set -u ${_arg_canmount} '${_ds_dest}${_ds_relname}'"
+                fi
+            fi
+        fi
+        _idx=$((${_idx} + 1))
+    done
+
+    return 0
+}
+
+
+#:
 #: Implement the "create-tree" command
 #:
 #: Create a ZFS dataset tree from a source tree tree including important properties
@@ -550,6 +736,35 @@
 }
 
 
+#:
+#: Helper to destroy some created ZFS filesystems
+#:
+#: Args:
+#:   $1 (str): The name of the array that contains the datasets to destroy to
+#:
+#: Returns:
+#:   0 if successfully destroyed all given datasets,
+#:   1 otherwise
+#:
+#: Destruction is done in the reverse order.
+#:
+_destroy_datasets() {
+    array_reversed_for_each "$1" _destroy_datasets_destroy
+}
+
+
+#:
+#: Array callback to destroy a single ZFS filesystem
+#:
+_destroy_datasets_destroy() {
+    if ! zfs destroy -v "$3" ; then
+        warn "Dataset \`${3}' cannot unmounted"
+        return 1
+    fi
+    return 0
+}
+
+
 #
 # Global option handling
 #
@@ -590,6 +805,9 @@
     umount|unmount)
         command_umount "$@"
         ;;
+    clone-tree)
+        command_clone_tree "$@"
+        ;;
     copy-tree)
         command_copy_tree "$@"
         ;;