changeset 341:a204a7415d4a

"ftjail freebsd-update" is implemented. It supports custom mountpoints also.
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 03 Dec 2022 21:12:23 +0100
parents d3b5fe2712ca
children 89877869a665
files docs/man/man8/ftjail-freebsd-update.rst sbin/ftjail
diffstat 2 files changed, 207 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/docs/man/man8/ftjail-freebsd-update.rst	Sat Dec 03 09:46:18 2022 +0100
+++ b/docs/man/man8/ftjail-freebsd-update.rst	Sat Dec 03 21:12:23 2022 +0100
@@ -6,7 +6,7 @@
 Synopsis
 --------
 
-**ftjail freebsd-update** [**-k**] [**-o** `old-origin`] `directory`
+**ftjail freebsd-update** [**-k**] [**-o** `old-origin`] `directory` `new-origin` [`etcupdate-tarball`]
 
 
 Description
@@ -14,6 +14,12 @@
 
 A :manpage:`freebsd-update(8)` for a Thin Jail.
 
+Make the ZFS dataset mounted at `directory` a read-only clone of `new-origin`.
+
+If `etcupdate-tarball` is given also call :manpage:`etcupdate(8)` on
+the fully re-mounted directory tree that is rooted at `directory`
+using `etcupdate-tarball` as new "current".
+
 
 Options
 -------
@@ -22,7 +28,7 @@
 
 .. option:: -k
 
-   Keep all temporary files.
+   Keep all temporary files. Temporary files are created in :file:`/var/tmp`.
 
    .. note:: On unexpected errors temp files are automatically kept.
 
--- a/sbin/ftjail	Sat Dec 03 09:46:18 2022 +0100
+++ b/sbin/ftjail	Sat Dec 03 21:12:23 2022 +0100
@@ -44,7 +44,7 @@
 
   build-etcupdate-current-tmpl DIRECTORY TARBALL
 
-  freebsd-update [-k] [-o OLD-ORIGIN] DIRECTORY
+  freebsd-update [-k] [-o OLD-ORIGIN] DIRECTORY NEW-ORIGIN [ETCUPDATE-TARBALL]
 
 ENVIRONMENT:
 
@@ -874,15 +874,104 @@
 
 
 #:
