view sbin/ftjail @ 723:a97ec3f07bdb

farray.sh: REFACTOR: More flexible metadata retrieval. Using an array or alist variable name or token value (with prefix) is now supported in every function. This is possible because the value prefixes contain questin marks (?) which are not allowed in shell variable names. This again is a major precondition for recursive data structures (arrays/alists in arrays/alists).
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 05 Oct 2024 21:55:55 +0200
parents 425b1ae264a9
children 8f1583faf9ea
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: A very minimal BSD Thin Jail management tool.
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2022-2024 Franz Glasner.
#:             All rights reserved.
#: :License:   BSD 3-Clause "New" or "Revised" License.
#:             See LICENSE for details.
#:             If you cannot find LICENSE see
#:             <https://opensource.org/licenses/BSD-3-Clause>
#: :ID:        @(#)@@SIMPLEVERSIONTAG@@
#:

set -eu

# shellcheck disable=SC2034    # VERSION appears unused
VERSION='@@VERSION@@'

USAGE='
USAGE: ftjail [ OPTIONS ] COMMAND [ COMMAND OPTIONS ] [ ARG ... ]

OPTIONS:

  -V    Print the program name and version number to stdout and exit

  -h    Print this help message to stdout and exit

COMMANDS:

  datasets-tmpl -L|-P PARENT-BASE PARENT-SKELETON NAME

  mount-tmpl -L|-P [-n] [-u] BASE-RO SKELETON-RW MOUNTPOINT

  umount-tmpl BASE-RO SKELETON-RW

  interlink-tmpl MOUNTPOINT

  populate-tmpl -L|-P [-b] DIRECTORY BASETXZ [KERNELTXZ]

  snapshot-tmpl BASE-RO SKELETON-RW SNAPSHOT-NAME

  copy-skel [-A] [-D] [-L] [-M MOUNTPOINT] [-P] [-u]  SOURCE-DS SNAPSHOT-NAME TARGET-DS

  build-etcupdate-current-tmpl DIRECTORY TARBALL

  check-freebsd-update [-k] [-o OLD-ORIGIN] [-R SNAPSHOT] DIRECTORY NEW-ORIGIN [ETCUPDATE-TARBALL]

  freebsd-update [-k] [-o OLD-ORIGIN] [-R SNAPSHOT] DIRECTORY NEW-ORIGIN [ETCUPDATE-TARBALL]

ENVIRONMENT:

  All environment variables that affect "zfs" are effective also.

DESCRIPTION:

  All commands with the exception of "populate-tmpl" require ZFS as
  filesystem.
'


_p_datadir='@@DATADIR@@'
[ "${_p_datadir#@@DATADIR}" = '@@' ] && _p_datadir="$(dirname "$0")"/../share/local-bsdtools
. "${_p_datadir}/common.subr"
. "${_p_datadir}/farray.sh"


# Reset to standard umask
umask 0022


#
# PARENT-BASE NAME DRY-RUN
#
command_datasets_tmpl_base() {
    local _p_base _name

    local _opt_dry_run

    local _ds_base _opt

    _opt_dry_run=""
    while getopts "nu" _opt ; do
        case "${_opt}" in
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _p_base="${1-}"
    _name="${2-}"

    if [ -z "${_p_base}" ]; then
        echo "ERROR: no parent dataset for base given" >&2
        return 2
    fi
    if [ -z "${_name}" ]; then
        echo "ERROR: no name given" >&2
        return 2
    fi

    if ! zfs list -H -o mountpoint -t filesystem "${_p_base}" >/dev/null 2>/dev/null; then
        echo "ERROR: parent dataset \`${_p_base}' does not exist" >&2
        return 1
    fi
    _ds_base="${_p_base}/${_name}"
    if zfs list -H -o mountpoint -t filesystem "${_ds_base}" >/dev/null 2>/dev/null; then
        echo "ERROR: dataset \`${_ds_base}' does already exist" >&2
        return 1
    fi


    [ "${_opt_dry_run}" = "yes" ] && return 0

    echo "Creating RO base datasets in:"
    printf "\\t%s\\n" "${_ds_base}"

    zfs create -u -o canmount=noauto "${_ds_base}"

}


