changeset 380:6be930eb7490

Implement the "fzfs create-tree" command
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 20 Feb 2023 00:43:30 +0100
parents b4173e88c57c
children a39acafe9e29
files docs/conf.py docs/man/index8.rst docs/man/man8/ftjail-copy-skel.rst docs/man/man8/fzfs-create-tree.rst docs/man/man8/fzfs.rst docs/man/man8/local-bsdtools.rst sbin/ftjail sbin/fzfs
diffstat 8 files changed, 263 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/docs/conf.py	Sun Feb 19 14:47:17 2023 +0100
+++ b/docs/conf.py	Mon Feb 20 00:43:30 2023 +0100
@@ -98,6 +98,7 @@
     ("man/man8/ftjail-snapshot-tmpl", "ftjail-snapshot-tmpl", "Recursively create ZFS snapshots of the RO base datasets and the RW skeleton datasets", [author], 8),
     ("man/man8/ftjail-umount-tmpl", "ftjail-umount-tmpl", "Unmount mounted Thin Jail template datasets", [author], 8),
     ("man/man8/fzfs", "fzfs", "A ZFS management helper tool", [author], 8),
+    ("man/man8/fzfs-create-tree", "fzfs-create-tree", "Create a ZFS dataset tree based on an existing tree", [author], 8),
     ("man/man8/fzfs-mount", "fzfs-mount", "Recursively mount a ZFS dataset and its children", [author], 8),
     ("man/man8/fzfs-umount", "fzfs-umount", "Recursively unmount a ZFS datasets and its children", [author], 8),
 ]
--- a/docs/man/index8.rst	Sun Feb 19 14:47:17 2023 +0100
+++ b/docs/man/index8.rst	Mon Feb 20 00:43:30 2023 +0100
@@ -27,5 +27,6 @@
    man8/ftjail-snapshot-tmpl
    man8/ftjail-umount-tmpl
    man8/fzfs
+   man8/fzfs-create-tree
    man8/fzfs-mount
    man8/fzfs-umount
--- a/docs/man/man8/ftjail-copy-skel.rst	Sun Feb 19 14:47:17 2023 +0100
+++ b/docs/man/man8/ftjail-copy-skel.rst	Mon Feb 20 00:43:30 2023 +0100
@@ -29,6 +29,11 @@
    Set the ZFS property `canmount=noauto` for all datasets in the
    target dataset.
 
+.. option:: -D
+
+   Just copy the dataset tree including properties but no filesystem
+   data. This option is incompatible to option :option:`-L`.
+
 .. option:: -L
 
    Copy dataset properties optimized for employing a :file:`skeleton`
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/man/man8/fzfs-create-tree.rst	Mon Feb 20 00:43:30 2023 +0100
@@ -0,0 +1,72 @@
+.. -*- coding: utf-8; indent-tabs-mode: nil; -*-
+
+fzfs-create-tree
+================
+
+.. program:: fzfs create-tree
+
+
+Synopsis
+--------
+
+**fzfs create-tree** [**-A**] [**-M** `mountpoint`] [**-n**] [**-p**] [**-u**] `source-dataset` `dest-dataset`
+
+Description
+-----------
+
+Copy the ZFS filesystem structure that is rooted at `source-dataset`
+to the destination rooted at `dest-dataset`.
+
+The content of the filesystems is **not** copied. This is also true for
+permissions and/or ACLs.
+
+`dest-dataset` must not exist already.
+
+By default some important dataset properties that are set locally in
+the source tree are copied to the destination. This includes `atime`,
+`exec`, `setuid`, `compression`, `primarycache`, `sync` and
+`readonly`.
+
+By default `canmount` is also copied. But this can be modified with
+options :option:`-A` and :option:`-p`.
+
+
+Options
+-------
+
+.. option:: -A
+
+   Unconditionally set the ZFS property `canmount=noauto` for all
+   created datasets in the destination tree. This option overwrites
+   option :option:`-p`.
+
+.. option:: -M mountpoint
+
+   Set the `mountpoint` property for the root `dest-dataset` to `mountpoint`.
+   All children will be set to inherit it.
+
+.. option:: -n
+
+   Dry-run. Do not really create datasets but show what would be done.
+
+.. option:: -p
+
+   Copy the `canmount` property only from the source to the root
+   destination dataset This will be overwritten by the option
+   :option:`-A`.
+
+.. option:: -u
+
+   Do not mount the newly created datasets.
+
+
+Environment
+-----------
+
+All environment variables that affect :command:`zfs` are effective also.
+
+
+See Also
+--------
+
+:manpage:`fzfs(8)`
--- a/docs/man/man8/fzfs.rst	Sun Feb 19 14:47:17 2023 +0100
+++ b/docs/man/man8/fzfs.rst	Mon Feb 20 00:43:30 2023 +0100
@@ -17,6 +17,7 @@
 Helper for ZFS management:
 
 - recursively mount and unmount a ZFS dataset tree
