├── README.md └── debian-buster-zfs-root.sh /README.md: -------------------------------------------------------------------------------- 1 | # debian-buster-zfs-root 2 | Installs Debian GNU/Linux 10 Buster to a native ZFS root filesystem using a [Debian Live CD](https://www.debian.org/CD/live/). The resulting system is a fully updateable debian system with no quirks or workarounds. 3 | 4 | ## Warning 5 | 6 | Due to [problems with grub-efi-amd64-signed](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=925309) UEFI secure boot has been disabled until a proper solution is available ([SoerenBusse](https://github.com/hn/debian-buster-zfs-root/issues/3#issuecomment-537257899) has created [a fix](https://github.com/hn/debian-buster-zfs-root/pull/4) which has not been reviewd yet). 7 | 8 | ## Usage 9 | 10 | 1. Boot [Debian Live CD](https://www.debian.org/CD/live/) 11 | 1. Login (user: `user`, password: `live`) and become root 12 | 1. Setup network and export `http_proxy` environment variable (if needed) 13 | 1. Run [this script](https://raw.githubusercontent.com/hn/debian-buster-zfs-root/master/debian-buster-zfs-root.sh) 14 | 1. User interface: Select disks and RAID level 15 | 1. User interface: Decide if you want Legacy BIOS or EFI boot (only if your hardware supports EFI) 16 | 1. Let the installer do the work 17 | 1. User interface: install grub to *all* disks participating in the array (only if you're using Legacy BIOS boot) 18 | 1. User interface: enter root password and select timezone 19 | 1. Reboot 20 | 1. Star [this repository](https://github.com/hn/debian-buster-zfs-root) :) 21 | 22 | ## Fixes included 23 | 24 | * Some mountpoints, notably `/var`, need to be mounted via fstab as the ZFS mount script runs too late during boot. 25 | * The EFI System Partition (ESP) is a single point of failure on one disk, [this is arguably a mis-design in the UEFI specification](https://wiki.debian.org/UEFI#RAID_for_the_EFI_System_Partition). This script installs an ESP partition to every RAID disk, accompanied with a corresponding EFI boot menu. 26 | 27 | ## Bugs 28 | 29 | * `grub-install` sometimes mysteriously fails for disk numbers >= 4 (`grub-install: error: cannot find a GRUB drive for /dev/disk/by-id/...`). 30 | 31 | ## Credits 32 | 33 | * https://github.com/hn/debian-stretch-zfs-root 34 | * https://github.com/zfsonlinux/zfs/wiki/Ubuntu-16.04-Root-on-ZFS 35 | * https://janvrany.github.io/2016/10/fun-with-zfs-part-1-installing-debian-jessie-on-zfs-root.html 36 | 37 | -------------------------------------------------------------------------------- /debian-buster-zfs-root.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # debian-buster-zfs-root.sh 4 | # 5 | # Install Debian GNU/Linux 10 Buster to a native ZFS root filesystem 6 | # 7 | # (C) 2018-2020 Hajo Noerenberg 8 | # 9 | # 10 | # http://www.noerenberg.de/ 11 | # https://github.com/hn/debian-buster-zfs-root 12 | # 13 | # 14 | # This program is free software: you can redistribute it and/or modify 15 | # it under the terms of the GNU General Public License version 3.0 as 16 | # published by the Free Software Foundation. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 21 | # GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License along 24 | # with this program. If not, see . 25 | # 26 | 27 | ### Static settings, overridable by TARGET_* environment variables 28 | 29 | ZPOOL=${TARGET_ZPOOL:-rpool} 30 | TARGETDIST=${TARGET_DIST:-buster} 31 | 32 | PARTBIOS=${TARGET_PARTBIOS:-1} 33 | PARTEFI=${TARGET_PARTEFI:-2} 34 | PARTZFS=${TARGET_PARTZFS:-3} 35 | 36 | SIZESWAP=${TARGET_SIZESWAP:-2G} 37 | SIZETMP=${TARGET_SIZETMP:-3G} 38 | SIZEVARTMP=${TARGET_VARTMP:-3G} 39 | 40 | NEWHOST=${TARGET_HOSTNAME} 41 | NEWDNS=${TARGET_DNS:-8.8.8.8 8.8.4.4} 42 | 43 | ### User settings 44 | 45 | declare -A BYID 46 | while read -r IDLINK; do 47 | BYID["$(basename "$(readlink "$IDLINK")")"]="$IDLINK" 48 | done < <(find /dev/disk/by-id/ -type l) 49 | 50 | for DISK in $(lsblk -I8,254,259 -dn -o name); do 51 | if [ -z "${BYID[$DISK]}" ]; then 52 | SELECT+=("$DISK" "(no /dev/disk/by-id persistent device name available)" off) 53 | else 54 | SELECT+=("$DISK" "${BYID[$DISK]}" off) 55 | fi 56 | done 57 | 58 | TMPFILE=$(mktemp) 59 | whiptail --backtitle "$0" --title "Drive selection" --separate-output \ 60 | --checklist "\nPlease select ZFS drives\n" 20 74 8 "${SELECT[@]}" 2>"$TMPFILE" 61 | 62 | if [ $? -ne 0 ]; then 63 | exit 1 64 | fi 65 | 66 | while read -r DISK; do 67 | if [ -z "${BYID[$DISK]}" ]; then 68 | DISKS+=("/dev/$DISK") 69 | ZFSPARTITIONS+=("/dev/$DISK$PARTZFS") 70 | EFIPARTITIONS+=("/dev/$DISK$PARTEFI") 71 | else 72 | DISKS+=("${BYID[$DISK]}") 73 | ZFSPARTITIONS+=("${BYID[$DISK]}-part$PARTZFS") 74 | EFIPARTITIONS+=("${BYID[$DISK]}-part$PARTEFI") 75 | fi 76 | done < "$TMPFILE" 77 | 78 | whiptail --backtitle "$0" --title "RAID level selection" --separate-output \ 79 | --radiolist "\nPlease select ZFS RAID level\n" 20 74 8 \ 80 | "RAID0" "Striped disks or single disk" off \ 81 | "RAID1" "Mirrored disks (RAID10 for n>=4)" on \ 82 | "RAIDZ" "Distributed parity, one parity block" off \ 83 | "RAIDZ2" "Distributed parity, two parity blocks" off \ 84 | "RAIDZ3" "Distributed parity, three parity blocks" off 2>"$TMPFILE" 85 | 86 | if [ $? -ne 0 ]; then 87 | exit 1 88 | fi 89 | 90 | RAIDLEVEL=$(head -n1 "$TMPFILE" | tr '[:upper:]' '[:lower:]') 91 | 92 | case "$RAIDLEVEL" in 93 | raid0) 94 | RAIDDEF="${ZFSPARTITIONS[*]}" 95 | ;; 96 | raid1) 97 | if [ $((${#ZFSPARTITIONS[@]} % 2)) -ne 0 ]; then 98 | echo "Need an even number of disks for RAID level '$RAIDLEVEL': ${ZFSPARTITIONS[@]}" >&2 99 | exit 1 100 | fi 101 | I=0 102 | for ZFSPARTITION in "${ZFSPARTITIONS[@]}"; do 103 | if [ $((I % 2)) -eq 0 ]; then 104 | RAIDDEF+=" mirror" 105 | fi 106 | RAIDDEF+=" $ZFSPARTITION" 107 | ((I++)) || true 108 | done 109 | ;; 110 | *) 111 | if [ ${#ZFSPARTITIONS[@]} -lt 3 ]; then 112 | echo "Need at least 3 disks for RAID level '$RAIDLEVEL': ${ZFSPARTITIONS[@]}" >&2 113 | exit 1 114 | fi 115 | RAIDDEF="$RAIDLEVEL ${ZFSPARTITIONS[*]}" 116 | ;; 117 | esac 118 | 119 | GRUBPKG=grub-pc 120 | if [ -d /sys/firmware/efi ]; then 121 | whiptail --backtitle "$0" --title "EFI boot" --separate-output \ 122 | --menu "\nYour hardware supports EFI. Which boot method should be used in the new to be installed system?\n" 20 74 8 \ 123 | "EFI" "Extensible Firmware Interface boot" \ 124 | "BIOS" "Legacy BIOS boot" 2>"$TMPFILE" 125 | 126 | if [ $? -ne 0 ]; then 127 | exit 1 128 | fi 129 | if grep -qi EFI $TMPFILE; then 130 | GRUBPKG=grub-efi-amd64 131 | fi 132 | fi 133 | 134 | whiptail --backtitle "$0" --title "Confirmation" \ 135 | --yesno "\nAre you sure to destroy ZFS pool '$ZPOOL' (if existing), wipe all data of disks '${DISKS[*]}' and create a RAID '$RAIDLEVEL'?\n" 20 74 136 | 137 | if [ $? -ne 0 ]; then 138 | exit 1 139 | fi 140 | 141 | ### Start the real work 142 | 143 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=595790 144 | if [ "$(hostid | cut -b-6)" == "007f01" ]; then 145 | dd if=/dev/urandom of=/etc/hostid bs=1 count=4 146 | fi 147 | 148 | DEBRELEASE=$(head -n1 /etc/debian_version) 149 | case $DEBRELEASE in 150 | 9*) 151 | echo "deb http://deb.debian.org/debian/ stretch contrib non-free" >/etc/apt/sources.list.d/contrib-non-free.list 152 | test -f /var/lib/apt/lists/deb.debian.org_debian_dists_stretch_non-free_binary-amd64_Packages || apt-get update 153 | if [ ! -d /usr/share/doc/zfs-dkms ]; then NEED_PACKAGES+=(zfs-dkms); fi 154 | ;; 155 | 10*) 156 | echo "deb http://deb.debian.org/debian/ buster contrib non-free" >/etc/apt/sources.list.d/contrib-non-free.list 157 | test -f /var/lib/apt/lists/deb.debian.org_debian_dists_buster_non-free_binary-amd64_Packages || apt-get update 158 | if [ ! -d /usr/share/doc/zfs-dkms ]; then NEED_PACKAGES+=(zfs-dkms); fi 159 | ;; 160 | *) 161 | echo "Unsupported Debian Live CD release" >&2 162 | exit 1 163 | ;; 164 | esac 165 | if [ ! -f /sbin/zpool ]; then NEED_PACKAGES+=(zfsutils-linux); fi 166 | if [ ! -f /usr/sbin/debootstrap ]; then NEED_PACKAGES+=(debootstrap); fi 167 | if [ ! -f /sbin/sgdisk ]; then NEED_PACKAGES+=(gdisk); fi 168 | if [ ! -f /sbin/mkdosfs ]; then NEED_PACKAGES+=(dosfstools); fi 169 | echo "Need packages: ${NEED_PACKAGES[@]}" 170 | if [ -n "${NEED_PACKAGES[*]}" ]; then DEBIAN_FRONTEND=noninteractive apt-get install --yes "${NEED_PACKAGES[@]}"; fi 171 | 172 | modprobe zfs 173 | if [ $? -ne 0 ]; then 174 | echo "Unable to load ZFS kernel module" >&2 175 | exit 1 176 | fi 177 | test -d /proc/spl/kstat/zfs/$ZPOOL && zpool destroy $ZPOOL 178 | 179 | for DISK in "${DISKS[@]}"; do 180 | echo -e "\nPartitioning disk $DISK" 181 | 182 | sgdisk --zap-all $DISK 183 | 184 | sgdisk -a1 -n$PARTBIOS:34:2047 -t$PARTBIOS:EF02 \ 185 | -n$PARTEFI:2048:+512M -t$PARTEFI:EF00 \ 186 | -n$PARTZFS:0:0 -t$PARTZFS:BF01 $DISK 187 | done 188 | 189 | sleep 2 190 | 191 | zpool create -f -o ashift=12 -o altroot=/target -O atime=off -O mountpoint=none $ZPOOL $RAIDDEF 192 | if [ $? -ne 0 ]; then 193 | echo "Unable to create zpool '$ZPOOL'" >&2 194 | exit 1 195 | fi 196 | 197 | zfs set compression=lz4 $ZPOOL 198 | # The two properties below improve performance but reduce compatibility with non-Linux ZFS implementations 199 | # Commented out by default 200 | #zfs set xattr=sa $ZPOOL 201 | #zfs set acltype=posixacl $ZPOOL 202 | 203 | zfs create $ZPOOL/ROOT 204 | zfs create -o mountpoint=/ $ZPOOL/ROOT/debian-$TARGETDIST 205 | zpool set bootfs=$ZPOOL/ROOT/debian-$TARGETDIST $ZPOOL 206 | 207 | zfs create -o mountpoint=/tmp -o setuid=off -o exec=off -o devices=off -o com.sun:auto-snapshot=false -o quota=$SIZETMP $ZPOOL/tmp 208 | chmod 1777 /target/tmp 209 | 210 | # /var needs to be mounted via fstab, the ZFS mount script runs too late during boot 211 | zfs create -o mountpoint=legacy $ZPOOL/var 212 | mkdir -v /target/var 213 | mount -t zfs $ZPOOL/var /target/var 214 | 215 | # /var/tmp needs to be mounted via fstab, the ZFS mount script runs too late during boot 216 | zfs create -o mountpoint=legacy -o com.sun:auto-snapshot=false -o quota=$SIZEVARTMP $ZPOOL/var/tmp 217 | mkdir -v -m 1777 /target/var/tmp 218 | mount -t zfs $ZPOOL/var/tmp /target/var/tmp 219 | chmod 1777 /target/var/tmp 220 | 221 | zfs create -V $SIZESWAP -b "$(getconf PAGESIZE)" -o primarycache=metadata -o com.sun:auto-snapshot=false -o logbias=throughput -o sync=always $ZPOOL/swap 222 | # sometimes needed to wait for /dev/zvol/$ZPOOL/swap to appear 223 | sleep 2 224 | mkswap -f /dev/zvol/$ZPOOL/swap 225 | 226 | zpool status 227 | zfs list 228 | 229 | debootstrap --include=openssh-server,locales,linux-headers-amd64,linux-image-amd64,joe,rsync,sharutils,psmisc,htop,patch,less --components main,contrib,non-free $TARGETDIST /target http://deb.debian.org/debian/ 230 | 231 | test -n "$NEWHOST" || NEWHOST=debian-$(hostid) 232 | echo "$NEWHOST" >/target/etc/hostname 233 | sed -i "1s/^/127.0.1.1\t$NEWHOST\n/" /target/etc/hosts 234 | 235 | # Copy hostid as the target system will otherwise not be able to mount the misleadingly foreign file system 236 | cp -va /etc/hostid /target/etc/ 237 | 238 | cat << EOF >/target/etc/fstab 239 | # /etc/fstab: static file system information. 240 | # 241 | # Use 'blkid' to print the universally unique identifier for a 242 | # device; this may be used with UUID= as a more robust way to name devices 243 | # that works even if disks are added and removed. See fstab(5). 244 | # 245 | # 246 | /dev/zvol/$ZPOOL/swap none swap defaults 0 0 247 | $ZPOOL/var /var zfs defaults 0 0 248 | $ZPOOL/var/tmp /var/tmp zfs defaults 0 0 249 | EOF 250 | 251 | mount --rbind /dev /target/dev 252 | mount --rbind /proc /target/proc 253 | mount --rbind /sys /target/sys 254 | ln -s /proc/mounts /target/etc/mtab 255 | 256 | perl -i -pe 's/# (en_US.UTF-8)/$1/' /target/etc/locale.gen 257 | echo 'LANG="en_US.UTF-8"' > /target/etc/default/locale 258 | chroot /target /usr/sbin/locale-gen 259 | 260 | chroot /target /usr/bin/apt-get update 261 | 262 | chroot /target /usr/bin/apt-get install --yes grub2-common $GRUBPKG zfs-initramfs zfs-dkms 263 | grep -q zfs /target/etc/default/grub || perl -i -pe 's/quiet/boot=zfs quiet/' /target/etc/default/grub 264 | chroot /target /usr/sbin/update-grub 265 | 266 | if [ "${GRUBPKG:0:8}" == "grub-efi" ]; then 267 | 268 | # "This is arguably a mis-design in the UEFI specification - the ESP is a single point of failure on one disk." 269 | # https://wiki.debian.org/UEFI#RAID_for_the_EFI_System_Partition 270 | mkdir -pv /target/boot/efi 271 | I=0 272 | for EFIPARTITION in "${EFIPARTITIONS[@]}"; do 273 | mkdosfs -F 32 -n EFI-$I $EFIPARTITION 274 | mount $EFIPARTITION /target/boot/efi 275 | chroot /target /usr/sbin/grub-install --target=x86_64-efi --no-uefi-secure-boot --efi-directory=/boot/efi --bootloader-id="Debian $TARGETDIST (RAID disk $I)" --recheck --no-floppy 276 | umount $EFIPARTITION 277 | if [ $I -gt 0 ]; then 278 | EFIBAKPART="#" 279 | fi 280 | echo "${EFIBAKPART}PARTUUID=$(blkid -s PARTUUID -o value $EFIPARTITION) /boot/efi vfat defaults 0 1" >> /target/etc/fstab 281 | ((I++)) || true 282 | done 283 | fi 284 | 285 | if [ -d /proc/acpi ]; then 286 | chroot /target /usr/bin/apt-get install --yes acpi acpid 287 | chroot /target service acpid stop 288 | fi 289 | 290 | ETHDEV=$(udevadm info -e | grep "ID_NET_NAME_ONBOARD=" | head -n1 | cut -d= -f2) 291 | test -n "$ETHDEV" || ETHDEV=$(udevadm info -e | grep "ID_NET_NAME_PATH=" | head -n1 | cut -d= -f2) 292 | test -n "$ETHDEV" || ETHDEV=enp0s1 293 | echo -e "\nauto $ETHDEV\niface $ETHDEV inet dhcp\n" >>/target/etc/network/interfaces 294 | for DNS in $NEWDNS; do 295 | echo -e "nameserver $DNS" >> /target/etc/resolv.conf 296 | done 297 | 298 | chroot /target /usr/bin/passwd 299 | chroot /target /usr/sbin/dpkg-reconfigure tzdata 300 | 301 | sync 302 | 303 | #zfs umount -a 304 | 305 | ## chroot /target /bin/bash --login 306 | ## zpool import -R /target rpool 307 | 308 | --------------------------------------------------------------------------------