4
\$\begingroup\$

I've written a bash script that creates a root filesystem on ZFS for installing Debian or Arch. The purpose of this script is to make it easier installing e.g. Arch without the need to make a ZFS pool and create datasets one by one manually.

Below is my bash script. Any advice on what you would improve if you were the author of that script is appreciated. Thanks!

lib/utils

#!/usr/bin/env bash

die() {
    echo "Error: $1. Exiting..." 1>&2
    exit 1
}

info() {
    echo "Info: $1"
}

warn() {
    echo "Warning: $1"
}

require_binary() {
    local binaries=("$@")
    for binary in "${binaries[@]}"; do
        if ! command -v "${binary}" &> /dev/null; then
            die "${binary} binary does not exist"
        fi
    done
}

is_mounted() {  # Copied from https://www.baeldung.com/linux/bash-is-directory-mounted
    mount | awk -v DIR="$1" '{if ($3 == DIR) { exit 0}} ENDFILE{exit -1}'
}

run_as_root() {
    if [ "$EUID" -ne 0 ]; then
        echo "Please run as root"
        exit 1
    fi
}

create_lock() {
    readonly lockdir=/tmp/$1.lock
    if ! mkdir "$lockdir"; then
        die "Lock file already exists"
    fi
    trap 'stty echo; rm -rf "${lockdir}"; exit $?' EXIT
}

read_password() {
    local passwd confirm_passwd
    until
        stty -echo
        read -rp "Password: " passwd
        echo
        read -rp "Confirm password: " confirm_passwd
        echo
        stty echo
        [[ "$passwd" = "$confirm_passwd" ]]
    do
        echo "Error: passwords don't match." >&2
    done
    echo "${passwd}"
}

prepare_zfs_rootfs:

#!/usr/bin/env bash

. ./lib/utils

run_as_root
require_binary "zpool" "zfs" "sgdisk"
create_lock "prepare_zfs_rootfs"

ZFS_AVAILABLE_ENC_TYPES=( "password" "keyfile" )
ZFS_AVAILABLE_PRESETS=( "gentoo" "gnome" "libvirt" "lxc" "docker" "nfs" "webserver" "mailserver" "snap" "systemd" )
ZFS_AVAILABLE_BOOT_MODES=( "uefi" "legacy_bios" )

usage() {
    cat <<EOF
$0 - Prepare ZFS root pool and create datasets for the new rootfs
Usage:
    $0 [ options ]
    $0 --help
Available presets:
    ${ZFS_AVAILABLE_PRESETS[*]}
Options:
    --device|--devices value - target device or quoted device list like /dev/sda,
                                "/dev/sda /dev/sdb" when setting up raid, required
    -m|--mnt value - rootfs mount path, required
    -u|--user value - target user name, required (default: user)
    --boot-pool value - boot pool name (default: bpool)
    --root-pool value - root pool name (default: rpool)
    --raid-type (none|raid0|raid1) - raid type (default: none)
    --presets "quoted list of values" - instructs script how to configure ZFS for user needs, required
    --b|-boot-mode (uefi|legacy_bios) - specifies target system boot mode (default: uefi)
    --encrypt - encrypts ZFS root pool (default: unset)
    --key-type (password|keyfile) - specifies encryption type
    --key-path value - specifies keyfile path, required when key type is keyfile
    --rpool-size value - specifies root partition size (default: unset)
    --swap-size value - specifies swap size (default: unset)
    --enable-swap - specifies whether swap should be enabled (default: set)
    --swap-on-zfs - specifies whether swap should be put onto ZFS (default: unset)
    --encrypt-swap - specifies whether swap should be encrypted with LUKS (default: unset)
    --swap-hibernate - specifies wheter will be used hibernation (default: unset)
    --auto-mount y|n - specifies if rootfs should be automatically mounted (default: y)
    -c|--config value - specifies config file path
    --apply - tells script that partitioning and preparing ZFS pools should be applied, use with caution
    -h|--help - displays this help
EOF
    exit 0
}