+- create a ZFS dataset tree stucture from a source tree
 
 The following global options are implemented:  
 
@@ -34,6 +35,11 @@
 Subcommands
 -----------
 
+:manpage:`fzfs-create-tree(8)`
+
+    Recursively create a ZFS dataset tree based on an existing tree while
+    copying some important properties also
+
 :manpage:`fzfs-mount(8)`
 
     Recursively mount a ZFS dataset and its children
--- a/docs/man/man8/local-bsdtools.rst	Sun Feb 19 14:47:17 2023 +0100
+++ b/docs/man/man8/local-bsdtools.rst	Mon Feb 20 00:43:30 2023 +0100
@@ -60,6 +60,7 @@
 - :manpage:`fpkg(8)`
 - :manpage:`fzfs(8)`
 
+  * :manpage:`fzfs-create-tree(8)`
   * :manpage:`fzfs-mount(8)`
   * :manpage:`fzfs-umount(8)`
 
--- a/sbin/ftjail	Sun Feb 19 14:47:17 2023 +0100
+++ b/sbin/ftjail	Mon Feb 20 00:43:30 2023 +0100
@@ -40,7 +40,7 @@
 
   snapshot-tmpl BASE-RO SKELETON-RW SNAPSHOT-NAME
 
-  copy-skel [-A] [-L] [-M MOUNTPOINT] [-P] [-u]  SOURCE-DS SNAPSHOT-NAME TARGET-DS
+  copy-skel [-A] [-D] [-L] [-M MOUNTPOINT] [-P] [-u]  SOURCE-DS SNAPSHOT-NAME TARGET-DS
 
   build-etcupdate-current-tmpl DIRECTORY TARBALL
 
@@ -769,7 +769,7 @@
 #:
 command_copy_skel() {
     local _ds_source _snapshot_name _ds_target
-    local _opt_symlink _opt_nomount _opt_canmount _opt_mountpoint
+    local _opt_symlink _opt_nomount _opt_canmount _opt_mountpoint _opt_nodata
 
     local _opt _name _relative_name  _root_canmount
 
@@ -777,12 +777,16 @@
     _opt_nomount=""
     _opt_canmount="-o canmount=on"
     _opt_mountpoint=""
+    _opt_nodata=""
 
-    while getopts "ALM:Pu" _opt ; do
+    while getopts "ADLM:Pu" _opt ; do
         case ${_opt} in
             A)
                 _opt_canmount="-o canmount=noauto"
                 ;;
+            D)
+                _opt_nodata="yes"
+                ;;
             L)
                 _opt_symlink="yes"
                 ;;
@@ -804,6 +808,7 @@
     OPTIND=1
 
     [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }
+    [ \( "${_opt_nodata}" = "yes" \) -a \( "${_opt_symlink}" = "yes" \) ] && { echo "ERROR: -L and -D are incompatible" 1>&2; return 2; }
 
     _ds_source="${1-}"
     _snapshot_name="${2-}"
--- a/sbin/fzfs	Sun Feb 19 14:47:17 2023 +0100
+++ b/sbin/fzfs	Mon Feb 20 00:43:30 2023 +0100
@@ -28,6 +28,8 @@
 
 COMMANDS:
 
+  create-tree [-A] [-M MOUNTPOINT] [-n] [-p] [-u] SOURCE-DATASET DEST-DATASET
+
   mount [-O] [-N] [-P] [-u] [-n] DATASET [MOUNTPOINT]
 
   umount DATASET
@@ -36,6 +38,48 @@
 
 '
 
+
+#:
+#: Determine some important dataset properties that are set locally
+#:
+#: Args:
+#:   $1: the dataset
+#:   $2 ...: the properties to check for
+#:
+#: Output (stdout):
+#:   An option string suited for use in "zfs create"
+#:
+_get_local_properties() {
+    local ds
+
+    local _res _prop _value _source
+
+    ds="${1}"
+    shift
+
+    _res=""
+
+    for _prop in "$@" ; do
+        IFS=$'\t' read -r _value _source <<EOF73GHASGJKKJ354
+$(zfs get -H -p -o value,source "${_prop}" "${ds}")
+EOF73GHASGJKKJ354
+        case "${_source}" in
+            local)
+                if [ -z "${_res}" ]; then
+                    _res="-o ${_prop}=${_value}"
+                else
+                    _res="${_res} -o ${_prop}=${_value}"
+                fi
+                ;;
+            *)
+                # VOID
+                ;;
+        esac
+    done
+    echo "${_res}"
+}
+
+
 #
 #: Implementation of the "mount" command.
 #:
