Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/ftjail @ 782:11f3101c1980
farray.sh: Implemented plain Insertionsort.
Using the optimized algorithm already used in Shell sort, just without manx
gaps and using only the fixed last step 1.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Sat, 26 Oct 2024 18:35:37 +0200 |
| parents | 8f1583faf9ea |
| children | e2f262ec2bf4 |
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_release _errors farray_release _warnings falist_release _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_release _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