#
# SKELETON NAME DRY-RUN
#
command_datasets_tmpl_skel() {
    local _p_base _name

    local _opt_dry_run _opt_symlink

    local _ds_skel _child _child_zfsopts _opt

    _opt_dry_run=""
    _opt_symlink=""

    while getopts "LPnu" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _p_skel="${1-}"
    _name="${2-}"

    if [ -z "${_p_skel}" ]; then
        echo "ERROR: no parent dataset for skeleton given" >&2
        return 2
    fi
    if [ -z "${_name}" ]; then
        echo "ERROR: no name given" >&2
        return 2
    fi

    if ! zfs list -H -o mountpoint -t filesystem "${_p_skel}" >/dev/null 2>/dev/null; then
        echo "ERROR: parent dataset \`${_p_skel}' does not exist" >&2
        return 1
    fi
    _ds_skel="${_p_skel}/${_name}"
    if zfs list -H -o mountpoint -t filesystem "${_ds_skel}" >/dev/null 2>/dev/null; then
        echo "ERROR: dataset \`${_ds_skel}' does already exist" >&2
        return 1
    fi

    [ "${_opt_dry_run}" = "yes" ] && return 0

    echo "Creating RW skeleton datasets in:"
    printf "\\t%s\\n" "${_ds_skel}"

    if [ "${_opt_symlink}" = "yes" ]; then
        # In this case the skeleton root needs to be mounted into a "skeleton" subdir
        zfs create -u -o canmount=noauto "${_ds_skel}"
    else
        # Only children are to be mounted
        zfs create -u -o canmount=off "${_ds_skel}"
    fi
    # "usr" is only a container holding "usr/local"
    zfs create -u -o canmount=off "${_ds_skel}/usr"
    #
    # XXX FIXME: What about usr/ports/distfiles
    #            We typically want to use binary packages.
    #            And if we use ports they are not in usr/ports typically.
    #
    #zfs create -u -o canmount=off "${_ds_skel}/usr/ports"
    #
    # XXX FIXME: What about home
    #
    # /var/mail is here because it relies on atime
    #
    for _child in etc home root tmp usr/local var var/mail ; do
        case "${_child}" in
            "tmp"|"var/tmp")
                _child_zfsopts="-o sync=disabled -o setuid=off"
                ;;
            "home")
                _child_zfsopts="-o setuid=off"
                ;;
            "usr/ports/distfiles")
                _child_zfsopts="-o exec=off -o setuid=off -o compression=off -o primarycache=metadata"
                ;;
            "var/mail")
                _child_zfsopts="-o atime=on -o exec=off -o setuid=off"
                ;;
            *)
                _child_zfsopts=""
                ;;
        esac
        zfs create -u -o canmount=noauto ${_child_zfsopts} "${_ds_skel}/${_child}"
    done
}


#
# "datasets-tmpl" -- create the ZFS dataset tree
#
# PARENT-BASE PARENT-SKELETON NAME
#
command_datasets_tmpl() {
    # parent ZFS dataset -- child ZFS dataset name
    local _p_base _p_skel _name
    local _opt_symlink

    local _ds_base _ds_skel _opt

    _opt_symlink=""

    while getopts "LP" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="-L"
                ;;
            P)
                _opt_symlink="-P"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _p_base="${1-}"
    _p_skel="${2-}"
    _name="${3-}"

    # Check preconditions
    command_datasets_tmpl_base -n "${_p_base}" "${_name}" || return
    command_datasets_tmpl_skel -n ${_opt_symlink} "${_p_skel}" "${_name}" || return

    # Really do it
    command_datasets_tmpl_base "${_p_base}" "${_name}" || return
    command_datasets_tmpl_skel ${_opt_symlink} "${_p_skel}" "${_name}" || return
    return 0
}


#
# "populate-tmpl" -- populate the datasets with content from a FreeBSD base.txz
#
# command_populate_tmpl mountpoint basetxz
#
command_populate_tmpl() {
    # MOUNTPOINT -- base.txz
    local _mp _basetxz
    local _opt_symlink _opt_preserve_boot

    local _opt _dir

    _opt_symlink=""
    _opt_preserve_boot=""

    while getopts "LPb" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            b)
                _opt_preserve_boot="yes"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _mp="${1-}"
    _basetxz="${2-}"
    _kerneltxz="${3-}"

    if [ -z "${_mp}" ]; then
        echo "ERROR: no mountpoint given" >&2
        return 2
    fi
    if [ -z "${_basetxz}" ]; then
        echo "ERROR: no base.txz given" >&2
        return 2
    fi
    if [ ! -d "${_mp}" ]; then
        echo "ERROR: mountpoint \`${_mp}' does not exist" >&2
        return 1
    fi
    if [ ! -r "${_basetxz}" ]; then
        echo "ERROR: file \`${_basetxz}' is not readable" >&2
        return 1
    fi

    if [ "${_opt_symlink}" = "yes" ]; then
        echo "Extracting RO base ..."
        tar -C "${_mp}" --exclude=./etc --exclude=./root --exclude=./tmp --exclude=./usr/local --exclude=./var --no-safe-writes -xJp -f "${_basetxz}" || return
        # "home" is not part of base
        for _dir in etc root tmp usr/local var ; do
            echo "Extracting RW skeleton: ${_dir} ..."
            tar -C "${_mp}/skeleton" --include="./${_dir}" --exclude=./root/.cshrc --exclude=./root/.profile -xJp -f "${_basetxz}" || return
        done
        # In the original archive they are archived as hardlinks: make proper symlinks here
        (cd "${_mp}/skeleton/root" && ln -s ../../.profile .profile) || return
        (cd "${_mp}/skeleton/root" && ln -s ../../.cshrc .cshrc) || return
    else
        echo "Extracting base ..."
        tar -C "${_mp}" --exclude=./root/.cshrc --exclude=./root/.profile --no-safe-writes -xJp -f "${_basetxz}" || return
        # In the original archive they are archived as hardlinks: make proper symlinks here
        (cd "${_mp}/root" && ln -s ../.profile .profile) || return
        (cd "${_mp}/root" && ln -s ../.cshrc .cshrc) || return
    fi

    if [ \( "${_opt_preserve_boot}" = "yes" \) -o \( -n "${_kerneltxz}" \) ]; then
        if [ -n "${_kerneltxz}" ]; then
            echo "Extracting kernel ..."
            tar -C "${_mp}" -xJp -f "${_kerneltxz}" || return
        else
            echo "Preserved \"boot\""
        fi
    else
        find "${_mp}/boot" -type f -delete || true
    fi
}


