96 Commits

Author SHA1 Message Date
Andrey Prokopenko
69ff5f955b new OS versions added (#87) 2025-10-26 17:25:17 +01:00
Gunnar
784ee5fe96 Update hetzner-debian12-zfs-setup.sh (#81)
* Update hetzner-debian12-zfs-setup.sh

fix for 
===========remove unused kernels in rescue system=========
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
E: Unable to locate package linux-headers-6.11.3
E: Couldn't find any package by glob 'linux-headers-6.11.3'

* try to fix Configuration file '/etc/issue'

* try to fix mountpoint issue

mountpoint '/mnt/' exists and is not empty
use '-m' option to provide a different default
2025-06-24 19:53:17 +02:00
Tom Janssen
8858afa4a3 Update hetzner-debian12-zfs-setup.sh (#71) 2025-04-21 23:03:14 +02:00
Piotr Plenzler
d11cae2671 Fix typo in Debian 12 script (#67)
"Debian 11" -> "Debian 12"
2025-04-21 23:02:36 +02:00
tyl0re
373f77fa73 Rechtschreibfehler (#65) 2025-04-21 23:02:02 +02:00
Cong Zhang
587d3a1f4f fix grub install error (#75)
Co-authored-by: Nemo <9478806+terem42@users.noreply.github.com>
2025-04-21 23:01:11 +02:00
greenpsi
556157ed33 add ability to install debian 12 in UEFI mode (#76)
check, whether the machine is booted in EFI mode, then setup an EFI
partition and install efi grub instead of legacy grub.
This is required, since new hetzner machines don't have the ability
to boot in legacy mode anymore.

Co-authored-by: Micha Goetz <mg@altasigma.com>
Co-authored-by: Nemo <9478806+terem42@users.noreply.github.com>
2025-04-21 22:58:25 +02:00
Andrey Prokopenko
56e36a0987 fix for issue 74 2025-04-21 22:53:35 +02:00
Andrey Prokopenko
d5b4bf3034 fix for issue 77, originally authored by brianharte 2025-04-21 22:35:29 +02:00
rcn
f79b735143 Install GRUB on all selected disks (#66)
Co-authored-by: Robert Flechtner <robert.flechtner@power.cloud>
2023-11-28 17:22:23 +01:00
terem42
dc23d826e3 Grub fix (#64)
* zed initial load proc fix
2023-10-11 11:27:43 +02:00
terem42
3b9724e6af noauto set rpool name fix (#63)
* rpool ref fix

* dropbear update
2023-10-10 17:05:37 +02:00
terem42
23ec2c0e89 Grub fix (#62)
* rpool dynamic naming fix
2023-10-10 14:16:11 +02:00
terem42
00ed04d44e dropbear fixes for encrypted root (#61)
dropbear fixes for encrypted root
2023-10-07 14:27:41 +02:00
terem42
27bb8d8302 Update hetzner-ubuntu22-zfs-setup.sh 2023-10-06 21:50:03 +02:00
terem42
6c015b895d Update README.md 2023-10-06 20:32:34 +02:00
terem42
b40c5e0699 Fix dropbear setup (#60)
fix dropbear across Ubuntu and Debian variants
2023-10-06 20:31:31 +02:00
terem42
0053d63360 rescue updated (#59)
* updated zfs for new rescue
2023-10-05 16:05:31 +02:00
terem42
6a9eb55877 renamed 2023-10-01 21:31:44 +02:00
terem42
d38db64e43 Rename deb12 (#57)
* set tmpdir

* array fix
2023-10-01 21:28:27 +02:00
terem42
b08730586a set tmpdir (#56)
* set tmpdir

* array fix
2023-10-01 19:39:54 +02:00
terem42
99a63e2892 Update hetzner-ubuntu22-zfs-setup.sh 2023-10-01 18:19:29 +02:00
terem42
9bc5080df3 Update README.md 2023-10-01 17:26:12 +02:00
terem42
b3f1a8624f Update hetzner-ubuntu22-zfs-setup.sh 2023-09-30 22:58:50 +02:00
terem42
5dc4fe0ed0 Update hetzner-ubuntu22-zfs-setup.sh 2023-09-30 22:25:23 +02:00
terem42
9fcedceb52 Update hetzner-ubuntu22-zfs-setup.sh (#55) 2023-09-30 21:05:53 +02:00
Cong Zhang
a47d2bd6ce enable debian 12, not pin to hetzner and enable zfs mirror/strip switch (#53)
* debian 12 copy from debian 11

* upgrade debian 12 and enable non-free-firmware

* use offical repo which use mirror automate

* enable raid mirror or strip
2023-09-15 14:35:15 +02:00
terem42
ad8f7627d8 update to new rescue (#43)
updated to new rescue, added script for ubuntu 22
2023-04-22 00:50:38 +02:00
terem42
e26d27f692 Merge pull request #40 from driops/ub20fix
ubuntu20: install zfs from bullseye-backports (on recovery)
2023-03-21 12:44:11 +01:00
driops
e632244d69 ubuntu20: install zfs from bullseye-backports (on recovery)
Hetzner's current recovery uses debian, Ubuntu PPA
packages don't work. This simplifies things.
2023-03-20 02:28:10 -04:00
terem42
e78d834bad Merge pull request #39 from driops/ub20fix
ubuntu20: update dropbear URL's
2022-11-19 15:02:29 +01:00
driops
f54fa2a68a ubuntu20: update dropbear URL's
From Debian, old links were dead
These are slightly newer and not dead
2022-11-13 19:57:00 -05:00
terem42
b31d78992d Merge pull request #37 from westfeld/fix_quotes
fix quotes for DKMS configuration file
2022-08-18 18:49:58 +02:00
Thomas Westfeld
3e99938566 fix quotes for DKMS configuration file 2022-08-07 19:14:06 +01:00
terem42
292def7e53 Merge pull request #35 from crpb/dkms-force-rebuild
dkms: force rebuild terem42/zfs-hetzner-vm#28
2022-07-27 23:12:31 +02:00
terem42
5ca06e4580 Merge pull request #36 from westfeld/fix-dns
Start systemd-resolved to enable correct DNS resolution
2022-07-27 23:10:43 +02:00
Thomas Westfeld
6bf81c2d60 start systemd-resolved to enable correct DNS resolution 2022-07-27 19:46:40 +02:00
Christopher Bock
3826932a79 dkms: force rebuild terem42/zfs-hetzner-vm#28 2022-06-13 05:51:33 +02:00
terem42
4dc9ac2ddb Merge pull request #33 from jlsjonas/patch-1
Add note when dealing with software raid
2022-05-31 09:54:04 +02:00
Jonas De Kegel
c8c839d96c Add note when dealing with software raid 2022-05-29 13:55:33 +02:00
terem42
1542404c0e Merge pull request #32 from driops/master
Increase bpool size to 2G so we don't risk boot breaking
2022-04-13 18:06:00 +02:00
driops
d86ec88cdc Increase bpool size to 2G so we don't risk boot breaking when both ZFS and kernel update 2022-04-07 02:09:02 -04:00
terem42
c2bd03377d Merge pull request #27 from westfeld/fix_hostname_regexp
correct hostname regexp fixes issue #24
2022-01-29 23:50:15 +01:00
Thomas Westfeld
2592d744bb correct hostname regexp fixes issue #24 2022-01-24 19:19:59 +01:00
Prokopenko Andrey
dc72f24c43 removed double quoting from zpool statements 2021-11-25 23:37:05 +01:00
Prokopenko Andrey
f2331df185 zpool options update for debian 2021-11-25 22:27:06 +01:00
Andrey Prokopenko
effc9964ee shellcheck code fixes 2021-11-18 20:22:53 +01:00
terem42
c1391bb65f Update shellcheck.yml 2021-09-25 12:10:32 +02:00
Andrey Prokopenko
e322cf49b4 modified permissions 2021-09-25 12:08:54 +02:00
Andrey Prokopenko
762993e7e8 updated shellcheck workflow 2021-09-25 12:04:16 +02:00
Prokopenko Andrey
a283542850 shellcheck workflow updated 2021-09-16 19:22:35 +02:00
Prokopenko Andrey
c23b735462 shellcheck workflow updated 2021-09-16 19:20:58 +02:00
Prokopenko Andrey
c7b8d13be8 shellcheck workflow updated 2021-09-16 19:20:29 +02:00
Prokopenko Andrey
d8f796dc38 shellcheck workflow updated 2021-09-16 19:19:13 +02:00
Prokopenko Andrey
3d93256d17 shellcheck workflow updated 2021-09-16 19:18:26 +02:00
Prokopenko Andrey
37baec84ca shellcheck workflow added 2021-09-16 19:16:27 +02:00
Prokopenko Andrey
3b9a132a1d updated compression 2021-09-16 16:18:10 +02:00
terem42
fecb5ca25c Merge pull request #20 from terem42/mountservice
remove zfs-boot helper service, obsolete in OpenZFS 2.0
2021-09-16 15:28:35 +02:00
Prokopenko Andrey
59e7e2352f update 2021-09-16 14:51:39 +02:00
terem42
c1122406eb Update README.md 2021-09-15 22:33:58 +02:00
terem42
40d0221d18 Update README.md 2021-09-15 22:30:39 +02:00
terem42
f41b62ad32 Merge pull request #18 from terem42/newres
updated to load stable prebuilt zfs package and cleanup rescue
2021-09-15 22:24:34 +02:00
Prokopenko Andrey
6a6d5f35f4 updated to focal package 2021-09-15 21:13:53 +02:00
Prokopenko Andrey
86cfdfc693 updated to focal package 2021-09-15 20:57:45 +02:00
Prokopenko Andrey
1dc0204c44 update 2021-09-15 20:13:24 +02:00
Prokopenko Andrey
6b3e6f8328 new install 2021-09-15 20:10:01 +02:00
terem42
ccccb6f175 Update hetzner-ubuntu20-zfs-setup.sh 2021-09-15 19:30:13 +02:00
Prokopenko Andrey
13de08bf77 rearrange experimental package sequence 2021-09-15 17:39:21 +02:00
Prokopenko Andrey
5ccb823bd1 updated default choices for encrypted fs and experimental modules usage 2021-09-15 17:18:57 +02:00
Prokopenko Andrey
9d498be7b3 updated default choices for encrypted fs and experimental modules usage 2021-09-15 17:18:16 +02:00
Prokopenko Andrey
e24d9850f4 update 2021-09-15 17:12:52 +02:00
Prokopenko Andrey
8dffe7a859 update for new Hetzner rescue 2021-09-15 17:12:00 +02:00
Prokopenko Andrey
e8fd733f79 merged into master 2021-09-15 17:09:36 +02:00
Prokopenko Andrey
dc1f2223ff update 2021-09-15 16:41:11 +02:00
terem42
c3c9348c6f Update hetzner-debian11-zfs-setup.sh 2021-09-15 16:39:41 +02:00
terem42
a4b1ed941e Update README.md 2021-09-15 16:27:13 +02:00
terem42
0a8e665893 Merge pull request #16 from terem42/ubuntu20
new script for ubuntu 20
2021-09-15 16:26:48 +02:00
Prokopenko Andrey
b5a71bb5cc new script for ubuntu 20 2021-09-15 16:25:55 +02:00
terem42
ef7fd557b4 Merge pull request #12 from Joshua2504/patch-1
add --force flag to wipefs
2021-09-15 11:37:57 +02:00
terem42
e4f34d912e Merge pull request #13 from Joshua2504/patch-2
added --force flag to wipefs
2021-09-15 11:37:34 +02:00
Prokopenko Andrey
3ef2bba59f readme update 2021-09-15 11:30:06 +02:00
Prokopenko Andrey
d314efd524 updates for Debian 11 script due recent Hetzner rescue console changes 2021-09-15 11:28:51 +02:00
terem42
a9f235fef9 Merge pull request #15 from marcoboers/master
Add support for Debian 11
2021-09-15 11:27:03 +02:00
Marco
7c1951a807 Add support for debian 11 installed from debian 11 rescue mode 2021-09-14 16:35:37 +02:00
Joshua Treudler
b8084c40a8 added --force flag to wipefs
--force flag on wipefs fixes an issue with existing raid
2021-07-11 23:30:20 +02:00
Joshua Treudler
9e639583d8 add --force flag to wipefs
the --force flag on wipefs fixes an issue with existing raids
2021-07-11 23:28:47 +02:00
terem42
19df86b386 Merge pull request #11 from mdbraber/master
Change ubuntu to debian
2021-07-08 18:01:51 +02:00
Maarten den Braber
f4b9479585 Add -d option to bpool import to prevent import from ZVOL devices (e.g. /dev/zd*) with bpool datasets 2021-07-08 02:27:17 +02:00
Maarten den Braber
164456055c Change ubuntu to debian 2021-07-07 20:53:25 +02:00
terem42
4d82379e35 Merge pull request #10 from DigiDr/master
Update hetzner-debian10-zfs-setup.sh
2021-07-02 22:21:54 +02:00
Max Birge
60b9dbae18 Update hetzner-debian10-zfs-setup.sh 2021-06-25 21:07:40 +01:00
Prokopenko Andrey
e8deaea7a5 updated repository reference 2020-12-22 17:15:11 +01:00
terem42
61ee498182 Update hetzner-ubuntu18-zfs-setup.sh 2020-10-15 20:10:41 +02:00
Andrey Prokopenko
4ea0e787a3 repo name updated 2020-04-18 00:25:25 +02:00
Andrey Prokopenko
072ef6610c Update hetzner-debian10-zfs-setup.sh 2020-04-11 19:54:35 +02:00
Andrey Prokopenko
d801c54a3a Update hetzner-debian10-zfs-setup.sh 2020-04-11 19:07:06 +02:00
11 changed files with 5717 additions and 185 deletions

11
.github/workflows/shellcheck.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: shellcheck
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run shellcheck
run: ci/run_shellcheck.sh

View File

@@ -1,6 +1,8 @@
# zfs-hetzner-vm
Scripts to install Debian 10 or Ubuntu 18 with ZFS root on Hetzner root servers (virtual and dedicated).<br/>
[![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, 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:
@@ -12,25 +14,64 @@ __WARNING:__ all data on the disk will be destroyed.
Debian 10 minimal setup with SSH server
````bash
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian10-zfs-setup.sh | bash -
````
wget -qO- https://raw.githubusercontent.com/andrey42/zfs-hetzner-vm/master/hetzner-debian10-zfs-setup.sh | bash -
Debian 11 minimal setup with SSH server
````bash
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian11-zfs-setup.sh | bash -
````
Debian 12 minimal setup with SSH server
````bash
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
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu18-zfs-setup.sh | bash -
````
wget -qO- https://raw.githubusercontent.com/andrey42/zfs-hetzner-vm/master/hetzner-ubuntu18-zfs-setup.sh | bash -
Ubuntu 20 LTS minimal setup with SSH server
````bash
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu20-zfs-setup.sh | bash -
````
Ubuntu 22 LTS minimal setup with SSH server
````bash
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.
Example of screen utility usage:
````
screen -S zfs
````bash
export LC_ALL=en_US.UTF-8 && screen -S zfs
````
To detach from screen console, hit Ctrl-d then a
To reattach, type `screen -r zfs`
Upon succesfull run, the script will reboot system, and you will be able to login into it, using the same SSH key you have used within rescue console
Please note that the drives you intend to format can not be in use,
you can execute `mdadm --stop --scan` before running the script to halt default software raid operations.

25
ci/run_shellcheck.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -o pipefail
set -o errexit
set -o nounset
set -o errtrace
shopt -s inherit_errexit
# Always download the latest version:
#
# - it's fast and stable enough not to worry about it;
# - the workflow is basically single-person, so there's no risk of a new dev encountering an error found
# by a new shellcheck version.
mkdir -p /opt/shellcheck
wget -qO- https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz \
| tar xJv -O shellcheck-stable/shellcheck \
> /opt/shellcheck/shellcheck
chmod +x /opt/shellcheck/shellcheck
/opt/shellcheck/shellcheck --version
grep -lZP '^#!/bin/\w+sh' -R | xargs -0 /opt/shellcheck/shellcheck

View File

@@ -17,6 +17,8 @@ set -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
# Variables
v_bpool_name=
v_bpool_tweaks=
@@ -40,7 +42,7 @@ c_deb_security_repo=http://mirror.hetzner.de/debian/security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=lz4 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
@@ -62,7 +64,9 @@ function print_step_info_header {
###############################################################################
# ${FUNCNAME[1]}"
[[ "${1:-}" != "" ]] && echo -n " $1" || true
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
@@ -106,7 +110,7 @@ This script will prepare the ZFS pools, then install and configure minimal Debia
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --ascii-lines --msgbox "$dialog_message" 30 100
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
@@ -134,20 +138,20 @@ function check_prerequisites {
function initial_load_debian_zed_cache {
chroot_execute "mkdir /etc/zfs/zfs-list.cache"
chroot_execute "touch /etc/zfs/zfs-list.cache/rpool"
chroot_execute "ln -s /usr/lib/zfs-linux/zed.d/history_event-zfs-list-cacher.sh /etc/zfs/zed.d/"
chroot_execute "touch /etc/zfs/zfs-list.cache/$v_rpool_name"
chroot_execute "ln -sf /usr/lib/zfs-linux/zed.d/history_event-zfs-list-cacher.sh /etc/zfs/zed.d/"
chroot_execute "zed -F &"
local success=0
if [[ ! -e /mnt/etc/zfs/zfs-list.cache/rpool ]] || [[ -e /mnt/etc/zfs/zfs-list.cache/rpool && (( $(ls -l /mnt/etc/zfs/zfs-list.cache/rpool 2> /dev/null | cut -d ' ' -f 5) == 0 )) ]]; then
chroot_execute "zfs set canmount=noauto rpool"
if [[ ! -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] || [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) == 0 )) ]]; then
chroot_execute "zfs set canmount=noauto $v_rpool_name"
SECONDS=0
while (( SECONDS++ <= 120 )); do
if [[ -e /mnt/etc/zfs/zfs-list.cache/rpool ]] && (( "$(ls -l /mnt/etc/zfs/zfs-list.cache/rpool | cut -d ' ' -f 5)" > 0 )); then
if [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) > 0 )); then
success=1
break
else
@@ -165,7 +169,7 @@ function initial_load_debian_zed_cache {
chroot_execute "pkill zed"
sed -Ei 's|/mnt/?|/|g' /mnt/etc/zfs/zfs-list.cache/rpool
sed -Ei "s|/$c_zfs_mount_dir/?|/|g" "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name"
}
function find_suitable_disks {
@@ -208,9 +212,9 @@ LOG
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/andrey42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --ascii-lines --msgbox "$dialog_message" 30 100
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
@@ -239,7 +243,7 @@ function select_disks {
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --ascii-lines --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
@@ -256,7 +260,7 @@ function ask_swap_size {
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --ascii-lines --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
@@ -271,7 +275,7 @@ function ask_free_tail_space {
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --ascii-lines --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
@@ -286,7 +290,7 @@ function ask_zfs_arc_max_size {
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --ascii-lines --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
@@ -302,14 +306,14 @@ function ask_pool_names {
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --ascii-lines --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --ascii-lines --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
@@ -321,8 +325,8 @@ function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --ascii-lines --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --ascii-lines --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
@@ -337,8 +341,8 @@ function ask_root_password {
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --ascii-lines --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --ascii-lines --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
@@ -348,7 +352,7 @@ function ask_root_password {
function ask_encryption {
print_step_info_header
if dialog --ascii-lines --yesno 'Do you want to encrypt the root pool?' 30 100; then
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
@@ -356,8 +360,8 @@ function ask_encryption {
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --ascii-lines --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --ascii-lines --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
@@ -368,7 +372,7 @@ function ask_encryption {
function ask_zfs_experimental {
print_step_info_header
if dialog --ascii-lines --yesno 'Do you want to use experimental zfs module build?' 30 100; then
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
@@ -379,8 +383,8 @@ function ask_hostname {
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_hostname=$(dialog --ascii-lines --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
@@ -395,7 +399,7 @@ function determine_kernel_variant {
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "$1"
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
@@ -431,9 +435,8 @@ function unmount_and_export_fs {
zpools_exported=99
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
zpool export -a 2> /dev/null
if [[ $? == 0 ]]; then
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
@@ -450,6 +453,7 @@ function unmount_and_export_fs {
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
@@ -490,11 +494,20 @@ for kver in $(find /lib/modules/* -maxdepth 0 -type d | grep -v "$(uname -r)" |
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
apt install --yes -t buster-backports libelf-dev zfs-dkms
modprobe zfs
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
@@ -506,9 +519,9 @@ echo "======= partitioning the disk =========="
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all "$selected_disk"
wipefs --all --force "$selected_disk"
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
sgdisk -n2:0:+512M -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
@@ -535,12 +548,16 @@ echo "======= create zfs pools and datasets =========="
pools_mirror_option=
fi
# shellcheck disable=SC2086
zpool create \
$v_bpool_tweaks -O canmount=off -O devices=off \
-o cachefile=/etc/zpool.cache \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
-o cachefile=/etc/zpool.cache \
$v_rpool_tweaks \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
@@ -552,13 +569,11 @@ zfs create -o canmount=off -o mountpoint=none "$v_bpool_name/BOOT"
zfs create -o canmount=noauto -o mountpoint=/ "$v_rpool_name/ROOT/debian"
zfs mount "$v_rpool_name/ROOT/debian"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/ubuntu"
zfs mount "$v_bpool_name/BOOT/ubuntu"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/debian"
zfs mount "$v_bpool_name/BOOT/debian"
zfs create "$v_rpool_name/home"
zfs create -o mountpoint=/root "$v_rpool_name/home/root"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create -o canmount=off "$v_rpool_name/var/lib"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
@@ -609,7 +624,7 @@ ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p')
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > /mnt/etc/systemd/network/10-eth0.network
[Match]
@@ -621,9 +636,7 @@ Address=${ip6addr_prefix}:1/64
Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
cp /etc/resolv.conf $c_zfs_mount_dir/etc/resolv.conf
chroot_execute "systemctl enable systemd-resolved.service"
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
@@ -698,20 +711,23 @@ echo "======= installing aux packages =========="
chroot_execute "apt install --yes man wget curl software-properties-common nano htop gnupg"
echo "======= installing zfs packages =========="
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://andrey42.github.io/zfs-ubuntu/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://andrey42.github.io/zfs-ubuntu/public zfs-debian-experimental main'"
chroot_execute "apt update"
else
chroot_execute "apt install --yes zfs-initramfs zfs-dkms"
fi
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "apt install --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "apt install --yes -t buster-backports zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
@@ -726,37 +742,22 @@ chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs services =========="
chroot_execute "cat > /etc/systemd/system/zfs-import-bpool.service <<UNIT
[Unit]
DefaultDependencies=no
Before=zfs-import-scan.service
Before=zfs-import-cache.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c '[ -f /etc/zfs/zpool.cache ] && mv /etc/zfs/zpool.cache /etc/zfs/preboot_zpool.cache || true'
ExecStart=/sbin/zpool import -N -o cachefile=none $v_bpool_name
ExecStartPost=/bin/sh -c '[ -f /etc/zfs/preboot_zpool.cache ] && mv /etc/zfs/preboot_zpool.cache /etc/zfs/zpool.cache || true'
[Install]
WantedBy=zfs-import.target
UNIT"
chroot_execute "systemctl enable zfs-import-bpool.service"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache /mnt/etc/zfs/zpool.cache
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "DEBIAN_FRONTEND=noninteractive apt install --yes grub-pc"
chroot_execute "grub-install ${v_selected_disks[0]}"
chroot_execute "apt install --yes grub-pc"
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install $disk"
done
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=rpool/ROOT/debian\"|g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/debian\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
@@ -770,7 +771,8 @@ if [[ $v_encrypt_rpool == "1" ]]; then
echo "=========set up dropbear=============="
chroot_execute "apt install --yes dropbear-initramfs"
mkdir -p "$c_zfs_mount_dir/etc/dropbear-initramfs"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/etc/dropbear-initramfs/authorized_keys"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
@@ -787,18 +789,6 @@ if [[ $v_encrypt_rpool == "1" ]]; then
rm -rf "$c_zfs_mount_dir/etc/dropbear-initramfs/dropbear_dss_host_key"
fi
#cd "$c_zfs_mount_dir/root"
#wget http://ftp.de.debian.org/debian/pool/main/libt/libtommath/libtommath1_1.1.0-3_amd64.deb
#wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-bin_2018.76-5_amd64.deb
#wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-initramfs_2018.76-5_all.deb
#chroot_execute "dpkg -i /root/libtommath1_1.1.0-3_amd64.deb"
#chroot_execute "dpkg -i /root/dropbear-bin_2018.76-5_amd64.deb"
#chroot_execute "dpkg -i /root/dropbear-initramfs_2018.76-5_all.deb"
#rm $c_zfs_mount_dir/root/*.deb
#cd /root
echo "============setup root prompt============"
cat > "$c_zfs_mount_dir/root/.bashrc" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
@@ -809,9 +799,45 @@ CONF
echo "========running packages upgrade==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
#echo "===========add static route to initramfs via hook to add default routes due to initramfs DHCP bug ========="
# removed
echo "===========add static route to initramfs via hook to add default routes for Hetzner due to Debian/Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
cat > "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route" <<'CONF'
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
# Begin real processing below this line
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev eth0
ip route add default via 172.31.1.1 dev eth0
CONF
cat > "$c_zfs_mount_dir/etc/network/interfaces" <<'CONF'
auto lo
iface lo inet loopback
iface lo inet6 loopback
allow-hotplug eth0
iface eth0 inet dhcp
iface eth0 inet6 dhcp
CONF
chmod 755 "$c_zfs_mount_dir/etc/network/interfaces"
echo "======= update initramfs =========="
chroot_execute "update-initramfs -u -k all"
@@ -821,14 +847,14 @@ chroot_execute "update-grub"
echo "======= setting up zed =========="
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "zfs set canmount=noauto rpool"
chroot_execute "zfs set canmount=noauto $v_rpool_name"
else
initial_load_debian_zed_cache
fi
echo "======= setting mountpoints =========="
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/ubuntu"
chroot_execute "echo $v_bpool_name/BOOT/ubuntu /boot zfs nodev,relatime,x-systemd.requires=zfs-import-bpool.service 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/debian"
chroot_execute "echo $v_bpool_name/BOOT/debian /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
@@ -840,7 +866,9 @@ chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
[[ $v_swap_size -gt 0 ]] && chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab" || true
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="

View File

@@ -0,0 +1,876 @@
#!/bin/bash
: <<'end_header_info'
(c) Andrey Prokopenko job@terem.fr
fully automatic script to install Debian 11 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, set it OS to linux64, then press "mount rescue and power cycle" button
Next, connect via SSH to console, and run the script
Answer script questions about desired hostname, ZFS ARC cache size et cetera
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 -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
# Variables
v_bpool_name=
v_bpool_tweaks=
v_rpool_name=
v_rpool_tweaks=
declare -a v_selected_disks
v_swap_size= # integer
v_free_tail_space= # integer
v_hostname=
v_kernel_variant=
v_zfs_arc_max_mb=
v_root_password=
v_encrypt_rpool= # 0=false, 1=true
v_passphrase=
v_zfs_experimental=
v_suitable_disks=()
# Constants
c_deb_packages_repo=https://mirror.hetzner.com/debian/packages
c_deb_security_repo=https://mirror.hetzner.com/debian/security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
c_install_log=$c_log_dir/install.log
c_lsb_release_log=$c_log_dir/lsb_release.log
c_disks_log=$c_log_dir/disks.log
function activate_debug {
mkdir -p "$c_log_dir"
exec 5> "$c_install_log"
BASH_XTRACEFD="5"
set -x
}
# shellcheck disable=SC2120
function print_step_info_header {
echo -n "
###############################################################################
# ${FUNCNAME[1]}"
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
"
}
function print_variables {
for variable_name in "$@"; do
declare -n variable_reference="$variable_name"
echo -n "$variable_name:"
case "$(declare -p "$variable_name")" in
"declare -a"* )
for entry in "${variable_reference[@]}"; do
echo -n " \"$entry\""
done
;;
"declare -A"* )
for key in "${!variable_reference[@]}"; do
echo -n " $key=\"${variable_reference[$key]}\""
done
;;
* )
echo -n " $variable_reference"
;;
esac
echo
done
echo
}
function display_intro_banner {
# shellcheck disable=SC2119
print_step_info_header
local dialog_message='Hello!
This script will prepare the ZFS pools, then install and configure minimal Debian 11 with ZFS root on Hetzner hosting VPS instance
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
# shellcheck disable=SC2119
print_step_info_header
lsb_release --all > "$c_lsb_release_log"
}
function check_prerequisites {
# shellcheck disable=SC2119
print_step_info_header
if [[ $(id -u) -ne 0 ]]; then
echo 'This script must be run with administrative privileges!'
exit 1
fi
if [[ ! -r /root/.ssh/authorized_keys ]]; then
echo "SSH pubkey file is absent, please add it to the rescue system setting, then reboot into rescue system and run the script"
exit 1
fi
if ! dpkg-query --showformat="\${Status}" -W dialog 2> /dev/null | grep -q "install ok installed"; then
apt install --yes dialog
fi
}
function initial_load_debian_zed_cache {
chroot_execute "mkdir /etc/zfs/zfs-list.cache"
chroot_execute "touch /etc/zfs/zfs-list.cache/$v_rpool_name"
chroot_execute "ln -sf /usr/lib/zfs-linux/zed.d/history_event-zfs-list-cacher.sh /etc/zfs/zed.d/"
chroot_execute "zed -F &"
local success=0
if [[ ! -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] || [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) == 0 )) ]]; then
chroot_execute "zfs set canmount=noauto $v_rpool_name"
SECONDS=0
while (( SECONDS++ <= 120 )); do
if [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) > 0 )); then
success=1
break
else
sleep 1
fi
done
else
success=1
fi
if (( success != 1 )); then
echo "Fatal zed daemon error: the ZFS cache hasn't been updated by ZED!"
exit 1
fi
chroot_execute "pkill zed"
sed -Ei "s|/$c_zfs_mount_dir/?|/|g" "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name"
}
function find_suitable_disks {
# shellcheck disable=SC2119
print_step_info_header
udevadm trigger
# shellcheck disable=SC2012
ls -l /dev/disk/by-id | tail -n +2 | perl -lane 'print "@F[8..10]"' > "$c_disks_log"
local candidate_disk_ids
local mounted_devices
candidate_disk_ids=$(find /dev/disk/by-id -regextype awk -regex '.+/(ata|nvme|scsi)-.+' -not -regex '.+-part[0-9]+$' | sort)
mounted_devices="$(df | awk 'BEGIN {getline} {print $1}' | xargs -n 1 lsblk -no pkname 2> /dev/null | sort -u || true)"
while read -r disk_id || [[ -n "$disk_id" ]]; do
local device_info
device_info="$(udevadm info --query=property "$(readlink -f "$disk_id")")"
block_device_basename="$(basename "$(readlink -f "$disk_id")")"
if ! grep -q '^ID_TYPE=cd$' <<< "$device_info"; then
if ! grep -q "^$block_device_basename\$" <<< "$mounted_devices"; then
v_suitable_disks+=("$disk_id")
fi
fi
cat >> "$c_disks_log" << LOG
## DEVICE: $disk_id ################################
$(udevadm info --query=property "$(readlink -f "$disk_id")")
LOG
done < <(echo -n "$candidate_disk_ids")
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
print_variables v_suitable_disks
}
function select_disks {
# shellcheck disable=SC2119
print_step_info_header
while true; do
local menu_entries_option=()
if [[ ${#v_suitable_disks[@]} -eq 1 ]]; then
local disk_selection_status=ON
else
local disk_selection_status=OFF
fi
for disk_id in "${v_suitable_disks[@]}"; do
menu_entries_option+=("$disk_id" "($block_device_basename)" "$disk_selection_status")
done
local dialog_message="Select the ZFS devices (multiple selections will be in mirror).
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
fi
done
print_variables v_selected_disks
}
function ask_swap_size {
# shellcheck disable=SC2119
print_step_info_header
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
print_variables v_swap_size
}
function ask_free_tail_space {
# shellcheck disable=SC2119
print_step_info_header
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
print_variables v_free_tail_space
}
function ask_zfs_arc_max_size {
# shellcheck disable=SC2119
print_step_info_header
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
print_variables v_zfs_arc_max_mb
}
function ask_pool_names {
# shellcheck disable=SC2119
print_step_info_header
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
print_variables v_bpool_name v_rpool_name
}
function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
function ask_root_password {
# shellcheck disable=SC2119
print_step_info_header
set +x
local password_invalid_message=
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
set -x
}
function ask_encryption {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
if [[ $v_encrypt_rpool == "1" ]]; then
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
fi
set -x
}
function ask_zfs_experimental {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
function ask_hostname {
# shellcheck disable=SC2119
print_step_info_header
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
print_variables v_hostname
}
function determine_kernel_variant {
if dmidecode | grep -q vServer; then
v_kernel_variant="-cloud"
fi
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
# shellcheck disable=SC2119
print_step_info_header
for virtual_fs_dir in dev sys proc; do
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
done
local max_unmount_wait=5
echo -n "Waiting for virtual filesystems to unmount "
SECONDS=0
for virtual_fs_dir in dev sys proc; do
while mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir" && [[ $SECONDS -lt $max_unmount_wait ]]; do
sleep 0.5
echo -n .
done
done
echo
for virtual_fs_dir in dev sys proc; do
if mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir"; then
echo "Re-issuing umount for $c_zfs_mount_dir/$virtual_fs_dir"
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
fi
done
SECONDS=0
zpools_exported=99
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
else
sleep 1
fi
done
set -e
if (( zpools_exported != 1 )); then
echo "failed to export zfs pools"
exit 1
fi
}
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
activate_debug
display_intro_banner
find_suitable_disks
select_disks
ask_swap_size
ask_free_tail_space
ask_pool_names
ask_pool_tweaks
ask_encryption
ask_zfs_arc_max_size
ask_zfs_experimental
ask_root_password
ask_hostname
determine_kernel_variant
clear
echo "===========remove 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
apt purge --yes "linux-headers-$kver"
apt purge --yes "linux-image-$kver"
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
if [[ $v_free_tail_space -eq 0 ]]; then
tail_space_parameter=0
else
tail_space_parameter="-${v_free_tail_space}G"
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all --force "$selected_disk"
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
udevadm settle
echo "======= create zfs pools and datasets =========="
encryption_options=()
rpool_disks_partitions=()
bpool_disks_partitions=()
if [[ $v_encrypt_rpool == "1" ]]; then
encryption_options=(-O "encryption=aes-256-gcm" -O "keylocation=prompt" -O "keyformat=passphrase")
fi
for selected_disk in "${v_selected_disks[@]}"; do
rpool_disks_partitions+=("${selected_disk}-part3")
bpool_disks_partitions+=("${selected_disk}-part2")
done
if [[ ${#v_selected_disks[@]} -gt 1 ]]; then
pools_mirror_option=mirror
else
pools_mirror_option=
fi
# shellcheck disable=SC2086
zpool create \
$v_bpool_tweaks -O canmount=off -O devices=off \
-o cachefile=/etc/zpool.cache \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
$v_rpool_tweaks \
-o cachefile=/etc/zpool.cache \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
$v_rpool_name $pools_mirror_option "${rpool_disks_partitions[@]}"
zfs create -o canmount=off -o mountpoint=none "$v_rpool_name/ROOT"
zfs create -o canmount=off -o mountpoint=none "$v_bpool_name/BOOT"
zfs create -o canmount=noauto -o mountpoint=/ "$v_rpool_name/ROOT/debian"
zfs mount "$v_rpool_name/ROOT/debian"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/debian"
zfs mount "$v_bpool_name/BOOT/debian"
zfs create "$v_rpool_name/home"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/cache"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/tmp"
chmod 1777 "$c_zfs_mount_dir/var/tmp"
zfs create "$v_rpool_name/srv"
zfs create -o canmount=off "$v_rpool_name/usr"
zfs create "$v_rpool_name/usr/local"
zfs create "$v_rpool_name/var/mail"
zfs create -o com.sun:auto-snapshot=false -o canmount=on -o mountpoint=/tmp "$v_rpool_name/tmp"
chmod 1777 "$c_zfs_mount_dir/tmp"
if [[ $v_swap_size -gt 0 ]]; then
zfs create \
-V "${v_swap_size}G" -b "$(getconf PAGESIZE)" \
-o compression=zle -o logbias=throughput -o sync=always -o primarycache=metadata -o secondarycache=none -o com.sun:auto-snapshot=false \
"$v_rpool_name/swap"
udevadm settle
mkswap -f "/dev/zvol/$v_rpool_name/swap"
fi
echo "======= setting up initial system packages =========="
debootstrap --arch=amd64 bullseye "$c_zfs_mount_dir" "$c_deb_packages_repo"
zfs set devices=off "$v_rpool_name"
echo "======= setting up the network =========="
echo "$v_hostname" > $c_zfs_mount_dir/etc/hostname
cat > "$c_zfs_mount_dir/etc/hosts" <<CONF
127.0.1.1 ${v_hostname}
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > /mnt/etc/systemd/network/10-eth0.network
[Match]
Name=eth0
[Network]
DHCP=ipv4
Address=${ip6addr_prefix}:1/64
Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
chroot_execute "systemctl enable systemd-resolved.service"
#cp /etc/resolv.conf $c_zfs_mount_dir/etc/resolv.conf
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
mount --rbind "/$virtual_fs_dir" "$c_zfs_mount_dir/$virtual_fs_dir"
done
echo "======= setting apt repos =========="
cat > "$c_zfs_mount_dir/etc/apt/sources.list" <<CONF
deb $c_deb_packages_repo bullseye main contrib non-free
deb $c_deb_packages_repo bullseye-updates main contrib non-free
deb $c_deb_security_repo bullseye-security main contrib non-free
deb $c_deb_packages_repo bullseye-backports main contrib non-free
CONF
chroot_execute "apt update"
echo "======= setting locale, console and language =========="
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils"
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_AT.UTF-8/de_AT.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_DE.UTF-8/de_DE.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
chroot_execute 'cat <<CONF | debconf-set-selections
locales locales/default_environment_locale select en_US.UTF-8
keyboard-configuration keyboard-configuration/store_defaults_in_debconf_db boolean true
keyboard-configuration keyboard-configuration/variant select German
keyboard-configuration keyboard-configuration/unsupported_layout boolean true
keyboard-configuration keyboard-configuration/modelcode string pc105
keyboard-configuration keyboard-configuration/unsupported_config_layout boolean true
keyboard-configuration keyboard-configuration/layout select German
keyboard-configuration keyboard-configuration/layoutcode string de
keyboard-configuration keyboard-configuration/optionscode string
keyboard-configuration keyboard-configuration/toggle select No toggling
keyboard-configuration keyboard-configuration/xkb-keymap select de
keyboard-configuration keyboard-configuration/switch select No temporary switch
keyboard-configuration keyboard-configuration/unsupported_config_options boolean true
keyboard-configuration keyboard-configuration/ctrl_alt_bksp boolean false
keyboard-configuration keyboard-configuration/variantcode string
keyboard-configuration keyboard-configuration/model select Generic 105-key PC (intl.)
keyboard-configuration keyboard-configuration/altgr select The default for the keyboard layout
keyboard-configuration keyboard-configuration/compose select No compose key
keyboard-configuration keyboard-configuration/unsupported_options boolean true
console-setup console-setup/fontsize-fb47 select 8x16
console-setup console-setup/store_defaults_in_debconf_db boolean true
console-setup console-setup/codeset47 select # Latin1 and Latin5 - western Europe and Turkic languages
console-setup console-setup/fontface47 select Fixed
console-setup console-setup/fontsize string 8x16
console-setup console-setup/charmap47 select UTF-8
console-setup console-setup/fontsize-text47 select 8x16
console-setup console-setup/codesetcode string Lat15
tzdata tzdata/Areas select Europe
tzdata tzdata/Zones/Europe select Vienna
grub-pc grub-pc/install_devices_empty boolean true
CONF'
chroot_execute "dpkg-reconfigure locales -f noninteractive"
echo -e "LC_ALL=en_US.UTF-8\nLANG=en_US.UTF-8\n" >> "$c_zfs_mount_dir/etc/environment"
chroot_execute "apt install -qq --yes keyboard-configuration console-setup"
chroot_execute "dpkg-reconfigure keyboard-configuration -f noninteractive"
chroot_execute "dpkg-reconfigure console-setup -f noninteractive"
chroot_execute "setupcon"
chroot_execute "rm -f /etc/localtime /etc/timezone"
chroot_execute "dpkg-reconfigure tzdata -f noninteractive "
echo "======= installing latest kernel============="
chroot_execute "apt install --yes linux-image${v_kernel_variant}-amd64 linux-headers${v_kernel_variant}-amd64"
echo "======= installing aux packages =========="
chroot_execute "apt install --yes man wget curl software-properties-common nano htop gnupg"
echo "======= installing zfs packages =========="
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "apt install -t bullseye-backports --yes zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
echo "======= setup OpenSSH =========="
mkdir -p "$c_zfs_mount_dir/root/.ssh/"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/root/.ssh/authorized_keys"
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
chroot_execute "rm /etc/ssh/ssh_host_*"
chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache /mnt/etc/zfs/zpool.cache
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "apt install --yes grub-legacy"
chroot_execute "apt install --yes grub-pc"
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install --recheck $disk"
done
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/debian\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
chroot_execute "echo 'GRUB_DISABLE_OS_PROBER=true' >> /etc/default/grub"
for ((i = 1; i < ${#v_selected_disks[@]}; i++)); do
dd if="${v_selected_disks[0]}-part1" of="${v_selected_disks[i]}-part1"
done
if [[ $v_encrypt_rpool == "1" ]]; then
echo "=========set up dropbear=============="
chroot_execute "apt install --yes dropbear-initramfs"
mkdir -p "$c_zfs_mount_dir/etc/dropbear/initramfs"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/etc/dropbear/initramfs/authorized_keys"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_rsa_key_temp /etc/dropbear/initramfs/dropbear_rsa_host_key"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_ecdsa_key_temp /etc/dropbear/initramfs/dropbear_ecdsa_host_key"
chroot_execute "rm -rf /etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/dropbear/initramfs/dropbear_dss_host_key"
fi
echo "============setup root prompt============"
cat > "$c_zfs_mount_dir/root/.bashrc" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
echo "========running packages upgrade==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
echo "===========add static route to initramfs via hook to add default routes for Hetzner due to Debian/Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
cat > "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route" <<'CONF'
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
# Begin real processing below this line
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev eth0
ip route add default via 172.31.1.1 dev eth0
CONF
chmod 755 "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route"
chmod 755 "$c_zfs_mount_dir/etc/network/interfaces"
echo "======= update initramfs =========="
chroot_execute "update-initramfs -u -k all"
echo "======= update grub =========="
chroot_execute "update-grub"
echo "======= setting up zed =========="
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "zfs set canmount=noauto $v_rpool_name"
else
initial_load_debian_zed_cache
fi
echo "======= setting mountpoints =========="
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/debian"
chroot_execute "echo $v_bpool_name/BOOT/debian /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/spool"
chroot_execute "echo $v_rpool_name/var/spool /var/spool zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/tmp"
chroot_execute "echo $v_rpool_name/var/tmp /var/tmp zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="
unmount_and_export_fs
echo "======== setup complete, rebooting ==============="
reboot

View File

@@ -0,0 +1,916 @@
#!/bin/bash
: <<'end_header_info'
(c) Andrey Prokopenko job@terem.fr
fully automatic script to install Debian 12 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, set it OS to linux64, then press "mount rescue and power cycle" button
Next, connect via SSH to console, and run the script
Answer script questions about desired hostname, ZFS ARC cache size et cetera
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 -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
export DEBIAN_FRONTEND=noninteractive
# Variables
v_bpool_name=
v_bpool_tweaks=
v_rpool_name=
v_rpool_tweaks=
declare -a v_selected_disks
v_swap_size= # integer
v_free_tail_space= # integer
v_hostname=
v_kernel_variant=
v_zfs_arc_max_mb=
v_root_password=
v_encrypt_rpool= # 0=false, 1=true
v_passphrase=
v_zfs_experimental=
v_suitable_disks=()
# Constants
c_deb_packages_repo=https://deb.debian.org/debian
c_deb_security_repo=https://deb.debian.org/debian-security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
c_install_log=$c_log_dir/install.log
c_lsb_release_log=$c_log_dir/lsb_release.log
c_disks_log=$c_log_dir/disks.log
c_efimode_enabled="$(if [[ -d /sys/firmware/efi/efivars ]]; then echo 1; else echo 0; fi)"
function activate_debug {
mkdir -p "$c_log_dir"
exec 5> "$c_install_log"
BASH_XTRACEFD="5"
set -x
}
# shellcheck disable=SC2120
function print_step_info_header {
echo -n "
###############################################################################
# ${FUNCNAME[1]}"
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
"
}
function print_variables {
for variable_name in "$@"; do
declare -n variable_reference="$variable_name"
echo -n "$variable_name:"
case "$(declare -p "$variable_name")" in
"declare -a"* )
for entry in "${variable_reference[@]}"; do
echo -n " \"$entry\""
done
;;
"declare -A"* )
for key in "${!variable_reference[@]}"; do
echo -n " $key=\"${variable_reference[$key]}\""
done
;;
* )
echo -n " $variable_reference"
;;
esac
echo
done
echo
}
function display_intro_banner {
# shellcheck disable=SC2119
print_step_info_header
local dialog_message='Hello!
This script will prepare the ZFS pools, then install and configure minimal Debian 12 with ZFS root on Hetzner hosting VPS instance
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
# shellcheck disable=SC2119
print_step_info_header
lsb_release --all > "$c_lsb_release_log"
}
function check_prerequisites {
# shellcheck disable=SC2119
print_step_info_header
if [[ $(id -u) -ne 0 ]]; then
echo 'This script must be run with administrative privileges!'
exit 1
fi
if [[ ! -r /root/.ssh/authorized_keys ]]; then
echo "SSH pubkey file is absent, please add it to the rescue system setting, then reboot into rescue system and run the script"
exit 1
fi
if ! dpkg-query --showformat="\${Status}" -W dialog 2> /dev/null | grep -q "install ok installed"; then
apt install --yes dialog
fi
}
function initial_load_debian_zed_cache {
chroot_execute "mkdir /etc/zfs/zfs-list.cache"
chroot_execute "touch /etc/zfs/zfs-list.cache/$v_rpool_name"
chroot_execute "ln -sf /usr/lib/zfs-linux/zed.d/history_event-zfs-list-cacher.sh /etc/zfs/zed.d/"
chroot_execute "zed -F &"
local success=0
if [[ ! -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] || [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) == 0 )) ]]; then
chroot_execute "zfs set canmount=noauto $v_rpool_name"
SECONDS=0
while (( SECONDS++ <= 120 )); do
if [[ -e "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" ]] && (( $(find "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name" -type f -printf '%s' 2> /dev/null) > 0 )); then
success=1
break
else
sleep 1
fi
done
else
success=1
fi
if (( success != 1 )); then
echo "Fatal zed daemon error: the ZFS cache hasn't been updated by ZED!"
exit 1
fi
chroot_execute "pkill zed"
sed -Ei "s|/$c_zfs_mount_dir/?|/|g" "$c_zfs_mount_dir/etc/zfs/zfs-list.cache/$v_rpool_name"
}
function find_suitable_disks {
# shellcheck disable=SC2119
print_step_info_header
udevadm trigger
# shellcheck disable=SC2012
ls -l /dev/disk/by-id | tail -n +2 | perl -lane 'print "@F[8..10]"' > "$c_disks_log"
local candidate_disk_ids
local mounted_devices
candidate_disk_ids=$(find /dev/disk/by-id -regextype awk -regex '.+/(ata|nvme|scsi)-.+' -not -regex '.+-part[0-9]+$' | sort)
mounted_devices="$(df | awk 'BEGIN {getline} {print $1}' | xargs -n 1 lsblk -no pkname 2> /dev/null | sort -u || true)"
while read -r disk_id || [[ -n "$disk_id" ]]; do
local device_info
device_info="$(udevadm info --query=property "$(readlink -f "$disk_id")")"
block_device_basename="$(basename "$(readlink -f "$disk_id")")"
if ! grep -q '^ID_TYPE=cd$' <<< "$device_info"; then
if ! grep -q "^$block_device_basename\$" <<< "$mounted_devices"; then
v_suitable_disks+=("$disk_id")
fi
fi
cat >> "$c_disks_log" << LOG
## DEVICE: $disk_id ################################
$(udevadm info --query=property "$(readlink -f "$disk_id")")
LOG
done < <(echo -n "$candidate_disk_ids")
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
print_variables v_suitable_disks
}
function select_disks {
# shellcheck disable=SC2119
print_step_info_header
while true; do
local menu_entries_option=()
if [[ ${#v_suitable_disks[@]} -eq 1 ]]; then
local disk_selection_status=ON
else
local disk_selection_status=OFF
fi
for disk_id in "${v_suitable_disks[@]}"; do
menu_entries_option+=("$disk_id" "($block_device_basename)" "$disk_selection_status")
done
local dialog_message="Select the ZFS devices (multiple selections can be in mirror or strip).
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
fi
done
print_variables v_selected_disks
}
function ask_swap_size {
# shellcheck disable=SC2119
print_step_info_header
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
print_variables v_swap_size
}
function ask_free_tail_space {
# shellcheck disable=SC2119
print_step_info_header
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
print_variables v_free_tail_space
}
function ask_zfs_arc_max_size {
# shellcheck disable=SC2119
print_step_info_header
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
print_variables v_zfs_arc_max_mb
}
function ask_pool_names {
# shellcheck disable=SC2119
print_step_info_header
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
print_variables v_bpool_name v_rpool_name
}
function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
function ask_root_password {
# shellcheck disable=SC2119
print_step_info_header
set +x
local password_invalid_message=
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
set -x
}
function ask_encryption {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
if [[ $v_encrypt_rpool == "1" ]]; then
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
fi
set -x
}
function ask_zfs_experimental {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
function ask_hostname {
# shellcheck disable=SC2119
print_step_info_header
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
print_variables v_hostname
}
function determine_kernel_variant {
if dmidecode | grep -q vServer; then
v_kernel_variant="-cloud"
fi
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
# shellcheck disable=SC2119
print_step_info_header
for virtual_fs_dir in dev sys proc; do
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
done
local max_unmount_wait=5
echo -n "Waiting for virtual filesystems to unmount "
SECONDS=0
for virtual_fs_dir in dev sys proc; do
while mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir" && [[ $SECONDS -lt $max_unmount_wait ]]; do
sleep 0.5
echo -n .
done
done
echo
for virtual_fs_dir in dev sys proc; do
if mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir"; then
echo "Re-issuing umount for $c_zfs_mount_dir/$virtual_fs_dir"
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
fi
done
SECONDS=0
zpools_exported=99
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
else
sleep 1
fi
done
set -e
if (( zpools_exported != 1 )); then
echo "failed to export zfs pools"
exit 1
fi
}
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
activate_debug
display_intro_banner
find_suitable_disks
select_disks
ask_swap_size
ask_free_tail_space
ask_pool_names
ask_pool_tweaks
ask_encryption
ask_zfs_arc_max_size
ask_zfs_experimental
ask_root_password
ask_hostname
determine_kernel_variant
clear
echo "===========remove 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
if dpkg -l "linux-headers-$kver" 2>/dev/null | grep -q "^ii"; then
apt purge --yes "linux-headers-$kver"
fi
if dpkg -l "linux-image-$kver" 2>/dev/null | grep -q "^ii"; then
apt purge --yes "linux-image-$kver"
fi
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
if [[ $v_free_tail_space -eq 0 ]]; then
tail_space_parameter=0
else
tail_space_parameter="-${v_free_tail_space}G"
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all --force "$selected_disk"
if (( c_efimode_enabled == 1 )); then
sgdisk -a1 -n1:24K:+1G -t1:EF00 "$selected_disk" # EFI partition
else
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
fi
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
udevadm settle
echo "======= create zfs pools and datasets =========="
encryption_options=()
rpool_disks_partitions=()
bpool_disks_partitions=()
if [[ $v_encrypt_rpool == "1" ]]; then
encryption_options=(-O "encryption=aes-256-gcm" -O "keylocation=prompt" -O "keyformat=passphrase")
fi
for selected_disk in "${v_selected_disks[@]}"; do
rpool_disks_partitions+=("${selected_disk}-part3")
bpool_disks_partitions+=("${selected_disk}-part2")
done
pools_mirror_option=
if [[ ${#v_selected_disks[@]} -gt 1 ]]; then
if dialog --defaultno --yesno "Do you want to use mirror mode for ${v_selected_disks[*]}?" 30 100; then
pools_mirror_option=mirror
fi
fi
# shellcheck disable=SC2086
zpool create \
-m none \
-o cachefile=/etc/zpool.cache \
-o compatibility=grub2 \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
-m none \
$v_rpool_tweaks \
-o cachefile=/etc/zpool.cache \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
$v_rpool_name $pools_mirror_option "${rpool_disks_partitions[@]}"
zfs create -o canmount=off -o mountpoint=none "$v_rpool_name/ROOT"
zfs create -o canmount=off -o mountpoint=none "$v_bpool_name/BOOT"
zfs create -o canmount=noauto -o mountpoint=/ "$v_rpool_name/ROOT/debian"
zfs mount "$v_rpool_name/ROOT/debian"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/debian"
zfs mount "$v_bpool_name/BOOT/debian"
zfs create "$v_rpool_name/home"
#zfs create -o mountpoint=/root "$v_rpool_name/home/root"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/cache"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/tmp"
chmod 1777 "$c_zfs_mount_dir/var/tmp"
zfs create "$v_rpool_name/srv"
zfs create -o canmount=off "$v_rpool_name/usr"
zfs create "$v_rpool_name/usr/local"
zfs create "$v_rpool_name/var/mail"
zfs create -o com.sun:auto-snapshot=false -o canmount=on -o mountpoint=/tmp "$v_rpool_name/tmp"
chmod 1777 "$c_zfs_mount_dir/tmp"
if [[ $v_swap_size -gt 0 ]]; then
zfs create \
-V "${v_swap_size}G" -b "$(getconf PAGESIZE)" \
-o compression=zle -o logbias=throughput -o sync=always -o primarycache=metadata -o secondarycache=none -o com.sun:auto-snapshot=false \
"$v_rpool_name/swap"
udevadm settle
mkswap -f "/dev/zvol/$v_rpool_name/swap"
fi
if (( c_efimode_enabled == 1 )); then
echo "======= create filesystem on EFI partition(s) =========="
for selected_disk in "${v_selected_disks[@]}"; do
mkfs.fat -F32 "${selected_disk}-part1"
done
mkdir -p "$c_zfs_mount_dir/boot/efi"
mount "${v_selected_disks[0]}-part1" "$c_zfs_mount_dir/boot/efi"
fi
echo "======= setting up initial system packages =========="
debootstrap --arch=amd64 bookworm "$c_zfs_mount_dir" "$c_deb_packages_repo"
zfs set devices=off "$v_rpool_name"
echo "======= setting up the network =========="
echo "$v_hostname" > $c_zfs_mount_dir/etc/hostname
cat > "$c_zfs_mount_dir/etc/hosts" <<CONF
127.0.1.1 ${v_hostname}
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > "$c_zfs_mount_dir/etc/systemd/network/10-eth0.network"
[Match]
Name=eth0
[Network]
DHCP=ipv4
Address=${ip6addr_prefix}:1/64
Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
mount --rbind "/$virtual_fs_dir" "$c_zfs_mount_dir/$virtual_fs_dir"
done
echo "======= setting apt repos =========="
cat > "$c_zfs_mount_dir/etc/apt/sources.list" <<CONF
deb $c_deb_packages_repo bookworm main contrib non-free non-free-firmware
deb $c_deb_packages_repo bookworm-updates main contrib non-free non-free-firmware
deb $c_deb_security_repo bookworm-security main contrib non-free non-free-firmware
deb $c_deb_packages_repo bookworm-backports main contrib non-free non-free-firmware
CONF
chroot_execute "apt update"
echo "======= setting locale, console and language =========="
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils"
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_AT.UTF-8/de_AT.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_DE.UTF-8/de_DE.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
chroot_execute 'cat <<CONF | debconf-set-selections
locales locales/default_environment_locale select en_US.UTF-8
keyboard-configuration keyboard-configuration/store_defaults_in_debconf_db boolean true
keyboard-configuration keyboard-configuration/variant select German
keyboard-configuration keyboard-configuration/unsupported_layout boolean true
keyboard-configuration keyboard-configuration/modelcode string pc105
keyboard-configuration keyboard-configuration/unsupported_config_layout boolean true
keyboard-configuration keyboard-configuration/layout select German
keyboard-configuration keyboard-configuration/layoutcode string de
keyboard-configuration keyboard-configuration/optionscode string
keyboard-configuration keyboard-configuration/toggle select No toggling
keyboard-configuration keyboard-configuration/xkb-keymap select de
keyboard-configuration keyboard-configuration/switch select No temporary switch
keyboard-configuration keyboard-configuration/unsupported_config_options boolean true
keyboard-configuration keyboard-configuration/ctrl_alt_bksp boolean false
keyboard-configuration keyboard-configuration/variantcode string
keyboard-configuration keyboard-configuration/model select Generic 105-key PC (intl.)
keyboard-configuration keyboard-configuration/altgr select The default for the keyboard layout
keyboard-configuration keyboard-configuration/compose select No compose key
keyboard-configuration keyboard-configuration/unsupported_options boolean true
console-setup console-setup/fontsize-fb47 select 8x16
console-setup console-setup/store_defaults_in_debconf_db boolean true
console-setup console-setup/codeset47 select # Latin1 and Latin5 - western Europe and Turkic languages
console-setup console-setup/fontface47 select Fixed
console-setup console-setup/fontsize string 8x16
console-setup console-setup/charmap47 select UTF-8
console-setup console-setup/fontsize-text47 select 8x16
console-setup console-setup/codesetcode string Lat15
tzdata tzdata/Areas select Europe
tzdata tzdata/Zones/Europe select Vienna
CONF'
chroot_execute "dpkg-reconfigure locales -f noninteractive"
echo -e "LC_ALL=en_US.UTF-8\nLANG=en_US.UTF-8\n" >> "$c_zfs_mount_dir/etc/environment"
chroot_execute "apt install -qq --yes keyboard-configuration console-setup"
chroot_execute "dpkg-reconfigure keyboard-configuration -f noninteractive"
chroot_execute "dpkg-reconfigure console-setup -f noninteractive"
chroot_execute "setupcon"
chroot_execute "rm -f /etc/localtime /etc/timezone"
chroot_execute "dpkg-reconfigure tzdata -f noninteractive"
echo "======= installing latest kernel============="
# linux-headers-generic linux-image-generic
chroot_execute "apt install --yes linux-image${v_kernel_variant}-amd64 linux-headers${v_kernel_variant}-amd64 dpkg-dev"
echo "======= installing aux packages =========="
chroot_execute "apt install --yes man wget curl software-properties-common nano htop gnupg"
echo "======= installing zfs packages =========="
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "apt install -t bookworm-backports --yes zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
echo "======= setup OpenSSH =========="
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
chroot_execute "rm /etc/ssh/ssh_host_*"
chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache "$c_zfs_mount_dir/etc/zfs/zpool.cache"
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
if (( c_efimode_enabled == 1 )); then
chroot_execute "apt install --yes grub-efi-amd64"
else
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "apt install --yes grub-legacy"
chroot_execute "apt install --yes grub-pc"
fi
if (( c_efimode_enabled == 1 )); then
#chroot_execute grub-probe /boot
chroot_execute grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck
else
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install --recheck $disk"
done
fi
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/debian\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
chroot_execute "echo 'GRUB_DISABLE_OS_PROBER=true' >> /etc/default/grub"
for ((i = 1; i < ${#v_selected_disks[@]}; i++)); do
dd if="${v_selected_disks[0]}-part1" of="${v_selected_disks[i]}-part1"
done
if [[ $v_encrypt_rpool == "1" ]]; then
echo "=========set up dropbear=============="
chroot_execute "apt install --yes dropbear-initramfs"
mkdir -p "$c_zfs_mount_dir/etc/dropbear/initramfs"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/etc/dropbear/initramfs/authorized_keys"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_rsa_key_temp /etc/dropbear/initramfs/dropbear_rsa_host_key"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_ecdsa_key_temp /etc/dropbear/initramfs/dropbear_ecdsa_host_key"
chroot_execute "rm -rf /etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/dropbear/initramfs/dropbear_dss_host_key"
fi
echo "============setup root prompt============"
cat > "$c_zfs_mount_dir/root/.bashrc" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
echo "========= add root pubkey for login via SSH"
mkdir -p "$c_zfs_mount_dir/root/.ssh/"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/root/.ssh/authorized_keys"
echo "========running packages upgrade and autoremove==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
echo "===========add static route to initramfs via hook to add default routes for Hetzner due to Debian/Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
cat > "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route" <<'CONF'
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
# Begin real processing below this line
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev eth0
ip route add default via 172.31.1.1 dev eth0
CONF
chmod 755 "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route"
chmod 755 "$c_zfs_mount_dir/etc/network/interfaces"
echo "======= update initramfs =========="
chroot_execute "update-initramfs -u -k all"
chroot_execute "apt remove cryptsetup* --yes"
echo "======= update grub =========="
chroot_execute "update-grub"
echo "======= setting up zed =========="
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "zfs set canmount=noauto $v_rpool_name"
else
initial_load_debian_zed_cache
fi
echo "======= setting mountpoints =========="
if (( c_efimode_enabled == 1 )); then
umount "$c_zfs_mount_dir/boot/efi"
fi
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/debian"
chroot_execute "echo $v_bpool_name/BOOT/debian /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/spool"
chroot_execute "echo $v_rpool_name/var/spool /var/spool zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/tmp"
chroot_execute "echo $v_rpool_name/var/tmp /var/tmp zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="
unmount_and_export_fs
echo "======== setup complete, rebooting ==============="
reboot

View File

@@ -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 <<EOF
set -euo pipefail
# Set hostname from variable
echo "$SYSTEM_HOSTNAME" > /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" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
}
# ---- Bootloader Functions ----
function setup_efi_boot {
echo "======= Setting up EFI boot =========="
# Mount EFI System Partition
mkdir -p "$MAIN_BOOT"
mount "$BOOT_PART" "$MAIN_BOOT"
# Create EFI directory structure
mkdir -p "$MAIN_BOOT/EFI/Boot"
# Download ZFSBootMenu EFI binary
echo "Downloading ZFSBootMenu EFI binary from: $ZBM_EFI_URL"
curl -L "$ZBM_EFI_URL" -o "$MAIN_BOOT/EFI/Boot/bootx64.efi"
}
function setup_bios_boot {
echo "======= Setting up BIOS boot =========="
# Mount boot partition
mkdir -p "$MAIN_BOOT"
mount "$BOOT_PART" "$MAIN_BOOT"
# Install extlinux in rescue system if needed
if ! command -v extlinux &> /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

View File

@@ -17,6 +17,8 @@ set -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
# Variables
v_bpool_name=
v_bpool_tweaks=
@@ -40,7 +42,7 @@ c_deb_security_repo=http://mirror.hetzner.de/ubuntu/security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=lz4 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
@@ -62,7 +64,9 @@ function print_step_info_header {
###############################################################################
# ${FUNCNAME[1]}"
[[ "${1:-}" != "" ]] && echo -n " $1" || true
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
@@ -106,7 +110,7 @@ This script will prepare the ZFS pools, then install and configure minimal Ubunt
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --ascii-lines --msgbox "$dialog_message" 30 100
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
@@ -173,9 +177,9 @@ LOG
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/andrey42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --ascii-lines --msgbox "$dialog_message" 30 100
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
@@ -204,7 +208,7 @@ function select_disks {
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --ascii-lines --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
@@ -221,7 +225,7 @@ function ask_swap_size {
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --ascii-lines --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
@@ -236,7 +240,7 @@ function ask_free_tail_space {
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --ascii-lines --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
@@ -251,7 +255,7 @@ function ask_zfs_arc_max_size {
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --ascii-lines --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
@@ -267,14 +271,14 @@ function ask_pool_names {
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --ascii-lines --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --ascii-lines --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
@@ -286,8 +290,8 @@ function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --ascii-lines --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --ascii-lines --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
@@ -302,8 +306,8 @@ function ask_root_password {
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --ascii-lines --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --ascii-lines --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
@@ -313,7 +317,7 @@ function ask_root_password {
function ask_encryption {
print_step_info_header
if dialog --ascii-lines --yesno 'Do you want to encrypt the root pool?' 30 100; then
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
@@ -321,8 +325,8 @@ function ask_encryption {
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --ascii-lines --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --ascii-lines --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
@@ -333,7 +337,7 @@ function ask_encryption {
function ask_zfs_experimental {
print_step_info_header
if dialog --ascii-lines --yesno 'Do you want to use experimental zfs module build?' 30 100; then
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
@@ -344,8 +348,8 @@ function ask_hostname {
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_hostname=$(dialog --ascii-lines --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
@@ -362,7 +366,7 @@ function determine_kernel_variant {
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "$1"
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
@@ -399,8 +403,8 @@ function unmount_and_export_fs {
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
zpool export -a 2> /dev/null
if [[ $? == 0 ]]; then
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
@@ -417,6 +421,7 @@ function unmount_and_export_fs {
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
@@ -457,11 +462,20 @@ for kver in $(find /lib/modules/* -maxdepth 0 -type d | grep -v "$(uname -r)" |
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
apt install --yes -t buster-backports libelf-dev zfs-dkms
modprobe zfs
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
@@ -473,9 +487,9 @@ echo "======= partitioning the disk =========="
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all "$selected_disk"
wipefs --all --force "$selected_disk"
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
sgdisk -n2:0:+512M -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
@@ -502,13 +516,17 @@ echo "======= create zfs pools and datasets =========="
pools_mirror_option=
fi
# shellcheck disable=SC2086
zpool create \
$v_bpool_tweaks -O canmount=off -O devices=off \
-o cachefile=/etc/zpool.cache \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
$v_rpool_tweaks \
-o cachefile=/etc/zpool.cache \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
$v_rpool_name $pools_mirror_option "${rpool_disks_partitions[@]}"
@@ -523,9 +541,7 @@ zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/ubuntu"
zfs mount "$v_bpool_name/BOOT/ubuntu"
zfs create "$v_rpool_name/home"
zfs create -o mountpoint=/root "$v_rpool_name/home/root"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create -o canmount=off "$v_rpool_name/var/lib"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
@@ -576,7 +592,7 @@ ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p')
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > /mnt/etc/systemd/network/10-eth0.network
[Match]
@@ -589,7 +605,9 @@ Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
chroot_execute "systemctl enable systemd-resolved.service"
#cp /etc/resolv.conf $c_zfs_mount_dir/etc/resolv.conf
mkdir -p "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/"
cat > "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg" <<CONF
@@ -599,7 +617,6 @@ CONF
rm -rf $c_zfs_mount_dir/etc/network/interfaces.d/50-cloud-init.cfg
cp /etc/resolv.conf $c_zfs_mount_dir/etc/resolv.conf
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
@@ -618,7 +635,7 @@ CONF
chroot_execute "apt update"
echo "======= setting locale, console and language =========="
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils"
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils keyboard-configuration console-setup"
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
@@ -660,7 +677,6 @@ CONF'
chroot_execute "dpkg-reconfigure locales -f noninteractive"
echo -e "LC_ALL=en_US.UTF-8\nLANG=en_US.UTF-8\n" >> "$c_zfs_mount_dir/etc/environment"
chroot_execute "apt install -qq --yes keyboard-configuration console-setup"
chroot_execute "dpkg-reconfigure keyboard-configuration -f noninteractive"
chroot_execute "dpkg-reconfigure console-setup -f noninteractive"
chroot_execute "setupcon"
@@ -669,32 +685,35 @@ chroot_execute "rm -f /etc/localtime /etc/timezone"
chroot_execute "dpkg-reconfigure tzdata -f noninteractive "
echo "======= installing latest kernel============="
chroot_execute "DEBIAN_FRONTEND=noninteractive apt install --yes linux-headers${v_kernel_variant}-hwe-18.04 linux-image${v_kernel_variant}-hwe-18.04"
chroot_execute "apt install --yes linux-headers${v_kernel_variant}-hwe-18.04 linux-image${v_kernel_variant}-hwe-18.04"
if [[ $v_kernel_variant == "-virtual" ]]; then
# linux-image-extra is only available for virtual hosts
chroot_execute "DEBIAN_FRONTEND=noninteractive apt install --yes linux-image-extra-virtual-hwe-18.04"
chroot_execute "apt install --yes linux-image-extra-virtual-hwe-18.04"
fi
echo "======= installing aux packages =========="
chroot_execute "apt install --yes man wget curl software-properties-common nano htop gnupg"
chroot_execute "systemctl disable thermald"
echo "======= installing zfs packages =========="
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://andrey42.github.io/zfs-ubuntu/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://andrey42.github.io/zfs-ubuntu/public zfs-debian-experimental main'"
chroot_execute "apt update"
else
chroot_execute "add-apt-repository --yes ppa:jonathonf/zfs"
fi
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "add-apt-repository --yes ppa:jonathonf/zfs"
chroot_execute "apt install --yes zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
@@ -709,37 +728,22 @@ chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs services =========="
chroot_execute "cat > /etc/systemd/system/zfs-import-bpool.service <<UNIT
[Unit]
DefaultDependencies=no
Before=zfs-import-scan.service
Before=zfs-import-cache.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c '[ -f /etc/zfs/zpool.cache ] && mv /etc/zfs/zpool.cache /etc/zfs/preboot_zpool.cache || true'
ExecStart=/sbin/zpool import -N -o cachefile=none $v_bpool_name
ExecStartPost=/bin/sh -c '[ -f /etc/zfs/preboot_zpool.cache ] && mv /etc/zfs/preboot_zpool.cache /etc/zfs/zpool.cache || true'
[Install]
WantedBy=zfs-import.target
UNIT"
chroot_execute "systemctl enable zfs-import-bpool.service"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache /mnt/etc/zfs/zpool.cache
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "DEBIAN_FRONTEND=noninteractive apt install --yes grub-pc"
chroot_execute "grub-install ${v_selected_disks[0]}"
chroot_execute "apt install --yes grub-pc"
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install $disk"
done
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=rpool/ROOT/ubuntu\"|g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/ubuntu\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
@@ -767,18 +771,6 @@ if [[ $v_encrypt_rpool == "1" ]]; then
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/dropbear-initramfs/dropbear_dss_host_key"
cd "$c_zfs_mount_dir/root"
wget http://ftp.de.debian.org/debian/pool/main/libt/libtommath/libtommath1_1.1.0-3_amd64.deb
wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-bin_2018.76-5_amd64.deb
wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-initramfs_2018.76-5_all.deb
chroot_execute "dpkg -i /root/libtommath1_1.1.0-3_amd64.deb"
chroot_execute "dpkg -i /root/dropbear-bin_2018.76-5_amd64.deb"
chroot_execute "dpkg -i /root/dropbear-initramfs_2018.76-5_all.deb"
rm $c_zfs_mount_dir/root/*.deb
cd /root
fi
echo "============setup root prompt============"
@@ -791,6 +783,7 @@ CONF
echo "========running packages upgrade==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
echo "===========add static route to initramfs via hook to add default routes due to Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
@@ -814,8 +807,8 @@ esac
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev ens3
ip route add default via 172.31.1.1 dev ens3
ip route add 172.31.1.1/255.255.255.255 dev eth0
ip route add default via 172.31.1.1 dev eth0
CONF
chmod 755 "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route"
@@ -828,11 +821,11 @@ chroot_execute "update-grub"
echo "======= setting up zed =========="
chroot_execute "zfs set canmount=noauto rpool"
chroot_execute "zfs set canmount=noauto $v_rpool_name"
echo "======= setting mountpoints =========="
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/ubuntu"
chroot_execute "echo $v_bpool_name/BOOT/ubuntu /boot zfs nodev,relatime,x-systemd.requires=zfs-import-bpool.service 0 0 > /etc/fstab"
chroot_execute "echo $v_bpool_name/BOOT/ubuntu /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
@@ -844,7 +837,10 @@ chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
[[ $v_swap_size -gt 0 ]] && chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab" || true
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="

View File

@@ -0,0 +1,863 @@
#!/bin/bash
: <<'end_header_info'
(c) Andrey Prokopenko job@terem.fr
fully automatic script to install Ubuntu 20 LTS 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, set it OS to linux64, 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 -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
# Variables
v_bpool_name=
v_bpool_tweaks=
v_rpool_name=
v_rpool_tweaks=
declare -a v_selected_disks
v_swap_size= # integer
v_free_tail_space= # integer
v_hostname=
v_kernel_variant=
v_zfs_arc_max_mb=
v_root_password=
v_encrypt_rpool= # 0=false, 1=true
v_passphrase=
v_zfs_experimental=
v_suitable_disks=()
# Constants
c_deb_packages_repo=http://mirror.hetzner.de/ubuntu/packages
c_deb_security_repo=http://mirror.hetzner.de/ubuntu/security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
c_install_log=$c_log_dir/install.log
c_lsb_release_log=$c_log_dir/lsb_release.log
c_disks_log=$c_log_dir/disks.log
function activate_debug {
mkdir -p "$c_log_dir"
exec 5> "$c_install_log"
BASH_XTRACEFD="5"
set -x
}
# shellcheck disable=SC2120
function print_step_info_header {
echo -n "
###############################################################################
# ${FUNCNAME[1]}"
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
"
}
function print_variables {
for variable_name in "$@"; do
declare -n variable_reference="$variable_name"
echo -n "$variable_name:"
case "$(declare -p "$variable_name")" in
"declare -a"* )
for entry in "${variable_reference[@]}"; do
echo -n " \"$entry\""
done
;;
"declare -A"* )
for key in "${!variable_reference[@]}"; do
echo -n " $key=\"${variable_reference[$key]}\""
done
;;
* )
echo -n " $variable_reference"
;;
esac
echo
done
echo
}
function display_intro_banner {
# shellcheck disable=SC2119
print_step_info_header
local dialog_message='Hello!
This script will prepare the ZFS pools, then install and configure minimal Ubuntu 20 LTS with ZFS root on Hetzner hosting VPS instance
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
# shellcheck disable=SC2119
print_step_info_header
lsb_release --all > "$c_lsb_release_log"
}
function check_prerequisites {
# shellcheck disable=SC2119
print_step_info_header
if [[ $(id -u) -ne 0 ]]; then
echo 'This script must be run with administrative privileges!'
exit 1
fi
if [[ ! -r /root/.ssh/authorized_keys ]]; then
echo "SSH pubkey file is absent, please add it to the rescue system setting, then reboot into rescue system and run the script"
exit 1
fi
if ! dpkg-query --showformat="\${Status}" -W dialog 2> /dev/null | grep -q "install ok installed"; then
apt install --yes dialog
fi
}
function find_suitable_disks {
# shellcheck disable=SC2119
print_step_info_header
udevadm trigger
# shellcheck disable=SC2012
ls -l /dev/disk/by-id | tail -n +2 | perl -lane 'print "@F[8..10]"' > "$c_disks_log"
local candidate_disk_ids
local mounted_devices
candidate_disk_ids=$(find /dev/disk/by-id -regextype awk -regex '.+/(ata|nvme|scsi)-.+' -not -regex '.+-part[0-9]+$' | sort)
mounted_devices="$(df | awk 'BEGIN {getline} {print $1}' | xargs -n 1 lsblk -no pkname 2> /dev/null | sort -u || true)"
while read -r disk_id || [[ -n "$disk_id" ]]; do
local device_info
device_info="$(udevadm info --query=property "$(readlink -f "$disk_id")")"
block_device_basename="$(basename "$(readlink -f "$disk_id")")"
if ! grep -q '^ID_TYPE=cd$' <<< "$device_info"; then
if ! grep -q "^$block_device_basename\$" <<< "$mounted_devices"; then
v_suitable_disks+=("$disk_id")
fi
fi
cat >> "$c_disks_log" << LOG
## DEVICE: $disk_id ################################
$(udevadm info --query=property "$(readlink -f "$disk_id")")
LOG
done < <(echo -n "$candidate_disk_ids")
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
print_variables v_suitable_disks
}
function select_disks {
# shellcheck disable=SC2119
print_step_info_header
while true; do
local menu_entries_option=()
if [[ ${#v_suitable_disks[@]} -eq 1 ]]; then
local disk_selection_status=ON
else
local disk_selection_status=OFF
fi
for disk_id in "${v_suitable_disks[@]}"; do
menu_entries_option+=("$disk_id" "($block_device_basename)" "$disk_selection_status")
done
local dialog_message="Select the ZFS devices (multiple selections will be in mirror).
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
fi
done
print_variables v_selected_disks
}
function ask_swap_size {
# shellcheck disable=SC2119
print_step_info_header
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
print_variables v_swap_size
}
function ask_free_tail_space {
# shellcheck disable=SC2119
print_step_info_header
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
print_variables v_free_tail_space
}
function ask_zfs_arc_max_size {
# shellcheck disable=SC2119
print_step_info_header
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
print_variables v_zfs_arc_max_mb
}
function ask_pool_names {
# shellcheck disable=SC2119
print_step_info_header
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
print_variables v_bpool_name v_rpool_name
}
function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
function ask_root_password {
# shellcheck disable=SC2119
print_step_info_header
set +x
local password_invalid_message=
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
set -x
}
function ask_encryption {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
if [[ $v_encrypt_rpool == "1" ]]; then
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
fi
set -x
}
function ask_zfs_experimental {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
function ask_hostname {
# shellcheck disable=SC2119
print_step_info_header
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
print_variables v_hostname
}
function determine_kernel_variant {
if dmidecode | grep -q vServer; then
v_kernel_variant="-virtual"
else
v_kernel_variant="-generic"
fi
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
# shellcheck disable=SC2119
print_step_info_header
for virtual_fs_dir in dev sys proc; do
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
done
local max_unmount_wait=5
echo -n "Waiting for virtual filesystems to unmount "
SECONDS=0
for virtual_fs_dir in dev sys proc; do
while mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir" && [[ $SECONDS -lt $max_unmount_wait ]]; do
sleep 0.5
echo -n .
done
done
echo
for virtual_fs_dir in dev sys proc; do
if mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir"; then
echo "Re-issuing umount for $c_zfs_mount_dir/$virtual_fs_dir"
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
fi
done
SECONDS=0
zpools_exported=99
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
else
sleep 1
fi
done
set -e
if (( zpools_exported != 1 )); then
echo "failed to export zfs pools"
exit 1
fi
}
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
display_intro_banner
activate_debug
find_suitable_disks
select_disks
ask_swap_size
ask_free_tail_space
ask_pool_names
ask_pool_tweaks
ask_encryption
ask_zfs_arc_max_size
ask_zfs_experimental
ask_root_password
ask_hostname
determine_kernel_variant
clear
echo "===========remove 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
apt purge --yes "linux-headers-$kver"
apt purge --yes "linux-image-$kver"
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
if [[ $v_free_tail_space -eq 0 ]]; then
tail_space_parameter=0
else
tail_space_parameter="-${v_free_tail_space}G"
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all --force "$selected_disk"
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
udevadm settle
echo "======= create zfs pools and datasets =========="
encryption_options=()
rpool_disks_partitions=()
bpool_disks_partitions=()
if [[ $v_encrypt_rpool == "1" ]]; then
encryption_options=(-O "encryption=aes-256-gcm" -O "keylocation=prompt" -O "keyformat=passphrase")
fi
for selected_disk in "${v_selected_disks[@]}"; do
rpool_disks_partitions+=("${selected_disk}-part3")
bpool_disks_partitions+=("${selected_disk}-part2")
done
if [[ ${#v_selected_disks[@]} -gt 1 ]]; then
pools_mirror_option=mirror
else
pools_mirror_option=
fi
# shellcheck disable=SC2086
zpool create \
$v_bpool_tweaks -O canmount=off -O devices=off \
-o cachefile=/etc/zpool.cache \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
$v_rpool_tweaks \
-o cachefile=/etc/zpool.cache \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
$v_rpool_name $pools_mirror_option "${rpool_disks_partitions[@]}"
zfs create -o canmount=off -o mountpoint=none "$v_rpool_name/ROOT"
zfs create -o canmount=off -o mountpoint=none "$v_bpool_name/BOOT"
zfs create -o canmount=noauto -o mountpoint=/ "$v_rpool_name/ROOT/ubuntu"
zfs mount "$v_rpool_name/ROOT/ubuntu"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/ubuntu"
zfs mount "$v_bpool_name/BOOT/ubuntu"
zfs create "$v_rpool_name/home"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/cache"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/tmp"
chmod 1777 "$c_zfs_mount_dir/var/tmp"
zfs create "$v_rpool_name/srv"
zfs create -o canmount=off "$v_rpool_name/usr"
zfs create "$v_rpool_name/usr/local"
zfs create "$v_rpool_name/var/mail"
zfs create -o com.sun:auto-snapshot=false -o canmount=on -o mountpoint=/tmp "$v_rpool_name/tmp"
chmod 1777 "$c_zfs_mount_dir/tmp"
if [[ $v_swap_size -gt 0 ]]; then
zfs create \
-V "${v_swap_size}G" -b "$(getconf PAGESIZE)" \
-o compression=zle -o logbias=throughput -o sync=always -o primarycache=metadata -o secondarycache=none -o com.sun:auto-snapshot=false \
"$v_rpool_name/swap"
udevadm settle
mkswap -f "/dev/zvol/$v_rpool_name/swap"
fi
echo "======= setting up initial system packages =========="
debootstrap --arch=amd64 focal "$c_zfs_mount_dir" "$c_deb_packages_repo"
zfs set devices=off "$v_rpool_name"
echo "======= setting up the network =========="
echo "$v_hostname" > $c_zfs_mount_dir/etc/hostname
cat > "$c_zfs_mount_dir/etc/hosts" <<CONF
127.0.1.1 ${v_hostname}
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > /mnt/etc/systemd/network/10-eth0.network
[Match]
Name=eth0
[Network]
DHCP=ipv4
Address=${ip6addr_prefix}:1/64
Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
chroot_execute "systemctl enable systemd-resolved.service"
mkdir -p "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/"
cat > "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg" <<CONF
network:
config: disabled
CONF
rm -rf $c_zfs_mount_dir/etc/network/interfaces.d/50-cloud-init.cfg
#cp /etc/resolv.conf $c_zfs_mount_dir/etc/resolv.conf
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
mount --rbind "/$virtual_fs_dir" "$c_zfs_mount_dir/$virtual_fs_dir"
done
echo "======= setting apt repos =========="
cat > "$c_zfs_mount_dir/etc/apt/sources.list" <<CONF
deb [arch=i386,amd64] $c_deb_packages_repo focal main restricted
deb [arch=i386,amd64] $c_deb_packages_repo focal-updates main restricted
deb [arch=i386,amd64] $c_deb_packages_repo focal-backports main restricted
deb [arch=i386,amd64] $c_deb_packages_repo focal universe
deb [arch=i386,amd64] $c_deb_security_repo focal-security main restricted
CONF
chroot_execute "apt update"
echo "======= setting locale, console and language =========="
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils keyboard-configuration console-setup"
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_AT.UTF-8/de_AT.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_DE.UTF-8/de_DE.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
chroot_execute 'cat <<CONF | debconf-set-selections
locales locales/default_environment_locale select en_US.UTF-8
keyboard-configuration keyboard-configuration/store_defaults_in_debconf_db boolean true
keyboard-configuration keyboard-configuration/variant select German
keyboard-configuration keyboard-configuration/unsupported_layout boolean true
keyboard-configuration keyboard-configuration/modelcode string pc105
keyboard-configuration keyboard-configuration/unsupported_config_layout boolean true
keyboard-configuration keyboard-configuration/layout select German
keyboard-configuration keyboard-configuration/layoutcode string de
keyboard-configuration keyboard-configuration/optionscode string
keyboard-configuration keyboard-configuration/toggle select No toggling
keyboard-configuration keyboard-configuration/xkb-keymap select de
keyboard-configuration keyboard-configuration/switch select No temporary switch
keyboard-configuration keyboard-configuration/unsupported_config_options boolean true
keyboard-configuration keyboard-configuration/ctrl_alt_bksp boolean false
keyboard-configuration keyboard-configuration/variantcode string
keyboard-configuration keyboard-configuration/model select Generic 105-key PC (intl.)
keyboard-configuration keyboard-configuration/altgr select The default for the keyboard layout
keyboard-configuration keyboard-configuration/compose select No compose key
keyboard-configuration keyboard-configuration/unsupported_options boolean true
console-setup console-setup/fontsize-fb47 select 8x16
console-setup console-setup/store_defaults_in_debconf_db boolean true
console-setup console-setup/codeset47 select # Latin1 and Latin5 - western Europe and Turkic languages
console-setup console-setup/fontface47 select Fixed
console-setup console-setup/fontsize string 8x16
console-setup console-setup/charmap47 select UTF-8
console-setup console-setup/fontsize-text47 select 8x16
console-setup console-setup/codesetcode string Lat15
tzdata tzdata/Areas select Europe
tzdata tzdata/Zones/Europe select Vienna
grub-pc grub-pc/install_devices_empty boolean true
CONF'
chroot_execute "dpkg-reconfigure locales -f noninteractive"
echo -e "LC_ALL=en_US.UTF-8\nLANG=en_US.UTF-8\n" >> "$c_zfs_mount_dir/etc/environment"
chroot_execute "dpkg-reconfigure keyboard-configuration -f noninteractive"
chroot_execute "dpkg-reconfigure console-setup -f noninteractive"
chroot_execute "setupcon"
chroot_execute "rm -f /etc/localtime /etc/timezone"
chroot_execute "dpkg-reconfigure tzdata -f noninteractive "
echo "======= installing latest kernel============="
chroot_execute "apt install --yes linux-headers${v_kernel_variant} linux-image${v_kernel_variant}"
if [[ $v_kernel_variant == "-virtual" ]]; then
# linux-image-extra is only available for virtual hosts
chroot_execute "apt install --yes linux-image-extra-virtual"
fi
echo "======= installing aux packages =========="
chroot_execute "apt install --yes man-db wget curl software-properties-common nano htop gnupg"
chroot_execute "systemctl disable thermald"
echo "======= installing zfs packages =========="
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "add-apt-repository --yes ppa:jonathonf/zfs"
chroot_execute "apt install --yes zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
echo "======= setup OpenSSH =========="
mkdir -p "$c_zfs_mount_dir/root/.ssh/"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/root/.ssh/authorized_keys"
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
chroot_execute "rm /etc/ssh/ssh_host_*"
chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache /mnt/etc/zfs/zpool.cache
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "apt install --yes grub-pc"
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install $disk"
done
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/ubuntu\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
chroot_execute "echo 'GRUB_DISABLE_OS_PROBER=true' >> /etc/default/grub"
for ((i = 1; i < ${#v_selected_disks[@]}; i++)); do
dd if="${v_selected_disks[0]}-part1" of="${v_selected_disks[i]}-part1"
done
if [[ $v_encrypt_rpool == "1" ]]; then
echo "=========set up dropbear=============="
chroot_execute "apt install --yes dropbear-initramfs"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/etc/dropbear-initramfs/authorized_keys"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_rsa_key_temp /etc/dropbear-initramfs/dropbear_rsa_host_key"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_ecdsa_key_temp /etc/dropbear-initramfs/dropbear_ecdsa_host_key"
chroot_execute "rm -rf /etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/dropbear-initramfs/dropbear_dss_host_key"
cd "$c_zfs_mount_dir/root"
wget http://ftp.de.debian.org/debian/pool/main/libt/libtommath/libtommath1_1.2.0-6_amd64.deb
wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-bin_2020.81-3_amd64.deb
wget http://ftp.de.debian.org/debian/pool/main/d/dropbear/dropbear-initramfs_2020.81-3_all.deb
chroot_execute "dpkg -i /root/libtommath1_1.2.0-6_amd64.deb"
chroot_execute "dpkg -i /root/dropbear-bin_2020.81-3_amd64.deb"
chroot_execute "dpkg -i /root/dropbear-initramfs_2020.81-3_all.deb"
rm $c_zfs_mount_dir/root/*.deb
cd /root
fi
echo "============setup root prompt============"
cat > "$c_zfs_mount_dir/root/.bashrc" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
echo "========running packages upgrade==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
echo "===========add static route to initramfs via hook to add default routes due to Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
cat > "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route" <<'CONF'
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
# Begin real processing below this line
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev ens3
ip route add default via 172.31.1.1 dev ens3
CONF
chmod 755 "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route"
echo "======= update initramfs =========="
chroot_execute "update-initramfs -u -k all"
echo "======= update grub =========="
chroot_execute "update-grub"
echo "======= setting up zed =========="
chroot_execute "zfs set canmount=noauto $v_rpool_name"
echo "======= setting mountpoints =========="
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/ubuntu"
chroot_execute "echo $v_bpool_name/BOOT/ubuntu /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/spool"
chroot_execute "echo $v_rpool_name/var/spool /var/spool zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/tmp"
chroot_execute "echo $v_rpool_name/var/tmp /var/tmp zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="
unmount_and_export_fs
echo "======== setup complete, rebooting ==============="
reboot

View File

@@ -0,0 +1,852 @@
#!/bin/bash
: <<'end_header_info'
(c) Andrey Prokopenko job@terem.fr
fully automatic script to install Ubuntu 20 LTS 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, set it OS to linux64, 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 -o errexit
set -o pipefail
set -o nounset
export TMPDIR=/tmp
# Variables
v_bpool_name=
v_bpool_tweaks=
v_rpool_name=
v_rpool_tweaks=
declare -a v_selected_disks
v_swap_size= # integer
v_free_tail_space= # integer
v_hostname=
v_kernel_variant=
v_zfs_arc_max_mb=
v_root_password=
v_encrypt_rpool= # 0=false, 1=true
v_passphrase=
v_zfs_experimental=
v_suitable_disks=()
# Constants
c_deb_packages_repo=http://mirror.hetzner.de/ubuntu/packages
c_deb_security_repo=http://mirror.hetzner.de/ubuntu/security
c_default_zfs_arc_max_mb=250
c_default_bpool_tweaks="-o ashift=12 -O compression=lz4"
c_default_rpool_tweaks="-o ashift=12 -O acltype=posixacl -O compression=zstd-9 -O dnodesize=auto -O relatime=on -O xattr=sa -O normalization=formD"
c_default_hostname=terem
c_zfs_mount_dir=/mnt
c_log_dir=$(dirname "$(mktemp)")/zfs-hetzner-vm
c_install_log=$c_log_dir/install.log
c_lsb_release_log=$c_log_dir/lsb_release.log
c_disks_log=$c_log_dir/disks.log
function activate_debug {
mkdir -p "$c_log_dir"
exec 5> "$c_install_log"
BASH_XTRACEFD="5"
set -x
}
# shellcheck disable=SC2120
function print_step_info_header {
echo -n "
###############################################################################
# ${FUNCNAME[1]}"
if [[ "${1:-}" != "" ]]; then
echo -n " $1"
fi
echo "
###############################################################################
"
}
function print_variables {
for variable_name in "$@"; do
declare -n variable_reference="$variable_name"
echo -n "$variable_name:"
case "$(declare -p "$variable_name")" in
"declare -a"* )
for entry in "${variable_reference[@]}"; do
echo -n " \"$entry\""
done
;;
"declare -A"* )
for key in "${!variable_reference[@]}"; do
echo -n " $key=\"${variable_reference[$key]}\""
done
;;
* )
echo -n " $variable_reference"
;;
esac
echo
done
echo
}
function display_intro_banner {
# shellcheck disable=SC2119
print_step_info_header
local dialog_message='Hello!
This script will prepare the ZFS pools, then install and configure minimal Ubuntu 20 LTS with ZFS root on Hetzner hosting VPS instance
The script with minimal changes may be used on any other hosting provider supporting KVM virtualization and offering Debian-based rescue system.
In order to stop the procedure, hit Esc twice during dialogs (excluding yes/no ones), or Ctrl+C while any operation is running.
'
dialog --msgbox "$dialog_message" 30 100
}
function store_os_distro_information {
# shellcheck disable=SC2119
print_step_info_header
lsb_release --all > "$c_lsb_release_log"
}
function check_prerequisites {
# shellcheck disable=SC2119
print_step_info_header
if [[ $(id -u) -ne 0 ]]; then
echo 'This script must be run with administrative privileges!'
exit 1
fi
if [[ ! -r /root/.ssh/authorized_keys ]]; then
echo "SSH pubkey file is absent, please add it to the rescue system setting, then reboot into rescue system and run the script"
exit 1
fi
if ! dpkg-query --showformat="\${Status}" -W dialog 2> /dev/null | grep -q "install ok installed"; then
apt install --yes dialog
fi
}
function find_suitable_disks {
# shellcheck disable=SC2119
print_step_info_header
udevadm trigger
# shellcheck disable=SC2012
ls -l /dev/disk/by-id | tail -n +2 | perl -lane 'print "@F[8..10]"' > "$c_disks_log"
local candidate_disk_ids
local mounted_devices
candidate_disk_ids=$(find /dev/disk/by-id -regextype awk -regex '.+/(ata|nvme|scsi)-.+' -not -regex '.+-part[0-9]+$' | sort)
mounted_devices="$(df | awk 'BEGIN {getline} {print $1}' | xargs -n 1 lsblk -no pkname 2> /dev/null | sort -u || true)"
while read -r disk_id || [[ -n "$disk_id" ]]; do
local device_info
device_info="$(udevadm info --query=property "$(readlink -f "$disk_id")")"
block_device_basename="$(basename "$(readlink -f "$disk_id")")"
if ! grep -q '^ID_TYPE=cd$' <<< "$device_info"; then
if ! grep -q "^$block_device_basename\$" <<< "$mounted_devices"; then
v_suitable_disks+=("$disk_id")
fi
fi
cat >> "$c_disks_log" << LOG
## DEVICE: $disk_id ################################
$(udevadm info --query=property "$(readlink -f "$disk_id")")
LOG
done < <(echo -n "$candidate_disk_ids")
if [[ ${#v_suitable_disks[@]} -eq 0 ]]; then
local dialog_message='No suitable disks have been found!
If you think this is a bug, please open an issue on https://github.com/terem42/zfs-hetzner-vm/issues, and attach the file `'"$c_disks_log"'`.
'
dialog --msgbox "$dialog_message" 30 100
exit 1
fi
print_variables v_suitable_disks
}
function select_disks {
# shellcheck disable=SC2119
print_step_info_header
while true; do
local menu_entries_option=()
if [[ ${#v_suitable_disks[@]} -eq 1 ]]; then
local disk_selection_status=ON
else
local disk_selection_status=OFF
fi
for disk_id in "${v_suitable_disks[@]}"; do
menu_entries_option+=("$disk_id" "($block_device_basename)" "$disk_selection_status")
done
local dialog_message="Select the ZFS devices (multiple selections will be in mirror).
Devices with mounted partitions, cdroms, and removable devices are not displayed!
"
mapfile -t v_selected_disks < <(dialog --separate-output --checklist "$dialog_message" 30 100 $((${#menu_entries_option[@]} / 3)) "${menu_entries_option[@]}" 3>&1 1>&2 2>&3)
if [[ ${#v_selected_disks[@]} -gt 0 ]]; then
break
fi
done
print_variables v_selected_disks
}
function ask_swap_size {
# shellcheck disable=SC2119
print_step_info_header
local swap_size_invalid_message=
while [[ ! $v_swap_size =~ ^[0-9]+$ ]]; do
v_swap_size=$(dialog --inputbox "${swap_size_invalid_message}Enter the swap size in GiB (0 for no swap):" 30 100 2 3>&1 1>&2 2>&3)
swap_size_invalid_message="Invalid swap size! "
done
print_variables v_swap_size
}
function ask_free_tail_space {
# shellcheck disable=SC2119
print_step_info_header
local tail_space_invalid_message=
while [[ ! $v_free_tail_space =~ ^[0-9]+$ ]]; do
v_free_tail_space=$(dialog --inputbox "${tail_space_invalid_message}Enter the space to leave at the end of each disk (0 for none):" 30 100 0 3>&1 1>&2 2>&3)
tail_space_invalid_message="Invalid size! "
done
print_variables v_free_tail_space
}
function ask_zfs_arc_max_size {
# shellcheck disable=SC2119
print_step_info_header
local zfs_arc_max_invalid_message=
while [[ ! $v_zfs_arc_max_mb =~ ^[0-9]+$ ]]; do
v_zfs_arc_max_mb=$(dialog --inputbox "${zfs_arc_max_invalid_message}Enter ZFS ARC cache max size in Mb (minimum 64Mb, enter 0 for ZFS default value, the default will take up to 50% of memory):" 30 100 "$c_default_zfs_arc_max_mb" 3>&1 1>&2 2>&3)
zfs_arc_max_invalid_message="Invalid size! "
done
print_variables v_zfs_arc_max_mb
}
function ask_pool_names {
# shellcheck disable=SC2119
print_step_info_header
local bpool_name_invalid_message=
while [[ ! $v_bpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_bpool_name=$(dialog --inputbox "${bpool_name_invalid_message}Insert the name for the boot pool" 30 100 bpool 3>&1 1>&2 2>&3)
bpool_name_invalid_message="Invalid pool name! "
done
local rpool_name_invalid_message=
while [[ ! $v_rpool_name =~ ^[a-z][a-zA-Z_:.-]+$ ]]; do
v_rpool_name=$(dialog --inputbox "${rpool_name_invalid_message}Insert the name for the root pool" 30 100 rpool 3>&1 1>&2 2>&3)
rpool_name_invalid_message="Invalid pool name! "
done
print_variables v_bpool_name v_rpool_name
}
function ask_pool_tweaks {
# shellcheck disable=SC2119
print_step_info_header
v_bpool_tweaks=$(dialog --inputbox "Insert the tweaks for the boot pool" 30 100 -- "$c_default_bpool_tweaks" 3>&1 1>&2 2>&3)
v_rpool_tweaks=$(dialog --inputbox "Insert the tweaks for the root pool" 30 100 -- "$c_default_rpool_tweaks" 3>&1 1>&2 2>&3)
print_variables v_bpool_tweaks v_rpool_tweaks
}
function ask_root_password {
# shellcheck disable=SC2119
print_step_info_header
set +x
local password_invalid_message=
local password_repeat=-
while [[ "$v_root_password" != "$password_repeat" || "$v_root_password" == "" ]]; do
v_root_password=$(dialog --passwordbox "${password_invalid_message}Please enter the root account password (can't be empty):" 30 100 3>&1 1>&2 2>&3)
password_repeat=$(dialog --passwordbox "Please repeat the password:" 30 100 3>&1 1>&2 2>&3)
password_invalid_message="Passphrase empty, or not matching! "
done
set -x
}
function ask_encryption {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to encrypt the root pool?' 30 100; then
v_encrypt_rpool=1
fi
set +x
if [[ $v_encrypt_rpool == "1" ]]; then
local passphrase_invalid_message=
local passphrase_repeat=-
while [[ "$v_passphrase" != "$passphrase_repeat" || ${#v_passphrase} -lt 8 ]]; do
v_passphrase=$(dialog --passwordbox "${passphrase_invalid_message}Please enter the passphrase for the root pool (8 chars min.):" 30 100 3>&1 1>&2 2>&3)
passphrase_repeat=$(dialog --passwordbox "Please repeat the passphrase:" 30 100 3>&1 1>&2 2>&3)
passphrase_invalid_message="Passphrase too short, or not matching! "
done
fi
set -x
}
function ask_zfs_experimental {
print_step_info_header
if dialog --defaultno --yesno 'Do you want to use experimental zfs module build?' 30 100; then
v_zfs_experimental=1
fi
}
function ask_hostname {
# shellcheck disable=SC2119
print_step_info_header
local hostname_invalid_message=
while [[ ! $v_hostname =~ ^[a-z][a-zA-Z0-9_:.-]+$ ]]; do
v_hostname=$(dialog --inputbox "${hostname_invalid_message}Set the host name" 30 100 "$c_default_hostname" 3>&1 1>&2 2>&3)
hostname_invalid_message="Invalid host name! "
done
print_variables v_hostname
}
function determine_kernel_variant {
if dmidecode | grep -q vServer; then
v_kernel_variant="-virtual"
else
v_kernel_variant="-generic"
fi
}
function chroot_execute {
chroot $c_zfs_mount_dir bash -c "DEBIAN_FRONTEND=noninteractive $1"
}
function unmount_and_export_fs {
# shellcheck disable=SC2119
print_step_info_header
for virtual_fs_dir in dev sys proc; do
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
done
local max_unmount_wait=5
echo -n "Waiting for virtual filesystems to unmount "
SECONDS=0
for virtual_fs_dir in dev sys proc; do
while mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir" && [[ $SECONDS -lt $max_unmount_wait ]]; do
sleep 0.5
echo -n .
done
done
echo
for virtual_fs_dir in dev sys proc; do
if mountpoint -q "$c_zfs_mount_dir/$virtual_fs_dir"; then
echo "Re-issuing umount for $c_zfs_mount_dir/$virtual_fs_dir"
umount --recursive --force --lazy "$c_zfs_mount_dir/$virtual_fs_dir"
fi
done
SECONDS=0
zpools_exported=99
echo "===========exporting zfs pools============="
set +e
while (( zpools_exported == 99 )) && (( SECONDS++ <= 60 )); do
if zpool export -a 2> /dev/null; then
zpools_exported=1
echo "all zfs pools were succesfully exported"
break;
else
sleep 1
fi
done
set -e
if (( zpools_exported != 1 )); then
echo "failed to export zfs pools"
exit 1
fi
}
#################### MAIN ################################
export LC_ALL=en_US.UTF-8
export NCURSES_NO_UTF8_ACS=1
check_prerequisites
display_intro_banner
activate_debug
find_suitable_disks
select_disks
ask_swap_size
ask_free_tail_space
ask_pool_names
ask_pool_tweaks
ask_encryption
ask_zfs_arc_max_size
ask_zfs_experimental
ask_root_password
ask_hostname
determine_kernel_variant
clear
echo "===========remove 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
apt purge --yes "linux-headers-$kver"
apt purge --yes "linux-image-$kver"
done
echo "======= installing zfs on rescue system =========="
echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
# echo "y" | zfs
# linux-headers-generic linux-image-generic
apt install --yes software-properties-common dpkg-dev dkms
rm -f "$(which zfs)"
rm -f "$(which zpool)"
echo -e "deb http://deb.debian.org/debian/ testing main contrib non-free\ndeb http://deb.debian.org/debian/ testing main contrib non-free\n" >/etc/apt/sources.list.d/bookworm-testing.list
echo -e "Package: src:zfs-linux\nPin: release n=testing\nPin-Priority: 990\n" > /etc/apt/preferences.d/90_zfs
apt update
apt install -t testing --yes zfs-dkms zfsutils-linux
rm /etc/apt/sources.list.d/bookworm-testing.list
rm /etc/apt/preferences.d/90_zfs
apt update
export PATH=$PATH:/usr/sbin
zfs --version
echo "======= partitioning the disk =========="
if [[ $v_free_tail_space -eq 0 ]]; then
tail_space_parameter=0
else
tail_space_parameter="-${v_free_tail_space}G"
fi
for selected_disk in "${v_selected_disks[@]}"; do
wipefs --all --force "$selected_disk"
sgdisk -a1 -n1:24K:+1000K -t1:EF02 "$selected_disk"
sgdisk -n2:0:+2G -t2:BF01 "$selected_disk" # Boot pool
sgdisk -n3:0:"$tail_space_parameter" -t3:BF01 "$selected_disk" # Root pool
done
udevadm settle
echo "======= create zfs pools and datasets =========="
encryption_options=()
rpool_disks_partitions=()
bpool_disks_partitions=()
if [[ $v_encrypt_rpool == "1" ]]; then
encryption_options=(-O "encryption=aes-256-gcm" -O "keylocation=prompt" -O "keyformat=passphrase")
fi
for selected_disk in "${v_selected_disks[@]}"; do
rpool_disks_partitions+=("${selected_disk}-part3")
bpool_disks_partitions+=("${selected_disk}-part2")
done
if [[ ${#v_selected_disks[@]} -gt 1 ]]; then
pools_mirror_option=mirror
else
pools_mirror_option=
fi
# shellcheck disable=SC2086
zpool create \
$v_bpool_tweaks -O canmount=off -O devices=off \
-o cachefile=/etc/zpool.cache \
-O mountpoint=/boot -R $c_zfs_mount_dir -f \
$v_bpool_name $pools_mirror_option "${bpool_disks_partitions[@]}"
# shellcheck disable=SC2086
echo -n "$v_passphrase" | zpool create \
$v_rpool_tweaks \
-o cachefile=/etc/zpool.cache \
"${encryption_options[@]}" \
-O mountpoint=/ -R $c_zfs_mount_dir -f \
$v_rpool_name $pools_mirror_option "${rpool_disks_partitions[@]}"
zfs create -o canmount=off -o mountpoint=none "$v_rpool_name/ROOT"
zfs create -o canmount=off -o mountpoint=none "$v_bpool_name/BOOT"
zfs create -o canmount=noauto -o mountpoint=/ "$v_rpool_name/ROOT/ubuntu"
zfs mount "$v_rpool_name/ROOT/ubuntu"
zfs create -o canmount=noauto -o mountpoint=/boot "$v_bpool_name/BOOT/ubuntu"
zfs mount "$v_bpool_name/BOOT/ubuntu"
zfs create "$v_rpool_name/home"
#zfs create -o mountpoint=/root "$v_rpool_name/home/root"
zfs create -o canmount=off "$v_rpool_name/var"
zfs create "$v_rpool_name/var/log"
zfs create "$v_rpool_name/var/spool"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/cache"
zfs create -o com.sun:auto-snapshot=false "$v_rpool_name/var/tmp"
chmod 1777 "$c_zfs_mount_dir/var/tmp"
zfs create "$v_rpool_name/srv"
zfs create -o canmount=off "$v_rpool_name/usr"
zfs create "$v_rpool_name/usr/local"
zfs create "$v_rpool_name/var/mail"
zfs create -o com.sun:auto-snapshot=false -o canmount=on -o mountpoint=/tmp "$v_rpool_name/tmp"
chmod 1777 "$c_zfs_mount_dir/tmp"
if [[ $v_swap_size -gt 0 ]]; then
zfs create \
-V "${v_swap_size}G" -b "$(getconf PAGESIZE)" \
-o compression=zle -o logbias=throughput -o sync=always -o primarycache=metadata -o secondarycache=none -o com.sun:auto-snapshot=false \
"$v_rpool_name/swap"
udevadm settle
mkswap -f "/dev/zvol/$v_rpool_name/swap"
fi
echo "======= setting up initial system packages =========="
debootstrap --arch=amd64 jammy "$c_zfs_mount_dir" "$c_deb_packages_repo"
zfs set devices=off "$v_rpool_name"
echo "======= setting up the network =========="
echo "$v_hostname" > $c_zfs_mount_dir/etc/hostname
cat > "$c_zfs_mount_dir/etc/hosts" <<CONF
127.0.1.1 ${v_hostname}
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
CONF
ip6addr_prefix=$(ip -6 a s | grep -E "inet6.+global" | sed -nE 's/.+inet6\s(([0-9a-z]{1,4}:){4,4}).+/\1/p' | head -n 1)
cat <<CONF > /mnt/etc/systemd/network/10-eth0.network
[Match]
Name=eth0
[Network]
DHCP=ipv4
Address=${ip6addr_prefix}:1/64
Gateway=fe80::1
CONF
chroot_execute "systemctl enable systemd-networkd.service"
chroot_execute "systemctl enable systemd-resolved.service"
mkdir -p "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/"
cat > "$c_zfs_mount_dir/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg" <<CONF
network:
config: disabled
CONF
rm -rf $c_zfs_mount_dir/etc/network/interfaces.d/50-cloud-init.cfg
echo "======= preparing the jail for chroot =========="
for virtual_fs_dir in proc sys dev; do
mount --rbind "/$virtual_fs_dir" "$c_zfs_mount_dir/$virtual_fs_dir"
done
echo "======= setting apt repos =========="
cat > "$c_zfs_mount_dir/etc/apt/sources.list" <<CONF
deb [arch=i386,amd64] $c_deb_packages_repo jammy main restricted
deb [arch=i386,amd64] $c_deb_packages_repo jammy-updates main restricted
deb [arch=i386,amd64] $c_deb_packages_repo jammy-backports main restricted
deb [arch=i386,amd64] $c_deb_packages_repo jammy universe
deb [arch=i386,amd64] $c_deb_security_repo jammy-security main restricted
CONF
chroot_execute "apt update"
echo "======= setting locale, console and language =========="
chroot_execute "apt --yes --fix-broken install"
chroot_execute "apt install --yes -qq locales debconf-i18n apt-utils keyboard-configuration console-setup"
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# fr_FR.UTF-8/fr_FR.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_AT.UTF-8/de_AT.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
sed -i 's/# de_DE.UTF-8/de_DE.UTF-8/' "$c_zfs_mount_dir/etc/locale.gen"
chroot_execute 'cat <<CONF | debconf-set-selections
locales locales/default_environment_locale select en_US.UTF-8
keyboard-configuration keyboard-configuration/store_defaults_in_debconf_db boolean true
keyboard-configuration keyboard-configuration/variant select German
keyboard-configuration keyboard-configuration/unsupported_layout boolean true
keyboard-configuration keyboard-configuration/modelcode string pc105
keyboard-configuration keyboard-configuration/unsupported_config_layout boolean true
keyboard-configuration keyboard-configuration/layout select German
keyboard-configuration keyboard-configuration/layoutcode string de
keyboard-configuration keyboard-configuration/optionscode string
keyboard-configuration keyboard-configuration/toggle select No toggling
keyboard-configuration keyboard-configuration/xkb-keymap select de
keyboard-configuration keyboard-configuration/switch select No temporary switch
keyboard-configuration keyboard-configuration/unsupported_config_options boolean true
keyboard-configuration keyboard-configuration/ctrl_alt_bksp boolean false
keyboard-configuration keyboard-configuration/variantcode string
keyboard-configuration keyboard-configuration/model select Generic 105-key PC (intl.)
keyboard-configuration keyboard-configuration/altgr select The default for the keyboard layout
keyboard-configuration keyboard-configuration/compose select No compose key
keyboard-configuration keyboard-configuration/unsupported_options boolean true
console-setup console-setup/fontsize-fb47 select 8x16
console-setup console-setup/store_defaults_in_debconf_db boolean true
console-setup console-setup/codeset47 select # Latin1 and Latin5 - western Europe and Turkic languages
console-setup console-setup/fontface47 select Fixed
console-setup console-setup/fontsize string 8x16
console-setup console-setup/charmap47 select UTF-8
console-setup console-setup/fontsize-text47 select 8x16
console-setup console-setup/codesetcode string Lat15
tzdata tzdata/Areas select Europe
tzdata tzdata/Zones/Europe select Vienna
grub-pc grub-pc/install_devices_empty boolean true
CONF'
chroot_execute "dpkg-reconfigure locales -f noninteractive"
echo -e "LC_ALL=en_US.UTF-8\nLANG=en_US.UTF-8\n" >> "$c_zfs_mount_dir/etc/environment"
chroot_execute "dpkg-reconfigure keyboard-configuration -f noninteractive"
chroot_execute "dpkg-reconfigure console-setup -f noninteractive"
chroot_execute "setupcon"
chroot_execute "rm -f /etc/localtime /etc/timezone"
chroot_execute "dpkg-reconfigure tzdata -f noninteractive "
echo "======= installing latest kernel============="
chroot_execute "apt install --yes linux-headers${v_kernel_variant} linux-image${v_kernel_variant}"
if [[ $v_kernel_variant == "-virtual" ]]; then
# linux-image-extra is only available for virtual hosts
chroot_execute "apt install --yes linux-image-extra-virtual"
fi
echo "======= installing aux packages =========="
chroot_execute "apt install --yes man-db wget curl software-properties-common nano htop gnupg"
chroot_execute "systemctl disable thermald"
echo "======= installing zfs packages =========="
chroot_execute 'echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections'
if [[ $v_zfs_experimental == "1" ]]; then
chroot_execute "wget -O - https://terem42.github.io/zfs-debian/apt_pub.gpg | apt-key add -"
chroot_execute "add-apt-repository 'deb https://terem42.github.io/zfs-debian/public zfs-debian-experimental main'"
chroot_execute "apt update"
chroot_execute "apt install -t zfs-debian-experimental --yes zfs-initramfs zfs-dkms zfsutils-linux"
else
chroot_execute "add-apt-repository --yes ppa:jonathonf/zfs"
chroot_execute "apt install --yes zfs-initramfs zfs-dkms zfsutils-linux"
fi
chroot_execute 'cat << DKMS > /etc/dkms/zfs.conf
# override for /usr/src/zfs-*/dkms.conf:
# always rebuild initrd when zfs module has been changed
# (either by a ZFS update or a new kernel version)
REMAKE_INITRD="yes"
DKMS'
echo "======= installing OpenSSH and network tooling =========="
chroot_execute "apt install --yes openssh-server net-tools"
echo "======= setup OpenSSH =========="
mkdir -p "$c_zfs_mount_dir/root/.ssh/"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/root/.ssh/authorized_keys"
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$c_zfs_mount_dir/etc/ssh/sshd_config"
chroot_execute "rm /etc/ssh/ssh_host_*"
chroot_execute "dpkg-reconfigure openssh-server -f noninteractive"
echo "======= set root password =========="
chroot_execute "echo root:$(printf "%q" "$v_root_password") | chpasswd"
echo "======= setting up zfs cache =========="
cp /etc/zpool.cache /mnt/etc/zfs/zpool.cache
echo "========setting up zfs module parameters========"
chroot_execute "echo options zfs zfs_arc_max=$((v_zfs_arc_max_mb * 1024 * 1024)) >> /etc/modprobe.d/zfs.conf"
echo "======= setting up grub =========="
chroot_execute "echo 'grub-pc grub-pc/install_devices_empty boolean true' | debconf-set-selections"
chroot_execute "apt install --yes grub-pc"
for disk in ${v_selected_disks[@]}; do
chroot_execute "grub-install $disk"
done
chroot_execute "sed -i 's/#GRUB_TERMINAL=console/GRUB_TERMINAL=console/g' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"net.ifnames=0\"|' /etc/default/grub"
chroot_execute "sed -i 's|GRUB_CMDLINE_LINUX=\"\"|GRUB_CMDLINE_LINUX=\"root=ZFS=$v_rpool_name/ROOT/ubuntu\"|g' /etc/default/grub"
chroot_execute "sed -i 's/quiet//g' /etc/default/grub"
chroot_execute "sed -i 's/splash//g' /etc/default/grub"
chroot_execute "echo 'GRUB_DISABLE_OS_PROBER=true' >> /etc/default/grub"
for ((i = 1; i < ${#v_selected_disks[@]}; i++)); do
dd if="${v_selected_disks[0]}-part1" of="${v_selected_disks[i]}-part1"
done
if [[ $v_encrypt_rpool == "1" ]]; then
echo "=========set up dropbear=============="
chroot_execute "apt install --yes dropbear-initramfs"
mkdir -p "$c_zfs_mount_dir/etc/dropbear/initramfs"
cp /root/.ssh/authorized_keys "$c_zfs_mount_dir/etc/dropbear/initramfs/authorized_keys"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_rsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_rsa_key_temp /etc/dropbear/initramfs/dropbear_rsa_host_key"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_rsa_key_temp"
cp "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key" "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "ssh-keygen -p -i -m pem -N '' -f /etc/ssh/ssh_host_ecdsa_key_temp"
chroot_execute "/usr/lib/dropbear/dropbearconvert openssh dropbear /etc/ssh/ssh_host_ecdsa_key_temp /etc/dropbear/initramfs/dropbear_ecdsa_host_key"
chroot_execute "rm -rf /etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/ssh/ssh_host_ecdsa_key_temp"
rm -rf "$c_zfs_mount_dir/etc/dropbear/initramfs/dropbear_dss_host_key"
fi
echo "============setup root prompt============"
cat > "$c_zfs_mount_dir/root/.bashrc" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
echo "========running packages upgrade==========="
chroot_execute "apt upgrade --yes"
chroot_execute "apt purge cryptsetup* --yes"
echo "===========add static route to initramfs via hook to add default routes due to Ubuntu initramfs DHCP bug ========="
mkdir -p "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount"
cat > "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route" <<'CONF'
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
# Begin real processing below this line
configure_networking
ip route add 172.31.1.1/255.255.255.255 dev ens3
ip route add default via 172.31.1.1 dev ens3
CONF
chmod 755 "$c_zfs_mount_dir/usr/share/initramfs-tools/scripts/init-premount/static-route"
echo "======= update initramfs =========="
chroot_execute "update-initramfs -u -k all"
echo "======= update grub =========="
chroot_execute "update-grub"
echo "======= setting up zed =========="
chroot_execute "zfs set canmount=noauto $v_rpool_name"
echo "======= setting mountpoints =========="
chroot_execute "zfs set mountpoint=legacy $v_bpool_name/BOOT/ubuntu"
chroot_execute "echo $v_bpool_name/BOOT/ubuntu /boot zfs nodev,relatime,x-systemd.requires=zfs-mount.service,x-systemd.device-timeout=10 0 0 > /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/log"
chroot_execute "echo $v_rpool_name/var/log /var/log zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/spool"
chroot_execute "echo $v_rpool_name/var/spool /var/spool zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/var/tmp"
chroot_execute "echo $v_rpool_name/var/tmp /var/tmp zfs nodev,relatime 0 0 >> /etc/fstab"
chroot_execute "zfs set mountpoint=legacy $v_rpool_name/tmp"
chroot_execute "echo $v_rpool_name/tmp /tmp zfs nodev,relatime 0 0 >> /etc/fstab"
echo "========= add swap, if defined"
if [[ $v_swap_size -gt 0 ]]; then
chroot_execute "echo /dev/zvol/$v_rpool_name/swap none swap discard 0 0 >> /etc/fstab"
fi
chroot_execute "echo RESUME=none > /etc/initramfs-tools/conf.d/resume"
echo "======= unmounting filesystems and zfs pools =========="
unmount_and_export_fs
echo "======== setup complete, rebooting ==============="
reboot

View File

@@ -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 <<EOF
set -euo pipefail
# Set hostname from variable
echo "$SYSTEM_HOSTNAME" > /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" <<CONF
export PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;32m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
umask 022
export LS_OPTIONS='--color=auto -h'
eval "\$(dircolors)"
CONF
}
# ---- Bootloader Functions ----
function setup_efi_boot {
echo "======= Setting up EFI boot =========="
# Mount EFI System Partition
mkdir -p "$MAIN_BOOT"
mount "$BOOT_PART" "$MAIN_BOOT"
# Create EFI directory structure
mkdir -p "$MAIN_BOOT/EFI/Boot"
# Download ZFSBootMenu EFI binary
echo "Downloading ZFSBootMenu EFI binary from: $ZBM_EFI_URL"
curl -L "$ZBM_EFI_URL" -o "$MAIN_BOOT/EFI/Boot/bootx64.efi"
}
function setup_bios_boot {
echo "======= Setting up BIOS boot =========="
# Mount boot partition
mkdir -p "$MAIN_BOOT"
mount "$BOOT_PART" "$MAIN_BOOT"
# Install extlinux in rescue system if needed
if ! command -v extlinux &> /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 "$@"