view sbin/fjail @ 276:3c24b07240f2

Move the implementation of "mount" and "umount" into the new tool fzfs. It is not jail-specific but in reality a helper for some ZFS management issues.
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 17 Sep 2022 16:47:32 +0200
parents 95a81116471a
children 90fa5b68bb62
line wrap: on
line source

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

:Author:    Franz Glasner
:Copyright: (c) 2019-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: 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 MOUNPOINT

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

   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

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


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


#
# "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="$(echo -n ${_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 _pcl

    _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

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

        echo "security_status_chkmounts_enable=\"NO\"" >> "${_pcl}"

    else
        echo "WARNING: \"${_pcl}\" exists already -- not changed"
    fi

    command_hostid
}


#
# "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=""
    _veds="$(_get_dataset_for_varempty "${_mp}")"
    if [ $? -eq 0 ]; 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
}


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