#
# _do_mount dataset mountpoint dry-run mount-natural childs-only
#
_do_mount() {
    local _dsname _mountpoint _dry_run _mount_natural _childs_only

    local _name _mp _canmount _mounted
    local _rootds_mountpoint _relative_mp _real_mp

    _dsname="${1}"
    _mountpoint="${2}"
    _dry_run="${3}"
    _mount_natural="${4}"
    _childs_only="${5}"

    if [ -z "${_dsname}" ]; then
        echo "ERROR: no dataset given" >&2
        return 2
    fi

    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")"  || \
        { echo "ERROR: root dataset \`${_dsname}' does not exist" >&2; return 1; }

    if [ -z "${_mountpoint}" ]; then
        if [ "${_mount_natural}" = "yes" ]; then
            _mountpoint="${_rootds_mountpoint}"
        else
            echo "ERROR: no mountpoint given" >&2
            return 2
        fi
    else
        if [ "${_mount_natural}" = "yes" ]; then
            echo "ERROR: Cannot have a custom mountpoint when mount-natural is activated" >&2
            return 2
        fi
    fi

    # Eventually remove a trailing slash
    _mountpoint="${_mountpoint%/}"
    if [ -z "${_mountpoint}" ]; then
        echo "ERROR: would mount over the root filesystem" >&2
        return 1
    fi

    zfs list -H -o name,mountpoint,canmount,mounted -s mountpoint -t filesystem -r "${_dsname}" \
    | {
        while IFS=$'\t' read -r _name _mp _canmount _mounted ; do
            # Skip filesystems that are already mounted
            [ "${_mounted}" = "yes" ] && continue
            # Skip filesystems that must not be mounted
            [ "${_canmount}" = "off" ] && continue
            case "${_mp}" in
                "none"|"legacy")
                    # Do nothing for filesystem with unset or legacy mountpoints
                    ;;
                "${_rootds_mountpoint}"|"${_rootds_mountpoint}/"*)
                    #
                    # Handle only mountpoints that have a mountpoint below
                    # the parent datasets mountpoint
                    #

                    # Determine the mountpoint relative to the parent mountpoint
                    _relative_mp="${_mp#"${_rootds_mountpoint}"}"
                    # Eventually remove a trailing slash
                    _relative_mp="${_relative_mp%/}"
                    # The real effective full mountpoint
                    _real_mp="${_mountpoint}${_relative_mp}"

                    #
                    # Consistency and sanity check: computed real mountpoint must
                    # be equal to the configured mountpoint when no custom mountpoint
                    # is given.
                    #
                    if [ "${_mount_natural}" = "yes" ]; then
                        if [ "${_real_mp}" != "${_mp}" ]; then
                            echo "ERROR: mountpoint mismatch" 1>&2
                            return 1
                        fi
                    fi

                    if [ \( "${_childs_only}" = "yes" \) -a \( "${_name}" = "${_dsname}" \) ]; then
                        echo "Skipping ${_name} because mounting childs only" 1>&2
                    else
                        if [ "${_dry_run}" = "yes" ]; then
                            echo "Would mount ${_name} on ${_real_mp}"
                        else
                            mkdir -p "${_real_mp}" 1> /dev/null 2> /dev/null || \
                                { echo "ERROR: cannot create mountpoint ${_real_mp}" 1>&2; return 1; }
                            echo "Mounting ${_name} on ${_real_mp}"
                            /sbin/mount -t zfs "${_name}" "${_real_mp}" || return 1
                        fi
                    fi
                    ;;
                *)
                    echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 1>&2
                    ;;
            esac
        done

        return 0
    }
}


#
# "mount-tmpl" -- recursively mount a base and skeleton datasets including subordinate datasets
#
# command_mount_tmpl base-ro skeleton-rw  mountpoint
#
command_mount_tmpl() {
    local _ds_base _ds_skel _mountpoint
    local _opt_dry_run _opt_symlink

    local _opt

    _opt_dry_run=""
    _opt_symlink=""

    while getopts "LPnu" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _ds_base="${1-}"
    _ds_skel="${2-}"
    _mountpoint="${3-}"

    _do_mount "${_ds_base}" "${_mountpoint}" "${_opt_dry_run}" "" "" || return
    if [ "${_opt_symlink}" = "yes" ]; then
        if [ "${_opt_dry_run}" != "yes" ]; then
            if [ ! -d "${_mountpoint}/skeleton" ]; then
                mkdir "${_mountpoint}/skeleton" || return
            fi
        fi
        _do_mount "${_ds_skel}" "${_mountpoint}/skeleton" "${_opt_dry_run}" "" "" || return
    else
        _do_mount "${_ds_skel}" "${_mountpoint}" "${_opt_dry_run}" "" "yes" || return
    fi

    return 0
}


#
# _do_umount dataset
#
_do_umount() {
    local _dsname

    local _name _mp _rest
    local _rootds_mountpoint

    _dsname="${1}"
    [ -z "${_dsname}" ] && { echo "ERROR: no dataset given" >&2; return 2; }

    # Just determine whether the given dataset name exists
    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}" 2>/dev/null)" || { err "dataset not found"; return 1; }

    _get_zfs_mounts_for_dataset_tree -r "${_dsname}" \
    | {
        while IFS=$'\t' read -r _name _mp _rest ; do
            echo "Umounting ${_name} on ${_mp}"
            /sbin/umount "${_mp}" || return 1
        done
        return 0
      }
}


