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