[ $# -eq 0 ] && usage

while [ "$#" -gt 0 ]; do
    case $1 in
        --device|--devices)
            ZFS_TARGET_DEVICES="$2"
            shift
            shift
            ;;
        -m|--mnt)
            ZFS_MNT_PATH="$2"
            shift
            shift
            ;;
        -u|--user)
            ZFS_TARGET_USER="$2"
            shift
            shift
            ;;
        --boot-pool)
            ZFS_BOOT_POOL="$2"
            shift
            shift
            ;;
        --root-pool)
            ZFS_ROOT_POOL="$2"
            shift
            shift
            ;;
        --raid-type)
            ZFS_RAID_TYPE="$2"
            shift
            shift
            ;;
        --presets)
            ZFS_PRESETS="$2"
            shift
            shift
            ;;
        -b|--boot-mode)
            ZFS_BOOT_MODE="$2"
            shift
            shift
            ;;
        --encrypt)
            ZFS_ENC_ENABLED="y"
            shift
            ;;
        --key-type)
            ZFS_KEY_TYPE="$2"
            shift
            shift
            ;;
        --key-path)
            ZFS_KEY_PATH="$2"
            shift
            shift
            ;;
        --swap-size)
            INST_PARTSIZE_SWAP="$2"
            shift
            shift
            ;;
        --enable-swap)
            SWAP_ENABLED="y"
            shift
            ;;
        --swap-on-zfs)
            SWAP_ON_ZFS="y"
            shift
            ;;
        --encrypt-swap)
            SWAP_ENCRYPT="y"
            shift
            ;;
        --swap-hibernate)
            SWAP_HIBERNATE="y"
            shift
            ;;
        --auto-mount)
            AUTO_MOUNT="$2"
            shift
            shift
            ;;
        -c|--config)
            CONFIG_FILE="$2"
            shift
            shift
            ;;
        --apply)
            APPLY="y"
            shift
            ;;
        -h|--help)
            usage
            ;;
        *)
            echo "Error: command '$1' not recognized."
            usage
            ;;
    esac
done

# shellcheck source=/dev/null
if [[ -f "${CONFIG_FILE}" ]]; then
    . "${CONFIG_FILE}"
else
    die "config file does not exist"
fi

# assign default values to variables
ZFS_TARGET_DEVICES=${ZFS_TARGET_DEVICES:-}
ZFS_MNT_PATH=${ZFS_MNT_PATH:-}
ZFS_TARGET_USER=${ZFS_TARGET_USER:-"user"}
ZFS_BOOT_POOL=${ZFS_BOOT_POOL:-"bpool"}
ZFS_ROOT_POOL=${ZFS_ROOT_POOL:-"rpool"}
ZFS_RAID_TYPE=${ZFS_RAID_TYPE:-"none"}
ZFS_PRESETS=${ZFS_PRESETS:-}
ZFS_BOOT_MODE=${ZFS_BOOT_MODE:-"uefi"}
ZFS_ENC_ENABLED=${ZFS_ENC_ENABLED:-}
ZFS_KEY_TYPE=${ZFS_KEY_TYPE:-}
ZFS_KEY_PATH=${ZFS_KEY_PATH:-}
ZFS_ENC_PASSWD=${ZFS_ENC_PASSWD:-}
INST_PARTSIZE_RPOOL=${INST_PARTSIZE_RPOOL:-}
INST_PARTSIZE_SWAP=${INST_PARTSIZE_SWAP:-}
SWAP_ENABLED=${SWAP_ENABLED:-"y"}
SWAP_ON_ZFS=${SWAP_ON_ZFS:-}
SWAP_ENCRYPT=${SWAP_ENCRYPT:-}
SWAP_HIBERNATE=${SWAP_HIBERNATE:-}
AUTO_MOUNT=${AUTO_MOUNT:-"y"}
APPLY=${APPLY:-}

# check if options specified to the script are correct
if [[ -n "${ZFS_TARGET_DEVICES[*]}" ]]; then
    for device in "${ZFS_TARGET_DEVICES[@]}"; do
        if [[ ! -b "${device}" ]]; then
            die "'${device}' is not a block device"
        fi
    done
else
    die "target devices not specified"
fi

if [[ -n "${ZFS_MNT_PATH}" ]]; then
    if [[ ! -d "${ZFS_MNT_PATH}" ]]; then
        die "directory '${ZFS_MNT_PATH}' does not exist"
    else
        if is_mounted "${ZFS_MNT_PATH}"; then
            die "directory '${ZFS_MNT_PATH}' is already mounted"
        fi
    fi
else
    die "mount path not specified"
fi

[[ -z "${ZFS_TARGET_USER}" ]] && die "target user name is not specified"

[[ -z "${ZFS_BOOT_POOL}" ]] && die "boot pool name is not specified"

[[ -z "${ZFS_ROOT_POOL}" ]] && die "root pool name is not specified"