#
# "umount-tmpl" -- umount skeleton and base datasets
#
# command_umount_tmpl ds-base ds-skeleton
#
command_umount_tmpl() {
    local _ds_base _ds_skel

    _ensure_no_options "$@"

    _ds_base="${1-}"
    _ds_skel="${2-}"

    [ -z "${_ds_base}" ] && { echo "ERROR: no RO base dataset given" >&2; return 2; }
    [ -z "${_ds_skel}" ] && { echo "ERROR: no RW skeleton dataset given" >&2; return 2; }

    _do_umount "${_ds_skel}" || return
    _do_umount "${_ds_base}" || return

    return 0
}


#
# "interlink-tmpl" -- create links from base to skeleton
#
# command_interlink_tmpl mountpint
#
command_interlink_tmpl() {
    local _mountpoint

    local _dir _dirpart _basepart

    _ensure_no_options "$@"

    _mountpoint="${1-}"

    [ -z "${_mountpoint}" ] && { echo "ERROR: no mountpoint given" 1>&2; return 2; }
    [ -d "${_mountpoint}" ] || { echo "ERROR: mountpoint \`${_mountpoint}' does not exist" 1>&2; return 1; }
    [ -d "${_mountpoint}/skeleton" ] || { echo "WARNING: skeleton is not mounted at \`${_mountpoint}/skeleton'" 1>&2; }

    for _dir in etc home root tmp usr/local var ; do
        case "${_dir}" in
            "usr/local")
                _dirpart="$(dirname "${_dir}")"
                _basepart="$(basename "${_dir}")"
                [ -d "${_mountpoint}/${_dirpart}" ] || mkdir "${_mountpoint}/${_dirpart}" || return
                ( cd "${_mountpoint}/${_dirpart}" && ln -s "../skeleton/${_dir}" "${_basepart}" ) || return
                ;;
            *)
                ( cd "${_mountpoint}" && ln -s "skeleton/${_dir}" "${_dir}" ) || return
                ;;
        esac
    done
    return 0
}

#:
#: Create a snapshot for a dataset and all of its children.
#:
#: Args:
#:     $1: the datasets
#:     $2: the name of the snapshot
#:
_do_snapshot() {
    local _ds _snap_name

    _ds="${1}"
    _snap_name="${2}"

    [ -z "${_ds}" ] && { echo "ERROR: no dataset given" 1>&2; return 2; }
    [ -z "${_snap_name}" ] && { echo "ERROR: no snapshot name given" 1>&2; return 2; }

    if ! zfs list -H -o name -t filesystem "${_ds}" 1> /dev/null 2> /dev/null ; then
        echo "ERROR: parent dataset \`${_ds}' does not exist or is not available" 1>&2
        return 1
    fi

    zfs snapshot -r "${_ds}@${_snap_name}" || return
}


#:
#: Implement the "snapshot-tmpl" command
#:
command_snapshot_tmpl() {
    local _ds_base _ds_skel _snap_name

    _ensure_no_options "$@"

    _ds_base="${1-}"
    _ds_skel="${2-}"
    _snap_name="${3-}"

    # Here extra checks because of better error messages possible
    [ -z "${_ds_base}" ] && { echo "ERROR: no RO base dataset name given" 1>&2; return 2; }
    [ -z "${_ds_skel}" ] && { echo "ERROR: no RW skeleton dataset name given" 1>&2; return 2; }

    _do_snapshot "${_ds_base}" "${_snap_name}" || return
    _do_snapshot "${_ds_skel}" "${_snap_name}" || return
    return 0
}


#:
#: Implementation of "copy-skel"
#:
command_copy_skel() {
    local _ds_source _snapshot_name _ds_target
    local _opt_symlink _opt_nomount _opt_canmount _opt_mountpoint _opt_nodata

    local _opt _name _relative_name  _root_canmount

    _opt_symlink=""
    _opt_nomount=""
    _opt_canmount="-o canmount=on"
    _opt_mountpoint=""
    _opt_nodata=""

    while getopts "ADLM:Pu" _opt ; do
        case "${_opt}" in
            A)
                _opt_canmount="-o canmount=noauto"
                ;;
            D)
                _opt_nodata="yes"
                ;;
            L)
                _opt_symlink="yes"
                ;;
            M)
                _opt_mountpoint="${OPTARG}"
                ;;
            P)
                _opt_symlink="no"
                ;;
            u)
                _opt_nomount="-u"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    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-}"
    _ds_target="${3-}"

    [ -z "${_ds_source}" ] && { echo "ERROR: no source given" 1>&2; return 2; }
    [ -z "${_snapshot_name}" ] && { echo "ERROR: no snapshot name given" 1>&2; return 2; }
    [ -z "${_ds_target}" ] && { echo "ERROR: no target given" 1>&2; return 2; }

    zfs list -r -t all -o name "${_ds_source}" \
    | {
        while IFS=$'\t' read -r _name ; do
            if [ "${_name}" = "${_name%@*}@${_snapshot_name}" ]; then
                echo "FOUND: $_name"
                # Determine the relative name of the dataset
                _relative_name="${_name#"${_ds_source}"}"
                _relative_name="${_relative_name%@*}"
                echo "  -> $_relative_name"
                if [ -z "${_relative_name}" ]; then
                    #
                    # Root
                    #
                    if [ "${_opt_symlink}" = "yes" ]; then
                        _root_canmount="${_opt_canmount}"
                    else
                        _root_canmount="-o canmount=off"
                    fi
                    if [ -n "${_opt_mountpoint}" ]; then
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v ${_root_canmount} -o "mountpoint=${_opt_mountpoint}" -o readonly=off "${_ds_target}${_relative_name}"
                    else
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -o readonly=off -v ${_root_canmount} -x mountpoint "${_ds_target}${_relative_name}"
                    fi
                else
                    #
                    # Children
                    #
                    if [ "${_relative_name}" = "/usr" ]; then
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v -o canmount=off -x mountpoint "${_ds_target}${_relative_name}"
                    else
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v ${_opt_canmount} -x mountpoint "${_ds_target}${_relative_name}"
                    fi
                fi
            fi
        done
    }
    # Need only the filesystem data (no associated snapshots)
    echo "Destroying unneeded snapshots ..."
    zfs destroy -rv "${_ds_target}@${_snapshot_name}"
}


