Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/ftjail @ 245:61861d36758c
Ensure that no options are really given when no options are allowed
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Sat, 10 Sep 2022 18:16:37 +0200 |
| parents | 6e632f459818 |
| children | bdee72ff7dbd |
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. mount-tmpl [ OPTIONS ] BASE-RO SKELETON-RW MOUNTPOINT Canonically mount the RO base and the RW skeleton into MOUNTPOINT and MOUNTPOINT/skeleton -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 MOUNTPOINT BASETXZ Populate the directory in MOUNTPOINT with the base system in BASETXZ 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 options are given # _ensure_no_options() { local _opt while getopts ":" _opt ; do [ "${_opt}" = '?' ] && { echo "ERROR: no option allowed" 1>&2; exit 2; } done } _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 _dry_run local _ds_base _ensure_no_options "$@" _p_base="${1-}" _name="${2-}" _dry_run="${3-}" 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 [ "${_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 _dry_run local _ds_skel _child _child_zfsopts _ensure_no_options "$@" _p_skel="${1-}" _name="${2-}" _dry_run="${3-}" 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 [ "${_dry_run}" = "yes" ] && return 0 echo "Creating RW skeleton datasets in:" printf "\\t%s\\n" "${_ds_skel}" zfs create -u -o canmount=noauto "${_ds_skel}" 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 _zfsopts local _ds_base _ds_skel _ensure_no_options "$@" _zfsopts="-u -o canmount=noauto" _p_base="${1-}" _p_skel="${2-}" _name="${3-}" # Check preconditions command_datasets_tmpl_base "${_p_base}" "${_name}" "yes" || return command_datasets_tmpl_skel "${_p_skel}" "${_name}" "yes" || return # Really do it command_datasets_tmpl_base "${_p_base}" "${_name}" || return command_datasets_tmpl_skel "${_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 _dir _ensure_no_options "$@" _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 # # Handle /var/empty separately later: could be already there and # mounted read-only. # 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 symlinks here (cd "${_mp}/skeleton/root" && ln -s ../../.profile .profile) || return (cd "${_mp}/skeleton/root" && ln -s ../../.cshrc .cshrc) || return find "${_mp}/boot" -type f -delete || true } # # _do_mount dataset mountpoint dry-run mount-natural # _do_mount() { local _dsname _mountpoint _dry_run _mount_natural local _name _mp _canmount _mounted local _rootds_mountpoint _relative_mp _real_mp _dsname="${1}" _mountpoint="${2}" _dry_run="${3}" _mount_natural="${4}" 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" >&2 return 1 fi fi 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}" >&2; return 1; } echo "Mounting ${_name} on ${_real_mp}" mount -t zfs "${_name}" "${_real_mp}" || return 1 fi ;; *) echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 2>&1 ;; 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 _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 _ds_base="${1-}" _ds_skel="${2-}" _mountpoint="${3-}" _do_mount "${_ds_base}" "${_mountpoint}" "${_opt_dry_run}" "" || return 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 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" 2>&1; return 2; } [ -d "${_mountpoint}" ] || { echo "ERROR: mountpoint \`${_mountpoint}' does not exist" 2>&1; return 1; } [ -d "${_mountpoint}/skeleton" ] || { echo "WARNING: skeleton is not mounted at \`${_mountpoint}/skeleton'" 2>&1; } 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 } # # 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" >&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" >&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 "$@" ;; *) echo "ERROR: unknown command \`${command}'" >&2 exit 2 ;; esac
