Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
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 "$@" ;;
