Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/ftjail @ 443:071f24359eef
Move "_ensure_no_optios()" into common.subr
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Fri, 03 May 2024 09:41:38 +0200 |
| parents | 9c3b1966ba91 |
| children | 84e43d1bd128 |
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 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 DIRECTORY BASETXZ 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 freebsd-update [-k] [-o OLD-ORIGIN] 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="$(dirname "$0")"/../share/local-bsdtools . "${_p_datadir}/common.subr" # Reset to standard umask umask 0022 _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 } } #: #: Search for a running jail where it's "path" points to a given location #: #: Args: #: $1: the location to search for #: #: Output (stdout): #: The name if the jail with a "path" that is equal to the input param. #: Nothing if a jail is not found. #: #: Return: #: - 0: if a running jail is found #: - 1: error #: - 2: no running jail found #: - 3: jail found but currently dying #: _get_jail_from_path() { local _location local _name _path _dying _location="${1-}" [ -z "${_location}" ] && { echo "ERROR: no mountpoint given" 1>&2; return 1; } jls -d name path dying \ | { while IFS=' '$'\t' read -r _name _path _dying ; do if [ "${_path}" = "${_location}" ]; then if [ "${_dying}" != "false" ]; then echo "Jail \`${_name}' is currently dying" 1>&2 return 3 fi echo "${_name}" return 0 fi done return 2 } } #: #: Search for mounts and sub-mounts at a given directory. #: #: The output is sorted by the mountpoint. #: #: Args: #: $1: the directory where to start for mounts and sub-mounts #: #: Output (stdout): #: The sorted list (lines) of mounts in :manpage:`fstab(5)` format. #: This list may be empty. #: #: Exit: #: 1: on fatal errors (usage et al.) #: #: Important: #: The input directory **must** be an absolute path. #: _get_mounts_at_directory() { local _directory local _fstab _directory=${1-} case "${_directory}" in */) echo "ERROR: a trailing slash in directory name given" 1>&2; exit 1; ;; /*) : ;; '') echo "ERROR: no directory given" 1>&2; exit 1; ;; *) echo "ERROR: directory must be an absolute path" 1>&2; exit 1; ;; esac _fstab="$(mount -p | awk -v pa1="^${_directory}\$" -v pa2="^${_directory}/" '($2 ~ pa1) || ($2 ~ pa2 ) { print; }' | sort -k3)" echo "${_fstab}" } # # 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 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 _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) 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 #: #: .. 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 local _res _jailname _dir_mounts _dir_fn_fstab _dir_basename _dir_fn_tldir local _root_dataset _root_mountpoint _root_type _root_options local _clone_extra_props _canmount_prop local _line _opt local _root_readonly _root_origin local _u_tmpdir local _add_log_sock _opt_keep="no" _opt_old_origin="" while getopts "ko:" _opt ; do case ${_opt} in 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 || { 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})" set +e _jailname=$(_get_jail_from_path "${_directory}")ยด _res=$? set -e if [ ${_res} -ne 2 ] ; then if [ ${_res} -ne 0 ] ; then return ${_res} else echo "ERROR: Please stop the \`${_jailname}' jail" >&2 return 1 fi fi # # 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 within the jail. # # "procstat file" also lists fifo, socket, message queue, kgueue et al. # file types. # # Note that procstat places extra whitespace at the end of lines sometimes. # # if procstat -a file | egrep '['$'\t '']+'"${_directory}"'(/|(['$'\t '']*)$)' ; then echo "ERROR: There are open files within the jail" >&2 return 1 fi # The same for memory mappings if procstat -a vm | egrep '['$'\t '']+'"${_directory}"'(/|(['$'\t '']*)$)' ; then echo "ERROR: There are open memory mappings within the jail" >&2 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 <<EOF4tHGCSS ${_dir_mounts} EOF4tHGCSS [ "${_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; 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; return 1; } else [ "${_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}")" _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; } _dir_fn_fstab="${_u_tmpdir}/fstab" echo -n "${_dir_mounts}" >>"${_dir_fn_fstab}" _dir_fn_tldir="${_u_tmpdir}/tldirs" find "${_directory}" -depth 1 -type d 2>/dev/null | sort >>"${_dir_fn_tldir}" # Unmount in reverse order: unmount can do it for us 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 # 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}" 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 and/or merge 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 "${_u_tmpdir}" ] && [ -d "${_u_tmpdir}" ] && rm -rvf "${_u_tmpdir}" fi echo "Done." } # # 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 ;; freebsd-update) command_freebsd_update "$@" ;; *) echo "ERROR: unknown command \`${command}'" 1>&2 exit 2 ;; esac
