view sbin/fjail @ 161:57b9b899bf77

Provide functions that will be the base for "/var/empty" handling (manipulate the "readonly" property when doing some special operations)
author Franz Glasner <fzglas.hg@dom66.de>
date Wed, 20 Nov 2019 09:11:48 +0100
parents 3f9cae8f5862
children 9bd38c55a75c
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
: 'A very minimal BSD Jail management tool.

:Author:    Franz Glasner
:Copyright: (c) 2019 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: 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 [-u] PARENT CHILD

    Create ZFS datasets to be used within a jail

    PARENT must exist already and CHILD must not exist.

    -u        Do not automatically mount newly created datasets

  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

  copy 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.

    -u        Do not automatically mount received datasets

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 `zfs mount` to determine the ZFS dataset for a given mountpoint.

    '
    local _mountpoint
    local _ds _mount

    _mountpoint="$1"

    while read -r _ds _mount; do
        if [ "$_mount" = "$_mountpoint" ]; then
            echo $_ds
            return 0
        fi
    done <<EOF__GDSFM
$(zfs mount)
EOF__GDSFM
    return 1
}


_get_dataset_for_varempty() {
    : 'Allow special handling for <mountpoint>/var/empty which may be
    mounted read-only.

    '
    local _mountpoint
    local _ve_mount

    _mountpoint="$1"

    if [ "$_mountpoint" = '/' ]; then
        _ve_mount='/var/empty'
    else
        _ve_mount="${_mountpoint}/var/empty"
    fi

    _get_dataset_for_mountpoint "${_ve_mount}"
}


#
# "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 _dummy
    # full name of the dataset
    local _ds
    # dynamic ZFS options
    local _zfsopts

    _zfsopts=""
    while getopts "u" _opt ; do
        case ${_opt} in
            u)
                # do not mount newly created datasets
                _zfsopts="${_zfsopts} -u"
                ;;
            \?|:)
                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
    _get=$(zfs get -H mountpoint "${_pds}" 2>/dev/null) || { echo "ERROR: dataset \`${_pds}' does not exist" >&2; return 1; }
    IFS=$'\t' read _dummy _dummy _pmp _dummy <<EOF
${_get}
EOF
    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 get -H mountpoint "${_ds}" >/dev/null 2>/dev/null; then
        echo "ERROR: dataset \`${_ds}' does already exist" >&2
        return 1
    fi
    zfs create ${_zfsopts} -o atime=off                                                                      "${_ds}"
    zfs create ${_zfsopts} -o sync=disabled -o setuid=off                                                    "${_ds}/tmp"
    zfs create ${_zfsopts}                                                                                   "${_ds}/usr"
    zfs create ${_zfsopts}                                                                                   "${_ds}/usr/local"
    zfs create ${_zfsopts}                                                                                   "${_ds}/var"
    zfs create ${_zfsopts} -o exec=off -o setuid=off                                                         "${_ds}/var/audit"
    zfs create ${_zfsopts} -o exec=off -o setuid=off                                                         "${_ds}/var/cache"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o compression=off                                      "${_ds}/var/cache/pkg"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o compression=off                                      "${_ds}/var/crash"
    zfs create ${_zfsopts} -o exec=off -o setuid=off                                                         "${_ds}/var/db"
    zfs create ${_zfsopts} -o exec=on -o setuid=off                                                          "${_ds}/var/db/pkg"
    zfs create ${_zfsopts} -o readonly=on -o exec=off -o setuid=off                                          "${_ds}/var/empty"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o primarycache=metadata                                "${_ds}/var/log"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o atime=on                                             "${_ds}/var/mail"
    zfs create ${_zfsopts} -o sync=disabled -o exec=off -o setuid=off -o compression=off -o primarycache=all "${_ds}/var/run"
    zfs create ${_zfsopts} -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

    tar -C "${_mp}" --exclude=./var/empty -xJp -f "${_basetxz}" || { echo "ERROR: tar encountered errors" >&2; return 1; }
}


#
# "copy" -- ZFS copy of datasets
#
# command_copy source-dataset destination-dataset
#
command_copy() {
    # source dataset -- destination dataset
    local _source _dest
    # dynamic ZFS options
    local _zfsopts

    _zfsopts=""
    while getopts "u" _opt ; do
        case ${_opt} in
            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 -n -v ${_source} || { echo "ERROR: ZFS operation failed in no-op mode" >&2; return 1; }
    zfs send -R "${_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

    _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"
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case ${_opt} in
        V)
            printf 'fjail 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 "$@"
        ;;
    privs)
        command_privs "$@"
        ;;
    populate)
        command_populate "$@"
        ;;
    copy)
        command_copy "$@"
        ;;
    *)
        echo "ERROR: unknown command \`${command}'" >&2
        exit 2
        ;;
esac