Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
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