if [[ -n "${ZFS_RAID_TYPE}" ]]; then
    case ${ZFS_RAID_TYPE} in
        none)
            [[ ${#ZFS_TARGET_DEVICES[@]} -gt 1 ]] && die "too many devices, specified raid type is '${ZFS_RAID_TYPE}'"
            ;;
        raid0|raid1)
            [[ ${#ZFS_TARGET_DEVICES[@]} -lt 2 ]] && die "required at least 2 devices, not a raid"
            ;;
        *)
            die "unknown raid type '${ZFS_RAID_TYPE}'"
            ;;
    esac
else
    die "raid type list is empty"
fi

if [[ -n "${ZFS_PRESETS[*]}" ]]; then
    for preset in "${ZFS_PRESETS[@]}"; do
        if [[ ! ${ZFS_AVAILABLE_PRESETS[*]} =~ ${preset} ]]; then
            die "unknown preset '${preset}'"
        fi
    done
else
    die "preset list is empty"
fi

if [[ -n "${ZFS_BOOT_MODE[*]}" ]]; then
    for boot_mode in "${ZFS_BOOT_MODE[@]}"; do
        if [[ ! ${ZFS_AVAILABLE_BOOT_MODES[*]} =~ ${boot_mode} ]]; then
            die "unknown boot mode '${boot_mode}'"
        fi
    done
else
    die "boot mode list is empty"
fi

if [[ -n "${ZFS_ENC_ENABLED}" ]]; then
    if [[ ! ${ZFS_AVAILABLE_ENC_TYPES[*]} =~ ${ZFS_KEY_TYPE} ]]; then
        die "unknown encryption type '${ZFS_KEY_TYPE}'"
    fi
    if [[ "${ZFS_KEY_TYPE}" = "keyfile" ]] && [[ -z "${ZFS_KEY_PATH}" ]]; then
        die "encryption key path not specified"
    fi
fi

declare -a SWAP_PARTITION=""

calculate_ashift() {
    local device
    device=$1
    local zpool_ashift
    local sector_size
    sector_size=$(blockdev --getpbsz "${device}")
    case ${sector_size} in
        "512")
            zpool_ashift=9
            ;;
        "4096")
            zpool_ashift=12
            ;;
        "8192")
            zpool_ashift=13
            ;;
        *)
            die "calculate_ashift: cannot calculate ashift for device '${device}'"
            ;;
    esac
    echo "${zpool_ashift}"
}

check_ashift() {
    local devices
    devices=( "$@" )
    local zpool_ashift
    local prev
    read -r -a devices <<< "$@"
    for part in "${devices[@]}"; do
        zpool_ashift=$(calculate_ashift "${part}")
        if [[ ${prev} && ${zpool_ashift} != $(calculate_ashift "${prev}") ]]; then
            die "check_ashift: devices ashifts don't match"
        fi
        prev=${part}
    done
    echo "${zpool_ashift}"
}

calculate_swap_size() {
    local swap_size
    local total_memory
    total_memory=$(free -m | awk '/^Mem:/{print $2}')
    if [ "${total_memory}" -le 2048 ]; then
        [[ -n "${SWAP_HIBERNATE}" ]] && swap_size=$(( 3 * total_memory )) || swap_size=$(( 2 * total_memory ))
    elif [ "${total_memory}" -gt 2048 ] && [ "${total_memory}" -le 8192 ]; then
        [[ -n "${SWAP_HIBERNATE}" ]] && swap_size=$(( 2 * total_memory )) || swap_size=${total_memory}
    elif [ "${total_memory}" -gt 8192 ] && [ "${total_memory}" -le 65536 ]; then
        [[ -n "${SWAP_HIBERNATE}" ]] && swap_size=$(perl -w -e "use POSIX; print ceil(1.5 * ${total_memory}), qq{\n}") || swap_size=4096
    else
        swap_size=4096
    fi
    echo "${swap_size}M"
}

create_swap_on_zfs() {
    echo "Creating swap dataset"
    zfs create -V "$(calculate_swap_size)" -b 4096 -o logbias=throughput -o sync=always -o primarycache=metadata\
        -o com.sun:auto-snapshot=false "${ZFS_ROOT_POOL}"/swap
    mkswap -f /dev/zvol/"${ZFS_ROOT_POOL}"/swap
}

generate_keyfile() {
    local target
    target=$1
    tr -d '\n' < /dev/urandom | head -c 512 > "${target}"
    echo "Successfully created encryption key"
}

