Last active
June 17, 2023 13:29
-
-
Save pweldon/82c05505543ea194e9c704980389c3bf to your computer and use it in GitHub Desktop.
zfs nixos bootstrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# credit: https://github.com/a-schaefers/themelios | |
set -euxo pipefail | |
die() { | |
[ $# -gt 0 ] && printf -- "%s\n" "$*" | |
exit 1 | |
} | |
uefi_or_legacy() { | |
[[ -d "/sys/firmware/efi/efivars" ]] && uefi_install="1" | |
} | |
disk_prep() { | |
# some initial translation for whether or not the script was provided disks with sd* or /dev/disk/by-id/*, etc. | |
echo "${zfs_pool_disks[0]}" | grep -q "/dev/sd" && use_sdX="1" | |
echo "${zfs_pool_disks[0]}" | grep -q "/dev/nvme" && use_nvme="1" | |
if [[ $use_sdX ]] | |
then | |
boot_part="2" | |
slog_partition="3" | |
zpool_partition="4" | |
elif [[ $use_nvme ]] | |
then | |
boot_part="p2" | |
slog_partition="p3" | |
zpool_partition="p4" | |
else | |
boot_part="-part2" | |
slog_partition="-part3" | |
zpool_partition="-part4" | |
fi | |
sgdisk_clear() { | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
echo "clearing disk with sgdisk..." | |
sgdisk --zap-all "$disk_id" || die "sgdisk_clear failed" | |
done | |
} | |
[[ $use_sgdisk_clear == "true" ]] && sgdisk_clear | |
wipefs_all() { | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
echo "wiping disk signatures with wipefs..." | |
wipefs -fa "$disk_id" || die "wipefs_all failed" | |
done | |
} | |
[[ $use_wipefs_all == "true" ]] && wipefs_all | |
dd_zero() { | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
echo "writing zeros to ${disk_id}..." | |
dd if=/dev/zero of="$disk_id" bs=1M oflag=direct status=progress & | |
done | |
wait | |
} | |
[[ $use_zero_disks == "true" ]] && dd_zero | |
} | |
zpool_create() { | |
echo "creating zpool..." | |
zpool create -f \ | |
-O compression=lz4 \ | |
-O atime=on \ | |
-O relatime=on \ | |
-O normalization=formD \ | |
-O xattr=sa \ | |
-O acltype=posixacl \ | |
-m none \ | |
-R /mnt \ | |
$zfs_pool_name \ | |
$zfs_pool_type \ | |
${zfs_pool_disks[@]/%/$zpool_partition} || die "zpool_create failed" | |
} | |
disk_part() { | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
sgdisk -og "$disk_id" | |
echo "making bios boot partition..." | |
sgdisk -a 1 -n 1:48:2047 -t 1:ef02 -c 0:"BIOS Boot" "$disk_id" || die "disk_part failed" | |
partx -u "$disk_id" | |
echo "making 1G /boot fat32 ESP partition..." | |
sgdisk -n 2:0:1G -c 0:"Fat32 ESP Boot" -t 0:ef00 "$disk_id" || die "disk_part failed" | |
partx -u "$disk_id" | |
echo "making SLOG partition..." | |
sgdisk -n 3:0:4G -c 0:"SLOG" -t 0:ef00 "$disk_id" || die "disk_part failed" | |
partx -u "$disk_id" | |
echo "making zpool partition..." | |
sgdisk -n 4:0:0 -c 0:"ZPOOL" -t 0:8300 "$disk_id" || die "disk_part failed" | |
sgdisk -p "$disk_id" || die "disk_part failed" | |
partx -u "$disk_id" | |
sleep 5 | |
done | |
sleep 10 | |
} | |
mount_boots() { | |
sleep 10 | |
declare -i bootnum=0 | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
mkfs.vfat -F32 "${disk_id}${boot_part}" || die "mount_boots mkfs.vfat failed" | |
mkdir -p "/mnt/boot${bootnum}" | |
mount "${disk_id}${boot_part}" "/mnt/boot${bootnum}" | |
((bootnum++)) | |
done | |
} | |
mount_create_datasets() { | |
echo "Creating and mounting datasets in /mnt..." | |
# / (root) datasets | |
zfs create -o mountpoint=none -o canmount=off -o encryption=aes-128-gcm -o keyformat=passphrase "$zfs_pool_name/ROOT" | |
zfs create -o mountpoint=legacy -o canmount=on "$zfs_pool_name/ROOT/nixos" | |
mount -t zfs "$zfs_pool_name/ROOT/nixos" /mnt | |
zpool set bootfs="$zfs_pool_name/ROOT/nixos" "$zfs_pool_name" | |
# 1G fat32 /boot(X) ESP | |
mount_boots | |
# mount /nix outside of the root dataset | |
zfs create -o mountpoint=legacy -o canmount=on "$zfs_pool_name/nix" | |
mkdir /mnt/nix | |
mount -t zfs "$zfs_pool_name/nix" /mnt/nix | |
mkdir -p /mnt/{home,tmp} | |
# /home datasets | |
zfs create -o mountpoint=legacy -o canmount=on "$zfs_pool_name/ROOT/home" | |
mount -t zfs "$zfs_pool_name/ROOT/home" /mnt/home | |
# /tmp datasets | |
zfs create -o mountpoint=legacy -o canmount=on -o sync=disabled "$zfs_pool_name/tmp" | |
mount -t zfs "$zfs_pool_name/tmp" /mnt/tmp | |
chmod 1777 /mnt/tmp | |
# Reservation | |
zfs create -o refreservation=1G -o mountpoint=none "$zfs_pool_name/reserved" | |
zfs_auto_snapshot() { | |
for dataset in "${zfs_auto_snapshot[@]}" | |
do | |
echo "Setting property com.sun:auto-snapshot=true to ${dataset}..." | |
zfs set com.sun:auto-snapshot=true "$dataset" | |
done | |
} | |
zfs_auto_snapshot | |
} | |
bootstrap_nixcfg() { | |
# this is for generating our ./hardware-configuration.nix files. | |
# ./configuration.nix will be overwritten shortly hereafter. | |
echo "executing nixos-generate-config --root /mnt" | |
nixos-generate-config --root /mnt || die "nixos-generate-config --root /mnt failed" | |
echo "generating random hostid..." | |
zfs_host_id="$(head -c4 /dev/urandom | od -A none -t x4 | cut -d ' ' -f 2)" | |
echo "$zfs_host_id" | |
cp /mnt/etc/nixos/configuration.nix{,.orig} | |
# create /mnt/etc/nixos/configuration.nix and import user's top_level_nixfile. | |
cat << EOF > /mnt/etc/nixos/configuration.nix | |
{ ... }: | |
{ | |
imports = [ | |
./hardware-configuration.nix | |
./zfs-configuration.nix | |
]; | |
services.xserver.desktopManager.plasma5.enable = true; | |
services.xserver.displayManager.sddm.enable = true; | |
services.xserver.xkbVariant = "dvorak"; | |
console.useXkbConfig = true; | |
nixpkgs.config.allowUnfree = true; | |
programs.bash.enable = true; | |
users.extraUsers.peter = { | |
isNormalUser = true; | |
home = "/home/peter"; | |
extragroups = [ "wheel" "networkmanager" ]; | |
hashedPassword = "$6$yQwAqlTX$o5i1KMWOUlymUzkZ451aC1JjIsudcj54WPSNERGiHJVoE3aFqocTLabWMUWTXj7u8pKlyKEJdQaB.trxr3ppd."; | |
} | |
} | |
EOF | |
# create /mnt/etc/nixos/zfs-configuration.nix | |
cat << EOF > /mnt/etc/nixos/zfs-configuration.nix | |
{ ... }: | |
{ imports = []; | |
boot.supportedFilesystems = [ "zfs" ]; | |
boot.loader = { | |
$(if [[ $uefi_install ]]; then # reinstall in legacy mode, it's better. :P | |
cat <<- UEFI | |
efi = { | |
canTouchEfiVariables = true; | |
efiSysMountPoint = "/boot0"; # use the same mount point here. | |
}; | |
UEFI | |
fi) | |
grub = { | |
enable = true; | |
version = 2; | |
copyKernels = true; | |
$(if [[ $uefi_install ]]; then | |
cat <<- UEFI | |
efiSupport = true; | |
UEFI | |
fi) | |
$(if [ ${#zfs_pool_disks[@]} -gt 1 ] | |
then | |
cat <<- MIRROREDBOOTS | |
mirroredBoots = [ | |
$(declare -i bootnum=0 | |
for disk_id in "${zfs_pool_disks[@]}" | |
do | |
echo "{devices = [ \"${disk_id}\" ]; path = \"/boot${bootnum}\";}" | |
((bootnum++)) | |
done) | |
]; | |
MIRROREDBOOTS | |
else | |
cat <<- SINGLEBOOT | |
devices = [ | |
$(for disk_id in "${zfs_pool_disks[@]}" | |
do | |
echo "\"$disk_id\"" | |
done) | |
]; | |
SINGLEBOOT | |
fi) | |
}; | |
}; | |
# The 32-bit host id of the machine, formatted as 8 hexadecimal characters. | |
# You should try to make this id unique among your machines. | |
networking.hostId = "$zfs_host_id"; | |
# noop, the recommended elevator with zfs. | |
# shell_on_fail allows to force import manually in the case of zfs import failure. | |
boot.kernelParams = [ "elevator=noop" "boot.shell_on_fail" ]; | |
# Uncomment [on a working system] to ensure extra safeguards are active that zfs uses to protect zfs pools: | |
boot.zfs.forceImportAll = false; | |
boot.zfs.forceImportRoot = false; | |
boot.zfs.requestEncryptionCredentials = true; | |
) | |
$([[ $nix_zfs_configuration_extra_enabled == "true" ]] && cat <<- EXTRA | |
# Enables periodic scrubbing of ZFS pools. | |
services.zfs.autoScrub.enable = $nix_zfs_extra_auto_scrub; | |
# Enable the (OpenSolaris-compatible) ZFS auto-snapshotting service. | |
services.zfs.autoSnapshot = { | |
enable = $nix_zfs_extra_auto_snapshot_enabled; | |
frequent = $nix_zfs_extra_auto_snapshot_frequent; | |
hourly = $nix_zfs_extra_auto_snapshot_hourly; | |
daily = $nix_zfs_extra_auto_snapshot_daily; | |
weekly = $nix_zfs_extra_auto_snapshot_weekly; | |
monthly = $nix_zfs_extra_auto_snapshot_monthly; | |
}; | |
# Use gc.automatic to keep disk space under control. | |
nix.autoOptimiseStore = $nix_zfs_extra_auto_optimise_store; | |
nix.gc.automatic = $nix_zfs_extra_gc_automatic; | |
nix.gc.dates = "$nix_zfs_extra_gc_dates"; | |
nix.gc.options = "$nix_zfs_extra_gc_options"; | |
# Clean /tmp automatically on boot. | |
boot.cleanTmpDir = $nix_zfs_extra_clean_tmp_dir; | |
EXTRA | |
) | |
} | |
EOF | |
# give user a retry option with --show-trace to have a chance to fix imports on another tty. :) | |
nixos-install-show-trace() { | |
nixos-install $install_arguments --show-trace || nixos-install_fail_retry | |
} | |
nixos-install_fail_retry() { | |
echo "themelios hint: check /mnt/etc/nixos/configuration.nix and your other files in /mnt/nix-config before trying again." | |
echo "themelios hint: make sure you are using relative path imports for all of your .nix files." | |
read -p "nixos-install failed, retry? will add --show-trace (y or n) " -n 1 -r | |
if [[ $REPLY =~ ^[Yy]$ ]] | |
then | |
nixos-install-show-trace | |
else | |
die "the only steps remaining after nixos-install should be unmounting /mnt and exporting the pool :) good luck." | |
fi | |
} | |
install() { | |
echo "executing nixos-install" | |
nixos-install $install_arguments || nixos-install_fail_retry | |
} | |
# install | |
} | |
installation_complete() { | |
echo "unmounting /mnt" | |
umount -lR /mnt | |
echo "exporting $zfs_pool_name" | |
zpool export "$zfs_pool_name" | |
read -p "finished. reboot now? (y or n) " -n 1 -r | |
[[ $REPLY =~ ^[Yy]$ ]] && reboot | |
} | |
# start executing code ! | |
use_sgdisk_clear="true" # use sgdisk --clear | |
use_wipefs_all="true" # use wipefs --all | |
use_zero_disks="false" # use dd if=/dev/zero ... | |
install_arguments="" # any extra arguments to nixos-install, like --no-root-passwd | |
zfs_pool_name="nixpool" | |
zfs_pool_disks=("/dev/disk/by-id/ata-Crucial_CT525MX300SSD4_173818E39F49" "/dev/disk/by-id/ata-Crucial_CT525MX300SSD4_173818E39F8C") # Note: using /dev/disk/by-id is also preferable. | |
zfs_pool_type="mirror" # use "" for single, or "mirror", "raidz1", etc. | |
zfs_auto_snapshot=("$zfs_pool_name/ROOT") # datasets to be set with com.sun:auto-snapshot=true | |
nix_zfs_configuration_extra_enabled="false" # uncomment below if set to true | |
nix_zfs_extra_auto_scrub="true" | |
nix_zfs_extra_auto_snapshot_enabled="true" # Enable the ZFS auto-snapshotting service | |
nix_zfs_extra_auto_snapshot_frequent="8" | |
nix_zfs_extra_auto_snapshot_hourly="24" | |
nix_zfs_extra_auto_snapshot_daily="0" | |
nix_zfs_extra_auto_snapshot_weekly="0" | |
nix_zfs_extra_auto_snapshot_monthly="0" | |
nix_zfs_extra_auto_optimise_store="true" | |
nix_zfs_extra_gc_automatic="true" | |
nix_zfs_extra_gc_dates="weekly" | |
nix_zfs_extra_gc_options="--delete-older-than 7d" | |
nix_zfs_extra_clean_tmp_dir="true" | |
uefi_or_legacy | |
disk_prep | |
disk_part | |
zpool_create | |
mount_create_datasets | |
bootstrap_nixcfg |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment