#!/bin/bash -ex # Automatic setup of Debian or Ubuntu on an Online.net or Hetzner rescue system. # # ref https://github.com/openzfs/zfs/wiki/Debian-Buster-Root-on-ZFS # # Author: Carl Kittelberger # Version: 3.0.0-beta.2 # Date: 2021-05-01 raidtype=auto disks=() hostname=${hostname:-$(hostname)} rpool="" bpool="" system="${system:-$(lsb_release -is | tr '[[:upper:]]' '[[:lower:]]')}" mirror="http://deb.debian.org/debian" # Ensure only valid defaults are set for system case "$system" in debian|ubuntu) ;; *) system="" ;; esac usage() { echo "Usage: $0 [options] [...]" >&2 echo "" >&2 echo "Disks need to be supplied as /dev/disk/by-id/... path references.">&2 echo "" >&2 echo "Options:">&2 echo "" >&2 echo " -h, --help Show this help message" >&2 echo " --hostname HOSTNAME Set hostname to be written to the installed operating system. Default: $hostname" >&2 echo " --rpool RPOOL Set name of ZFS data pool. Default: -rpool ($hostname-rpool)" >&2 echo " --bpool BPOOL Set name of ZFS data pool. Default: -bpool ($hostname-bpool)" >&2 if [ -n "$system" ] then echo " -s, --system SYSTEM The system to install. Valid values are debian or ubuntu. Defaults to currently running operating system ($system)." >&2 else echo " -s, --system SYSTEM The system to install. Valid values are debian or ubuntu. Required because the current operating system is not actually supported as target operating system." >&2 fi echo " --hetzner Use Hetzner network mirror." >&2 echo " --online Use Online.net network mirror." >&2 echo " --mirror MIRROR Use custom mirror . Default: $mirror" >&2 } noproc=0 while [ "$noproc" -eq 0 ] && [ "$#" -gt 0 ] do opt="$1" case "$opt" in -h|--help) usage exit ;; --hostname) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi hostname="$2" shift 2 ;; --rpool) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi rpool="$2" shift 2 ;; --bpool) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi bpool="$2" shift 2 ;; -s|--system) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi system="$2" shift 2 ;; -t|--type) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi raidtype="$2" shift 2 ;; --hetzner) mirror="http://mirror.hetzner.de/${system}/packages" shift 1 ;; --online) mirror="https://mirrors.online.net/${system}" shift 1 ;; --mirror) if [ -z "$2" ] then echo "ERROR: Expected a value following $opt." >&2 fi mirror="$2" shift 2 ;; --) noproc=1 shift 1 ;; *) noproc=1 ;; esac done rpool="${rpool:-${hostname}-rpool}" bpool="${bpool:-${hostname}-bpool}" echo "Data pool will be named $rpool." >&2 echo "Boot pool will be named $bpool." >&2 # Check disks use /dev/disk/by-id/... style reference for disk in "$@" do case "$disk" in /dev/disk/by-id/*) echo "Considering disk $disk." >&2 disks+=("$disk") ;; *) echo "ERROR: Invalid disk reference $disk, needs to be formatted like this: /dev/disk/by-id/..." >&2 exit 1 ;; esac done # Did we receive any disk information at all? if [ "${#disks[@]}" -lt 1 ] then echo "ERROR: Need at least one disk." >&2 exit 1 fi # Check if we got a valid target operating system to install case "$system" in debian|ubuntu) ;; "") echo "ERROR: You need to set a system to be installed." >&2 exit 1 ;; *) echo "ERROR: You need to set a valid system to be installed. $system is not supported, only debian and ubuntu are." >&2 exit 1 ;; esac # If raidtype is auto, automatically select one: # - 1 disk: striped # - 2 disks: mirror # - 3+ disks: raidz if [ "$raidtype" = auto ] then raidtype=striped if [ "${#disks[@]}" -gt 2 ] then raidtype=raidz elif [ "${#disks[@]}" -gt 1 ] then raidtype=mirror fi echo "Autoselected RAID type for ${#disks[@]} disks: $raidtype" >&2 fi alldisks() { for disk in "${disks[@]}" do "$@" "$disk" || return "$?" done } allpartref() { for disk in "${disks[@]}" do echo "${disk}-part$1" done } # Only apply a patch if it wasn't already applied. # Turns the error case of an already applied patch into a success case. softpatch() { patch "$@" || if [ "$?" -ne 1 ] then return $? else true fi } if [ "$(id -u)" != 0 ]; then sudo -v else sudo() { "$@" } export sudo fi export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" sudo apt install -y "linux-image-$(uname -r)" "linux-headers-$(uname -r)" eatmydata sudo eatmydata apt-get autoremove -y $(eatmydata apt list | grep -P '^linux-(image|headers)' | grep -vP "^linux-(image|headers)-$(uname -r)" | cut -d/ -f 1) for d in /lib/modules/*/ do kernel_version=$(basename "$d") if [ "${kernel_version}" != "$(uname -r)" ] then rm -rv "$d" fi done sudo umount -l /mnt/dev || true sudo umount -l /mnt/proc || true sudo umount -l /mnt/sys || true #sudo touch /boot/vmlinuz-4.4.0-97 /boot/vmlinuz-4.4.0-97-generic /boot/initrd.img-4.4.0-97 /boot/initrd.img-4.4.0-97-generic #sudo sed -i 's,\bmain\b,main contrib,g' /etc/apt/sources.list sudo eatmydata apt install -y mdadm gdisk "linux-headers-$(uname -r)" if ! modprobe zfs then # check for backports codename="$(lsb_release -cs)" has_backports="$(curl -Ifso /dev/null "https://deb.debian.org/debian/dists/${codename}-backports/Release" && echo 1 || echo 0)" if [ "${has_backports}" -ne 0 ] then echo "deb https://deb.debian.org/debian ${codename}-backports main contrib" > /etc/apt/sources.list.d/backports.list eatmydata apt update fi if [ "${has_backports}" -ne 0 ] then sudo eatmydata apt install -y -t "${codename}-backports" zfs-dkms # sudo eatmydata apt install -y devscripts # # Build spl # sudo eatmydata apt build-dep -y spl-linux # rm -rf spl-linux* # eatmydata apt source spl-linux # wget -O spl-pr-710.patch https://github.com/zfsonlinux/spl/pull/710.patch # if (cd spl-linux*/ && patch --dry-run --silent -N -p1 -i ../spl-pr-710.patch) # then # cd spl-linux*/ # patch -N -p1 -i ../spl-pr-710.patch # echo '4.19' > debian/linux_compat # dch -v 0.7.12 "Apply spl 0.7.12 patchset." # cd ../spl-linux-*/ # dpkg-buildpackage -b # cd .. # sudo eatmydata apt install -y ./*.deb # rm *.deb # elif [ "$?" -eq 1 ] # then # echo "Patch was already applied, skipping spl-linux build." >&2 # else # echo "ERROR: Failed to apply patch." >&2 # exit 1 # fi # rm spl-pr-710.patch # # Build zfs # sudo eatmydata apt build-dep -y zfs-linux # rm -rf zfs-linux* # eatmydata apt source zfs-linux # wget -O zfs-pr-8078.patch https://github.com/zfsonlinux/zfs/pull/8078.patch # if (cd zfs-linux*/ && patch --dry-run --silent -N -p1 -i ../zfs-pr-8078.patch) # then # cd zfs-linux*/ # patch -N -p1 -i ../zfs-pr-8078.patch # echo '4.19' > debian/linux_compat # dch -v 0.7.12 "Apply zfsonlinux 0.7.12 patchset." # cd ../zfs-linux-*/ # dpkg-buildpackage -b # cd .. # rm *dracut*.deb # sudo eatmydata apt install -y ./*.deb # rm *.deb # elif [ "$?" -eq 1 ] # then # echo "Patch was already applied, skipping zfs build and installing zfs-dkms binary distribution." >&2 # sudo eatmydata apt install -y zfs-dkms # else # echo "ERROR: Failed to apply patch." >&2 # exit 1 # fi # rm zfs-pr-8078.patch else sudo eatmydata apt install -y zfs-dkms fi sudo modprobe zfs fi sudo eatmydata apt install -y jq debootstrap_version=$(wget -qO- https://sources.debian.org/api/src/debootstrap/ | jq '.versions[0].version' -r) case "$system" in debian) sudo eatmydata apt install -y debian-archive-keyring wget -Odebootstrap.deb https://deb.debian.org/debian/pool/main/d/debootstrap/debootstrap_${debootstrap_version}_all.deb ;; ubuntu) sudo eatmydata apt install -y ubuntu-keyring wget -Odebootstrap.deb http://de.archive.ubuntu.com/ubuntu/pool/main/d/debootstrap/debootstrap_${debootstrap_version}ubuntu1_all.deb ;; *) echo "Unsupported system: $system" exit 1 ;; esac sudo eatmydata apt install -y --allow-downgrades ./debootstrap.deb sudo zpool export "${hostname}" || true sudo zpool export "${bpool}" || true sudo zpool export "${rpool}" || true sudo zpool destroy "${hostname}" || true sudo zpool destroy "${bpool}" || true sudo zpool destroy "${rpool}" || true alldisks sudo mdadm --zero-superblock --force || true alldisks sudo sgdisk --zap-all alldisks sudo blkdiscard || for d in "${disks[@]}"; do sudo dd if=/dev/zero of="${d}" bs=32M count=4 oflag=direct status=progress done partBIOS=1 partUEFI=2 partBootPool=3 partRootPool=4 alldisks sudo sgdisk -a1 -n1:24K:+1000K -t1:EF02 # bios boot alldisks sudo sgdisk -n2:1M:+512M -t2:EF00 # uefi boot alldisks sudo sgdisk -n3:0:+1G -t3:BF01 # boot pool alldisks sudo sgdisk -n4:0:0 -t4:BF01 # zfs pool sudo partprobe # sudo hdparm -z "${disks[@]}" sleep 1 for d in $( allpartref "${partBootPool}" allpartref "${partRootPool}" ) do sudo blkdiscard "$d" || sudo dd if=/dev/zero of="$d" bs=1M count=1 done # create boot pool sudo zpool create -o ashift=12 -d \ -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@spacemap_histogram=enabled \ -o feature@userobj_accounting=enabled \ -o feature@zpool_checkpoint=enabled \ -o feature@spacemap_v2=enabled \ -o feature@project_quota=enabled \ -o feature@resilver_defer=enabled \ -o feature@allocation_classes=enabled \ -O acltype=posixacl -O canmount=off -O compression=lz4 -O devices=off \ -O normalization=formD -O relatime=on -O xattr=sa \ -O mountpoint=/ -R /mnt -f \ "$bpool" "$raidtype" \ $(allpartref "${partBootPool}") # create root pool sudo zpool create -o ashift=12 \ -O acltype=posixacl -O canmount=off -O compression=lz4 \ -O dnodesize=auto -O normalization=formD -O relatime=on -O xattr=sa \ -O mountpoint=/ -R /mnt -f \ "$rpool" "$raidtype" \ $(allpartref "${partRootPool}") # create filesystem dataset sudo zfs create -o canmount=off -o mountpoint=none "$rpool/ROOT" sudo zfs create -o canmount=off -o mountpoint=none "$bpool/BOOT" # create filesystem dataset for root fs sudo zfs create -o canmount=noauto -o mountpoint=/ "$rpool/ROOT/operating_system" sudo zfs mount $rpool/ROOT/operating_system sudo zfs create -o canmount=noauto -o mountpoint=/boot "$bpool/BOOT/operating_system" sudo zfs mount $bpool/BOOT/operating_system #sudo zpool set bootfs=$rpool/ROOT/operating_system $rpool # create datasets for folders sudo zfs create -o setuid=off "$rpool/home" sudo zfs create -o mountpoint=/root "$rpool/home/root" sudo zfs create -o canmount=off -o setuid=off -o exec=off "$rpool/var" sudo zfs create -o canmount=off "$rpool/var/lib" sudo zfs create -o acltype=posixacl -o xattr=sa "$rpool/var/log" sudo zfs create "$rpool/var/mail" sudo zfs create "$rpool/var/spool" # Exclude cache and tmp files from snapshots sudo zfs create -o com.sun:auto-snapshot=false "$rpool/var/cache" sudo zfs create -o com.sun:auto-snapshot=false -o exec=on "$rpool/var/tmp" # We may be using /usr/local sudo zfs create -o canmount=off "$rpool/usr" sudo zfs create "$rpool/usr/local" # We may be using /opt sudo zfs create "$rpool/opt" # Do not snapshot ZFS sudo zfs create "$rpool/srv" # sudo zfs create -o com.sun:auto-snapshot=false -o mountpoint=/srv/nfs4 "$rpool/srv/nfs" # install system sudo chmod 1777 /mnt/var/tmp if [ -z "$codename" ] then case "$system" in debian) codename="$(curl -L https://deb.debian.org/debian/dists/stable/Release | grep -Po '^Codename: \K(.+)$')" ;; ubuntu) codename="bionic" # TODO ;; *) echo "ERROR: Unsupported system: $system" >&2 exit 1 ;; esac fi sudo eatmydata debootstrap --include=console-setup,eatmydata,ca-certificates,coreutils,gdisk,ssh,zsh,nano,lsb-release "${codename}" /mnt "$mirror" sudo zfs set devices=off "$rpool" # hostname/hosts configuration echo "${hostname}" | sudo tee /mnt/etc/hostname sudo tee /mnt/etc/hosts </dev/null sudo mkdir -p /mnt/etc/initramfs-tools/conf.d echo RESUME=none | sudo tee /mnt/etc/initramfs-tools/conf.d/resume >/dev/null # prepare for chroot sudo mount --bind /dev /mnt/dev sudo mount --bind /proc /mnt/proc sudo mount --bind /sys /mnt/sys # configure codename="$(sudo chroot /mnt lsb_release -cs)" case "$system" in debian) sudo chroot /mnt grep '\bcontrib\b' /etc/apt/sources.list ||\ sudo chroot /mnt sed -i 's,\bmain\b,main contrib,g' /etc/apt/sources.list url="https://deb.debian.org/debian" if curl -o /dev/null --fail "${url}/dists/${codename}-backports/Release" then echo "deb ${url} ${codename}-backports main contrib" |\ sudo chroot /mnt tee /etc/apt/sources.list.d/backports.list fi success=0 for url in \ https://deb.debian.org/debian-security \ http://security.debian.org do for name in \ "${codename}/updates" \ "${codename}" do if curl -o /dev/null --fail "${url}/dists/${codename}/updates/Release" then echo "deb ${url} ${name} main contrib" |\ sudo chroot /mnt tee /etc/apt/sources.list.d/security.list success=1 break fi done if [ "$success" -ne 0 ] then break fi done ;; ubuntu) sudo chroot /mnt eatmydata apt update sudo chroot /mnt eatmydata apt install -y software-properties-common sudo chroot /mnt eatmydata apt-add-repository "${mirror}" multiverse universe sudo chroot /mnt ln -s /proc/self/mounts /etc/mtab ;; esac sudo chroot /mnt eatmydata apt update sudo chroot /mnt eatmydata apt install -y locales sudo chroot /mnt dpkg-reconfigure locales sudo chroot /mnt dpkg-reconfigure tzdata case "$system" in debian) sudo chroot /mnt eatmydata apt install -y linux-headers-amd64 linux-image-amd64 ;; ubuntu) sudo chroot /mnt eatmydata apt install -y --no-install-recommends linux-image-generic linux-headers-generic ;; esac sudo chroot /mnt eatmydata apt install -y -t "${codename}-backports" zfsutils-linux zfs-dkms zfs-initramfs sudo chroot /mnt sh -c 'ZPOOL_VDEV_NAME_PATH=YES DEBIAN_NONINTERACTIVE=true eatmydata apt install -y grub-pc' sudo tee /etc/ssh/sshd_config <&2 exit 1 fi sudo chroot /mnt update-initramfs -u -k all sudo chroot /mnt sed -e 's#GRUB_CMDLINE_LINUX="\(.*\)"#GRUB_CMDLINE_LINUX="root=ZFS='"$rpool"'/ROOT/operating_system \1"#' -i /etc/default/grub sudo chroot /mnt update-grub for d in "${disks[@]}" do sudo chroot /mnt sh -c "ZPOOL_VDEV_NAME_PATH=YES grub-install '${d}'" done if [ "$(sudo chroot /mnt find /boot/grub -name zfs.mod | wc -l)" -lt 1 ] then echo "ERROR: Installed GRUB does not seem to have ZFS module. Aborting." >&2 exit 1 fi # fix filesystem mount ordering sudo zfs set mountpoint=legacy "$bpool/BOOT/operating_system" sudo tee -a /mnt/etc/fstab < /dev/null #sudo chroot /mnt ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf #sudo chroot /mnt systemctl enable systemd-resolved.service sudo chroot /mnt zfs snapshot "$bpool/BOOT/operating_system@install" sudo chroot /mnt zfs snapshot "$rpool/ROOT/operating_system@install" sudo chroot /mnt hostnamectl set-hostname "${hostname}" ||\ (printf "%s" "${hostname}" | sudo chroot /mnt tee /etc/hostname >/dev/null) sudo chroot /mnt bash -i # unmount sudo umount -l /mnt/dev sudo umount -l /mnt/proc sudo umount -l /mnt/sys #mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} sudo umount -lf {} sudo zpool export "$bpool" sudo zpool export "$rpool" echo "Installation done."