@@ -204,6 +248,128 @@
     return 0
 }
 
+
+#:
+#: Implement the "create-tree" command
+#:
+#: Create a ZFS dataset tree from a source tree tree including important properties
+#:
+command_create_tree() {
+    local _source_ds _target_ds
+    local _opt_dry_run _opt_mountpoint _opt_noauto _opt_nomount _opt_canmount_parent_only
+
+    local _opt _source_name _snapshot_name _current_name _current_type _relative_name _zfs_opts _zfs_canmount _zfs_mountpoint
+
+    _opt_noauto=""
+    _opt_dry_run=""
+    _opt_mountpoint=""
+    _opt_nomount=""
+    _opt_canmount_parent_only=""
+
+    while getopts "AM:npu" _opt ; do
+        case ${_opt} in
+            A)
+                _opt_noauto="-o canmount=noauto"
+                ;;
+            M)
+                _opt_mountpoint="${OPTARG}"
+                ;;
+            n)
+                _opt_dry_run="yes"
+                ;;
+            p)
+                _opt_canmount_parent_only="yes"
+                ;;
+            u)
+                _opt_nomount="-u"
+                ;;
+            \?|:)
+                return 2;
+                ;;
+        esac
+    done
+    shift $((OPTIND-1))
+    OPTIND=1
+
+    _source_ds="${1-}"
+    _target_ds="${2-}"
+
+    [ -z "${_source_ds}" ] && { echo "ERROR: no source dataset given" 1>&2; return 2; }
+    [ -z "${_target_ds}" ] && { echo "ERROR: no destination dataset given" 1>&2; return 2; }
+
+    _source_name="${_source_ds%@*}"
+    if [ "${_source_name}" != "${_source_ds}" ]; then
+        _snapshot_name="${_source_ds##*@}"
+    else
+        _snapshot_name=""
+    fi
+
+    [ -n "${_snapshot_name}" ] && { echo "ERROR: No snapshot sources are supported yet" 1>&2; return 2; }
+
+    zfs list -H -r -t filesystem -o name,type "${_source_ds}" \
+    | {
+        while IFS=$'\t' read -r _current_name _current_type ; do
+            # Determine the relative name of the dataset
+            _relative_name="${_current_name#${_source_name}}"
+            _relative_name="${_relative_name%@*}"
+            if [ "${_current_type}" != "filesystem" ]; then
+                echo "ERROR: Got a snapshot but expected a filesystem" 1>&2
+                return 1
+            fi
+            _zfs_opts="$(_get_local_properties "${_current_name}" atime exec setuid compression primarycache sync readonly)"
+            if [ -z "${_relative_name}" ]; then
+                #
+                # Root
+                #
+                if [ -z "${_opt_noauto}" ]; then
+                    _zfs_canmount="$(_get_local_properties "${_current_name}" canmount)"
+                else
+                    _zfs_canmount="${_opt_noauto}"
+                fi
+                if [ -n "${_opt_mountpoint}" ]; then
+                    _zfs_mountpoint="${_opt_mountpoint}"
+                else
+                    _zfs_mountpoint=""
+                fi
+
+                if [ "${_opt_dry_run}" = "yes" ]; then
+                    if [ -z "${_zfs_mountpoint}" ]; then
+                        echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} ${_target_ds}${_relative_name}"
+                    else
+                        echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} -o mountpoint=${_zfs_mountpoint} ${_target_ds}${_relative_name}"
+                    fi
+                else
+                    if [ -z "${_zfs_mountpoint}" ]; then
+                        zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} "${_target_ds}${_relative_name}"
+                    else
+                        zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} -o "mountpoint=${_zfs_mountpoint}" "${_target_ds}${_relative_name}"
+                    fi
+                fi
+            else
+                #
+                # Children
+                #
+                if [ -z "${_opt_noauto}" ]; then
+                    if [ "${_opt_canmount_parent_only}" = "yes" ]; then
+                        _zfs_canmount=""
+                    else
+                        _zfs_canmount="$(_get_local_properties "${_current_name}" canmount)"
+                    fi
+                else
+                    _zfs_canmount="${_opt_noauto}"
+                fi
+                if [ "${_opt_dry_run}" = "yes" ]; then
+                    echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} ${_target_ds}${_relative_name}"
+                else
+                    zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} "${_target_ds}${_relative_name}"
+                fi
+            fi
+        done
+    }
+    return 0
+}
+
+
 #
 # Global option handling
 #
@@ -245,6 +411,9 @@
     umount|unmount)
         command_umount "$@"
         ;;
+    create-tree)
+        command_create_tree "$@"
+        ;;
     *)
         echo "ERROR: unknown command \`${command}'" 1>&2
         exit 2