Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
diff sbin/ftjail @ 233:f745d3a216a6
Erste Basis für "ftjail": Management von Thin Jails
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 05 Sep 2022 09:45:13 +0200 |
| parents | |
| children | 8682cfa74f6a |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sbin/ftjail Mon Sep 05 09:45:13 2022 +0200 @@ -0,0 +1,468 @@ +#!/bin/sh +# -*- indent-tabs-mode: nil; -*- +: 'A very minimal BSD Thin Jail management tool. + +:Author: Franz Glasner +:Copyright: (c) 2022 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: @(#)@@PKGORIGIN@@ $HGid$ + +' + +set -eu + +VERSION="@@VERSION@@" + +USAGE=' +USAGE: ftjail [ OPTIONS ] COMMAND [ COMMAND OPTIONS ] [ ARG ... ] + +OPTIONS: + + -V Print the program name and version number to stdout and exit + + -h Print this help message to stdout and exit + +COMMANDS: + + datasets [OPTIONS] PARENT-BASE PARENT-SKELETON NAME + + Create ZFS datasets for the ro base and the rw skeleton to be used + within for jails + + PARENT-BASE and PARENT-SKELETON must exist already and NAME must + not exist. + + The datasets will not be mounted. + +ENVIRONMENT: + + All environment variables that affect "zfs" are effective also. + +DESCRIPTION: + + All commands with the exception of "populate" require ZFS as + filesystem. +' + +# Reset to standard umask +umask 0022 + + +_get_dataset_for_mountpoint() { + : 'Use `mount -t zfs -p` to determine the ZFS dataset for a given mountpoint. + + ' + local _mountpoint + local _ds _mount _rest + + _mountpoint="$1" + + mount -t zfs -p \ + | { + while IFS=' '$'\t' read -r _ds _mount _rest ; do + if [ "$_mount" = "$_mountpoint" ]; then + echo "${_ds}" + return 0 + fi + done + return 1 + } +} + + +# +# "datasets" -- create the ZFS dataset tree +# +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) || { echo "ERROR: dataset \`${_pds}' does not exist" >&2; return 1; } + case "${_pmp}" in + none) + echo "ERROR: dataset \`${_pds}' has no mountpoint" >&2 + return 1 + ;; + legacy) + echo "ERROR: dataset \`${_pds}' has a \`${_mp}' mountpoint" >&2 + 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 +} + + +# +# "mount" -- recursively mount a dataset including subordinate datasets +# +# command_mount dataset mountpoint +# +command_mount() { + local _dsname _mountpoint + local _name _mp _canmount _mounted + local _rootds_mountpoint _relative_mp _real_mp + local _dry_run _mount_outside _mount_natural + + _dry_run="" + _mount_outside="" + _mount_natural="" + while getopts "ONnu" _opt ; do + case ${_opt} in + O) + _mount_outside="yes" + ;; + N) + _mount_natural="yes" + ;; + n|u) + _dry_run="yes" + ;; + \?|:) + return 2; + ;; + esac + done + shift $((OPTIND-1)) + OPTIND=1 + + _dsname="${1-}" + _mountpoint="${2-}" + + 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 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 \"-O\" is given" >&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" >&2 + return 1 + fi + fi + + 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}" >&2; return 1; } + echo "Mounting ${_name} on ${_real_mp}" + mount -t zfs "${_name}" "${_real_mp}" || return 1 + fi + ;; + *) + if [ "${_mount_outside}" = "yes" ]; then + if [ "${_dry_run}" = "yes" ]; then + echo "Would mount ${_name} on configured ZFS dataset mountpoint ${_mp}" + else + echo "Mounting ${_name} on configured ZFS dataset mountpoint ${_mp}" + zfs mount "${_name}" || return 1 + fi + else + echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 2>&1 + fi + ;; + esac + done + + return 0 + } +} + + +# +# "umount" -- Recursively unmount ZFS datasets +# +# command_umount dataset +# +command_umount() { + local _dsname + local _name _mp _rest + local _rootds_mountpoint + + _dsname="${1-}" + [ -z "${_dsname}" ] && \ + { echo "ERROR: no dataset given" >&2; return 2; } + + # Just determine whether the given dataset name exists + _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")" || { echo "ERROR: dataset not found" >&2; return 1; } + + mount -t zfs -p \ + | grep -E "^${_dsname}(/|\s)" \ + | sort -n -r \ + | { + while IFS=' '$'\t' read -r _name _mp _rest ; do + echo "Umounting ${_name} on ${_mp}" + umount "${_mp}" || return 1 + done + return 0 + } +} + + +# +# Global option handling +# +while getopts "Vh" _opt ; do + case ${_opt} in + V) + printf 'ftjail v%s (rv:%s)\n' "${VERSION}" '@@HGREVISION@@' + 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 "$@" + ;; + populate) + command_populate "$@" + ;; + *) + echo "ERROR: unknown command \`${command}'" >&2 + exit 2 + ;; +esac
