5
\$\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
asked Feb 12, 2023 at 14:23
\$\endgroup\$

1 Answer 1

2
\$\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.

answered Feb 12, 2023 at 20:48
\$\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\$ 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\$ 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

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.