create_efi_dir() {
    if [[ "${ZFS_BOOT_MODE}" = "uefi" ]]; then
        mkdir -p "${ZFS_MNT_PATH}"/boot/efi

        [[ ${#ZFS_TARGET_DEVICES[@]} -gt 1 ]] || mkdir -p "${ZFS_MNT_PATH}"/boot/efis
        
        for device in "${ZFS_TARGET_DEVICES[@]}"; do
            if [[ "${device}" = /dev/disk/by-id* ]]; then
                mkdir -p "${ZFS_MNT_PATH}"/boot/efis/"${device##*/}"-part1
            else
                mkdir -p "${ZFS_MNT_PATH}"/boot/efis/"${device##*/}"1
            fi
        done
    fi
}

mount_efi_dir() {
    if [[ "${ZFS_BOOT_MODE}" = "uefi" ]]; then
        if [[ "${ZFS_TARGET_DEVICES[0]}" = /dev/disk/by-id* ]]; then
            mount -t vfat "${ZFS_TARGET_DEVICES[0]}-part1" "${ZFS_MNT_PATH}"/boot/efi
        else
            mount -t vfat "${ZFS_TARGET_DEVICES[0]}1" "${ZFS_MNT_PATH}"/boot/efi
        fi

        for device in "${ZFS_TARGET_DEVICES[@]}"; do
            if [[ "${device}" = /dev/disk/by-id* ]]; then
                mount -t vfat "${device}-part1" "${ZFS_MNT_PATH}"/boot/efis/"${device##*/}"-part1
            else
                mount -t vfat "${device}1" "${ZFS_MNT_PATH}"/boot/efis/"${device##*/}"1
            fi
        done
    fi
}

create_bpool() {
    echo "Creating bpool"
    local devices
    devices=( "$@" )
    local bpool_args
    bpool_args=(
            "-d"
            "-f"
            "-o feature@allocation_classes=enabled"
            "-o feature@async_destroy=enabled"
            "-o feature@bookmarks=enabled"
            "-o feature@embedded_data=enabled"
            "-o feature@empty_bpobj=enabled"
            "-o feature@enabled_txg=enabled"
            "-o feature@extensible_dataset=enabled"
            "-o feature@filesystem_limits=enabled"
            "-o feature@hole_birth=enabled"
            "-o feature@large_blocks=enabled"
            "-o feature@lz4_compress=enabled"
            "-o feature@project_quota=enabled"
            "-o feature@resilver_defer=enabled"
            "-o feature@spacemap_histogram=enabled"
            "-o feature@spacemap_v2=enabled"
            "-o feature@userobj_accounting=enabled"
            "-o feature@zpool_checkpoint=enabled"
            "-o ashift=$(check_ashift "${devices[*]}")"
            "-o cachefile=/etc/zfs/zpool.cache"
            "-o autotrim=on"
            "-O acltype=posixacl"
            "-O canmount=off"
            "-O compression=off"
            "-O devices=off"
            "-O normalization=formD"
            "-O relatime=on"
            "-O xattr=sa"
            "-O mountpoint=/boot"
            "-R ${ZFS_MNT_PATH}"
        )
    # shellcheck disable=SC2048
    # shellcheck disable=SC2086
    case ${ZFS_RAID_TYPE} in
        none)
            zpool create ${bpool_args[*]} "${ZFS_BOOT_POOL}" ${devices[*]}
            ;;
        raid0)
            zpool create ${bpool_args[*]} "${ZFS_BOOT_POOL}" ${devices[*]}
            ;;
        raid1)
            zpool create ${bpool_args[*]} "${ZFS_BOOT_POOL}" mirror ${devices[*]}
            ;;
    esac
    zfs create -o canmount=off -o mountpoint=none "${ZFS_BOOT_POOL}"/BOOT
    zfs create -o mountpoint=/boot "${ZFS_BOOT_POOL}"/BOOT/default
    create_efi_dir
    zpool export "${ZFS_BOOT_POOL}"
    rmdir "${ZFS_MNT_PATH}"/boot || true
}