#:
#: Implement the "build-etcupdate-current-tmpl" command
#:
command_build_etcupdate_current_tmpl() {
    local _directory _tarball

    _directory="${1-}"
    _tarball="${2-}"

    [ -z "${_directory}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
    [ -z "${_tarball}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
    [ -e "${_tarball}" ] && { echo "ERROR: \`${_tarball}' exists already" 1>&2; return 1; }

    if ! tar -cjf "${_tarball}" -C "${_directory}/var/db/etcupdate/current" . ; then
        rm -f "${_tarball}" || true
        return 1
    fi
}


#:
#: 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)
                printf '%s' "-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)
                printf '%s' "canmount=${_cm_value}"
                ;;
            default)
                printf '%s' "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
    }
}


#:
#: Callback for _print_check_errors
#:
_print_check_error() {
    printf '%s CHECK: %s\n' '-' "$3" 1>&2
    return 0
}


#:
#: Print all the errors to stderr
#:
_print_check_errors() {
    farray_istrue "$1" || return 0
    echo "There are ERRORs to be resolved before \`freebsd-update' can be run:" 1>&2
    farray_for_each "$1" _print_check_error
}


#:
#: Callback for _print_check_warnings
#:
_print_check_warning() {
    printf '%s WARNING: %s\n' '-' "$3" 1>&2
    return 0
}


#:
#: Print all the warnings to stderr
#:
_print_check_warnings() {
    farray_istrue "$1" || return 0
    echo "There are WARNINGs to be considered before \`freebsd-update' should be run:" 1>&2
    farray_for_each "$1" _print_check_warning
}


#:
#: Implement the "check-freebsd-update" command for a thin jail
#:
command_check_freebsd_update() {
    local _directory _new_origin _etcupdate_tarball
    local _opt_keep _opt_old_origin _opt_snapshots

    local _errors _warnings _rc
    local _directory _new_origin _etcupdate_tarball
    local _dir_basename _dir_mounts _jailname _running_jailname
    local _tmp _line _log_sock
    local _root_dataset _root_mountpoint _root_type _root_options
    local _mnt_device _mnt_mountpoint _mnt_type _mnt_options
    local _idx _sn_ds _sn_name _sn_ds_related
    local _etcupdate_status

    _rc=0

    _warnings=''
    farray_create _warnings
    _errors=''
    farray_create _errors

    _opt_snapshots=''
    falist_create _opt_snapshots
    _opt_keep="no"
    _opt_old_origin=""
    while getopts "R:ko:" _opt ; do
        case "${_opt}" in
            R)
                case "${OPTARG}" in
                    *?\@?*)
                        #
                        # Split in two parts: dataset hierarchy and the name of the
                        # snapshot
                        #
                        falist_set _opt_snapshots "${OPTARG%%@*}" "${OPTARG#*@}"
                        ;;
                    *)
                        farray_append _errors "argument \`${OPTARG}' is not a snapshot name"
                        ;;
                    esac
                ;;
            k)
                _opt_keep="yes"
                ;;
            o)
                _opt_old_origin="${OPTARG}"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _directory="${1-}"
    _new_origin="${2-}"
    _etcupdate_tarball="${3-}"

    if [ -z "${_directory}" ]; then
        farray_append _errors "no directory given"
    else
        [ -d "${_directory}" ] || farray_append _errors "directory \`${_directory}' does not exist"
    fi
    if [ -z "${_new_origin}" ]; then
        farray_append _errors "no new origin given"
    else
        zfs list -H -o name -t snapshot "${_new_origin}" >/dev/null 2>/dev/null || farray_append  _errors "ZFS dataset snapshot for the new origin \`${_new_origin}' does not exist"
    fi
    if [ -n "${_etcupdate_tarball}" ]; then
        [ -r "${_etcupdate_tarball}" ] || farray_append _errors "given etcupdate tarball does not exist and/or is not readable"
    fi

    # Check snapshotting
    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        if zfs get -H -o value name "${_sn_ds}" >/dev/null 2>/dev/null; then
            # yes dataset exists: check that snapshots do not exist
            while IFS=$'\t' read -r _line; do
                if zfs get -H -o value name "${_line}@${_sn_name}" >/dev/null 2>/dev/null; then
                    farray_append _errors "snapshot \`${_line}@${_sn_name}' already exists"
                fi
            done <<EOF2988ee715b2d93fd93bdce23
