Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/fjail @ 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 | 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