+#: Determine extra clone options with respect to the "mountpoint" property
+#:
+#: Args:
+#:   $1: the dataset
+#:
+#: Output (stdout)
+#:   The extra clone arguments
+#:
+#: Exit:
+#:   On unexpected source values
+#:
+_get_clone_extra_prop_for_mountpoint() {
+    local ds
+
+    local _mp_name _mp_property _mp_value _mp_source
+
+    ds="${1}"
+
+    zfs get -H mountpoint "${ds}" \
+    | {
+        IFS=$'\t' read -r _mp_name _mp_property _mp_value _mp_source
+        case "${_mp_source}" in
+            local)
+                echo -n "-o mountpoint=${_mp_value}"
+                ;;
+            default|inherited*)
+                ;;
+            temporary*|received*|'-'|none)
+                # XXX FIXME: Is this relevant on FreeBSD?
+                echo "ERROR: Unexpected SOURCE \"${_mp_source}\" for mountpoint at \`${_mp_value}'" 1>&2
+                exit 1
+                ;;
+            *)
+                echo "ERROR: Unexpected SOURCE for mountpoint property at \`${_mp_value}'" 1>&2
+                exit 1;
+                ;;
+        esac
+        if [ "${_mp_value}" != "${_directory}" ]; then
+            echo "WARNING: dataset is not mounted at its configured mountpoint but elsewhere (probably via \"mount -t zfs\")" 1>&2
+        fi
+    }
+}
+
+
+#:
+#: Determine the "canmount" property for a dataset
+#:
+#: Args:
+#:   $1: the dataset
+#:
+#: Output (stdout):
+#:   The local value or "DEFAULT" for the (unset) default
+#:
+#: Exit:
+#:   On unexpected source values
+#:
+_get_canmount_setting_for_dataset() {
+    local ds
+
+    local _cm_name _cm_property _cm_value _cm_source
+
+    ds="${1}"
+
+    zfs get -H canmount "${ds}" \
+    | {
+        IFS=$'\t' read -r _cm_name _cm_property _cm_value _cm_source
+        case "${_cm_source}" in
+            local)
+                echo -n "canmount=${_cm_value}"
+                ;;
+            default)
+                echo -n "DEFAULT"
+                ;;
+            inherited|temporary*|received*|'-'|none)
+                # XXX FIXME: Is this relevant on FreeBSD?
+                echo "ERROR: Unexpected SOURCE \"${_cm_source}\" for canmount at \`${_cm_name}'" 1>&2
+                exit 1
+                ;;
+            *)
+                echo "ERROR: Unexpected SOURCE for canmount property at \`${_cm_name}'" 1>&2
+                exit 1;
+                ;;
+        esac
+    }
+}
+
+
+#:
 #: Implement the "freebsd-update" command for a thin jail
 #:
 command_freebsd_update() {
-    local _directory
+    local _directory _new_origin _etcupdate_tarball
     local _opt_keep _opt_old_origin
 
-    local _res _jailname _dir_mounts _dir_fn_fstab _dir_basename
+    local _res _jailname _dir_mounts _dir_fn_fstab _dir_basename _dir_fn_tldir
     local _root_dataset _root_mountpoint _root_type _root_options
-    local _dummy _opt
+    local _clone_extra_props _canmount_prop
+    local _line _opt
     local _root_readonly _root_origin
 
     _opt_keep="no"
@@ -904,9 +993,17 @@
     OPTIND=1
 
     _directory="${1-}"
+    _new_origin="${2-}"
+    _etcupdate_tarball="${3-}"
 
     [ -z "${_directory}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
-    [ -d "${_directory}" ] || { echo "ERROR: directory \`${_directory}' does not exist" 1>&2; exit 1; }
+    [ -d "${_directory}" ] || { echo "ERROR: directory \`${_directory}' does not exist" 1>&2; return 1; }
+
+    [ -z "${_new_origin}" ] && { echo "ERROR: no new origin given" 1>&2; return 2; }
+    zfs list -H -o name -t snapshot "${_new_origin}" >/dev/null || { echo "ERROR: new origin does not exist" 1>&2; return 1; }
+    if [ -n "${_etcupdate_tarball}" ]; then
+        [ -f "${_etcupdate_tarball}" ] || { echo "ERROR: given etcupdate tarball does not exist " 1>&2; return 1; }
+    fi
 
     _dir_basename="$(basename ${_directory})"
 
@@ -916,10 +1013,10 @@
     set -e
     if [ ${_res} -ne 2 ] ; then
         if [ ${_res} -ne 0 ] ; then
-            exit ${_res}
+            return ${_res}
         else
             echo "ERROR: Please stop the \`${_jailname}' jail" >&2
-            exit 1
+            return 1
         fi
     fi
     _dir_mounts="$(_get_mounts_at_directory "${_directory}")"
@@ -928,32 +1025,43 @@
     # Check preconditions thoroughly!
     #
     # Check that the first item/line is a read-only ZFS mount directly
-    # at the given directory.
+    # at the given directory. This must also be its configured
+    # mountpoint in ZFS.
     # Also check that it is a clone proper.
     #
-    IFS=' '$'\t' read -r _root_dataset _root_mountpoint _root_type _root_options _dummy <<EOF4tHGCSS
+    IFS=' '$'\t' read -r _root_dataset _root_mountpoint _root_type _root_options _line <<EOF4tHGCSS
 ${_dir_mounts}
 EOF4tHGCSS
-    [ "${_root_mountpoint}" != "${_directory}" ] && { echo "ERROR: found root mountpoint does not match given directory" 1>&2; exit 1; }
-    [ "${_root_type}" != "zfs" ] && { echo "ERROR: root mountpoint is not from a ZFS dataset" 1>&2; exit 1; }
+    [ "${_root_mountpoint}" != "${_directory}" ] && { echo "ERROR: found root mountpoint does not match given directory" 1>&2; return 1; }
+    [ "${_root_type}" != "zfs" ] && { echo "ERROR: root mountpoint is not from a ZFS dataset" 1>&2; return 1; }
     _root_readonly="$(zfs list -H -o readonly "${_root_dataset}")"
-    [ "${_root_readonly}" != "on" ] &&  { echo "ERROR: the root dataset is not mounted read-only" 1>&2; exit 1; }
+    [ "${_root_readonly}" != "on" ] &&  { echo "ERROR: the root dataset is not mounted read-only" 1>&2; return 1; }
     _root_origin="$(zfs list -H -o origin "${_root_dataset}")"
     if [ -n "${_opt_old_origin}" ]; then
-        [ "${_opt_old_origin}" != "${_root_origin}" ] && { echo "ERROR: origin mismatch" 1>&2; exit 1; }
+        [ "${_opt_old_origin}" != "${_root_origin}" ] && { echo "ERROR: origin mismatch" 1>&2; return 1; }
     else
-        [ "${_root_origin}" = '-' ] &&  { echo "ERROR: the root dataset is not a ZFS clone" 1>&2; exit 1; }
+        [ "${_root_origin}" = '-' ] &&  { echo "ERROR: the root dataset is not a ZFS clone" 1>&2; return 1; }
     fi
+
+    # Determine we need to clone with a custom (non inherited) "mountpoint"
+    _clone_extra_props="$(_get_clone_extra_prop_for_mountpoint "${_root_dataset}") "
+    # Determine we need to clone with a custom (non inherited) "canmount"
+    _canmount_prop="$(_get_canmount_setting_for_dataset "${_root_dataset}")"
+
     #
     # XXX FIXME: should we check that _root_options equals "ro" or
     #            start with "ro,"
     #    _root_origin="$(zfs list -H -o origin "${_root_dataset}")"
 
-    _dir_fn_fstab="$(env TMPDIR=/var/tmp mktemp -t ftjail-fstab.${_dir_basename})"
+    _dir_fn_fstab="$(env TMPDIR=/var/tmp mktemp -t ftjail_${_dir_basename}.fstab)"
     echo -n "${_dir_mounts}" >>"${_dir_fn_fstab}"
 
+    _dir_fn_tldir="$(env TMPDIR=/var/tmp mktemp -t ftjail_${_dir_basename}.tldir)"
+    find "${_directory}" -depth 1 -type d 2>/dev/null | sort >>"${_dir_fn_tldir}"
+
     # Unmount in reverse order: unmount can do it for us
-    umount -a -F "${_dir_fn_fstab}" -v || exit 1
+    echo "Unmounting all datasets mounted at \`${_directory}'"
+    umount -a -F "${_dir_fn_fstab}" -v
 
     #
     # XXX TBD: Hooks to create some new top-level dirs (/srv /proc et
@@ -962,9 +1070,84 @@
     #          completely mounting the stored fstab.
     #
 
+    #
+    # Destroy the current read-only root clone and make a new clone based
+    # on the given new origin.
+    # The new clone temporarily is RW and is not to be mounted automatically.
+    # These both properties are set again below after the new base is
+    # adjusted properly.
+    #
+    echo "Destroying the cloned root dataset \`${_root_dataset}'"
+    zfs destroy -v "${_root_dataset}"
+    echo "Cloning a new root dataset \`${_root_dataset}' from new origin \`${_new_origin}'"
+    zfs clone -o readonly=off -o canmount=noauto ${_clone_extra_props} "${_new_origin}" "${_root_dataset}"
+    #
+    # NOTE: Always mount with "mount -t zfs" because a custom
+    #       mountpoint is not reflected in the "mountpoint"
+    #       property. So in scripts to be sure to unmount and re-mount
+    #       at the same location always use "mount -t zfs".
+    #
+    echo "Remounting only the root dataset at \`${_directory}'"
+    [ ! -d "${_directory}" ] && mkdir "${_directory}"
+    mount -t zfs "${_root_dataset}" "${_directory}"
+    #
+    # Re-create all currently missing top-level dirs (aka mountpoint)
+    # in the new clone. Most probably they serve as mountpoints for other
+    # datasets.
+    #
+    # XXX FIXME: Re-create the current mode bits and/or ACLs also.
+    #            But most probably they are set properly in the mounted
+    #            datasets.
+    #
+    echo "Recreating missing top-level directories"
+    cat "${_dir_fn_tldir}" \
+    | {
+        while IFS='' read -r _line ; do
+            if [ ! -d "${_line}" ]; then
+                echo "Recreating top-level directory: ${_line}"
+                mkdir "${_line}"
+            fi
+        done
+    }
+    echo "Unmounting the new root dataset"
+    umount "${_directory}"
+    echo "Re-setting some ZFS properties on the new cloned dataset"
+    zfs set readonly=on "${_root_dataset}"
+    #
+    # Copy "canmount" properly last because it has been set to "noauto"
+    # temporarily.
+    #
+    if [ -n "${_canmount_prop}" ]; then
+        if [ "${_canmount_prop}" = "DEFAULT" ]; then
+            #
+            # "zfs inherit" is not possible for "canmount".
+            # Use "inherit -S" to simulate a reset to "default" somewhat
+            #
+            # See also: https://github.com/openzfs/zfs/issues/5733
+            #
+            zfs inherit -S canmount "${_root_dataset}"
+        else
+            zfs set "${_canmount_prop}" "${_root_dataset}"
+        fi
+    fi
+
+    # Mount again
+    echo "Mounting all datasets rooted at \`${_directory}'"
+    [ ! -d "${_directory}" ] && mkdir "${_directory}"
+    mount -a -F "${_dir_fn_fstab}" -v
+
+    # Update configs
+    if [ -n "${_etcupdate_tarball}" ]; then
+        echo "Calling etcupdate for DESTDIR=${_directory}"
+        etcupdate -D "${_directory}" -t "${_etcupdate_tarball}"
+    fi
+
     if [ "${_opt_keep}" != "yes" ]; then
+        echo "Cleaning up..."""
+        [ -n "${_dir_fn_tldir}" } && [ -f "${_dir_fn_tldir}" ] && rm -f "${_dir_fn_tldir}"
         [ -n "${_dir_fn_fstab}" ] && [ -f "${_dir_fn_fstab}" ] && rm -f "${_dir_fn_fstab}"
     fi
+    echo "Done."
 }