create_rpool() {
    echo "Creating rpool"
    local devices
    devices=( "$@" )
    local rpool_args
    rpool_args=(
            "-f"
            "-o ashift=$(check_ashift "${devices[@]}")"
            "-o cachefile=/etc/zfs/zpool.cache"
            "-O acltype=posixacl"
            "-O relatime=on"
            "-O xattr=sa"
            "-O dnodesize=legacy"
            "-O normalization=formD"
            "-O mountpoint=none"
            "-O canmount=off"
            "-O devices=off"
            "-O compression=lz4"
            "-m none"
            "-R ${ZFS_MNT_PATH}"
        )

    if [[ -n "${ZFS_ENC_ENABLED}" ]]; then
        rpool_args+=( "-O encryption=aes-256-gcm" "-O keyformat=passphrase" )
        case ${ZFS_KEY_TYPE} in
            "password")
                rpool_args+=( "-O keylocation=prompt" )
                ;;
            "keyfile")
                mkdir -p "$(dirname "${ZFS_KEY_PATH}")"
                generate_keyfile "${ZFS_KEY_PATH}"
                rpool_args+=( "-O keylocation=file://${ZFS_KEY_PATH}" )
                ;;
        esac
    fi

    [[ -n "${SWAP_HIBERNATE}" ]] && warn "hibernation on ZFS is not recommended"

    # shellcheck disable=SC2048
    # shellcheck disable=SC2086
    case ${ZFS_RAID_TYPE} in
        none)
            zpool create ${rpool_args[*]} "${ZFS_ROOT_POOL}" ${devices[*]} <<< "${ZFS_ENC_PASSWD}"
            ;;
        raid0)
            zpool create ${rpool_args[*]} "${ZFS_ROOT_POOL}" ${devices[*]} <<< "${ZFS_ENC_PASSWD}"
            ;;
        raid1)
            zpool create ${rpool_args[*]} "${ZFS_ROOT_POOL}" mirror ${devices[*]} <<< "${ZFS_ENC_PASSWD}"
            ;;
    esac
    zfs create -o mountpoint=none -o compression=lz4 "${ZFS_ROOT_POOL}"/ROOT
    zfs create -o mountpoint=/ "${ZFS_ROOT_POOL}"/ROOT/default
    zfs create -o canmount=off -o mountpoint=/var -o xattr=sa "${ZFS_ROOT_POOL}"/ROOT/var
    zfs create -o canmount=off -o mountpoint=/var/lib "${ZFS_ROOT_POOL}"/ROOT/var/lib
    if [[ ${ZFS_PRESETS[*]} =~ "gentoo" ]]; then
        # Create portage directories
        zfs create -o mountpoint=/var/cache/distfiles "${ZFS_ROOT_POOL}"/ROOT/distfiles

        # Create portage build directory
        zfs create -o mountpoint=/var/tmp/portage -o compression=lz4 -o sync=disabled "${ZFS_ROOT_POOL}"/ROOT/build_dir

        # Create optional packages directory
        zfs create -o mountpoint=/var/cache/binpkgs "${ZFS_ROOT_POOL}"/ROOT/binpkgs

        # Create optional ccache directory
        zfs create -o mountpoint=/var/tmp/ccache -o compression=lz4 "${ZFS_ROOT_POOL}"/ROOT/ccache
    fi
    [[ "${ZFS_PRESETS[*]}" =~ "gnome" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/AccountsService "${ZFS_ROOT_POOL}"/ROOT/var/lib/AccountsService
    [[ "${ZFS_PRESETS[*]}" =~ "libvirt" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/libvirt "${ZFS_ROOT_POOL}"/ROOT/var/lib/libvirt
    [[ "${ZFS_PRESETS[*]}" =~ "lxc" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/lxc "${ZFS_ROOT_POOL}"/ROOT/var/lib/lxc
    [[ "${ZFS_PRESETS[*]}" =~ "docker" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/docker "${ZFS_ROOT_POOL}"/ROOT/var/lib/docker
    [[ "${ZFS_PRESETS[*]}" =~ "nfs" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/nfs "${ZFS_ROOT_POOL}"/ROOT/var/lib/nfs
    [[ "${ZFS_PRESETS[*]}" =~ "webserver" ]] && zfs create -o canmount=on -o mountpoint=/var/www "${ZFS_ROOT_POOL}"/ROOT/var/www
    [[ "${ZFS_PRESETS[*]}" =~ "mailserver" ]] && zfs create -o canmount=on -o mountpoint=/var/mail "${ZFS_ROOT_POOL}"/ROOT/var/mail
    [[ "${ZFS_PRESETS[*]}" =~ "snap" ]] && zfs create -o canmount=on -o mountpoint=/var/snap "${ZFS_ROOT_POOL}"/ROOT/var/snap
    [[ "${ZFS_PRESETS[*]}" =~ "systemd" ]] && zfs create -o canmount=off -o mountpoint=/var/lib/systemd "${ZFS_ROOT_POOL}"/ROOT/var/lib/systemd
    zfs create -o canmount=off -o mountpoint=/usr "${ZFS_ROOT_POOL}"/ROOT/usr
    zfs create -o mountpoint=/usr/local "${ZFS_ROOT_POOL}"/ROOT/usr/local
    zfs create -o mountpoint=/opt "${ZFS_ROOT_POOL}"/ROOT/opt
    [[ "${ZFS_PRESETS[*]}" =~ "systemd" ]] && zfs create -o mountpoint=/var/lib/systemd/coredump "${ZFS_ROOT_POOL}"/ROOT/var/lib/systemd/coredump
    zfs create -o mountpoint=/var/log "${ZFS_ROOT_POOL}"/ROOT/var/log
    [[ "${ZFS_PRESETS[*]}" =~ "systemd" ]] && zfs create -o acltype=posixacl -o mountpoint=/var/log/journal "${ZFS_ROOT_POOL}"/ROOT/var/log/journal
    zfs create -o mountpoint=/home "${ZFS_ROOT_POOL}"/home
    zfs create -o mountpoint=/root "${ZFS_ROOT_POOL}"/home/root
    zfs create -o mountpoint=/home/"${ZFS_TARGET_USER}" "${ZFS_ROOT_POOL}"/home/"${ZFS_TARGET_USER}"
    zpool set bootfs="${ZFS_ROOT_POOL}" "${ZFS_ROOT_POOL}"
    zfs set relatime=on "${ZFS_ROOT_POOL}"
    zfs set compression=lz4 "${ZFS_ROOT_POOL}"
    [[ -n "${SWAP_ON_ZFS}" ]] && create_swap_on_zfs

    zpool export "${ZFS_ROOT_POOL}"
}

create_partitions() {
    echo "Creating partitions"
    for device in "${ZFS_TARGET_DEVICES[@]}"; do
        sgdisk --zap-all "${device}"
        case ${ZFS_BOOT_MODE} in
            "legacy_bios")
                sgdisk -n 0:0:+1MiB -t 0:ef02 "${device}"
                ;;
            "uefi")
                sgdisk -n 0:1M:+1G -t 0:ef00 "${device}"
                sleep 3
                if [[ "${device}" = /dev/disk/by-id* ]]; then
                    mkfs.vfat -n EFI "${device}"-part1
                else
                    mkfs.vfat -n EFI "${device}"1
                fi
                ;;
        esac
        sgdisk -n 0:0:+4GiB -t 0:be00 "${device}"

        if [[ -n "${SWAP_ENABLED}" ]]; then
            if [[ -z "${INST_PARTSIZE_SWAP}" ]]; then
                sgdisk -n 0:0:+"$(calculate_swap_size)"G -t 0:8200 "${device}"
            else
                sgdisk -n 0:0:+"${INST_PARTSIZE_SWAP}"G -t 0:8200 "${device}"
            fi
        fi

        if test -z "${INST_PARTSIZE_RPOOL}"; then
            sgdisk -n 0:0:0 -t 0:bf00 "${device}"
        else
            sgdisk -n 0:0:+"${INST_PARTSIZE_RPOOL}"G -t 0:bf00 "${device}"
        fi
    done
    sleep 3

    # shellcheck disable=SC2048
    # shellcheck disable=SC2086
    if [[ -n "${SWAP_ENABLED}" ]] && [[ -z "${SWAP_ON_ZFS}" ]]; then
        for device in "${ZFS_TARGET_DEVICES[@]}"; do
            if [[ "${device}" = /dev/disk/by-id* ]]; then
                SWAP_PARTITION+=( "${device}-part3" )
            else
                SWAP_PARTITION+=( "${device}3" )
            fi
        done

        case ${ZFS_RAID_TYPE} in
            "none")
                [[ -z "${SWAP_ENCRYPT}" ]] && mkswap ${SWAP_PARTITION[*]}
                ;;
            "raid1")
                mdadm --create --verbose --level=1 --metadata=1.2 --raid-devices=2 /dev/md/swap ${SWAP_PARTITION[*]}
                [[ -z "${SWAP_ENCRYPT}" ]] && mkswap /dev/md/swap
                ;;
            **)
                die "create_partitions: unsupported raid type '${ZFS_RAID_TYPE}'"
                ;;
        esac
        info "If SWAP_ENCRYPT option is enabled without SWAP_ON_ZFS, you have to"
        info "manually enable swap encryption in /etc/crypttab on the target system."
        info "For raid configurations you must append /dev/md/swap in /etc/crypttab,"
        info "and /dev/mapper/swap_device_name to /etc/fstab."
    fi

    sleep 3
}

create_rootfs() {
    echo "Setting up ZFS pools"
    local partitions=( )
    for device in "${ZFS_TARGET_DEVICES[@]}"; do
        if [[ "${device}" = /dev/disk/by-id* ]]; then
            partitions+=( "${device}-part2" )
        else
            partitions+=( "${device}2" )
        fi
    done
    create_bpool "${partitions[@]}"
    partitions=( )
    for device in "${ZFS_TARGET_DEVICES[@]}"; do
        if [[ "${device}" = /dev/disk/by-id* ]]; then
            partitions+=( "${device}-part4" )
        else
            partitions+=( "${device}4" )
        fi
    done
    create_rpool "${partitions[@]}"
}

mount_rootfs() {
    echo "Mounting filesystem"
    zpool import -o cachefile=/etc/zfs/zpool.cache -R "${ZFS_MNT_PATH}" "${ZFS_ROOT_POOL}"
    if [[ -n "${ZFS_ENC_ENABLED}" ]]; then
        zfs load-key "${ZFS_ROOT_POOL}" <<< "${ZFS_ENC_PASSWD}"
        zfs mount -a
    fi
    if [[ -n "${SWAP_PARTITION[*]}" ]] && [[ -z "${SWAP_ENCRYPT}" ]]; then
        case ${ZFS_RAID_TYPE} in
            "none")
                swapon "${SWAP_PARTITION[*]}"
                ;;
            "raid1")
                swapon /dev/md/swap
                ;;
            **)
                die "mount_rootfs: unsupported raid type '${ZFS_RAID_TYPE}'"
                ;;
        esac
    fi
    zpool import -o cachefile=/etc/zfs/zpool.cache -R "${ZFS_MNT_PATH}" "${ZFS_BOOT_POOL}"
    mkdir -p "${ZFS_MNT_PATH}"/etc/zfs
    cp /etc/zfs/zpool.cache "${ZFS_MNT_PATH}"/etc/zfs/zpool.cache
    mount_efi_dir
    echo "Successfully mounted rootfs"
}

dump_config() {
    echo "ZFS_TARGET_DEVICES=\"${ZFS_TARGET_DEVICES[*]}\""
    echo "ZFS_MNT_PATH=\"${ZFS_MNT_PATH}\""
    echo "ZFS_TARGET_USER=\"${ZFS_TARGET_USER}\""
    echo "ZFS_BOOT_POOL=\"${ZFS_BOOT_POOL}\""
    echo "ZFS_ROOT_POOL=\"${ZFS_ROOT_POOL}\""
    echo "ZFS_RAID_TYPE=\"${ZFS_RAID_TYPE}\""
    echo "ZFS_PRESETS=\"${ZFS_PRESETS[*]}\""
    echo "ZFS_ENC_ENABLED=\"${ZFS_ENC_ENABLED}\""
    echo "ZFS_KEY_TYPE=\"${ZFS_KEY_TYPE}\""
    echo "ZFS_KEY_PATH=\"${ZFS_KEY_PATH}\""
    echo "SWAP_ENABLED=\"${SWAP_ENABLED}\""
    echo "SWAP_ENCRYPT=\"${SWAP_ENCRYPT}\""
    echo "SWAP_ON_ZFS=\"${SWAP_ON_ZFS}\""
    echo "SWAP_HIBERNATE=\"${SWAP_HIBERNATE}\""
}

if [[ -n "${APPLY}" ]]; then
    if [[ -n "${ZFS_ENC_ENABLED}" ]] && [[ "${ZFS_KEY_TYPE}" = "password" ]] && [[ -z "${ZFS_ENC_PASSWD}" ]]; then
        read_password ZFS_ENC_PASSWD
    fi
    create_partitions
    create_rootfs
    [[ "${AUTO_MOUNT}" = "y" ]] && mount_rootfs
else
    echo "Execute script with --apply option to confirm destructive action"
    echo "Actual config:"
    dump_config
fi
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$
#!/usr/bin/env bash

Kudos on the shebang! Highly portable. It will find bash even if it's hiding in /usr/local/bin.


require_binary() {

nit: This appears to be misnamed. Prefer plural over singular, as it checks for multiple binaries.

Also, it isn't bad, but the short-circuiting behavior is perhaps a bit inconvenient. Consider reporting all missing binaries, and then bailing if there were any missing. The idea is to let an engineer issue a single apt install ... command.


In is_mounted, possibly call out that we need GNU gawk for ENDFILE. Works on linux, might not on FreeBSD / MacOS.

Or document in the script that we're only targeting linux (debian + arch).


In run_as_root, this is perhaps a slightly tricky bash-ism:

    if [ "$EUID" -ne 0 ]; then

I thought I could inspect the env var with $ env | grep UID, and then eventually figured out that it isn't exported.

Clearly this works. I usually fork off id -u to test. Whatever.


. ./lib/utils

Clearly this works. But we're using bash, so we have an opportunity to spell it out:

source ./lib/utils

When speaking with colleagues over the phone, I find "dot" tedious, and prefer to work with scripts that use the more verbose synonym.

Similarly for . "${CONFIG_FILE}"


run_as_root

Maybe put the word "verify" or "insist" in there? Or even "running"? My naïve reading was "oh, it will use sudo to ensure we're UID 0."


IDK, maybe sort the ZFS_AVAILABLE_PRESETS ?


    -u|--user value - target user name, required (default: user)

I don't get that. I have to specify it. But if I don't, then "user" will be supplied for me?

[ $# -eq 0 ] && usage

Kudos, I really like the concise shell idioms throughout the script, and the consistent use of local. The use of shellcheck has done good things for this codebase.

The while loop and repeated shift to crack argv is tedious. Maybe banish it to a function, similar to usage? Isn't getopt supposed to help with such details?


Thank you for this helpful comment:

# check if options specified to the script are correct

Consider removing the comment and breaking out a validate_args function (which might call _target and _mount_path validators).


In check_ashift I confess I don't understand what's going on here.

    devices=( "$@" ) ...
    read -r -a devices <<< "$@"

We assign, and then immediately overwrite with a heredoc?


In generate_keyfile I don't understand what is special about \n newline.

    tr -d '\n' < /dev/urandom | head -c 512 > "${target}"

It's not completely obvious to me that tr & head are guaranteed to be operating in binary mode as opposed to say UTF8. (For example $ env LC_ALL=C sort can dramatically speed up some text file sorts, by avoiding en_US.utf8 locale.)

Maybe binary dd is the appropriate tool, here?


    [[ "${ZFS_PRESETS[*]}" =~ "gnome" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/AccountsService "${ZFS_ROOT_POOL}"/ROOT/var/lib/AccountsService
    [[ "${ZFS_PRESETS[*]}" =~ "libvirt" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/libvirt "${ZFS_ROOT_POOL}"/ROOT/var/lib/libvirt
    [[ "${ZFS_PRESETS[*]}" =~ "lxc" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/lxc "${ZFS_ROOT_POOL}"/ROOT/var/lib/lxc
    [[ "${ZFS_PRESETS[*]}" =~ "docker" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/docker "${ZFS_ROOT_POOL}"/ROOT/var/lib/docker
    [[ "${ZFS_PRESETS[*]}" =~ "nfs" ]] && zfs create -o canmount=on -o mountpoint=/var/lib/nfs "${ZFS_ROOT_POOL}"/ROOT/var/lib/nfs
    [[ "${ZFS_PRESETS[*]}" =~ "webserver" ]] && zfs create -o canmount=on -o mountpoint=/var/www "${ZFS_ROOT_POOL}"/ROOT/var/www
    [[ "${ZFS_PRESETS[*]}" =~ "mailserver" ]] && zfs create -o canmount=on -o mountpoint=/var/mail "${ZFS_ROOT_POOL}"/ROOT/var/mail
    [[ "${ZFS_PRESETS[*]}" =~ "snap" ]] && zfs create -o canmount=on -o mountpoint=/var/snap "${ZFS_ROOT_POOL}"/ROOT/var/snap
    [[ "${ZFS_PRESETS[*]}" =~ "systemd" ]] && zfs create -o canmount=off -o mountpoint=/var/lib/systemd "${ZFS_ROOT_POOL}"/ROOT/var/lib/systemd

This copy-n-paste logic seems tedious.

Can't we have a mapping from feature to mount point, and then use that in a loop?


In create_partitions we have some apparently random sleep 3 statements. I'm sure there is good reason for them. Spell it out, in the source code. It's unclear why legacy BIOS doesn't need it.

The four info statements could perhaps use a conditional to offer advice more tailored to the current setup.


In the mount_rootfs & create_partitions default clauses, is **) somehow more sweeping than *) ?


Consider adopting set -u, so unset vars cause fatal error.

Consider adopting set -e, so we bail on error. (So false causes exit, unless we suppress that with false || true.)


Overall?

Good job!

I am sad that it seems nearly impossible to put together automated unit / integration tests for the complex logic we find in this codebase.

\$\endgroup\$
6
  • \$\begingroup\$ What do you think about replacing names info and warn with log_info, log_warn? I consider doing it because info seems to be a command pointing to /usr/bin/info. \$\endgroup\$ Commented Feb 15, 2023 at 20:14
  • \$\begingroup\$ Each of them is used in just a single instance, in this codebase. Consider replacing them with echo. It's not obvious to me what use case would have a strong need to alter the verbosity level. They are not imposing uniformity on logged messages, e.g. showing the timestamp + pid. \$\endgroup\$
    – J_H
    Commented Feb 15, 2023 at 20:32
  • \$\begingroup\$ Do you have any ideas on how I can test bash scripts like the above that do stuff like system partitioning, installing Arch, etc.? \$\endgroup\$ Commented Feb 16, 2023 at 18:28
  • \$\begingroup\$ Well, it will be a bit expensive, with creating / tearing-down a VM with suitable attached storage, and in the sandbox applying "dangerous" commands like re-partitioning. Either that, or go the "mock" route, which I frankly find less convincing. There is also the "humble" route, which typically says "keep all fancy logic out of the (untested) UI, so UI is very very simple," but in this case it would say "keep all fancy logic out of dangerous command functions". \$\endgroup\$
    – J_H
    Commented Feb 16, 2023 at 18:51
  • \$\begingroup\$ So there isn't any smart way to perform tests of that script? Do you know tools or tricks that could make testing easier, at least? \$\endgroup\$ Commented Feb 16, 2023 at 18:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.