view sbin/ftjail @ 286:258a1dfd52eb

Separate manual page documentation and "normal" HTML documentation. No "orphaned" source files should be there now. Also the HTML theme for the is changed from "alabaster" to "agogo".
author Franz Glasner <fzglas.hg@dom66.de>
date Sun, 18 Sep 2022 10:25:55 +0200
parents a6c9eb25ae81
children d40e6e40c315
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 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:        @(#)@@PKGORIGIN@@ $HGid$
#:

set -eu

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 [OPTIONS] PARENT-BASE PARENT-SKELETON NAME

    Create the ZFS template datasets, i.e. the ro base and the rw
    skeleton to be used within thin jails jails

    PARENT-BASE and PARENT-SKELETON must exist already and NAME must
    not exist.

    The datasets will not be mounted.

    -L        Create dataset properties optimized for employing a
              "skeleton" subdirectory
    -P        Create dataset properties optimized for direct mounts
              of skeleton children over an already mounted base

  mount-tmpl [ OPTIONS ] BASE-RO SKELETON-RW MOUNTPOINT

    Canonically mount the RO base and the RW skeleton into MOUNTPOINT and
    MOUNTPOINT/skeleton

    -L        Mount the skeleton into a "skeleton" subdirectory
    -P        Mount the skeleton directly over the base
    -n        Do not really mount but show what would be mounted where
    -u        Alias of -n

  umount-tmpl BASE-RO SKELETON-RW

    Unmount mounted datasets BASE-RO and SKELETON-RW

  interlink-tmpl MOUNTPOINT

    Create symbolic links between the RO base and the RW skeleton.
    Base and skeleton must be canonically mounted already.

  populate [ OPTIONS ] MOUNTPOINT BASETXZ

    Populate the directory in MOUNTPOINT with the base system in BASETXZ

    -L        Populate having a "skeleton" subdirectory within the mountpoint
    -P        Populate directly into the mountpoint

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

  copy-skel [ OPTIONS ] SOURCE-DS SNAPSHOT-NAME TARGET-DS

    -A        Set "canmount=noauto" for all datasets in the target dataset
    -L        Copy dataset properties optimized for employing a
              "skeleton" subdirectory
    -M MOUNTPOINT   Set the "mountpoint" property for the TARGET-DS to
              MOUNTPOINT (children will inherit it)
    -P        Copy dataset properties optimized for direct mounts
              of skeleton children over an already mounted base
    -u        Do not mount the target dataset automatically

ENVIRONMENT:

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

DESCRIPTION:

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

# Reset to standard umask
umask 0022


#:
#: Ensure that no command line options are given
#:
#: Args:
#:   $@:
#:
#: Exit:
#:   2: If formally `getopts` finds options in "$@"
#:
#: Return:
#:   - 0
#:
_ensure_no_options() {
    local _opt

    while getopts ":" _opt ; do
        [ "${_opt}" = '?' ] && { echo "ERROR: no option allowed" 1>&2; exit 2; }
    done
    return 0
}


_get_dataset_for_mountpoint() {
    : 'Use `mount -t zfs -p` to determine the ZFS dataset for a given mountpoint.

    '
    local _mountpoint

    local _ds _mount _rest

    _mountpoint="$1"

    mount -t zfs -p \
    | {
        while IFS=' '$'\t' read -r _ds _mount _rest ; do
            if [ "$_mount" = "$_mountpoint" ]; then
                echo "${_ds}"
                return 0
            fi
        done
        return 1
    }
}


#
# 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
    #
    for _child in etc home root tmp usr/local var ; 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

    local _opt _dir

    _opt_symlink=""

    while getopts "LP" _opt ; do
        case ${_opt} in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            \?)
                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-}"

    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

    find "${_mp}/boot" -type f -delete || true
}


#
# _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}"
                            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}")" || \
        { echo "ERROR: dataset not found" >&2; return 1; }

    mount -t zfs -p \
    | grep -E "^${_dsname}(/|\s)" \
    | sort -n -r \
    | {
        while IFS=' '$'\t' read -r _name _mp _rest ; do
            echo "Umounting ${_name} on ${_mp}"
            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

    local _opt _name _relative_name  _root_canmount

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

    while getopts "ALM:Pu" _opt ; do
        case ${_opt} in
            A)
                _opt_canmount="-o canmount=noauto"
                ;;
            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; }

    _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}" "${_ds_target}${_relative_name}"
                    else
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v ${_root_canmount} -x mountpoint "${_ds_target}${_relative_name}"
                    fi
                else
                    # child
                    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}"
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case ${_opt} in
        V)
            printf 'ftjail v%s (rv:%s)\n' "${VERSION}" '@@HGREVISION@@'
            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 "$@"
        ;;
    configure)
        echo "ERROR: use \`fjail configure' instead" 1>&2;
        exit 2
        ;;
    *)
        echo "ERROR: unknown command \`${command}'" 1>&2
        exit 2
        ;;
esac