From 69ff5f955bafd35d363fc4cc20560ef716522629 Mon Sep 17 00:00:00 2001 From: Andrey Prokopenko <9478806+terem42@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:25:17 +0100 Subject: [PATCH] new OS versions added (#87) --- README.md | 14 +- hetzner-debian13-zfs-setup.sh | 951 +++++++++++++++++++++++++++++++++ hetzner-ubuntu24-zfs-setup.sh | 973 ++++++++++++++++++++++++++++++++++ 3 files changed, 1937 insertions(+), 1 deletion(-) create mode 100644 hetzner-debian13-zfs-setup.sh create mode 100644 hetzner-ubuntu24-zfs-setup.sh diff --git a/README.md b/README.md index 5b5b62c..e7ea077 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![shellcheck](https://github.com/terem42/zfs-hetzner-vm/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/terem42/zfs-hetzner-vm/actions/workflows/shellcheck.yml) -Scripts to install Debian 10, 11, 12 or Ubuntu 18 LTS, 20 LTS, 22 LTS with ZFS root on Hetzner root servers (virtual and dedicated).
+Scripts to install Debian 10, 11, 12, 13 or Ubuntu 18 LTS, 20 LTS, 22 LTS, 24 LTS with ZFS root on Hetzner root servers (virtual and dedicated). __WARNING:__ all data on the disk will be destroyed. ## How to use: @@ -30,6 +30,12 @@ Debian 12 minimal setup with SSH server wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian12-zfs-setup.sh | bash - ```` +Debian 13 minimal setup with SSH server + +````bash +wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian13-zfs-setup.sh | bash - +```` + Ubuntu 18.04 LTS minimal setup with SSH server ````bash @@ -48,6 +54,12 @@ Ubuntu 22 LTS minimal setup with SSH server wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu22-zfs-setup.sh | bash - ```` +Ubuntu 24 LTS minimal setup with SSH server + +````bash +wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu24-zfs-setup.sh | bash - +```` + Answer script questions about desired hostname and ZFS ARC cache size. To cope with network failures its higly recommended to run the commands above inside screen console, type `man screen` for more info. diff --git a/hetzner-debian13-zfs-setup.sh b/hetzner-debian13-zfs-setup.sh new file mode 100644 index 0000000..876ae71 --- /dev/null +++ b/hetzner-debian13-zfs-setup.sh @@ -0,0 +1,951 @@ +#!/bin/bash + +: <<'end_header_info' +(c) Andrey Prokopenko job@terem.fr +fully automatic script to install Debian 13 with ZFS root on Hetzner VPS +WARNING: all data on the disk will be destroyed +How to use: add SSH key to the rescue console, then press "mount rescue and power cycle" button +Next, connect via SSH to console, and run the script +Answer script questions about desired hostname and ZFS ARC cache size +To cope with network failures its higly recommended to run the script inside screen console +screen -dmS zfs +screen -r zfs +To detach from screen console, hit Ctrl-d then a +end_header_info + +set -euo pipefail + +# ---- Configuration ---- +SYSTEM_HOSTNAME="" +ROOT_PASSWORD="" +ZFS_POOL="" +DEBIAN_CODENAME="trixie" # Debian 13 +TARGET="/mnt/debian" + +ZBM_BIOS_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.tar.gz" +ZBM_EFI_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.EFI" + +MAIN_BOOT="/main_boot" + +# Hetzner mirrors for Debian +MIRROR_SITE="https://mirror.hetzner.com" +MIRROR_MAIN="deb ${MIRROR_SITE}/debian/packages ${DEBIAN_CODENAME} main contrib non-free non-free-firmware" +MIRROR_UPDATES="deb ${MIRROR_SITE}/debian/packages ${DEBIAN_CODENAME}-updates main contrib non-free non-free-firmware" +MIRROR_SECURITY="deb ${MIRROR_SITE}/debian/security ${DEBIAN_CODENAME}-security main contrib non-free non-free-firmware" + +# Global variables +INSTALL_DISK="" +EFI_MODE=false +BOOT_LABEL="" +BOOT_TYPE="" +BOOT_PART="" +ZFS_PART="" + +# ---- User Input Functions ---- +function setup_whiptail_colors { + # Green text on black background - classic terminal theme + export NEWT_COLORS=' + root=green,black + window=green,black + shadow=green,black + border=green,black + title=green,black + textbox=green,black + button=black,green + listbox=green,black + actlistbox=black,green + actsellistbox=black,green + checkbox=green,black + actcheckbox=black,green + entry=green,black + label=green,black + ' +} + +function check_whiptail { + if ! command -v whiptail &> /dev/null; then + echo "Installing whiptail..." + apt update + apt install -y whiptail + fi + setup_whiptail_colors +} + +function get_hostname { + while true; do + SYSTEM_HOSTNAME=$(whiptail \ + --title " System Hostname " \ + --inputbox "\nEnter the hostname for the new system:" \ + 10 60 "zfs-debian" \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "Installation cancelled by user." + exit 1 + fi + + # Validate hostname + if [[ "$SYSTEM_HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$ ]] && [[ ${#SYSTEM_HOSTNAME} -le 63 ]]; then + break + else + whiptail \ + --title " Invalid Hostname " \ + --msgbox "Invalid hostname. Please use only letters, numbers, and hyphens. Must start and end with alphanumeric character. Maximum 63 characters." \ + 12 60 + fi + done +} + +function get_zfs_pool_name { + while true; do + ZFS_POOL=$(whiptail \ + --title " ZFS Pool Name " \ + --inputbox "\nEnter the name for the ZFS pool:" \ + 10 60 "rpool" \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "Installation cancelled by user." + exit 1 + fi + + # Validate ZFS pool name + if [[ "$ZFS_POOL" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] && [[ ${#ZFS_POOL} -le 255 ]]; then + break + else + whiptail \ + --title " Invalid Pool Name " \ + --msgbox "Invalid ZFS pool name. Must start with a letter and contain only letters, numbers, hyphens, and underscores. Maximum 255 characters." \ + 12 60 + fi + done +} + +function get_root_password { + while true; do + # Get first password input + local password1 + local password2 + + password1=$(whiptail \ + --title " Root Password " \ + --passwordbox "\nEnter root password (input hidden):" \ + 10 60 \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "Installation cancelled by user." + exit 1 + fi + + # Get password confirmation + password2=$(whiptail \ + --title " Confirm Root Password " \ + --passwordbox "\nConfirm root password (input hidden):" \ + 10 60 \ + 3>&1 1>&2 2>&3) + + exit_status=$? + if [ $exit_status -ne 0 ]; then + echo "Installation cancelled by user." + exit 1 + fi + + # Check if passwords match + if [ "$password1" = "$password2" ]; then + if [ -n "$password1" ]; then + ROOT_PASSWORD="$password1" + break + else + whiptail \ + --title " Empty Password " \ + --msgbox "Password cannot be empty. Please enter a password." \ + 10 50 + fi + else + whiptail \ + --title " Password Mismatch " \ + --msgbox "Passwords do not match. Please try again." \ + 10 50 + fi + done +} + +function show_summary_and_confirm { + local summary="Please review the installation settings: + +Hostname: $SYSTEM_HOSTNAME +ZFS Pool: $ZFS_POOL +Debian Version: $DEBIAN_CODENAME (13) +Target: $TARGET +Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS") +Install Disk: $INSTALL_DISK + +*** WARNING: This will DESTROY ALL DATA on $INSTALL_DISK! *** + +Do you want to continue with the installation?" + + if whiptail \ + --title " Installation Summary " \ + --yesno "$summary" \ + 18 60; then + # User confirmed - just continue silently + echo "User confirmed installation. Starting now..." + else + echo "Installation cancelled by user." + exit 1 + fi +} + +function get_user_input { + echo "======= Gathering Installation Parameters ==========" + check_whiptail + + # Show welcome message + whiptail \ + --title " ZFS Debian Installer " \ + --msgbox "Welcome to the ZFS Debian Installer for Hetzner Cloud.\n\nThis script will install Debian 13 with ZFS root on your server." \ + 12 60 + + # Get user inputs + get_hostname + get_zfs_pool_name + get_root_password +} + +# ---- System Detection Functions ---- +function detect_efi { + echo "======= Detecting EFI support ==========" + + if [ -d /sys/firmware/efi ]; then + echo "✓ EFI firmware detected" + EFI_MODE=true + BOOT_LABEL="EFI" + BOOT_TYPE="ef00" + else + echo "✓ Legacy BIOS mode detected" + EFI_MODE=false + BOOT_LABEL="boot" + BOOT_TYPE="8300" + fi +} + +function find_install_disk { + echo "======= Finding install disk ==========" + + local candidate_disks=() + + # Use lsblk to find all unmounted, writable disks + while IFS= read -r disk; do + [[ -n "$disk" ]] && candidate_disks+=("$disk") + done < <(lsblk -npo NAME,TYPE,RO,MOUNTPOINT | awk ' + $2 == "disk" && $3 == "0" && $4 == "" {print $1} + ') + + if [[ ${#candidate_disks[@]} -eq 0 ]]; then + echo "No suitable installation disks found" >&2 + echo "Looking for: unmounted, writable disks without partitions in use" >&2 + exit 1 + fi + + INSTALL_DISK="${candidate_disks[0]}" + echo "Using installation disk: $INSTALL_DISK" + + # Show all available disks for verification + echo "All available disks:" + lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO | grep -v loop +} + +# ---- Rescue System Preparation Functions ---- +function remove_unused_kernels { + echo "=========== Removing unused kernels in rescue system ==========" + for kver in $(find /lib/modules/* -maxdepth 0 -type d \ + | grep -v "$(uname -r)" \ + | cut -s -d "/" -f 4); do + + for pkg in "linux-headers-$kver" "linux-image-$kver"; do + if dpkg -l "$pkg" 2>/dev/null | grep -q '^ii'; then + echo "Purging $pkg ..." + apt purge --yes "$pkg" + else + echo "Package $pkg not installed, skipping." + fi + done + done +} + +function install_zfs_on_rescue_system { + echo "======= Installing ZFS on rescue system ==========" + echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections + # Enable backports for newer ZFS version + echo "deb http://mirror.hetzner.com/debian/packages bookworm-backports main contrib" > /etc/apt/sources.list.d/backports.list + apt update + apt -t bookworm-backports install -y zfsutils-linux +} + +# ---- Disk Partitioning Functions ---- +function partition_disk { + echo "======= Partitioning disk ==========" + sgdisk -Z "$INSTALL_DISK" + + if [ "$EFI_MODE" = true ]; then + echo "Creating EFI partition layout" + # EFI System Partition (ESP) - 64MB is plenty for ZFSBootMenu + sgdisk -n1:1M:+128M -t1:ef00 -c1:"EFI" "$INSTALL_DISK" + # ZFS partition + sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK" + else + echo "Creating BIOS partition layout" + # /boot partition - 64MB is also sufficient for BIOS ZFSBootMenu + sgdisk -n1:1M:+128M -t1:8300 -c1:"boot" "$INSTALL_DISK" + # ZFS partition + sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK" + # Set legacy BIOS bootable flag + sgdisk -A 1:set:2 "$INSTALL_DISK" + fi + + partprobe "$INSTALL_DISK" || true + udevadm settle + + # Set partition variables based on mode + if [ "$EFI_MODE" = true ]; then + BOOT_PART="$(blkid -t PARTLABEL='EFI' -o device)" + ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)" + # Format ESP as FAT32 + mkfs.fat -F 32 -n EFI "$BOOT_PART" + else + BOOT_PART="$(blkid -t PARTLABEL='boot' -o device)" + ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)" + mkfs.ext4 -F -L boot "$BOOT_PART" + fi +} + +# ---- ZFS Pool and Dataset Functions ---- +function create_zfs_pool { + echo "======= Creating ZFS pool ==========" + # Clean up any existing ZFS binaries in PATH + rm -f "$(which zfs)" 2>/dev/null || true + rm -f "$(which zpool)" 2>/dev/null || true + + export PATH=/usr/sbin:$PATH + modprobe zfs + + zpool create -f -o ashift=12 \ + -o cachefile="/etc/zfs/zpool.cache" \ + -O compression=lz4 \ + -O acltype=posixacl \ + -O xattr=sa \ + -O mountpoint=none \ + "$ZFS_POOL" "$ZFS_PART" + + zfs create -o mountpoint=none "$ZFS_POOL/ROOT" + zfs create -o mountpoint=legacy "$ZFS_POOL/ROOT/debian" + + echo "======= Assigning $ZFS_POOL/ROOT/debian dataset as bootable ==========" + zpool set bootfs="$ZFS_POOL/ROOT/debian" "$ZFS_POOL" + zpool set cachefile="/etc/zfs/zpool.cache" "$ZFS_POOL" +} + +function create_additional_zfs_datasets { + echo "======= Creating additional ZFS datasets with TEMPORARY mountpoints ==========" + + # Ensure parent datasets are created first + zfs create -o mountpoint=none "$ZFS_POOL/ROOT/debian/var" + zfs create -o mountpoint=none "$ZFS_POOL/ROOT/debian/var/cache" + + # Create leaf datasets with temporary mountpoints under $TARGET + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/tmp" "$ZFS_POOL/ROOT/debian/tmp" + zfs set devices=off "$ZFS_POOL/ROOT/debian/tmp" + + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/tmp" "$ZFS_POOL/ROOT/debian/var/tmp" + zfs set devices=off "$ZFS_POOL/ROOT/debian/var/tmp" + + zfs create -o mountpoint="$TARGET/var/log" "$ZFS_POOL/ROOT/debian/var/log" + zfs set atime=off "$ZFS_POOL/ROOT/debian/var/log" + + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/cache/apt" "$ZFS_POOL/ROOT/debian/var/cache/apt" + zfs set atime=off "$ZFS_POOL/ROOT/debian/var/cache/apt" + + # Create home dataset separately + zfs create -o mountpoint="$TARGET/home" "$ZFS_POOL/home" + + # Mount all datasets + zfs mount -a + + # Set permissions on the actual ZFS datasets + echo "Setting permissions on ZFS datasets..." + chmod 1777 "$TARGET/tmp" + chmod 1777 "$TARGET/var/tmp" + echo "✓ Temp directory permissions set (1777)" +} + +function set_final_mountpoints { + echo "======= Setting final mountpoints ==========" + + # Leaf datasets - actual system mountpoints + zfs set mountpoint=/tmp "$ZFS_POOL/ROOT/debian/tmp" + zfs set mountpoint=/var/tmp "$ZFS_POOL/ROOT/debian/var/tmp" + zfs set mountpoint=/var/log "$ZFS_POOL/ROOT/debian/var/log" + zfs set mountpoint=/var/cache/apt "$ZFS_POOL/ROOT/debian/var/cache/apt" + + # Home dataset - separate from OS + zfs set mountpoint=/home "$ZFS_POOL/home" + echo "" + echo "Detailed dataset listing:" + zfs list -o name,mountpoint -r "$ZFS_POOL" +} + +# ---- System Bootstrap Functions ---- +function bootstrap_debian_system { + echo "======= Bootstrapping Debian to temporary directory ==========" + + # Install debootstrap if not available + if ! command -v debootstrap &> /dev/null; then + echo "Installing debootstrap..." + apt update + apt install -y debootstrap + fi + + #echo "======= Copying staged system to ZFS datasets ==========" + # Mount root dataset for copying + mkdir -p "$TARGET" + mount -t zfs "$ZFS_POOL/ROOT/debian" "$TARGET" + + create_additional_zfs_datasets + + # Bootstrap Debian 13 (Trixie) - include dbus to satisfy systemd-resolved dependency + debootstrap \ + --components=main,contrib,non-free,non-free-firmware \ + --include=initramfs-tools,dbus,locales,debconf-i18n,apt-utils,keyboard-configuration,console-setup,kbd,zstd,systemd-resolved,systemd-timesyncd \ + "$DEBIAN_CODENAME" \ + "$TARGET" \ + "$MIRROR_SITE/debian/packages" + +} + +function setup_chroot_environment { + echo "======= Mounting virtual filesystems for chroot ==========" + mount -t proc proc "$TARGET/proc" + mount -t sysfs sysfs "$TARGET/sys" + + # Only mount specific tmpfs directories, not the entire /run + mkdir -p "$TARGET/run/lock" "$TARGET/run/shm" + mount -t tmpfs tmpfs "$TARGET/run/lock" + mount -t tmpfs tmpfs "$TARGET/run/shm" + mount -t tmpfs tmpfs "$TARGET/tmp" + + mount --bind /dev "$TARGET/dev" + mount --bind /dev/pts "$TARGET/dev/pts" +} + +# ---- System Configuration Functions ---- +function configure_basic_system { + echo "======= Configuring basic system settings ==========" + chroot "$TARGET" /bin/bash < /etc/hostname + +# Configure timezone (Vienna) +echo "Europe/Vienna" > /etc/timezone +ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime + +# Generate locales +cat > /etc/locale.gen <<'LOCALES' +en_US.UTF-8 UTF-8 +de_AT.UTF-8 UTF-8 +fr_FR.UTF-8 UTF-8 +ru_RU.UTF-8 UTF-8 +LOCALES + +locale-gen + +# Set default locale +update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + +# Configure keyboard for German and US with Alt+Shift toggle +cat > /etc/default/keyboard <<'KEYBOARD' +# KEYBOARD CONFIGURATION FILE + +# Consult the keyboard(5) manual page. + +XKBMODEL="pc105" +XKBLAYOUT="de,ru" +XKBVARIANT="," +XKBOPTIONS="grp:ctrl_shift_toggle" + +BACKSPACE="guess" +KEYBOARD + +# Apply keyboard configuration to console +setupcon --force + +# Update /etc/hosts with the hostname +echo "127.0.0.1 localhost" > /etc/hosts +echo "127.0.1.1 $SYSTEM_HOSTNAME" >> /etc/hosts +echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts +echo "ff02::1 ip6-allnodes" >> /etc/hosts +echo "ff02::2 ip6-allrouters" >> /etc/hosts + +# Set proper permissions for ZFS datasets +chmod 1777 /tmp +chmod 1777 /var/tmp +EOF + + echo "======= Configuration Summary =======" + chroot "$TARGET" /bin/bash <<'EOF' +echo "Hostname: $(cat /etc/hostname)" +echo "Timezone: $(cat /etc/timezone)" +echo "Current time: $(date)" +echo "Default locale: $(grep LANG /etc/default/locale)" +echo "Available locales:" +locale -a | grep -E "(en_US|de_AT|fr_FR|ru_RU)" +echo "Keyboard layout: $(grep XKBLAYOUT /etc/default/keyboard)" +EOF +} + +function install_system_packages { + echo "======= Installing ZFS and essential packages in chroot ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail + +# Configure sources.list for Debian 13 +cat > /etc/apt/sources.list <<'SOURCES' +deb https://mirror.hetzner.com/debian/packages trixie main contrib non-free non-free-firmware +deb https://mirror.hetzner.com/debian/packages trixie-updates main contrib non-free non-free-firmware +deb https://mirror.hetzner.com/debian/security trixie-security main contrib non-free non-free-firmware +deb https://mirror.hetzner.com/debian/packages trixie-backports main contrib non-free non-free-firmware +SOURCES + +# Update package lists +apt update + +# Install kernel +apt install -y --no-install-recommends linux-image-cloud-amd64 linux-headers-cloud-amd64 + +# Install essential packages +apt install -y curl nano htop net-tools ssh \ + apt-transport-https ca-certificates gnupg dirmngr \ + firmware-linux-free apparmor + +echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections + +apt install -y -t trixie-backports zfsutils-linux zfs-initramfs zfs-dkms + +# Get the actual kernel version installed in the chroot +KERNEL_VERSION=$(ls /lib/modules/ | head -n1) +echo "Detected kernel version: $KERNEL_VERSION" + +# Verify ZFS module is available in the chroot filesystem +echo "=== Verifying ZFS module in chroot ===" +if find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f | grep -q .; then + echo "✓ ZFS module files found in /lib/modules/$KERNEL_VERSION/" + find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f +else + echo "✗ ZFS module files not found - attempting DKMS rebuild" + dkms autoinstall -k "$KERNEL_VERSION" || true + depmod -a "$KERNEL_VERSION" + + # Check again after DKMS rebuild + if find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f | grep -q .; then + echo "✓ ZFS module files found after DKMS rebuild" + else + echo "✗ ZFS module files still not found - this may cause boot issues" + fi +fi + +# Ensure ZFS module is included in initramfs +echo "zfs" >> /etc/initramfs-tools/modules + +# Generate initramfs with ZFS support +update-initramfs -u -k all + +# Verify kernel installation +echo "Installed kernel packages:" +dpkg -l | grep linux-image +echo "Kernel version:" +ls /lib/modules/ +echo "Kernel files in ZFS dataset:" +ls -la /boot/vmlinuz* /boot/initrd.img* 2>/dev/null || echo "No kernel files found" +EOF +} + +function verify_initramfs { + echo "======= Verifying initramfs contents ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail + +echo "=== Checking initramfs for ZFS components ===" +for initrd in /boot/initrd.img-*; do + if [ -f "$initrd" ]; then + echo "Checking: $initrd" + lsinitramfs "$initrd" | grep -E "(zfs|pool|dataset|spl)" | head -10 || echo "No ZFS components found (this might be normal for first check)" + echo "---" + fi +done + +echo "=== Checking ZFS module files on disk ===" +KERNEL_VERSION=$(ls /lib/modules/ | head -n1) +find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f + +echo "=== Testing ZFS commands ===" +which zpool && zpool --version || echo "zpool not found" +which zfs && zfs --version || echo "zfs not found" + +echo "=== Checking DKMS status ===" +dkms status || echo "DKMS not available" + +echo "=== Checking if ZFS tools are properly installed ===" +dpkg -l | grep -E "(zfs|spl)" + +EOF +} + +function configure_ssh { + echo "======= Setting up OpenSSH ==========" + mkdir -p "$TARGET/root/.ssh/" + cp /root/.ssh/authorized_keys "$TARGET/root/.ssh/authorized_keys" + sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$TARGET/etc/ssh/sshd_config" + sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$TARGET/etc/ssh/sshd_config" + + chroot "$TARGET" /bin/bash <<'EOF' +rm /etc/ssh/ssh_host_* +dpkg-reconfigure openssh-server -f noninteractive +EOF +} + +function set_root_credentials { + echo "======= Setting root password ==========" + chroot "$TARGET" /bin/bash -c "echo root:$(printf "%q" "$ROOT_PASSWORD") | chpasswd" + + echo "============ Setting up root prompt ============" + cat > "$TARGET/root/.bashrc" < /dev/null; then + echo "Installing extlinux in rescue system..." + apt update + apt install -y extlinux + fi + + # Install extlinux + extlinux --install "$MAIN_BOOT" + + # Create extlinux configuration + cat > "$MAIN_BOOT/extlinux.conf" << 'EOF' +DEFAULT zfsbootmenu +PROMPT 0 +TIMEOUT 0 + +LABEL zfsbootmenu + LINUX /zfsbootmenu/vmlinuz-bootmenu + INITRD /zfsbootmenu/initramfs-bootmenu.img + APPEND ro quiet +EOF + + echo "Generated extlinux.conf:" + cat "$MAIN_BOOT/extlinux.conf" + + # Download and install ZFSBootMenu for BIOS + local TEMP_ZBM=$(mktemp -d) + echo "Downloading ZFSBootMenu for BIOS from: $ZBM_BIOS_URL" + curl -L "$ZBM_BIOS_URL" -o "$TEMP_ZBM/zbm.tar.gz" + tar -xz -C "$TEMP_ZBM" -f "$TEMP_ZBM/zbm.tar.gz" --strip-components=1 + + # Copy ZFSBootMenu to boot partition + mkdir -p "$MAIN_BOOT/zfsbootmenu" + cp "$TEMP_ZBM"/vmlinuz* "$MAIN_BOOT/zfsbootmenu/" + cp "$TEMP_ZBM"/initramfs* "$MAIN_BOOT/zfsbootmenu/" + + # Clean up + rm -rf "$TEMP_ZBM" + + echo "ZFSBootMenu files copied to boot partition:" + ls -la "$MAIN_BOOT/zfsbootmenu/" + + # Install MBR and set boot flag + dd bs=440 conv=notrunc count=1 if="/usr/lib/EXTLINUX/gptmbr.bin" of="$INSTALL_DISK" + parted "$INSTALL_DISK" set 1 boot on + + echo "BIOS boot setup complete" +} + +function configure_bootloader { + echo "======= Setting up boot based on firmware type ==========" + if [ "$EFI_MODE" = true ]; then + setup_efi_boot + else + setup_bios_boot + fi + + echo "======= Configuring ZFSBootMenu for auto-detection ==========" + zfs set org.zfsbootmenu:commandline="ro quiet" "$ZFS_POOL/ROOT/debian" + + echo "Boot configuration:" + zfs get org.zfsbootmenu:commandline "$ZFS_POOL/ROOT/debian" +} + +# ---- System Services Functions ---- +function configure_system_services { + echo "======= Configuring ZFS cachefile in chrooted system ==========" + mkdir -p "$TARGET/etc/zfs" + cp /etc/zfs/zpool.cache "$TARGET/etc/zfs/zpool.cache" + + echo "Cachefile status:" + zpool get cachefile "$ZFS_POOL" + ls -la "$TARGET/etc/zfs/zpool.cache" && echo "✓ Cachefile ready" || echo "✗ Cachefile failed" + + echo "======= Enabling essential system services ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail + +systemctl enable systemd-resolved +systemctl enable systemd-timesyncd +systemctl enable systemd-networkd + +systemctl enable zfs-import-cache +systemctl enable zfs-mount + +systemctl enable ssh +systemctl enable apt-daily.timer + +echo "Enabled services:" +systemctl list-unit-files | grep enabled +EOF +} + +function configure_networking { + echo "======= Configuring systemd-networkd for Hetzner Cloud ==========" + + # Create systemd-networkd configuration for all ethernet interfaces + mkdir -p "$TARGET/etc/systemd/network" + + cat > "$TARGET/etc/systemd/network/10-hetzner.network" <<'EOF' +[Match] +Name=ens* enp* eth* + +[Network] +DHCP=yes +IPv6PrivacyExtensions=yes + +[DHCP] +RouteMetric=100 +UseDNS=yes +UseDomains=yes + +[DHCPv4] +RouteMetric=100 +UseDNS=yes +UseDomains=yes + +[IPv6AcceptRA] +RouteMetric=100 +EOF + + echo "systemd-networkd configuration:" + cat "$TARGET/etc/systemd/network/10-hetzner.network" + echo "" +} + +# ---- Cleanup and Finalization Functions ---- +function unmount_all_datasets_and_partitions { + echo "======= Unmounting all datasets ==========" + + # First, unmount virtual filesystems that might be using the datasets + echo "Unmounting virtual filesystems..." + for dir in dev/pts dev tmp run/lock run/shm run sys proc; do + if mountpoint -q "$TARGET/$dir"; then + echo "Unmounting $TARGET/$dir" + umount "$TARGET/$dir" 2>/dev/null || true + fi + done + + # Give it a moment + sleep 2 + + # Try to unmount boot partition first + if mountpoint -q "$MAIN_BOOT"; then + echo "Unmounting boot partition from $MAIN_BOOT" + umount "$MAIN_BOOT" 2>/dev/null || true + fi + + # Unmount ZFS datasets + echo "Unmounting ZFS datasets..." + zfs umount -a 2>/dev/null || true + + # Wait for unmounts to complete + sleep 2 + + # If root dataset is still mounted, try lazy unmount + if mountpoint -q "$TARGET"; then + echo "Attempting lazy unmount of $TARGET" + umount -l "$TARGET" 2>/dev/null || true + fi + + # Force unmount any stubborn ZFS datasets + if zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -q "yes"; then + echo "Forcing unmount of remaining ZFS datasets..." + zfs umount -a -f 2>/dev/null || true + fi + + # Final verification and force unmount if still mounted + if mountpoint -q "$TARGET"; then + echo "WARNING: $TARGET is still mounted! Attempting final cleanup..." + # Use fuser to find what's using the mount + if command -v fuser &> /dev/null; then + fuser -mv "$TARGET" 2>/dev/null || true + fi + # Force lazy unmount as last resort + umount -l "$TARGET" 2>/dev/null || true + fi + + # Final ZFS unmount check + local mounted_count=0 + mounted_count=$(zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -c "yes" || true) + + if [ "$mounted_count" -gt 0 ]; then + echo "WARNING: $mounted_count dataset(s) still mounted:" + zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep "yes" || true + else + echo "✓ All ZFS datasets successfully unmounted" + fi + + # Verify $TARGET is unmounted + if mountpoint -q "$TARGET"; then + echo "WARNING: $TARGET is still mounted but continuing..." + else + echo "✓ $TARGET successfully unmounted" + fi + + # Verify $MAIN_BOOT is unmounted + if mountpoint -q "$MAIN_BOOT"; then + echo "WARNING: $MAIN_BOOT is still mounted!" + else + echo "✓ $MAIN_BOOT successfully unmounted" + fi +} + +function unmount_chroot_environment { + echo "======= Unmounting virtual filesystems ==========" + # Unmount virtual filesystems first + for dir in dev/pts dev tmp run sys proc; do + if mountpoint -q "$TARGET/$dir"; then + echo "Unmounting $TARGET/$dir" + umount "$TARGET/$dir" 2>/dev/null || true + fi + done +} + +function export_zfs_pool { + echo "======= Exporting ZFS pool ==========" + # Export the pool to ensure clean state + zpool export "$ZFS_POOL" + echo "✓ ZFS pool '$ZFS_POOL' exported successfully" +} + +function show_final_instructions { + echo "" + echo "==========================================" + echo " INSTALLATION COMPLETE! " + echo "==========================================" + echo "" + echo "System Information:" + echo " Hostname: $SYSTEM_HOSTNAME" + echo " ZFS Pool: $ZFS_POOL" + echo " Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")" + echo " Debian Version: $DEBIAN_CODENAME" + echo " Networking: systemd-networkd + systemd-resolved" + echo "" + echo "==========================================" + echo "Rebooting..." +} + +# ---- Main Installation Flow ---- +function main { + echo "Starting ZFS Debian 13 installation on Hetzner..." + + # Get user input first + get_user_input + + # System detection + detect_efi + find_install_disk + + # Show summary and get confirmation + show_summary_and_confirm + + # Rescue system preparation + remove_unused_kernels + install_zfs_on_rescue_system + + # Disk partitioning + partition_disk + + # ZFS setup + create_zfs_pool + + # System installation + bootstrap_debian_system + setup_chroot_environment + configure_basic_system + install_system_packages + + verify_initramfs + configure_ssh + set_root_credentials + + # System configuration + configure_system_services + configure_networking + + # Bootloader configuration + configure_bootloader + + # Finalization + unmount_chroot_environment + + unmount_all_datasets_and_partitions + set_final_mountpoints + export_zfs_pool + + # Completion + show_final_instructions + + reboot +} + +# Run main function +main \ No newline at end of file diff --git a/hetzner-ubuntu24-zfs-setup.sh b/hetzner-ubuntu24-zfs-setup.sh new file mode 100644 index 0000000..f5ce06e --- /dev/null +++ b/hetzner-ubuntu24-zfs-setup.sh @@ -0,0 +1,973 @@ +#!/bin/bash + +: <<'end_header_info' +(c) Andrey Prokopenko job@terem.fr +fully automatic script to install Ubuntu 24 with ZFS root on Hetzner VPS +WARNING: all data on the disk will be destroyed +How to use: add SSH key to the rescue console, then press "mount rescue and power cycle" button +Next, connect via SSH to console, and run the script +Answer script questions about desired hostname and ZFS ARC cache size +To cope with network failures its higly recommended to run the script inside screen console +screen -dmS zfs +screen -r zfs +To detach from screen console, hit Ctrl-d then a +end_header_info + +set -euo pipefail + +# ---- Configuration ---- +# These will be set by user input +SYSTEM_HOSTNAME="" +ROOT_PASSWORD="" +ZFS_POOL="" +UBUNTU_CODENAME="noble" # Ubuntu 24.04 +TARGET="/mnt/ubuntu" + +ZBM_BIOS_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.tar.gz" +ZBM_EFI_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.EFI" + +MAIN_BOOT="/main_boot" + +# Hetzner mirrors +MIRROR_SITE="https://mirror.hetzner.com" +MIRROR_MAIN="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME} main restricted universe multiverse" +MIRROR_UPDATES="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME}-updates main restricted universe multiverse" +MIRROR_BACKPORTS="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME}-backports main restricted universe multiverse" +MIRROR_SECURITY="deb ${MIRROR_SITE}/ubuntu/security ${UBUNTU_CODENAME}-security main restricted universe multiverse" + +# Global variables +INSTALL_DISK="" +EFI_MODE=false +BOOT_LABEL="" +BOOT_TYPE="" +BOOT_PART="" +ZFS_PART="" + +# ---- User Input Functions ---- +function setup_whiptail_colors { + # Green text on black background - classic terminal theme + export NEWT_COLORS=' + root=green,black + window=green,black + shadow=green,black + border=green,black + title=green,black + textbox=green,black + button=black,green + listbox=green,black + actlistbox=black,green + actsellistbox=black,green + checkbox=green,black + actcheckbox=black,green + entry=green,black + label=green,black + ' +} + +function check_whiptail { + if ! command -v whiptail &> /dev/null; then + echo "Installing whiptail..." + apt update + apt install -y whiptail + fi + setup_whiptail_colors +} + + +function get_hostname { + while true; do + SYSTEM_HOSTNAME=$(whiptail \ + --title "System Hostname" \ + --inputbox "Enter the hostname for the new system:" \ + 10 60 "zfs-ubuntu" \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50 + exit 1 + fi + + # Validate hostname + if [[ "$SYSTEM_HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$ ]] && [[ ${#SYSTEM_HOSTNAME} -le 63 ]]; then + break + else + whiptail \ + --title "Invalid Hostname" \ + --msgbox "Invalid hostname. Please use only letters, numbers, and hyphens. Must start and end with alphanumeric character. Maximum 63 characters." \ + 12 60 + fi + done +} + +function get_zfs_pool_name { + while true; do + ZFS_POOL=$(whiptail \ + --title "ZFS Pool Name" \ + --inputbox "Enter the name for the ZFS pool:" \ + 10 60 "rpool" \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50 + exit 1 + fi + + # Validate ZFS pool name + if [[ "$ZFS_POOL" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] && [[ ${#ZFS_POOL} -le 255 ]]; then + break + else + whiptail \ + --title "Invalid Pool Name" \ + --msgbox "Invalid ZFS pool name. Must start with a letter and contain only letters, numbers, hyphens, and underscores. Maximum 255 characters." \ + 12 60 + fi + done +} + +function get_root_password { + while true; do + # Get first password input + local password1 + local password2 + + password1=$(whiptail \ + --title "Root Password" \ + --passwordbox "Enter root password (input hidden):" \ + 10 60 \ + 3>&1 1>&2 2>&3) + + local exit_status=$? + if [ $exit_status -ne 0 ]; then + whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50 + exit 1 + fi + + # Get password confirmation + password2=$(whiptail \ + --title "Confirm Root Password" \ + --passwordbox "Confirm root password (input hidden):" \ + 10 60 \ + 3>&1 1>&2 2>&3) + + exit_status=$? + if [ $exit_status -ne 0 ]; then + whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50 + exit 1 + fi + + # Check if passwords match + if [ "$password1" = "$password2" ]; then + if [ -n "$password1" ]; then + ROOT_PASSWORD="$password1" + break + else + whiptail \ + --title "Empty Password" \ + --msgbox "Password cannot be empty. Please enter a password." \ + 10 50 + fi + else + whiptail \ + --title "Password Mismatch" \ + --msgbox "Passwords do not match. Please try again." \ + 10 50 + fi + done +} + +function show_summary_and_confirm { + local summary="Please review the installation settings: + +Hostname: $SYSTEM_HOSTNAME +ZFS Pool: $ZFS_POOL +Ubuntu Version: $UBUNTU_CODENAME (24.04) +Target: $TARGET +Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS") +Install Disk: $INSTALL_DISK + +*** WARNING: This will DESTROY ALL DATA on $INSTALL_DISK! *** + +Do you want to continue with the installation?" + + if whiptail \ + --title " Installation Summary " \ + --yesno "$summary" \ + 18 60; then + # User confirmed - just continue silently + echo "User confirmed installation. Starting now..." + else + echo "Installation cancelled by user." + exit 1 + fi +} + +function get_user_input { + echo "======= Gathering Installation Parameters ==========" + check_whiptail + + # Show welcome message + whiptail \ + --title "ZFS Ubuntu Installer" \ + --msgbox "Welcome to the ZFS Ubuntu Installer for Hetzner Cloud.\n\nThis script will install Ubuntu 24.04 with ZFS root on your server." \ + 12 60 + + # Get user inputs + get_hostname + get_zfs_pool_name + get_root_password +} + +# ---- System Detection Functions ---- +function detect_efi { + echo "======= Detecting EFI support ==========" + + if [ -d /sys/firmware/efi ]; then + echo "✓ EFI firmware detected" + EFI_MODE=true + BOOT_LABEL="EFI" + BOOT_TYPE="ef00" + else + echo "✓ Legacy BIOS mode detected" + EFI_MODE=false + BOOT_LABEL="boot" + BOOT_TYPE="8300" + fi +} + +function find_install_disk { + echo "======= Finding install disk ==========" + + local candidate_disks=() + + # Use lsblk to find all unmounted, writable disks + while IFS= read -r disk; do + [[ -n "$disk" ]] && candidate_disks+=("$disk") + done < <(lsblk -npo NAME,TYPE,RO,MOUNTPOINT | awk ' + $2 == "disk" && $3 == "0" && $4 == "" {print $1} + ') + + if [[ ${#candidate_disks[@]} -eq 0 ]]; then + echo "No suitable installation disks found" >&2 + echo "Looking for: unmounted, writable disks without partitions in use" >&2 + exit 1 + fi + + INSTALL_DISK="${candidate_disks[0]}" + echo "Using installation disk: $INSTALL_DISK" + + # Show all available disks for verification + echo "All available disks:" + lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO | grep -v loop +} + +# ---- Rescue System Preparation Functions ---- +function remove_unused_kernels { + echo "=========== Removing unused kernels in rescue system ==========" + for kver in $(find /lib/modules/* -maxdepth 0 -type d \ + | grep -v "$(uname -r)" \ + | cut -s -d "/" -f 4); do + + for pkg in "linux-headers-$kver" "linux-image-$kver"; do + if dpkg -l "$pkg" 2>/dev/null | grep -q '^ii'; then + echo "Purging $pkg ..." + apt purge --yes "$pkg" + else + echo "Package $pkg not installed, skipping." + fi + done + done +} + +function install_zfs_on_rescue_system { + echo "======= Installing ZFS on rescue system ==========" + echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections + # Enable Hetzner bookworm-backports + sed -i 's/^# deb http:\/\/mirror.hetzner.com\/debian\/packages bookworm-backports/deb http:\/\/mirror.hetzner.com\/debian\/packages bookworm-backports/' /etc/apt/sources.list + apt update + apt -t bookworm-backports install -y zfsutils-linux +} + +# ---- Disk Partitioning Functions ---- +function partition_disk { + echo "======= Partitioning disk ==========" + sgdisk -Z "$INSTALL_DISK" + + if [ "$EFI_MODE" = true ]; then + echo "Creating EFI partition layout" + # EFI System Partition (ESP) - 64MB is plenty for ZFSBootMenu + sgdisk -n1:1M:+128M -t1:ef00 -c1:"EFI" "$INSTALL_DISK" + # ZFS partition + sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK" + else + echo "Creating BIOS partition layout" + # /boot partition - 64MB is also sufficient for BIOS ZFSBootMenu + sgdisk -n1:1M:+128M -t1:8300 -c1:"boot" "$INSTALL_DISK" + # ZFS partition + sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK" + # Set legacy BIOS bootable flag + sgdisk -A 1:set:2 "$INSTALL_DISK" + fi + + partprobe "$INSTALL_DISK" || true + udevadm settle + + # Set partition variables based on mode + if [ "$EFI_MODE" = true ]; then + BOOT_PART="$(blkid -t PARTLABEL='EFI' -o device)" + ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)" + # Format ESP as FAT32 + mkfs.fat -F 32 -n EFI "$BOOT_PART" + else + BOOT_PART="$(blkid -t PARTLABEL='boot' -o device)" + ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)" + mkfs.ext4 -F -L boot "$BOOT_PART" + fi +} + +# ---- ZFS Pool and Dataset Functions ---- +function create_zfs_pool { + echo "======= Creating ZFS pool ==========" + # Clean up any existing ZFS binaries in PATH + rm -f "$(which zfs)" 2>/dev/null || true + rm -f "$(which zpool)" 2>/dev/null || true + + export PATH=/usr/sbin:$PATH + modprobe zfs + + zpool create -f -o ashift=12 \ + -o cachefile="/etc/zfs/zpool.cache" \ + -O compression=lz4 \ + -O acltype=posixacl \ + -O xattr=sa \ + -O mountpoint=none \ + "$ZFS_POOL" "$ZFS_PART" + + zfs create -o mountpoint=none "$ZFS_POOL/ROOT" + zfs create -o mountpoint=legacy "$ZFS_POOL/ROOT/ubuntu" + + echo "======= Assigning $ZFS_POOL/ROOT/ubuntu dataset as bootable ==========" + zpool set bootfs="$ZFS_POOL/ROOT/ubuntu" "$ZFS_POOL" + zpool set cachefile="/etc/zfs/zpool.cache" "$ZFS_POOL" +} + +function create_additional_zfs_datasets { + echo "======= Creating additional ZFS datasets with TEMPORARY mountpoints ==========" + + # Ensure parent datasets are created first + zfs create -o mountpoint=none "$ZFS_POOL/ROOT/ubuntu/var" + zfs create -o mountpoint=none "$ZFS_POOL/ROOT/ubuntu/var/cache" + + # Create leaf datasets with temporary mountpoints under $TARGET + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/tmp" "$ZFS_POOL/ROOT/ubuntu/tmp" + zfs set devices=off "$ZFS_POOL/ROOT/ubuntu/tmp" + + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/tmp" "$ZFS_POOL/ROOT/ubuntu/var/tmp" + zfs set devices=off "$ZFS_POOL/ROOT/ubuntu/var/tmp" + + zfs create -o mountpoint="$TARGET/var/log" "$ZFS_POOL/ROOT/ubuntu/var/log" + zfs set atime=off "$ZFS_POOL/ROOT/ubuntu/var/log" + + zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/cache/apt" "$ZFS_POOL/ROOT/ubuntu/var/cache/apt" + zfs set atime=off "$ZFS_POOL/ROOT/ubuntu/var/cache/apt" + + # Create home dataset separately + zfs create -o mountpoint="$TARGET/home" "$ZFS_POOL/home" + + # Mount all datasets + zfs mount -a + + # Set permissions on the actual ZFS datasets + echo "Setting permissions on ZFS datasets..." + chmod 1777 "$TARGET/tmp" + chmod 1777 "$TARGET/var/tmp" + echo "✓ Temp directory permissions set (1777)" +} + +function set_final_mountpoints { + echo "======= Setting final mountpoints ==========" + + # Leaf datasets - actual system mountpoints + zfs set mountpoint=/tmp "$ZFS_POOL/ROOT/ubuntu/tmp" + zfs set mountpoint=/var/tmp "$ZFS_POOL/ROOT/ubuntu/var/tmp" + zfs set mountpoint=/var/log "$ZFS_POOL/ROOT/ubuntu/var/log" + zfs set mountpoint=/var/cache/apt "$ZFS_POOL/ROOT/ubuntu/var/cache/apt" + + # Home dataset - separate from OS + zfs set mountpoint=/home "$ZFS_POOL/home" + echo "" + echo "Detailed dataset listing:" + zfs list -o name,mountpoint -r "$ZFS_POOL" +} + +# ---- System Bootstrap Functions ---- +function bootstrap_ubuntu_system { + echo "======= Bootstrapping Ubuntu to temporary directory ==========" + local TEMP_STAGE=$(mktemp -d) + echo "Created temporary staging directory: $TEMP_STAGE" + + # Cleanup function for temp directory + cleanup_temp_stage() { + if [ -d "$TEMP_STAGE" ]; then + echo "Cleaning up temporary staging directory..." + rm -rf "$TEMP_STAGE" + fi + } + + # Add trap to ensure cleanup on script exit + trap cleanup_temp_stage EXIT + + # Add Hetzner Ubuntu mirror as trusted + echo "deb [trusted=yes] http://mirror.hetzner.com/ubuntu/packages noble main" > /etc/apt/sources.list.d/ubuntu-temp.list + + # Update and download ubuntu-keyring without sandbox warning + apt-get update + apt-get -o APT::Sandbox::User=root download ubuntu-keyring + + # Verify download was successful + if [ ! -f ubuntu-keyring*.deb ]; then + echo "ERROR: Failed to download ubuntu-keyring package" + exit 1 + fi + + # Extract the keyring + dpkg-deb -x ubuntu-keyring*.deb /tmp/ubuntu-keyring-extract/ + mkdir -p /usr/share/keyrings + cp /tmp/ubuntu-keyring-extract/usr/share/keyrings/ubuntu-archive-keyring.gpg /usr/share/keyrings/ + + echo "✓ Downloaded and extracted ubuntu-keyring package" + + # Clean up - remove temporary repository and restore apt state + rm -f /etc/apt/sources.list.d/ubuntu-temp.list + apt update + + # Clean up downloaded package + rm -f ubuntu-keyring*.deb + + apt install -y mmdebstrap + + mmdebstrap --variant=debootstrap \ + --keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg \ + --include=systemd-resolved,locales,debconf-i18n,apt-utils,keyboard-configuration,console-setup,kbd,extlinux,initramfs-tools,zstd \ + "$UBUNTU_CODENAME" "$TEMP_STAGE" \ + "$MIRROR_MAIN" "$MIRROR_UPDATES" "$MIRROR_BACKPORTS" "$MIRROR_SECURITY" + + echo "======= Copying staged system to ZFS datasets ==========" + # Mount root dataset for copying + mkdir -p "$TARGET" + mount -t zfs "$ZFS_POOL/ROOT/ubuntu" "$TARGET" + + create_additional_zfs_datasets + + # Use rsync to copy the entire system (this will populate all datasets) + echo "Copying staged system to ZFS datasets..." + rsync -aAX "$TEMP_STAGE/" "$TARGET/" + + echo "Staged system copied successfully" + echo "Source size: $(du -sh "$TEMP_STAGE")" + echo "Target size: $(du -sh "$TARGET")" + + # Clean up temp directory + cleanup_temp_stage + trap - EXIT +} + +function setup_chroot_environment { + echo "======= Mounting virtual filesystems for chroot ==========" + mount -t proc proc "$TARGET/proc" + mount -t sysfs sysfs "$TARGET/sys" + mount -t tmpfs tmpfs "$TARGET/run" + mount -t tmpfs tmpfs "$TARGET/tmp" + mount --bind /dev "$TARGET/dev" + mount --bind /dev/pts "$TARGET/dev/pts" + + configure_dns_resolution +} + +function configure_dns_resolution { + echo "======= Configuring DNS resolution ==========" + mkdir -p "$TARGET/run/systemd/resolve" + + if command -v resolvectl >/dev/null 2>&1; then + echo "Getting DNS from resolvectl..." + + # First try Global DNS servers + local DNS_SERVERS=$(resolvectl dns | awk ' + /^Global:/ { + for(i=2; i<=NF; i++) print $i + } + ' | head -3) + + # If Global is empty, find first non-empty link + if [ -z "$DNS_SERVERS" ]; then + echo "No global DNS servers found, searching for first non-empty link..." + DNS_SERVERS=$(resolvectl dns | awk ' + /^Link [0-9]+ / && NF > 3 { + for(i=4; i<=NF; i++) print $i # Start from field 4 to skip the interface name + exit # Stop after first non-empty link + } + ') + fi + + if [ -n "$DNS_SERVERS" ]; then + # Create resolv.conf with the DNS servers + echo "$DNS_SERVERS" | while read -r dns; do + echo "nameserver $dns" + done > "$TARGET/run/systemd/resolve/stub-resolv.conf" + echo "Using DNS servers: $(echo "$DNS_SERVERS" | tr '\n' ' ')" + else + echo "ERROR: No DNS servers found in resolvectl output" + echo "resolvectl dns output:" + resolvectl dns + echo "Cannot continue without DNS configuration" + exit 1 + fi + else + echo "ERROR: resolvectl command not found" + echo "Cannot configure DNS without resolvectl" + exit 1 + fi +} + +# ---- System Configuration Functions ---- +function configure_basic_system { + echo "======= Configuring basic system settings ==========" + chroot "$TARGET" /bin/bash < /etc/hostname + +# Configure timezone (Vienna) +echo "Europe/Vienna" > /etc/timezone +ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime + +# Generate locales +cat > /etc/locale.gen <<'LOCALES' +en_US.UTF-8 UTF-8 +de_AT.UTF-8 UTF-8 +fr_FR.UTF-8 UTF-8 +ru_RU.UTF-8 UTF-8 +LOCALES + +locale-gen + +# Set default locale +update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + +# Configure keyboard for German and US with Alt+Shift toggle +cat > /etc/default/keyboard <<'KEYBOARD' +# KEYBOARD CONFIGURATION FILE + +# Consult the keyboard(5) manual page. + +XKBMODEL="pc105" +XKBLAYOUT="de,ru" +XKBVARIANT="," +XKBOPTIONS="grp:ctrl_shift_toggle" + +BACKSPACE="guess" +KEYBOARD + +# Apply keyboard configuration to console +setupcon --force + +# Update /etc/hosts with the hostname +echo "127.0.0.1 localhost" > /etc/hosts +echo "127.0.1.1 $SYSTEM_HOSTNAME" >> /etc/hosts +echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts +echo "ff02::1 ip6-allnodes" >> /etc/hosts +echo "ff02::2 ip6-allrouters" >> /etc/hosts + +# Set proper permissions for ZFS datasets +chmod 1777 /tmp +chmod 1777 /var/tmp +EOF + + echo "======= Configuration Summary =======" + chroot "$TARGET" /bin/bash <<'EOF' +echo "Hostname: $(cat /etc/hostname)" +echo "Timezone: $(cat /etc/timezone)" +echo "Current time: $(date)" +echo "Default locale: $(grep LANG /etc/default/locale)" +echo "Available locales:" +locale -a | grep -E "(en_US|de_AT|fr_FR|ru_RU)" +echo "Keyboard layout: $(grep XKBLAYOUT /etc/default/keyboard)" +EOF +} + +function install_system_packages { + echo "======= Installing ZFS and essential packages in chroot ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail +# Update package lists +apt update + +# Install generic kernel (creates files in ZFS dataset /boot) +apt install -y --no-install-recommends linux-image-generic linux-headers-generic + +# Install ZFS utilities and aux packages + +echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections + +apt install -y zfs-dkms zfsutils-linux zfs-initramfs software-properties-common bash curl nano htop net-tools ssh + +# Ensure ZFS module is included in initramfs +echo "zfs" >> /etc/initramfs-tools/modules + +# Generate initramfs with ZFS support +update-initramfs -u -k all + +# Verify kernel installation +echo "Installed kernel packages:" +dpkg -l | grep linux-image +echo "Kernel version:" +ls /lib/modules/ +echo "Kernel files in ZFS dataset:" +ls -la /boot/vmlinuz* /boot/initrd.img* 2>/dev/null || echo "No kernel files found" +EOF +} + +function configure_ssh { + echo "======= Setting up OpenSSH ==========" + mkdir -p "$TARGET/root/.ssh/" + cp /root/.ssh/authorized_keys "$TARGET/root/.ssh/authorized_keys" + sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$TARGET/etc/ssh/sshd_config" + sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$TARGET/etc/ssh/sshd_config" + + chroot "$TARGET" /bin/bash <<'EOF' +rm /etc/ssh/ssh_host_* +dpkg-reconfigure openssh-server -f noninteractive +EOF +} + +function set_root_credentials { + echo "======= Setting root password ==========" + chroot "$TARGET" /bin/bash -c "echo root:$(printf "%q" "$ROOT_PASSWORD") | chpasswd" + + echo "============ Setting up root prompt ============" + cat > "$TARGET/root/.bashrc" < /dev/null; then + echo "Installing extlinux in rescue system..." + apt update + apt install -y extlinux + fi + + # Install extlinux + extlinux --install "$MAIN_BOOT" + + # Create extlinux configuration + cat > "$MAIN_BOOT/extlinux.conf" << 'EOF' +DEFAULT zfsbootmenu +PROMPT 0 +TIMEOUT 0 + +LABEL zfsbootmenu + LINUX /zfsbootmenu/vmlinuz-bootmenu + INITRD /zfsbootmenu/initramfs-bootmenu.img + APPEND ro quiet +EOF + + echo "Generated extlinux.conf:" + cat "$MAIN_BOOT/extlinux.conf" + + # Download and install ZFSBootMenu for BIOS + local TEMP_ZBM=$(mktemp -d) + echo "Downloading ZFSBootMenu for BIOS from: $ZBM_BIOS_URL" + curl -L "$ZBM_BIOS_URL" -o "$TEMP_ZBM/zbm.tar.gz" + tar -xz -C "$TEMP_ZBM" -f "$TEMP_ZBM/zbm.tar.gz" --strip-components=1 + + # Copy ZFSBootMenu to boot partition + mkdir -p "$MAIN_BOOT/zfsbootmenu" + cp "$TEMP_ZBM"/vmlinuz* "$MAIN_BOOT/zfsbootmenu/" + cp "$TEMP_ZBM"/initramfs* "$MAIN_BOOT/zfsbootmenu/" + + # Clean up + rm -rf "$TEMP_ZBM" + + echo "ZFSBootMenu files copied to boot partition:" + ls -la "$MAIN_BOOT/zfsbootmenu/" + + # Install MBR and set boot flag + dd bs=440 conv=notrunc count=1 if="$TARGET/usr/lib/EXTLINUX/gptmbr.bin" of="$INSTALL_DISK" + parted "$INSTALL_DISK" set 1 boot on + + echo "BIOS boot setup complete" +} + +function configure_bootloader { + echo "======= Setting up boot based on firmware type ==========" + if [ "$EFI_MODE" = true ]; then + setup_efi_boot + else + setup_bios_boot + fi + + echo "======= Configuring ZFSBootMenu for auto-detection ==========" + zfs set org.zfsbootmenu:commandline="ro quiet" "$ZFS_POOL/ROOT/ubuntu" + + echo "Boot configuration:" + zfs get org.zfsbootmenu:commandline "$ZFS_POOL/ROOT/ubuntu" +} + +# ---- System Services Functions ---- +function configure_system_services { + echo "======= Configuring ZFS cachefile in chrooted system ==========" + mkdir -p "$TARGET/etc/zfs" + cp /etc/zfs/zpool.cache "$TARGET/etc/zfs/zpool.cache" + + echo "Cachefile status:" + zpool get cachefile "$ZFS_POOL" + ls -la "$TARGET/etc/zfs/zpool.cache" && echo "✓ Cachefile ready" || echo "✗ Cachefile failed" + + echo "======= Enabling essential system services ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail + +systemctl enable systemd-resolved +systemctl enable systemd-timesyncd + +systemctl enable zfs-import-cache +systemctl enable zfs-mount + +systemctl enable ssh +systemctl enable apt-daily.timer + +echo "Enabled services:" +systemctl list-unit-files | grep enabled +EOF +} + +function configure_networking { + echo "======= Configuring Netplan for Hetzner Cloud ==========" + chroot "$TARGET" /bin/bash <<'EOF' +set -euo pipefail +# Create Netplan configuration that matches all non-loopback interfaces +cat > /etc/netplan/01-hetzner.yaml <<'EOL' +network: + version: 2 + renderer: networkd + ethernets: + all-interfaces: + match: + name: "!lo" + dhcp4: true + dhcp6: true + dhcp4-overrides: + use-dns: true + use-hostname: true + use-domains: true + route-metric: 100 + dhcp6-overrides: + use-dns: true + use-hostname: true + use-domains: true + route-metric: 100 + critical: true +EOL + +# Set proper permissions - Netplan requires strict permissions (600) +chmod 600 /etc/netplan/01-hetzner.yaml +chown root:root /etc/netplan/01-hetzner.yaml + +# Apply the Netplan configuration +netplan generate +echo "Netplan configuration created for all interfaces" +EOF +} + +# ---- Cleanup and Finalization Functions ---- +function unmount_all_datasets_and_partitions { + echo "======= Unmounting all datasets ==========" + + # First, unmount all auto-mounted ZFS datasets (tmp, var/tmp, var/log, etc.) + echo "Unmounting auto-mounted ZFS datasets..." + zfs umount -a 2>/dev/null || true + + # Manually unmount the root legacy dataset from $TARGET + if mountpoint -q "$TARGET"; then + echo "Unmounting root dataset from $TARGET" + umount "$TARGET" 2>/dev/null || true + fi + + # Manually unmount boot partition if mounted + if mountpoint -q "$MAIN_BOOT"; then + echo "Unmounting boot partition from $MAIN_BOOT" + umount "$MAIN_BOOT" 2>/dev/null || true + fi + + # Wait for unmounts to complete + sleep 1 + + # Force unmount any stubborn datasets + if zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -q "yes"; then + echo "Forcing unmount of remaining ZFS datasets..." + zfs umount -a -f 2>/dev/null || true + fi + + # Final verification + local mounted_count=0 + mounted_count=$(zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -c "yes" || true) + + if [ "$mounted_count" -gt 0 ]; then + echo "WARNING: $mounted_count dataset(s) still mounted after unmount attempt:" + zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep "yes" || true + else + echo "✓ All ZFS datasets successfully unmounted" + fi + + # Verify $TARGET is unmounted + if mountpoint -q "$TARGET"; then + echo "WARNING: $TARGET is still mounted!" + mount | grep "$TARGET" || true + else + echo "✓ $TARGET successfully unmounted" + fi + + # Verify $MAIN_BOOT is unmounted + if mountpoint -q "$MAIN_BOOT"; then + echo "WARNING: $MAIN_BOOT is still mounted!" + mount | grep "$MAIN_BOOT" || true + else + echo "✓ $MAIN_BOOT successfully unmounted" + fi +} + +function unmount_chroot_environment { + echo "======= Unmounting virtual filesystems ==========" + # Unmount virtual filesystems first + for dir in dev/pts dev tmp run sys proc; do + if mountpoint -q "$TARGET/$dir"; then + echo "Unmounting $TARGET/$dir" + umount "$TARGET/$dir" 2>/dev/null || true + fi + done +} + +function finalize_system_resolved { + echo "======= Setting systemd-resolved configuration for final boot ==========" + # This must be done while $TARGET is still mounted + mkdir -p "$TARGET/run/systemd/resolve" + cat > "$TARGET/run/systemd/resolve/stub-resolv.conf" << 'EOF' +nameserver 127.0.0.53 +options edns0 trust-ad +search . +EOF + echo "✓ systemd-resolved configuration set" +} + +function export_zfs_pool { + echo "======= Exporting ZFS pool ==========" + zpool export "$ZFS_POOL" 2>/dev/null || true + + # Verify everything is unmounted + if mountpoint -q "$TARGET"; then + echo "WARNING: $TARGET is still mounted!" + mount | grep "$TARGET" + else + echo "✓ All filesystems successfully unmounted" + fi +} + +function show_final_instructions { + echo "" + echo "==========================================" + echo " INSTALLATION COMPLETE! " + echo "==========================================" + echo "" + echo "System Information:" + echo " Hostname: $SYSTEM_HOSTNAME" + echo " ZFS Pool: $ZFS_POOL" + echo " Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")" + echo " Ubuntu Version: $UBUNTU_CODENAME" + echo " Networking: systemd-networkd + systemd-resolved" + echo "" + echo "==========================================" + echo "Rebooting..." +} + +# ---- Main Execution Function ---- +function main { + echo "Starting ZFS Ubuntu installation on Hetzner Cloud..." + + # Phase 0: User input + get_user_input + + # Phase 1: System detection and preparation + detect_efi + find_install_disk + + # Show summary and get final confirmation + show_summary_and_confirm + + remove_unused_kernels + install_zfs_on_rescue_system + + # Phase 2: Disk partitioning and ZFS setup + partition_disk + create_zfs_pool + + # Phase 3: System bootstrap + bootstrap_ubuntu_system + setup_chroot_environment + + # Phase 4: System configuration + configure_basic_system + install_system_packages + configure_ssh + set_root_credentials + configure_system_services + configure_networking + + # Phase 5: Bootloader setup + configure_bootloader + + # Phase 6: Cleanup and finalization + unmount_chroot_environment + finalize_system_resolved + unmount_all_datasets_and_partitions + + # Phase 7: Final mountpoints and export + set_final_mountpoints + + export_zfs_pool + + show_final_instructions + + reboot +} + +# Execute main function +main "$@" \ No newline at end of file