$(zfs list -H -r -o name "${_sn_ds}")
EOF2988ee715b2d93fd93bdce23
        else
            farray_append _errors "dataset for snapshots \`${_sn_ds}' does not exist"
        fi
        _idx=$((_idx + 1))
    done

    _jailname=''
    _running_jailname=''
    if [ -n "${_directory}" ]; then

        _dir_basename="$(basename "${_directory}")"

        set +e
        _jailname="$(_get_jail_from_path "${_directory}")"
        _tmp=$?
        set -e
        case ${_tmp} in
            0)
                farray_append _errors "Jail \`${_jailname}' is running. Please stop it."
                _running_jailname="${_jailname}"
                ;;
            1)
                farray_append _errors "Cannot determine jail name"
                ;;
            3)
                true
                ;;
            2)
                farray_append _errors "Jail \`${_jailname}' is currently yet dying. Please wait."
                ;;
            *)
                farray_append _errors "UNHANDLED RETURN VALUE from _get_jail_from_path()"
                ;;
        esac

        #
        # Check whether additional log sockets are opened at their default
        # locations. Because they hinder proper unmounting of filesystems.
        #
        for _log_sock in /var/run/log /var/run/logpriv ; do
            if [ -S "${_directory}${_log_sock}" ]; then
                farray_append _errors "log socket is open at \`${_directory}${_log_sock}'"
            fi
        done

        # Check whether there are any open files or VM mappings  within the jail.
        if ! _check_no_open_files_from_all_proc "${_directory}" ; then
            farray_append _errors "There are open files or memory mappings within the jail"
        fi

        _dir_mounts="$(_get_mounts_at_directory "${_directory}")"

        #
        # Check preconditions thoroughly!
        #
        # Check that the first item/line is a read-only ZFS mount directly
        # 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 _line <<EOF4tHGCSSf5d7d9cf
