Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/fjail @ 793:3b5a59b0840d
common.subr: Shell variable quoting in eval in checkyesno() and checkyes()
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Wed, 30 Oct 2024 14:15:59 +0100 |
| parents | 70b16773945f |
| children | e2f262ec2bf4 |
line wrap: on
line source
#!/bin/sh # -*- indent-tabs-mode: nil; -*- #: #: A very minimal BSD Jail management tool. #: #: :Author: Franz Glasner #: :Copyright: (c) 2019-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@@ #: # Module-level setting # shellcheck disable=SC2129 : # separator for shellcheck: no module-level directives below set -eu # shellcheck disable=SC2034 # VERSION appears unused VERSION='@@VERSION@@' USAGE=' USAGE: fjail [ 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 [OPTIONS] PARENT CHILD Create ZFS datasets to be used within a jail PARENT must exist already and CHILD must not exist. -A Set "canmount=noauto" for datasets -o Do not create var/empty as read-only dataset but with normal settings -s Also create a dataset for freebsd-update data files -t Create a more tiny set of datasets -T Create only an extra tiny set of datasets -u Do not automatically mount newly created datasets mount See sibling tool `fzfs'"'"' umount See sibling tool `fzfs'"'"' privs MOUNTPOINT Adjust some Unix privileges to mounted jail datasets populate MOUNTPOINT BASETXZ Populate the jail directory in MOUNTPOINT with the base system in BASETXZ configure [OPTIONS] MOUNTPOINT Configure some basic parts of the system at MOUNTPOINT: disable root password, syslog and other basic configuration settings Also handle thin jails by checking whether "etc" is a symlink to "skeleton/etc". -d Temporarily mount a devfs filesystem to MOUNTPOINT/dev hostid Print proposals for a hostuuid and hostid copy [OPTIONS] SOURCE-DATASET DEST-DATASET Copy a tree of ZFS datasets with "zfs send -R" and "zfs receive". Note that the destination dataset must not exist already. -r Copy the datasets with the -Lec options (aka "raw") -u Do not automatically mount received datasets freebsd-update [OPTIONS] DIRECTORY OPERATIONS... -c CURRENTLY-RUNNING Assume the systen given in CURRENTLY-RUNNING is installed/running at given DIRECTORY ENVIRONMENT: All environment variables that affect "zfs" are effective also. DESCRIPTION: All commands with the exception of "populate" require ZFS as filesystem. ' _p_datadir='@@DATADIR@@' [ "${_p_datadir#@@DATADIR}" = '@@' ] && _p_datadir="$(dirname "$0")"/../share/local-bsdtools . "${_p_datadir}/common.subr" # Reset to standard umask umask 0022 #: #: Check whether a FreeBSD version at a given location matches the userland #: version of the host where the current process run. #: #: Args: #: $1: the location where to check for #: $2: an optional reference FreeBSD version to compare to (default is the #: version of the host) #: #: Returns: #: 0: if the userland versions match, 1 otherwise #: #: Exit: #: 1: on fatal errors (e.g. /bin/freebsd-version not found or errors) #: _has_same_userland_version() { local directory ref_version local _directory_version directory="$1" ref_version="${2:-}" if [ -z "${ref_version}" ]; then ref_version=$(/bin/freebsd-version -u) || exit 1 fi _directory_version=$(chroot -- "${directory}" /bin/freebsd-version -u) || exit 1 if [ "${ref_version%%-*}" = "${_directory_version%%-*}" ]; then return 0 fi return 1 } # # "datasets" -- create the ZFS dataset tree # # command_datasets [ -u ] parent-dataset child-dataset # # -u do not automatically mount newly created datasets # command_datasets() { # parent ZFS dataset -- child ZFS dataset name local _pds _cds # and its mount point local _pmp _get # full name of the dataset local _ds # dynamic ZFS options -- create cache for freebsd-update -- use a more tiny layout local _zfsopts _fbsdupdate _tiny _zfsnoauto _varempty_ro _zfsopts="" _fbsdupdate="" _tiny="no" _zfsnoauto="" _varempty_ro="-o readonly=on" while getopts "oustAT" _opt ; do case ${_opt} in A) # # set canmount=noauto where otherwise canmount=on would have been set # or inherited # _zfsnoauto="-o canmount=noauto" ;; o) # Clear out the default setting of creating var/empty as read-only dataset _varempty_ro="" ;; t) # use a more tiny layout _tiny="yes" ;; T) # extra tiny layout _tiny="extra" ;; u) # do not mount newly created datasets _zfsopts="${_zfsopts} -u" ;; s) # create also a dataset for freebsd-update data _fbsdupdate="yes" ;; \?|:) return 2; ;; esac done shift $((OPTIND-1)) OPTIND=1 _pds="$1" if [ -z "${_pds}" ]; then echo "ERROR: no parent dataset given" >&2 return 2 fi _pmp="$(zfs list -H -o mountpoint -t filesystem "${_pds}" 2>/dev/null)" || { err "dataset \`${_pds}' does not exist"; return 1; } case "${_pmp}" in none) err "dataset \`${_pds}' has no mountpoint" return 1 ;; legacy) err "dataset \`${_pds}' has a \`${_pmp}' mountpoint" return 1 ;; *) # VOID ;; esac _cds="$2" if [ -z "${_cds}" ]; then echo "ERROR: no child dataset given" >&2 return 2 fi _ds="${_pds}/${_cds}" echo "Resulting new root dataset is \`${_ds}' at mountpoint \`${_pmp}/${_cds}'" if zfs list -H -o mountpoint -t filesystem "${_ds}" >/dev/null 2>/dev/null; then echo "ERROR: dataset \`${_ds}' does already exist" >&2 return 1 fi # # NOTE: For BEs these directory will be *excluded* from the BE # # /tmp # /usr/home # /usr/ports # /usr/src # /var/audit # /var/crash # /var/log # /var/mail # /var/tmp # zfs create ${_zfsopts} ${_zfsnoauto} -o atime=off "${_ds}" zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off "${_ds}/tmp" if [ "${_tiny}" != "extra" ]; then if [ "${_tiny}" = "yes" ]; then zfs create ${_zfsopts} -o canmount=off "${_ds}/usr" else zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/usr" fi zfs create ${_zfsopts} ${_zfsnoauto} -o setuid=off "${_ds}/usr/home" zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/usr/local" fi if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then zfs create ${_zfsopts} -o canmount=off "${_ds}/var" else zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/var" fi if [ "${_tiny}" != "extra" ]; then zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/audit" zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/cache" zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off "${_ds}/var/cache/pkg" zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o compression=off "${_ds}/var/crash" fi if [ "$_fbsdupdate" = "yes" ]; then if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then zfs create ${_zfsopts} -o canmount=off -o exec=off -o setuid=off "${_ds}/var/db" else zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/db" fi zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off "${_ds}/var/db/freebsd-update" fi zfs create ${_zfsopts} ${_zfsnoauto} ${_varempty_ro} -o exec=off -o setuid=off "${_ds}/var/empty" zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata "${_ds}/var/log" zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o atime=on "${_ds}/var/mail" zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o exec=off -o setuid=off -o compression=off -o primarycache=all "${_ds}/var/run" zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off "${_ds}/var/tmp" } # # "populate" -- populate the datasets with content from a FreeBSD base.txz # # command_populate mountpoint basetxz # command_populate() { # MOUNTPOINT -- base.txz local _mp _basetxz _mp="$1" _basetxz="$2" if [ -z "${_mp}" ]; then echo "ERROR: no mountpoint given" >&2 return 2 fi if [ -z "${_basetxz}" ]; then echo "ERROR: no base.txz given" >&2 return 2 fi if [ ! -d "${_mp}" ]; then echo "ERROR: mountpoint \`${_mp}' does not exist" >&2 return 1 fi if [ ! -r "${_basetxz}" ]; then echo "ERROR: file \`${_basetxz}' is not readable" >&2 return 1 fi # # Handle /var/empty separately later: could be already there and # mounted read-only. # tar -C "${_mp}" --exclude=./var/empty -xJp -f "${_basetxz}" || { echo "ERROR: tar encountered errors" >&2; return 1; } if [ -d "${_mp}/var/empty" ]; then # # If /var/empty exists already try to extract with changing the # flags (e.g. `schg'). But be ignore errors here. # tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "tar warnings for handling ./var/empty ignored because ./var/empty exists already" >&2; } else # Just extract /var/empty normally tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "ERROR: tar encountered errors" >&2; return 1; } fi find "${_mp}/boot" -type f -delete } # # "hostid" -- print a proposal for hostid/hostuuid settings in a jail # # command_hostid # command_hostid() { # # hostid and hostuuid should be set (at least for consistency ressons) # in vnet jails (see /etc/rc.d/hostid and /etc/rc.d/hostid_save). # They can be set in the jail.conf. # Print one here that can be pasted into the jail.conf if needed. # # hostid and hostuuid for non-vnet jails are inherited from the parent/host. # # See also /etc/rc.d/hostid and /etc/rc.d/hostid_save. # local _new_hostuuid _new_hostid _new_hostuuid="$(uuidgen)" _new_hostid="$(printf "%s" "${_new_hostuuid}" | /sbin/md5)" _new_hostid="0x${_new_hostid%%????????????????????????}" echo "Proposed hostuuid/hostid:" echo " host.hostuuid = \"${_new_hostuuid}\";" echo " host.hostid = $((_new_hostid));" #echo " host.hostid = ${_new_hostid};" } # # "configure" -- configure the mountpoint # # command_configure mountpoint # command_configure() { # mountpoint local _mp local _opt_devfs local _pcl _umount_devfs _umount_devfs="" _opt_devfs="" while getopts "d" _opt ; do case ${_opt} in d) _opt_devfs="yes" ;; \?) return 2; ;; *) echo "ERROR: option handling failed" 1>&2 return 2 ;; esac done shift $((OPTIND-1)) OPTIND=1 _mp="$1" if [ -z "${_mp}" ]; then echo "ERROR: no mountpoint given" >&2 return 2 fi if [ ! -d "${_mp}" ]; then echo "ERROR: mountpoint \`${_mp}' does not exist" >&2 return 1 fi if [ -c "${_mp}/dev/null" ]; then if [ "${_opt_devfs}" = "yes" ]; then echo "WARNING: devfs is already mounted - mounting skipped" fi else if [ "${_opt_devfs}" = "yes" ]; then echo "Mounting devfs" /sbin/mount -t devfs devfs "${_mp}/dev" _umount_devfs="yes" else echo "ERROR: a working devfs is needed at \`{_mp}/dev' (use \`-d')" >&2 return 1 fi fi # Deactive the by default empty root password pw -R "${_mp}" usermod -w no -n root if [ -f "${_mp}/etc/defaults/rc.conf" ]; then sysrc -R "${_mp}" sendmail_enable=NONE sysrc -R "${_mp}" clear_tmp_enable=YES sysrc -R "${_mp}" clear_tmp_X=NO sysrc -R "${_mp}" syslogd_flags=-ss sysrc -R "${_mp}" bsdstats_enable=NO # no automatic BSD stats when booting (for periodic see below) else echo "WARNING: No \"${_mp}/etc/defaults/rc.conf\": not configuring \"rc.conf\"" fi if [ -f "${_mp}/usr/share/zoneinfo/Europe/Berlin" ]; then # Timezone to CET if [ ! -f "${_mp}/etc/localtime" ]; then echo "Setting timezone to Europe/Berlin" # Handle thin jails automatically (but check expectations very strictly) if [ \( -L "${_mp}/etc" \) -a \( "$(readlink "${_mp}/etc")" = "skeleton/etc" \) ]; then ln -s ../../usr/share/zoneinfo/Europe/Berlin "${_mp}/etc/localtime" else ln -s ../usr/share/zoneinfo/Europe/Berlin "${_mp}/etc/localtime" fi echo "Europe/Berlin" > "${_mp}/var/db/zoneinfo" else echo "WARNING: \"${_mp}/etc/localtime\" exists already -- not changed" fi else echo "WARNING: No timezone data file found at \"${_mp}/usr/share/zoneinfo/Europe/Berlin\": skipping timezone setup" fi # resolv.conf if [ ! -f "${_mp}/etc/resolv.conf" ]; then echo "Copying the host's resolv.conf into the jail" cp -p /etc/resolv.conf "${_mp}/etc/resolv.conf" else echo "WARNING: \"${_mp}/etc/resolv.conf\" exists already -- not changed" fi # Call newaliases within the jail echo "Calling \"newaliases\"" chroot -- "${_mp}" /usr/bin/newaliases _pcl="${_mp}/etc/periodic.conf.local" if [ ! -f "${_pcl}" ]; then echo "Adjusting periodic.conf.local" echo "Periodic script log into files ..." echo "daily_output=\"/var/log/daily.log\"" > "${_pcl}" echo "weekly_output=\"/var/log/weekly.log\"" >> "${_pcl}" echo "monthly_output=\"/var/log/monthly.log\"" >> "${_pcl}" echo "daily_status_security_output=\"/var/log/security\"" >> "${_pcl}" echo "weekly_status_security_output=\"/var/log/security\"" >> "${_pcl}" echo "monthly_status_security_output=\"/var/log/security\"" >> "${_pcl}" echo "security_status_chkmounts_enable=\"NO\"" >> "${_pcl}" echo "Disable some scripts that are enabled by default ..." echo "daily_ntpd_leapfile_enable=\"NO\"" >> "${_pcl}" echo "daily_status_zfs_zpool_list_enable=\"NO\"" >> "${_pcl}" echo "daily_status_disks_enable=\"NO\"" >> "${_pcl}" echo "daily_status_uptime_enable=\"NO\"" >> "${_pcl}" # # bsdstats # echo "" >> "${_pcl}" echo "#" >> "${_pcl}" echo "# bsdstats" >> "${_pcl}" echo "#" >> "${_pcl}" # Disabled by default but make it more explicit echo "monthly_statistics_enable=\"NO\"" >> "${_pcl}" # If enabled: because we are in a jail there are no devices echo "monthly_statistics_report_devices=\"NO\"" >> "${_pcl}" # If enabled: report ports echo "monthly_statistics_report_ports=\"YES\"" >> "${_pcl}" echo "Creating system logfiles that are marked for automatic creation ..." chroot -- "${_mp}" /usr/sbin/newsyslog -CN else echo "WARNING: \"${_pcl}\" exists already -- not changed" fi command_hostid if [ "${_umount_devfs}" = "yes" ]; then echo "Unmounting devfs" /sbin/umount "${_mp}/dev" fi } # # "copy" -- ZFS copy of datasets # # command_copy source-dataset destination-dataset # command_copy() { # source dataset -- destination dataset local _source _dest # dynamic ZFS options -- ZFS copy options local _zfsopts _zfscopyopts _zfsopts="" _zfscopyopts="" while getopts "ru" _opt ; do case ${_opt} in r) # Use raw datasets _zfscopyopts="-Lec" ;; u) # do not mount newly created datasets _zfsopts="${_zfsopts} -u" ;; \?|:) return 2; ;; esac done shift $((OPTIND-1)) OPTIND=1 _source="$1" if [ -z "${_source}" ]; then echo "ERROR: no source dataset given" >&2 return 2 fi _dest="$2" if [ -z "${_dest}" ]; then echo "ERROR: no source dataset given" >&2 return 2 fi zfs send -R ${_zfscopyopts} -n -v "${_source}" || { echo "ERROR: ZFS operation failed in no-op mode" >&2; return 1; } zfs send -R ${_zfscopyopts} "${_source}" | zfs receive ${_zfsopts} "${_dest}" || { echo "ERROR: ZFS operation failed" >&2; return 1; } } # # "privs" -- adjust privileges # # To be used when all ZFS datasets are mounted. # command_privs() { # mountpoint local _mp _d _veds _get _vestatus _mp="$1" if [ -z "${_mp}" ]; then echo "ERROR: no mountpoint given" >&2 return 2 fi if [ ! -d "${_mp}" ]; then echo "ERROR: directory \`${_mp}' does not exist" >&2 return 1 fi for _d in tmp var/tmp ; do chmod 01777 "${_mp}/${_d}" done chown root:mail "${_mp}/var/mail" chmod 0775 "${_mp}/var/mail" # # Handle <mountpoint>/var/empty specially: # make it writeable temporarily if it is mounted read-only: # _vestatus="" if _veds="$(_get_zfs_dataset_for_varempty "${_mp}")" ; then _vestatus=$(zfs list -H -o readonly -t filesystem ${_veds} 2>/dev/null) || { echo "ERROR: cannot determine readonly status of ${_mp}/var/empty" >&2; return 1; } if [ "${_vestatus}" = "on" ]; then zfs set readonly=off ${_veds} 1> /dev/null || { echo "ERROR: cannot reset readonly-status of ${_mp}/var/empty" >&2; return 1; } fi fi # Set the access rights and the file flags as given in mtree chmod 0555 "${_mp}/var/empty" || { echo "WARNING: Cannot chmod on var/empty" >&2; } chflags schg "${_mp}/var/empty" || { echo "WARNING: Cannot chflags on var/empty" >&2; } # Reset the read-only status of the mountpoint as it was before if [ "${_vestatus}" = "on" ]; then zfs set readonly=on ${_veds} 1> /dev/null || { echo "ERROR: cannot reactivate readonly-status of ${_mp}/var/empty" >&2; return 1; } fi } #: #: Implement the "freebsd-update" command #: command_freebsd_update() { local directory # + operations ... local opt_currently_running opt_currently_running="" while getopts "c:" _opt ; do case ${_opt} in c) opt_currently_running="$OPTARG" ;; \?|:) return 2; ;; esac done shift $((OPTIND-1)) OPTIND=1 directory="${1-}" [ -z "${directory}" ] && { echo "ERROR: no directory given" 1>&2; return 2; } [ -d "${directory}" ] || { echo "ERROR: directory \`${directory}' does not exist" 1>&2; return 1; } shift if _has_same_userland_version "${directory}" "${opt_currently_running}" ; then if [ -n "${opt_currently_running}" ]; then freebsd-update -b "${directory}" --currently-running "${opt_currently_running}" "$@" else freebsd-update -b "${directory}" "$@" fi else echo "ERROR: Userland version mismatch" 1>&2 return 1 fi } # # Global option handling # while getopts "Vh" _opt ; do case ${_opt} in V) printf 'fjail %s\n' '@@SIMPLEVERSIONSTR@@' exit 0 ;; h) echo "${USAGE}" exit 0 ;; \?) exit 2; ;; *) echo "ERROR: option handling failed" >&2 exit 2 ;; esac done # # Reset the Shell's option handling system to prepare for handling # command-local options. # shift $((OPTIND-1)) OPTIND=1 test $# -gt 0 || { echo "ERROR: no command given" >&2; exit 2; } command="$1" shift case "${command}" in datasets) command_datasets "$@" ;; mount) exec "$(dirname $0)/fzfs" mount "$@" ;; umount|unmount) exec "$(dirname $0)/fzfs" umount "$@" ;; privs) command_privs "$@" ;; populate) command_populate "$@" ;; configure) command_configure "$@" ;; hostid) command_hostid "$@" ;; copy) command_copy "$@" ;; freebsd-update) command_freebsd_update "$@" ;; *) echo "ERROR: unknown command \`${command}'" >&2 exit 2 ;; esac
