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
1 Answer 1
#!/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 export
ed.
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.
-
\$\begingroup\$ What do you think about replacing names
info
andwarn
withlog_info
,log_warn
? I consider doing it becauseinfo
seems to be a command pointing to/usr/bin/info
. \$\endgroup\$whiteman808– whiteman8082023年02月15日 20:14:16 +00:00Commented 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– J_H2023年02月15日 20:32:42 +00:00Commented 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\$whiteman808– whiteman8082023年02月16日 18:28:02 +00:00Commented 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– J_H2023年02月16日 18:51:45 +00:00Commented 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\$whiteman808– whiteman8082023年02月16日 18:57:18 +00:00Commented Feb 16, 2023 at 18:57