${_dir_mounts}
EOF4tHGCSSf5d7d9cf
        [ "${_root_mountpoint}" != "${_directory}" ] && farray_append _errors "found root mountpoint does not match given directory"
        [ "${_root_type}" != "zfs" ] && farray_append _errors "root mountpoint is not from a ZFS dataset"
        _root_readonly="$(zfs get -H -o value readonly "${_root_dataset}")"
        [ "${_root_readonly}" != "on" ] &&  farray_append _errors "the root dataset is not mounted read-only"
        _root_origin="$(zfs get -H -o value origin "${_root_dataset}")"
        if [ -n "${_opt_old_origin}" ]; then
            [ "${_opt_old_origin}" != "${_root_origin}" ] && farray_append _errors "origin mismatch"
        else
            [ "${_root_origin}" = '-' ] &&  farray_append _errors "the root dataset is not a ZFS clone"
        fi
        #
        # 1. Check for open files on all the mounted filesystems.
        # 2. Check for mounted filesystems that cannot re-mounted successfuly
        #    because the fstab does not contain all the needed informations
        #    (e.g. unionfs).
        # 3. If snapshots are requested check that they are related somehow to
        #    mounted filesystems.
        #
        _sn_ds_related=''
        while IFS=$'\t' read -r _mnt_device _mnt_mountpoint _mnt_type _mnt_options _line; do
            if ! _check_no_open_files_on_filesystem "${_mnt_mountpoint}" ; then
                farray_append _errors "There are open files or memory mapping on file system \`${_mnt_mountpoint}'"
            fi
            case "${_mnt_type}" in
                unionfs)
                    farray_append _errors "A \`${_mnt_type}' filesystem is mounted at \`${_mnt_mountpoint}' which cannot re-mounted properly"
                    ;;
                *)
                    true
                    ;;
            esac
            _idx=1
            while falist_tryget_key_at_index _sn_ds _opt_snapshots ${_idx}; do
                case "${_mnt_device}" in
                    "${_sn_ds}")
                        _sn_ds_related="yes"
                        ;;
                    "${_sn_ds}"/*)
                        _sn_ds_related="yes"
                        ;;
                    *)
                        ;;
                esac
                _idx=$((_idx + 1))
            done
        done <<EOF4tHGCAASL775f9f320205
${_dir_mounts}
EOF4tHGCAASL775f9f320205
    fi
    if falist_istrue _opt_snapshots; then
        if ! checkyes _sn_ds_related; then
            farray_append _warnings "snapshot datasets and mounted datasets are not related"
        fi
    fi

    #
    # Check whether conflicts remain from previous update, aborting.
    # This would result in errors when running etcupdate.
    #
    if [ -n "${_directory}" ]; then
        _etcupdate_status=''
        if [ -n "${_running_jailname}" ]; then
            _etcupdate_status="$(/usr/sbin/jexec -l -U root -- "${_running_jailname}" /usr/sbin/etcupdate status  2>&1 || true)"
        elif [ -d "${_directory}" ]; then
            _etcupdate_status="$(LC_ALL=C.UTF-8 /usr/sbin/etcupdate status -D "${_directory}" 2>&1 || true)"
        fi
        [ -n "${_etcupdate_status}" ] && farray_append _errors "Unresolved conflicts from last update. Please run \"etcupdate resolve\" first."
    fi

    if farray_istrue _errors; then
        _print_check_errors _errors
        _rc=1
    fi
    # Warnings do not influence the return code
    _print_check_warnings _warnings

    farray_destroy _errors
    farray_destroy _warnings
    falist_destroy _opt_snapshots
    return ${_rc}
}


#:
#: Implement the "freebsd-update" command for a thin jail
#:
#: .. note:: FreeBSD's :command:`etcupdate` also executes
#:           :command:`certctl rehash` if certs are to be added or removed!
#:
command_freebsd_update() {
    local _directory _new_origin _etcupdate_tarball
    local _opt_keep _opt_old_origin _opt_snapshots

    local _res _jailname _dir_mounts _dir_fn_fstab _dir_fn_fstab2
    local _dir_basename _dir_fn_tldir
    local _root_dataset _root_mountpoint _root_type _root_options
    local _mnt_device _mnt_mountpoint _mnt_type _mnt_options
    local _idx _sn_ds _sn_name
    local _clone_extra_props _canmount_prop
    local _line _opt
    local _root_readonly _root_origin
    local _u_tmpdir
    local _add_log_sock

    _opt_snapshots=''
    falist_create _opt_snapshots
    _opt_keep="no"
    _opt_old_origin=""
    while getopts "R:ko:" _opt ; do
        case "${_opt}" in
            R)
                case "${OPTARG}" in
                    *?\@?*)
                        #
                        # Split in two parts: dataset hierarchy and the name of the
                        # snapshot
                        #
                        falist_set _opt_snapshots "${OPTARG%%@*}" "${OPTARG#*@}"
                        ;;
                    *)
                        err "argument \`${OPTARG}' is not a snapshot name"
                        return 1
                        ;;
                    esac
                ;;
            k)
                _opt_keep="yes"
                ;;
            o)
                _opt_old_origin="${OPTARG}"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    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; 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 2>/dev/null || { echo "ERROR: ZFS dataset snapshot for the new origin \`${_new_origin}' does not exist" 1>&2; return 1; }
    if [ -n "${_etcupdate_tarball}" ]; then
        [ -r "${_etcupdate_tarball}" ] || { echo "ERROR: given etcupdate tarball does not exist and/or is not readable" 1>&2; return 1; }
    fi

    # Check snapshotting
    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        if zfs get -H -o value name "${_sn_ds}" >/dev/null 2>/dev/null; then
            # yes dataset exists: check that snapshots do not exist
            while IFS=$'\t' read -r _line; do
                if zfs get -H -o value name "${_line}@${_sn_name}" >/dev/null 2>/dev/null; then
                    err "snapshot \`${_line}@${_sn_name}' already exists"
                    return 1
                fi
            done <<EOF2988ee715b2d93fd93bdce23
$(zfs list -H -r -o name "${_sn_ds}")
EOF2988ee715b2d93fd93bdce23
        else
            err "dataset for snapshots \`${_sn_ds}' does not exist"
            return 1
        fi
        _idx=$((_idx + 1))
    done

    _dir_basename="$(basename "${_directory}")"

    set +e
    _jailname="$(_get_jail_from_path "${_directory}")"
    _res=$?
    set -e
    case ${_res} in
        0)
            err "Please stop the \`${_jailname}' jail"
            return 1
            ;;
        1)
            return 1
            ;;
        2)
            err "Jail \`${_jailname}' is currently yet dying"
            return 1
            ;;
        3)
            true
            ;;
        *)
            return ${_res}
            ;;
    esac

    #
    # Check whether additional log sockets are opened at their default
    # locations. Because they hinder proper unmounting of filesystems.
    #
    for _add_log_sock in /var/run/log /var/run/logpriv ; do
        if [ -S "${_directory}${_add_log_sock}" ]; then
            echo "ERROR: additional log socket is open at \`${_directory}${_add_log_sock}'" >&2
            return 1
        fi
    done
    # Check whether there are any open files or VM mappings  within the jail.
    if ! _check_no_open_files_from_all_proc "${_directory}" ; then
        err "There are open files or memory mappings within the jail"
        return 1
    fi

    _dir_mounts="$(_get_mounts_at_directory "${_directory}")"

    #
    # Check preconditions thoroughly!
    #
    # Check that the first item/line is a read-only ZFS mount directly
    # 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 _line <<EOF4tHGCSSf5d7d9cf
${_dir_mounts}
EOF4tHGCSSf5d7d9cf
    [ "${_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 get -H -o value readonly "${_root_dataset}")"
    [ "${_root_readonly}" != "on" ] &&  { echo "ERROR: the root dataset is not mounted read-only" 1>&2; return 1; }
    _root_origin="$(zfs get -H -o value origin "${_root_dataset}")"
    if [ -n "${_opt_old_origin}" ]; then
        [ "${_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; return 1; }
    fi
    #
    # 1. Check for open files on all the mounted filesystems.
    # 2. Check for mounted filesystems that cannot re-mounted successfuly
    #    because the fstab does not contain all the needed informations
    #    (e.g. unionfs).
    #
    while IFS=$'\t' read -r _mnt_device _mnt_mountpoint _mnt_type _mnt_options _line; do
        if ! _check_no_open_files_on_filesystem "${_mnt_mountpoint}" ; then
            err "There are open files or memory mapping on file system \`${_mnt_mountpoint}'"
            return 1
        fi
        case "${_mnt_type}" in
            unionfs)
                err "A \`${_mnt_type}' filesystem is mounted at \`${_mnt_mountpoint}' which cannot re-mounted properly"
                return 1
                ;;
            *)
                true
                ;;
        esac
    done <<EOF4tHGCAASLfafbf1b5
${_dir_mounts}
EOF4tHGCAASLfafbf1b5

    # 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}")"

    _u_tmpdir="$(env TMPDIR=/var/tmp mktemp -d -t ftjail_"${_dir_basename}")"
    [ -z "${_u_tmpdir}" ] && { echo "ERROR: cannot create unique temp dir" 1>&2; return 1; }
    # The fstab that is corrently mounted at relevant locations (normalized)
    _dir_fn_fstab="${_u_tmpdir}/fstab"
    # The very same fstab -- but with spaces replaced by \040
    _dir_fn_fstab2="${_u_tmpdir}/fstab2"
    printf '%s' "${_dir_mounts}" >"${_dir_fn_fstab}"
    # Replace all spaces with a sequence that is understood by mount
    LC_ALL=C /usr/bin/sed -e 's/ /\\040/g' <"${_dir_fn_fstab}" >"${_dir_fn_fstab2}"
    _dir_fn_tldir="${_u_tmpdir}/tldirs"
    LC_ALL=C /usr/bin/find "${_directory}" -depth 1 -type d 2>/dev/null | LC_ALL=C /usr/bin/sort >>"${_dir_fn_tldir}"

    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        echo "Creating snapshot \`${_sn_ds}@${_sn_name}'"
        zfs snapshot -r "${_sn_ds}@${_sn_name}" || { err "cannot snapshot \`${_sn_ds}@${_sn_name}'"; return 1; }
        _idx=$((_idx + 1))
    done

    # Unmount in reverse order: unmount can do it for us
    echo "Unmounting all datasets mounted at \`${_directory}'"
    /sbin/umount -a -F "${_dir_fn_fstab2}" -v

    #
    # XXX TBD: Hooks to create some new top-level dirs (/srv /proc et
    #          al.)  if needed: clone RW, mount, make the dirs,
    #          umount, make the clone RO and continue "normally" by
    #          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}"
    /sbin/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"
    while IFS='' read -r _line ; do
        if [ ! -d "${_line}" ]; then
            echo "Recreating top-level directory: ${_line}"
            mkdir "${_line}"
        fi
    done < "${_dir_fn_tldir}"
    echo "Unmounting the new root dataset"
    /sbin/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}"
    /sbin/mount -a -F "${_dir_fn_fstab2}" -v

    # Update and/or merge configs
    if [ -n "${_etcupdate_tarball}" ]; then
        # Note: Check for readability has been done above
        echo "Calling etcupdate for DESTDIR=${_directory}"
        LC_ALL=C.UTF-8 /usr/sbin/etcupdate -D "${_directory}" -t "${_etcupdate_tarball}"
    fi

    echo "Checking status of etcupdate at DESTDIR=${_directory}"
    LC_ALL=C.UTF-8 /usr/sbin/etcupdate status -D "${_directory}" || true

    if [ "${_opt_keep}" != "yes" ]; then
        echo "Cleaning up..."""
        [ -n "${_u_tmpdir}" ] && [ -d "${_u_tmpdir}" ] && rm -rvf "${_u_tmpdir}"
    fi
    echo "Done."

    falist_destroy _opt_snapshots
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case "${_opt}" in
        V)
            printf 'ftjail %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
            ;;
        h)
            echo "${USAGE}"
            exit 0
            ;;
        \?)
            exit 2;
            ;;
        *)
            echo "ERROR: option handling failed" 1>&2
            exit 2
            ;;
    esac
done

#
# Reset the Shell's option handling system to prepare for handling
# command-local options.
#
shift $((OPTIND-1))
OPTIND=1

test $# -gt 0 || { echo "ERROR: no command given" 1>&2; exit 2; }

command="$1"
shift

case "${command}" in
    datasets-tmpl)
        command_datasets_tmpl "$@"
        ;;
    mount-tmpl)
        command_mount_tmpl "$@"
        ;;
    umount-tmpl|unmount-tmpl)
        command_umount_tmpl "$@"
        ;;
    interlink-tmpl)
        command_interlink_tmpl "$@"
        ;;
    populate-tmpl)
        command_populate_tmpl "$@"
        ;;
    snapshot-tmpl)
        command_snapshot_tmpl "$@"
        ;;
    copy-skel)
        command_copy_skel "$@"
        ;;
    build-etcupdate-current-tmpl)
        command_build_etcupdate_current_tmpl "$@"
        ;;
    configure)
        echo "ERROR: use \`fjail configure' instead" 1>&2;
        exit 2
        ;;
    check-freebsd-update)
        command_check_freebsd_update "$@"
        ;;
    freebsd-update)
        command_freebsd_update "$@"
        ;;
    *)
        fatal 2 "unknown command \`${command}'"
        ;;
esac