view sbin/ftjail @ 236:661e35a9d6e5

Some work on ftjail: creating the very basic ZFS datasets
author Franz Glasner <fzglas.hg@dom66.de>
date Fri, 09 Sep 2022 09:42:52 +0200
parents 85aea8ca1ab8
children a38906739422
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 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-tmpl [OPTIONS] PARENT-BASE PARENT-SKELETON NAME

    Create the ZFS template datasets, i.e. the ro base and the rw
    skeleton to be used within thin jails 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
    }
}


#
# PARENT-BASE NAME DRY-RUN
#
command_datasets_tmpl_base() {
    local _p_base _name _dry_run

    local _ds_base

    _p_base="${1-}"
    _name="${2-}"
    _dry_run="${3-}"

    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


    [ "${_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 _dry_run

    local _ds_skel _child

    _p_skel="${1-}"
    _name="${2-}"
    _dry_run="${3-}"

    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 filesyhttps://docs.freebsd.org/en/books/handbook/jails/stem "${_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


    [ "${_dry_run}" = "yes" ] && return 0

    echo "Creating RW skeleton datasets in:"
    printf "\\t%s\\n" "${_ds_skel}"

    zfs create -u -o canmount=noauto "${_ds_skel}"
    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.
    #
    for _child in etc usr/local tmp var root ; do
        zfs create -u -o canmount=noauto "${_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 _zfsopts
    local _ds_base _ds_skel

    _zfsopts="-u -o canmount=noauto"

    _p_base="${1-}"
    _p_skel="${2-}"
    _name="${3-}"

    # Check preconditions
    command_datasets_tmpl_base "${_p_base}" "${_name}" "yes" || return
    command_datasets_tmpl_skel "${_p_skel}" "${_name}" "yes" || return

    # Really do it
    command_datasets_tmpl_base "${_p_base}" "${_name}" || return
    command_datasets_tmpl_skel "${_p_skel}" "${_name}" || return
    return 0

    #
    # 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-tmpl)
        command_datasets_tmpl "$@"
        ;;
    populate)
        command_populate "$@"
        ;;
    *)
        echo "ERROR: unknown command \`${command}'" >&2
        exit 2
        ;;
esac