#!/usr/bin/env bash # reroot: reinstall Debian/Ubuntu from a rescue system onto a software RAID of the disks you choose. # Usage: curl -fsSL reroot.run | bash -s -- [options] (see --help) # # Copyright (C) 2026 MadMakz # SPDX-License-Identifier: AGPL-3.0-only # # This program is free software: you can redistribute it and/or modify it under the terms of the # GNU Affero General Public License as published by the Free Software Foundation, version 3 of the # License. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License along with this program. # If not, see . set -Eeuo pipefail VERSION=dev # set on release; shown as the dialog title # Defaults; options and menus change them. defaults() { TARGET=/mnt/reroot WORK=/tmp/reroot DEB_POOL=https://deb.debian.org/debian/pool/main CACHE_RAM_SIZE=3G # tmpfs cap for the package cache (peak measured: see cache_in_ram) CACHE_RAM_MIN_MB=4096 # only use RAM when this much is available OS=debian-13 LEVEL=1 DISKS=() ROOT_SIZE= HOME_PART=0 SWAP_SIZE= BOOT_SIZE=1G ESP_SIZE=512M HOSTNAME_NEW=reroot NET_PEER= # point-to-point peer of the first IPv4 address (ifupdown's "pointopoint") NET_CUSTOM=0 KEYMAP= LOCALE= NET_DHCP=0 SSH_KEYS=() SSH=1 SSH_PASSWORD=1 USER_NEW= DISABLE_ROOT=0 ROOT_PW= USER_PW= # command apt dnf apk pacman DEPS=" sgdisk gdisk gdisk sgdisk gptfdisk mdadm mdadm mdadm mdadm mdadm wipefs util-linux util-linux util-linux util-linux mkfs.ext4 e2fsprogs e2fsprogs e2fsprogs e2fsprogs mkfs.vfat dosfstools dosfstools dosfstools dosfstools efibootmgr efibootmgr efibootmgr efibootmgr efibootmgr curl curl curl curl curl wget wget wget wget wget ar binutils binutils binutils binutils perl perl perl perl perl gpgv gpgv gnupg2 gpgv gnupg xz xz-utils xz xz xz zstd zstd zstd zstd zstd dialog dialog dialog dialog dialog numfmt coreutils coreutils coreutils coreutils ip iproute2 iproute iproute2 iproute2 " } defaults log() { printf '\033[1;32m==>\033[0m %s\n' "$*" >&2; } warn() { printf '\033[1;33mWARNING:\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } usage() { cat <<'EOF' reroot: install Debian/Ubuntu onto software RAID from a Linux rescue system. Usage: reroot.sh [options] curl -fsSL reroot.run | bash -s -- [options] Everything is chosen in menus: disks, RAID level, system, hostname, layout, network, user, SSH and passwords. The options below only preselect menu values. /boot is always RAID 1 across all chosen disks; /, /home and swap use the chosen level. --os OS debian-12, debian-13 (default), ubuntu-24.04, ubuntu-26.04 --root-size SIZE / partition size per disk, e.g. 50G (default: rest of disk, or 50G with --home) --home separate /home on the remaining space --swap SIZE add a swap array, SIZE per disk --boot-size SIZE /boot partition size per disk (default 1G; no menu) --hostname NAME default: reroot --keymap LAYOUT keyboard layout (XKB name: us, de, fr, ...; default: the rescue's) --locale LOCALE e.g. en_US.UTF-8, de_DE.UTF-8, C.UTF-8 (default: the rescue's, or en_US.UTF-8 when the rescue only runs C) --dhcp network by DHCP (preselected anyway when the rescue got its address by DHCP; otherwise static, copied from the rescue) --user NAME create a user with sudo rights --disable-root lock root's password and forbid root SSH login (needs --user) --ssh-key KEY authorized key, as a key line or a file; repeatable. Goes to root and the --user account. Default: the rescue's /root/.ssh/authorized_keys. The SSH menu can add one more. --ssh-keys-only SSH login with keys only (default: password or key) --no-ssh don't install the SSH server (console access only) -h, --help Sizes are per-disk partition sizes (M, G or T); usable space depends on the RAID level. EOF } parse_args() { while (($#)); do case $1 in --os) OS=${2:?--os needs a value}; shift ;; --root-size) ROOT_SIZE=${2:?--root-size needs a value}; shift ;; --home) HOME_PART=1 ;; --swap) SWAP_SIZE=${2:?--swap needs a value}; shift ;; --boot-size) BOOT_SIZE=${2:?--boot-size needs a value}; shift ;; --hostname) HOSTNAME_NEW=${2:?--hostname needs a value}; shift ;; --keymap) KEYMAP=${2:?--keymap needs a value}; shift ;; --locale) LOCALE=${2:?--locale needs a value}; shift ;; --dhcp) NET_DHCP=1 ;; --ssh-key) SSH_KEYS+=("${2:?--ssh-key needs a value}"); shift ;; --ssh-keys-only) SSH_PASSWORD=0 ;; --no-ssh) SSH=0 ;; --user) USER_NEW=${2:?--user needs a value}; shift ;; --disable-root) DISABLE_ROOT=1 ;; -h | --help) usage; exit 0 ;; *) die "unknown option: $1 (see --help)" ;; esac shift done if ((HOME_PART)) && [[ -z $ROOT_SIZE ]]; then ROOT_SIZE=50G; fi local s for s in "$ROOT_SIZE" "$SWAP_SIZE" "$BOOT_SIZE"; do [[ -z $s ]] || valid_size "$s" || die "invalid size: $s (use e.g. 512M, 50G, 2T)" done valid_hostname "$HOSTNAME_NEW" || die "invalid hostname: $HOSTNAME_NEW" [[ -z $KEYMAP ]] || valid_keymap "$KEYMAP" || die "invalid keymap: $KEYMAP (an XKB layout such as us, de, fr)" [[ -z $LOCALE ]] || valid_locale "$LOCALE" || die "invalid locale: $LOCALE (e.g. en_US.UTF-8, C.UTF-8)" valid_user "$USER_NEW" || die "invalid user name: $USER_NEW" if ((DISABLE_ROOT)) && [[ -z $USER_NEW ]]; then die "--disable-root needs --user, or nobody could log in"; fi set_os || die "unsupported --os: $OS (use debian-12, debian-13, ubuntu-24.04 or ubuntu-26.04)" } valid_size() { [[ $1 =~ ^[1-9][0-9]*[MGT]$ ]]; } valid_size_or_rest() { [[ -z $1 ]] || valid_size "$1"; } valid_hostname() { [[ $1 =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]]; } valid_user() { [[ -z $1 || ($1 =~ ^[a-z_][a-z0-9_-]{0,31}$ && $1 != root) ]]; } # empty = no user valid_ip4() { local o [[ $1 =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1 IFS=. read -ra o <<<"$1" ((10#${o[0]} <= 255 && 10#${o[1]} <= 255 && 10#${o[2]} <= 255 && 10#${o[3]} <= 255)) } valid_ip6() { [[ $1 =~ ^[0-9a-fA-F:]+$ && $1 =~ [0-9a-fA-F] && $1 == *:*:* ]]; } # loose: hex groups and colons valid_cidr4() { [[ $1 =~ /([0-9]{1,2})$ ]] && ((10#${BASH_REMATCH[1]} <= 32)) && valid_ip4 "${1%/*}"; } valid_cidr6() { [[ $1 =~ /([0-9]{1,3})$ ]] && ((10#${BASH_REMATCH[1]} <= 128)) && valid_ip6 "${1%/*}"; } valid_ip4_opt() { [[ -z $1 ]] || valid_ip4 "$1"; } valid_ip6_opt() { [[ -z $1 ]] || valid_ip6 "$1"; } valid_cidr4_opt() { [[ -z $1 ]] || valid_cidr4 "$1"; } valid_cidr6_opt() { [[ -z $1 ]] || valid_cidr6 "$1"; } valid_dns() { local d list read -ra list <<<"$1" ((${#list[@]})) || return 1 for d in "${list[@]}"; do valid_ip4 "$d" || valid_ip6 "$d" || return 1; done } valid_keymap() { [[ $1 =~ ^[a-z]{2,8}$ ]]; } # XKB layout name valid_locale() { [[ $1 == C.UTF-8 || $1 =~ ^[a-z]{2,3}_[A-Z]{2}\.UTF-8$ ]]; } # conf_value FILE VAR: value of VAR= in a shell-style config file, quotes stripped. conf_value() { local line [[ -r $1 ]] || return 0 while IFS= read -r line; do if [[ $line =~ ^[[:space:]]*$2=[\"\']?([^\"\']*) ]]; then echo "${BASH_REMATCH[1]}" return fi done <"$1" } # Console keymap name (vconsole.conf, loadkeys) to XKB layout: de-latin1-nodeadkeys -> de, uk -> gb. xkb_layout() { local l=${1%%-*} l=${l%%_*} if [[ $l == uk ]]; then l=gb; fi echo "$l" } # Preselect the rescue's keyboard and language, so the new system matches what you type on now. detect_locale_keymap() { local v if [[ -z $KEYMAP ]]; then v=$(conf_value /etc/default/keyboard XKBLAYOUT) v=${v%%,*} if [[ -z $v ]]; then v=$(conf_value /etc/vconsole.conf KEYMAP); fi if [[ -z $v ]] && command -v localectl >/dev/null; then # localectl fails without a running systemd/dbus (containers, some rescues): just no value. v=$({ localectl status 2>/dev/null || true; } | awk -F': *' '!v && /X11 Layout|VC Keymap/ && $2 != "n/a" {v = $2} END {print v}') fi KEYMAP=$(xkb_layout "${v:-us}") valid_keymap "$KEYMAP" || KEYMAP=us fi if [[ -z $LOCALE ]]; then v=$(conf_value /etc/locale.conf LANG) if [[ -z $v ]]; then v=$(conf_value /etc/default/locale LANG); fi v=${v/.utf8/.UTF-8} # A rescue running plain C/POSIX says nothing about the language you want. if valid_locale "$v" && [[ $v != C.UTF-8 ]]; then LOCALE=$v; else LOCALE=en_US.UTF-8; fi fi } set_os() { case $OS in debian-12) DISTRO=debian SUITE=bookworm MIRROR=http://deb.debian.org/debian ;; debian-13) DISTRO=debian SUITE=trixie MIRROR=http://deb.debian.org/debian ;; ubuntu-24.04) DISTRO=ubuntu SUITE=noble MIRROR=http://archive.ubuntu.com/ubuntu ;; ubuntu-26.04) DISTRO=ubuntu SUITE=resolute MIRROR=http://archive.ubuntu.com/ubuntu ;; *) return 1 ;; esac } min_disks() { case $1 in 0 | 1) echo 2 ;; 5) echo 3 ;; 6 | 10) echo 4 ;; *) return 1 ;; esac } # Partition plan, identical on every disk, as name:size (size 0 = rest of disk). plan_partitions() { PARTS=(bios:1M "esp:$ESP_SIZE" "boot:$BOOT_SIZE") if [[ $SWAP_SIZE ]]; then PARTS+=("swap:$SWAP_SIZE"); fi PARTS+=("root:${ROOT_SIZE:-0}") if ((HOME_PART)); then PARTS+=(home:0); fi } part_no() { local i for i in "${!PARTS[@]}"; do if [[ ${PARTS[i]%%:*} == "$1" ]]; then echo $((i + 1)); return; fi done return 1 } part() { echo "$1-part$(part_no "$2")"; } # Stable /dev/disk/by-id name for a whole disk; prefers model/serial names over wwn/eui. by_id() { local dev link fallback= dev=$(readlink -f "$1") for link in /dev/disk/by-id/*; do [[ $link != *-part* && $(readlink -f "$link") == "$dev" ]] || continue case ${link##*/} in wwn-* | nvme-eui.* | nvme-nvme.*) fallback=${fallback:-$link} ;; *) echo "$link"; return ;; esac done [[ $fallback ]] || die "no /dev/disk/by-id link for $1" echo "$fallback" } preflight() { ((EUID == 0)) || die "run as root" [[ $(uname -m) == x86_64 ]] || die "only x86_64 is supported for now" if [[ -d /sys/firmware/efi ]]; then UEFI=1; else UEFI=0; fi ensure_deps local m for m in md_mod raid0 raid1 raid456 raid10; do modprobe "$m" 2>/dev/null || true; done grep -q '\[raid1\]' /proc/mdstat 2>/dev/null || die "kernel lacks md RAID 1 support" if ((UEFI)) && ! mountpoint -q /sys/firmware/efi/efivars; then mount -t efivarfs efivarfs /sys/firmware/efi/efivars fi } ensure_deps() { local pm cmd apt dnf apk pac missing=() pkgs=() for pm in apt-get dnf apk pacman ""; do if [[ -z $pm ]] || command -v "$pm" >/dev/null; then break; fi done while read -r cmd apt dnf apk pac; do if [[ -z $cmd ]] || command -v "$cmd" >/dev/null; then continue; fi missing+=("$cmd") case $pm in apt-get) pkgs+=("$apt") ;; dnf) pkgs+=("$dnf") ;; apk) pkgs+=("$apk") ;; pacman) pkgs+=("$pac") ;; esac done <<<"$DEPS" ((${#missing[@]})) || return 0 # shortcut: no static-binary fallback yet; add one if a rescue without a package manager shows up. [[ $pm ]] || die "missing tools: ${missing[*]} (and no apt/dnf/apk/pacman to install them)" log "installing rescue tools: ${pkgs[*]}" case $pm in apt-get) apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends "${pkgs[@]}" ;; dnf) dnf install -y "${pkgs[@]}" ;; apk) apk add "${pkgs[@]}" ;; pacman) pacman -Sy --noconfirm --needed "${pkgs[@]}" ;; esac /dev/null || die "still missing after install: $cmd"; done } # dialog on the terminal even under curl | bash; the selection is printed to stdout. Buttons: OK, # Back (exit 3), Quit (1; Esc: 255). msg is for message boxes, which only need OK. msg() { dialog --backtitle "reroot $VERSION" --insecure "$@" 3>&1 1>/dev/tty 2>&3 /dev/null || true; } # reads the disk itself, not udev's cache # Existing content on a disk, one line each; empty if the disk looks blank. disk_data() { local dev fs label label=$(probe PTTYPE "$1") if [[ $label ]]; then echo "$label partition table"; fi for dev in $(lsblk -lnpo NAME "$1"); do fs=$(probe TYPE "$dev") [[ $fs ]] || continue label=$(probe LABEL "$dev") echo "${dev##*/}: $fs${label:+ \"$label\"}, $(blk SIZE "$dev")" done } # level_options N SMALLEST_BYTES: dialog radiolist items for the levels N disks allow. level_options() { local n=$1 s=$2 l usable desc want=$LEVEL if ((n < $(min_disks "$want"))); then want=1; fi for l in 1 0 5 6 10; do ((n >= $(min_disks "$l"))) || continue case $l in 1) usable=$s desc="mirror, survives $((n - 1)) failed disk(s)" ;; 0) usable=$((n * s)) desc="stripe, NO redundancy" ;; 5) usable=$(((n - 1) * s)) desc="parity, survives 1 failed disk" ;; 6) usable=$(((n - 2) * s)) desc="double parity, survives 2 failed disks" ;; 10) usable=$((n * s / 2)) desc="striped mirrors, survives 1 failed disk" ;; esac printf '%s\n%s\n%s\n' "$l" "RAID $l: $desc, ~$(numfmt --to=iec "$usable") total" \ "$(on "$l" "$want")" done } step_disks() { local name mark state items=() sel cur=' ' d i for d in "${DISKS[@]}"; do cur+="$(readlink -f "$d") "; done # the earlier choice, when coming back while read -r name; do mark='' state=OFF if [[ $(disk_data "/dev/$name") ]]; then mark=" [HAS DATA]"; fi if [[ $cur == *" /dev/$name "* ]]; then state=ON; fi items+=("$name" "$(disk_desc "/dev/$name")$mark" "$state") done < <(lsblk -dnbo NAME,TYPE,SIZE | awk '$2 == "disk" && $3 > 0 && $1 !~ /^(zram|ram)/ {print $1}') ((${#items[@]} >= 6)) || die "need at least 2 disks, found $((${#items[@]} / 3))" while :; do sel=$(tui --title "Disks" --separate-output --checklist \ "Choose the disks for the RAID (space toggles, enter confirms).\nEverything on them will be erased; other disks are not touched." \ 20 78 10 "${items[@]}") || { back $?; return 0; } mapfile -t DISKS < <(grep . <<<"$sel") ((${#DISKS[@]} >= 2)) && break msg --title "Disks" --msgbox "Choose at least 2 disks." 8 40 || true done for i in "${!DISKS[@]}"; do DISKS[i]=$(by_id "/dev/${DISKS[i]}"); done check_disks_free } step_level() { local d size smallest=0 opts=() v for d in "${DISKS[@]}"; do size=$(lsblk -dnbo SIZE "$(readlink -f "$d")") if ((smallest == 0 || size < smallest)); then smallest=$size; fi done mapfile -t opts < <(level_options ${#DISKS[@]} "$smallest") v=$(tui --title "RAID level" --radiolist \ "RAID level for /, /home and swap (/boot is always RAID 1).\nSizes assume the smallest chosen disk." \ 16 78 5 "${opts[@]}") || { back $?; return 0; } LEVEL=$v grep -q "\[raid$LEVEL\]" /proc/mdstat || die "kernel lacks md RAID $LEVEL support" } # Refuse rather than tear down anything the user may still be using. check_disks_free() { local d umount -R "$TARGET" 2>/dev/null || true for d in "${DISKS[@]}"; do # Capture, don't pipe into grep -q: with pipefail, SIGPIPE on a match would read as "no match". if [[ $(lsblk -lnpo MOUNTPOINT "$d") =~ [^[:space:]] ]]; then die "$d (or an array on it) is mounted or used as swap"; fi if [[ $(lsblk -lnpo TYPE "$d") =~ lvm|crypt|dm ]]; then die "$d has active LVM/dm-crypt; deactivate it first"; fi done } capture_network() { # ip -6 errors when the rescue has IPv6 disabled; awk reads all input so ip can't hit SIGPIPE. NET_DEV=$({ ip -4 route show default; ip -6 route show default 2>/dev/null || true; } | awk '!dev {for (i = 1; i < NF; i++) if ($i == "dev") dev = $(i + 1)} END {print dev}') [[ $NET_DEV ]] || die "rescue system has no default route; cannot carry over the network" NET_MAC=$(cat "/sys/class/net/$NET_DEV/address") local lines lines=$(ip -o addr show dev "$NET_DEV" scope global) mapfile -t NET_ADDRS < <(addrs_from_ip <<<"$lines") NET_PEER=$(peer_from_ip <<<"$lines") NET_RESCUE_ADDR=${NET_ADDRS[0]:-} # "dynamic" = leased by DHCP, so the network menu preselects DHCP for the new system too. NET_DYNAMIC=0 if [[ $(ip -o -4 addr show dev "$NET_DEV" scope global) == *dynamic* ]]; then NET_DYNAMIC=1; fi mapfile -t NET_ROUTES < <({ ip -4 route show dev "$NET_DEV" | sed 's|^default|0.0.0.0/0|' { ip -6 route show dev "$NET_DEV" 2>/dev/null || true; } | sed 's|^default|::/0|' } | awk '$2 == "via" && !seen[$1]++ {print $1, $3}') local conf for conf in /etc/resolv.conf /run/systemd/resolve/resolv.conf; do mapfile -t NET_DNS < <(awk '$1 == "nameserver" && $2 !~ /^127\./ {print $2}' "$conf" 2>/dev/null) ((${#NET_DNS[@]})) && break done ((${#NET_ADDRS[@]})) || die "no global address on $NET_DEV" ((${#NET_ROUTES[@]})) || die "no gateway route on $NET_DEV" ((${#NET_DNS[@]})) || die "no DNS server found in the rescue system" } # CIDRs from `ip -o addr` lines. A point-to-point line ("inet A peer P/32") has no prefix on A. addrs_from_ip() { awk '!/temporary/ {if ($5 == "peer") print $4 ($3 == "inet6" ? "/128" : "/32"); else print $4}'; } peer_from_ip() { awk '$3 == "inet" && $5 == "peer" && !p {p = $6; sub("/.*", "", p)} END {print p}'; } # systemd-networkd config for the new system, matched by MAC because interface names differ. render_network() { local a r peer_addr= printf '[Match]\nMACAddress=%s\n\n[Network]\n' "$NET_MAC" if ((NET_DHCP)); then printf 'DHCP=yes\n'; return; fi for a in "${NET_ADDRS[@]}"; do # The point-to-point peer belongs to the first IPv4 address, in its own [Address] section. if [[ $NET_PEER && -z $peer_addr && $a != *:* ]]; then peer_addr=$a continue fi printf 'Address=%s\n' "$a" done for a in "${NET_DNS[@]}"; do printf 'DNS=%s\n' "$a"; done if [[ $peer_addr ]]; then printf '\n[Address]\nAddress=%s\nPeer=%s/32\n' "$peer_addr" "$NET_PEER"; fi # GatewayOnLink: /32 setups have a gateway outside the address prefix. for r in "${NET_ROUTES[@]}"; do printf '\n[Route]\nDestination=%s\nGateway=%s\nGatewayOnLink=yes\n' "${r% *}" "${r#* }" done } load_ssh_keys() { local k AUTH_KEYS= if ((${#SSH_KEYS[@]} == 0)) && [[ -s /root/.ssh/authorized_keys ]]; then SSH_KEYS=(/root/.ssh/authorized_keys); fi if ((${#SSH_KEYS[@]})); then AUTH_KEYS=$(for k in "${SSH_KEYS[@]}"; do if [[ -f $k ]]; then cat "$k"; else printf '%s\n' "$k"; fi done | grep -Ev '^[[:space:]]*(#|$)') || die "no SSH key found in: ${SSH_KEYS[*]}" fi } # Keys-only SSH without any key would lock everyone out. login_possible() { ((!SSH || SSH_PASSWORD)) || [[ $AUTH_KEYS ]]; } key_count() { if [[ $AUTH_KEYS ]]; then grep -c . <<<"$AUTH_KEYS"; else echo 0; fi; } on() { if [[ $1 == "$2" ]]; then echo ON; else echo OFF; fi; } # radiolist state # ask VAR TITLE TEXT VALIDATOR: input box, repeated until VALIDATOR accepts the value. Back sets BACK. ask() { local v=${!1} while :; do v=$(tui --title "$2" --inputbox "$3" 10 78 "$v") || { back $?; return 0; } if "$4" "$v"; then printf -v "$1" '%s' "$v" return fi msg --title "$2" --msgbox "Not valid: \"$v\"" 8 60 || true done } # pick VAR TITLE TEXT VALIDATOR TAG DESC...: list menu preselecting $VAR; "other" asks for free input. pick() { local var=$1 title=$2 text=$3 valid=$4 cur=${!1} items=() found=0 v shift 4 while (($#)); do items+=("$1" "$2" "$(on "$cur" "$1")") if [[ $1 == "$cur" ]]; then found=1; fi shift 2 done if ((!found)); then items=("$cur" "(from the rescue system)" ON "${items[@]}"); fi items+=(other "Other: type it in" OFF) while :; do v=$(tui --title "$title" --radiolist "$text" 20 78 12 "${items[@]}") || { back $?; return 0; } if [[ $v != other ]]; then printf -v "$var" '%s' "$v" return 0 fi ask "$var" "$title" "$text" "$valid" if ((!BACK)); then return 0; fi BACK=0 # back from the input box: show the list again done } # Static network typed in by hand; every field starts with what "copy from the rescue" would use. # Back from any field returns to the network menu. ask_network_custom() { local t="Custom network" a a4='' g4='' a6='' g6='' r dns="${NET_DNS[*]}" for a in "${NET_ADDRS[@]}"; do if [[ $a == *:* ]]; then a6=${a6:-$a}; else a4=${a4:-$a}; fi done for r in "${NET_ROUTES[@]}"; do case ${r% *} in 0.0.0.0/0) g4=${r#* } ;; ::/0) g6=${r#* } ;; esac done while :; do ask a4 "$t" "IPv4 address with prefix, e.g. 203.0.113.5/24 (empty = no IPv4):" valid_cidr4_opt if ((BACK)); then return 0; fi if [[ $a4 ]]; then ask g4 "$t" "IPv4 gateway (empty = the point-to-point peer):" valid_ip4_opt if ((BACK)); then return 0; fi ask NET_PEER "$t" "Point-to-point peer, only if your hoster requires one (empty = none):" valid_ip4_opt if ((BACK)); then return 0; fi else g4='' NET_PEER='' fi ask a6 "$t" "IPv6 address with prefix, e.g. 2001:db8::2/64 (empty = no IPv6):" valid_cidr6_opt if ((BACK)); then return 0; fi if [[ $a6 ]]; then ask g6 "$t" "IPv6 gateway (often fe80::1):" valid_ip6_opt if ((BACK)); then return 0; fi else g6= fi ask dns "$t" "DNS servers, separated by spaces:" valid_dns if ((BACK)); then return 0; fi if [[ $a4 && -z $g4 ]]; then g4=$NET_PEER; fi if [[ $a4 || $a6 ]] && [[ -z $a4 || $g4 ]] && [[ -z $a6 || $g6 ]]; then break; fi msg --title "$t" --msgbox "Enter at least one address, and a gateway (or point-to-point peer) for each." 8 78 || true done NET_ADDRS=() NET_ROUTES=() if [[ $a4 ]]; then NET_ADDRS+=("$a4") && NET_ROUTES+=("0.0.0.0/0 $g4"); fi if [[ $a6 ]]; then NET_ADDRS+=("$a6") && NET_ROUTES+=("::/0 $g6"); fi read -ra NET_DNS <<<"$dns" } # Every screen in order; each step_NAME shows one, preselected with the current answer. # A step sets BACK to return to the previous screen, SKIP when it doesn't apply (passed in the # direction of travel), or JUMP to continue at a named step. Steps never return non-zero: a # function called as a condition runs without set -e. ask_settings() { local steps=(disks level os hostname keymap locale layout swap_size root_size network network_custom user root ssh ssh_key root_pw user_pw confirm) local i=0 dir=1 j while ((i < ${#steps[@]})); do BACK=0 SKIP=0 JUMP='' "step_${steps[i]}" if [[ $JUMP ]]; then for j in "${!steps[@]}"; do if [[ ${steps[j]} == "$JUMP" ]]; then i=$j; fi done dir=1 continue fi if ((BACK)); then dir=-1; elif ((!SKIP)); then dir=1; fi i=$((i + dir)) if ((i < 0)); then i=0; fi # Back on the first screen done } step_os() { local v v=$(tui --title "Operating system" --radiolist "Install which system?" 13 78 4 \ debian-12 "Debian 12 (bookworm)" "$(on "$OS" debian-12)" \ debian-13 "Debian 13 (trixie)" "$(on "$OS" debian-13)" \ ubuntu-24.04 "Ubuntu 24.04 LTS (noble)" "$(on "$OS" ubuntu-24.04)" \ ubuntu-26.04 "Ubuntu 26.04 LTS (resolute)" "$(on "$OS" ubuntu-26.04)") || { back $?; return 0; } OS=$v set_os } step_hostname() { ask HOSTNAME_NEW "Hostname" "Hostname of the new system:" valid_hostname; } step_keymap() { pick KEYMAP "Keyboard" "Keyboard layout of the new system (preselected: the rescue's):" valid_keymap \ us "English (US)" gb "English (UK)" de "German" ch "Swiss" fr "French" be "Belgian" \ nl "Dutch" es "Spanish" it "Italian" pt "Portuguese" br "Portuguese (Brazil)" pl "Polish" \ cz "Czech" se "Swedish" no "Norwegian" dk "Danish" "fi" "Finnish" ru "Russian" tr "Turkish" jp "Japanese" } step_locale() { pick LOCALE "Language" "Language and formats (locale) of the new system:" valid_locale \ en_US.UTF-8 "English (US)" en_GB.UTF-8 "English (UK)" de_DE.UTF-8 "German" \ de_AT.UTF-8 "German (Austria)" de_CH.UTF-8 "German (Switzerland)" fr_FR.UTF-8 "French" \ es_ES.UTF-8 "Spanish" it_IT.UTF-8 "Italian" nl_NL.UTF-8 "Dutch" pl_PL.UTF-8 "Polish" \ pt_BR.UTF-8 "Portuguese (Brazil)" C.UTF-8 "Minimal: no translations, no locales package" } step_layout() { local sel sel=$(tui --title "Layout" --separate-output --checklist \ "/boot (RAID 1) and / are always separate. Extras:" 11 78 2 \ home "Separate /home on the rest of the disks" "$( ((HOME_PART)) && echo ON || echo OFF)" \ swap "Swap (as a RAID array)" "$([[ $SWAP_SIZE ]] && echo ON || echo OFF)") || { back $?; return 0; } HOME_PART=0 if [[ $sel == *home* ]]; then HOME_PART=1; fi if [[ $sel == *swap* ]]; then SWAP_SIZE=${SWAP_SIZE:-8G}; else SWAP_SIZE=; fi } step_swap_size() { if [[ -z $SWAP_SIZE ]]; then SKIP=1; return 0; fi ask SWAP_SIZE "Swap" "Swap size per disk (e.g. 8G):" valid_size } step_root_size() { if ((HOME_PART)); then ROOT_SIZE=${ROOT_SIZE:-50G} ask ROOT_SIZE "Root" "Size of / per disk (e.g. 50G); /home gets the rest:" valid_size else ask ROOT_SIZE "Root" "Size of / per disk (e.g. 50G), empty = rest of the disks:" valid_size_or_rest fi } step_network() { local v=static if ((NET_DHCP)); then v=dhcp; elif ((NET_CUSTOM)); then v=custom; fi v=$(tui --title "Network" --radiolist \ "Network for the new system (card $NET_DEV in the rescue, matched by MAC).\nThe rescue got its address $( ((NET_DYNAMIC)) && echo "by DHCP" || echo "statically")." 14 78 3 \ dhcp "DHCP" "$(on "$v" dhcp)" \ static "Static: copy $NET_RESCUE_ADDR and its routes and DNS" "$(on "$v" static)" \ custom "Custom: enter addresses, gateways, point-to-point peer, DNS" "$(on "$v" custom)") || { back $?; return 0; } NET_DHCP=0 NET_CUSTOM=0 case $v in dhcp) NET_DHCP=1 ;; static) capture_network ;; # undoes an earlier custom entry custom) NET_CUSTOM=1 ;; esac } step_network_custom() { if ((!NET_CUSTOM)); then SKIP=1; return 0; fi ask_network_custom } step_user() { ask USER_NEW "User" "Create a user with sudo rights (empty = none):" valid_user; } step_root() { local v=keep if [[ -z $USER_NEW ]]; then DISABLE_ROOT=0 SKIP=1 return 0 fi if ((DISABLE_ROOT)); then v=lock; fi v=$(tui --title "Root" --radiolist "Root account ($USER_NEW gets sudo either way):" 10 78 2 \ keep "Keep root, with a password" "$(on "$v" keep)" \ lock "Disable root: password locked, no SSH login" "$(on "$v" lock)") || { back $?; return 0; } DISABLE_ROOT=0 if [[ $v == lock ]]; then DISABLE_ROOT=1; fi } step_ssh() { local v=password if ((!SSH)); then v=off; elif ((!SSH_PASSWORD)); then v=keys; fi v=$(tui --title "SSH" --radiolist "SSH login for the new system ($(key_count) key(s) found):" 12 78 3 \ password "Password or key" "$(on "$v" password)" \ keys "Keys only" "$(on "$v" keys)" \ off "No SSH server (console access only)" "$(on "$v" off)") || { back $?; return 0; } SSH=1 SSH_PASSWORD=0 case $v in password) SSH_PASSWORD=1 ;; off) SSH=0 ;; esac } step_ssh_key() { local k if ((!SSH)); then SKIP=1; return 0; fi k=$(tui --title "SSH key" --inputbox \ "Paste a public key to add (optional, $(key_count) found already):" 10 78) || { back $?; return 0; } if [[ $k =~ ^(ssh-|ecdsa-|sk-) ]]; then AUTH_KEYS+=${AUTH_KEYS:+$'\n'}$k elif [[ $k ]]; then msg --title "SSH key" --msgbox "That doesn't look like a public key (ssh-ed25519 AAAA...); not added." 8 78 || true fi if ! login_possible; then msg --title "SSH" --msgbox "Keys only needs at least one key." 8 50 || true BACK=1 fi } step_root_pw() { if ((DISABLE_ROOT)); then SKIP=1; return 0; fi ask_password root ROOT_PW } step_user_pw() { if [[ -z $USER_NEW ]]; then SKIP=1; return 0; fi ask_password "$USER_NEW" USER_PW } # ask_password ACCOUNT VAR: password box twice, store the password in VAR. ask_password() { local a b while :; do a=$(tui --title "Password" --passwordbox "Password for $1:" 9 78) || { back $?; return 0; } b=$(tui --title "Password" --passwordbox "Repeat the password for $1:" 9 78) || { back $?; BACK=0; continue; } if [[ $a && $a == "$b" ]]; then break; fi msg --title "Password" --msgbox "Empty or not matching, try again." 8 50 || true done printf -v "$2" '%s' "$a" } render_sshd() { local root=prohibit-password pw=no if ((SSH_PASSWORD)); then root=yes pw=yes; fi if ((DISABLE_ROOT)); then root=no; fi printf 'PermitRootLogin %s\nPasswordAuthentication %s\n' "$root" "$pw" } install_keys() { # HOME [OWNER] [[ $AUTH_KEYS ]] || return 0 install -d -m 700 "$TARGET$1/.ssh" printf '%s\n' "$AUTH_KEYS" >"$TARGET$1/.ssh/authorized_keys" chmod 600 "$TARGET$1/.ssh/authorized_keys" if [[ ${2:-} ]]; then in_target chown -R "$2:$2" "$1/.ssh"; fi } setup_accounts() { if ((DISABLE_ROOT)); then in_target passwd -l root >/dev/null else printf 'root:%s\n' "$ROOT_PW" | in_target chpasswd install_keys /root fi if [[ $USER_NEW ]]; then in_target useradd -m -s /bin/bash -G sudo "$USER_NEW" printf '%s:%s\n' "$USER_NEW" "$USER_PW" | in_target chpasswd install_keys "/home/$USER_NEW" "$USER_NEW" fi if ((SSH)); then # sshd uses the first value it reads and includes sshd_config.d first, so this file wins. render_sshd >"$TARGET/etc/ssh/sshd_config.d/10-reroot.conf" fi } step_confirm() { local d data answer v plan_partitions echo echo "Install $OS ($SUITE), $( ((UEFI)) && echo UEFI || echo BIOS) boot, hostname $HOSTNAME_NEW" echo "Keyboard $KEYMAP, locale $LOCALE" echo "Package downloads: $( ((CACHE_RAM)) && echo "RAM" || echo "disk (needs $CACHE_RAM_MIN_MB MB free RAM for RAM)"), $MEM_AVAIL_MB MB RAM available" echo "RAID $LEVEL for / $([[ $SWAP_SIZE ]] && echo "swap ")$( ((HOME_PART)) && echo "/home ")(/boot: RAID 1)" echo "Partitions per disk: ${PARTS[*]} (0 = rest of disk)" echo echo "ALL DATA ON THESE DISKS WILL BE ERASED:" for d in "${DISKS[@]}"; do printf ' %s %s\n' "$d" "$(disk_desc "$d")" data=$(disk_data "$d") if [[ $data ]]; then printf ' \033[1;33mWARNING: this disk contains data:\033[0m\n' printf ' %s\n' "${data//$'\n'/$'\n '}" fi done echo if ((NET_DHCP)); then echo "Network: DHCP on the card that is $NET_DEV in the rescue:" elif ((NET_CUSTOM)); then echo "Network: custom, on the card that is $NET_DEV in the rescue:" else echo "Network carried over from $NET_DEV:" fi render_network | sed 's/^/ /' echo echo "Root: $( ((DISABLE_ROOT)) && echo "locked, no SSH login" || echo "password set")" if [[ $USER_NEW ]]; then echo "User: $USER_NEW (sudo, password set)"; fi if ((SSH)); then echo "SSH: on, $(key_count) key(s), sshd config:" render_sshd | sed 's/^/ /' else echo "SSH: off (console access only)" fi echo printf 'Type "wipe" to install, "back" to change settings: ' read -r answer /dev/null || true wipefs -aq "$p" done wipefs -aq "$disk" sgdisk --zap-all "$disk" >/dev/null } partition_disk() { local i n name size type args=() for i in "${!PARTS[@]}"; do n=$((i + 1)) name=${PARTS[i]%%:*} size=${PARTS[i]#*:} case $name in bios) type=EF02 ;; esp) type=EF00 ;; *) type=FD00 ;; esac [[ $size == 0 ]] || size=+$size args+=(-n "$n:0:$size" -t "$n:$type" -c "$n:$name") done sgdisk "${args[@]}" "$1" >/dev/null } wait_for() { local i for i in {1..50}; do [[ -e $1 ]] && return sleep 0.2 done die "timed out waiting for $1" } create_array() { local name=$1 level=$2 d devs=() for d in "${DISKS[@]}"; do devs+=("$(part "$d" "$name")"); done wipefs -aq "${devs[@]}" mdadm --create "/dev/md/$name" --run --level="$level" --raid-devices=${#devs[@]} \ --metadata=1.2 --homehost="$HOSTNAME_NEW" --name="$name" "${devs[@]}" /dev/null >"/sys/block/$(basename "$(readlink -f "/dev/md/$1")")/md/sync_action" || true } setup_storage() { local d a for d in "${DISKS[@]}"; do log "wiping $d" wipe_disk "$d" partition_disk "$d" done udevadm settle for d in "${DISKS[@]}"; do wait_for "$(part "$d" root)"; done ARRAYS=(boot root) if [[ $SWAP_SIZE ]]; then ARRAYS+=(swap); fi if ((HOME_PART)); then ARRAYS+=(home); fi for a in "${ARRAYS[@]}"; do log "creating /dev/md/$a" if [[ $a == boot ]]; then create_array boot 1; else create_array "$a" "$LEVEL"; fi if [[ $a == swap ]]; then mkswap -L swap "/dev/md/$a" >/dev/null; else mkfs.ext4 -qF -L "$a" "/dev/md/$a"; fi done if ((UEFI)); then for d in "${DISKS[@]}"; do mkfs.vfat -F 32 -n EFI "$(part "$d" esp)" >/dev/null; done fi # noinit_itable: ext4 would zero its inode tables in the background now (GBs on a large disk); # the installed system does that instead. mkdir -p "$TARGET" mount -o noinit_itable /dev/md/root "$TARGET" mkdir -p "$TARGET/boot" mount -o noinit_itable /dev/md/boot "$TARGET/boot" if ((HOME_PART)); then mkdir -p "$TARGET/home" && mount -o noinit_itable /dev/md/home "$TARGET/home"; fi if ((UEFI)); then mkdir -p "$TARGET/boot/efi" && mount "$(part "${DISKS[0]}" esp)" "$TARGET/boot/efi"; fi } # Download the newest *_all.deb of a Debian package and unpack it into $WORK/root. fetch_deb() { local pkg=$1 dir file dir=$DEB_POOL/${pkg:0:1}/$pkg file=$(curl -fsSL "$dir/" | grep -o "${pkg}_[^\"/]*_all\.deb" | grep -v '~' | sort -uV | tail -n1) [[ $file ]] || die "could not find $pkg in the Debian archive" mkdir -p "$WORK/$pkg" "$WORK/root" curl -fsSL -o "$WORK/$pkg/$file" "$dir/$file" (cd "$WORK/$pkg" && ar x "$file" && tar -xf data.tar.* -C "$WORK/root") } in_target() { chroot "$TARGET" /usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 \ PATH=/usr/sbin:/usr/bin:/sbin:/bin "$@" } write_resolv() { rm -f "$TARGET/etc/resolv.conf" printf 'nameserver %s\n' "${NET_DNS[@]}" >"$TARGET/etc/resolv.conf" } # Package downloads (debootstrap's and apt's) go to RAM when there's room: fewer writes to the # new disks while the RAID syncs, and nothing left behind. Unmounted with $TARGET at the end. cache_in_ram() { if ((CACHE_RAM)); then mkdir -p "$TARGET/var/cache/apt/archives/partial" # on disk, for apt after the tmpfs is gone mount -t tmpfs -o "size=${CACHE_RAM_SIZE},mode=755" reroot-cache "$TARGET/var/cache/apt/archives" fi } # Decided up front so the confirmation screen can show it. check_ram() { MEM_AVAIL_MB=$(awk '/^MemAvailable:/ {print int($2 / 1024)}' /proc/meminfo) CACHE_RAM=0 if ((MEM_AVAIL_MB >= CACHE_RAM_MIN_MB)); then CACHE_RAM=1; fi } bootstrap() { local keyring script rm -rf "$WORK" log "fetching debootstrap and $DISTRO keyring from the Debian archive" fetch_deb debootstrap if [[ $DISTRO == debian ]]; then fetch_deb debian-archive-keyring script=sid else fetch_deb ubuntu-keyring script=gutsy fi for keyring in "$WORK"/root/usr/share/keyrings/"$DISTRO"-archive-keyring.{pgp,gpg} ""; do [[ -f $keyring ]] && break done [[ $keyring ]] || die "$DISTRO archive keyring not found in the downloaded package" cache_in_ram # dpkg fsyncs every file it unpacks: thousands of disk flushes, slow on HDDs. A crash mid-install # means reinstalling anyway, and main syncs before unmounting. Removed at the end of configure. mkdir -p "$TARGET/etc/dpkg/dpkg.cfg.d" echo force-unsafe-io >"$TARGET/etc/dpkg/dpkg.cfg.d/reroot-unsafe-io" log "debootstrap $SUITE" # Generic sid/gutsy scripts work for any release, so a codename newer than this debootstrap is fine. DEBOOTSTRAP_DIR=$WORK/root/usr/share/debootstrap "$WORK/root/usr/sbin/debootstrap" \ --arch=amd64 --keyring="$keyring" "$SUITE" "$TARGET" "$MIRROR" \ "$WORK/root/usr/share/debootstrap/scripts/$script" "$WORK/contents"; then cat /sys/bus/*/devices/*/modalias 2>/dev/null | sort -u >"$WORK/modalias" || true # modprobe -R resolves a device's modalias to its driver module without loading anything. # shellcheck disable=SC2016 # expanded by sh in the target in_target sh -c 'while read -r a; do modprobe -S "$1" -R "$a"; done 2>/dev/null | sort -u | xargs -r modinfo -k "$1" -F firmware 2>/dev/null' sh "${k[0]##*/}" \ <"$WORK/modalias" >"$WORK/firmware" || true firmware_pkgs "$WORK/firmware" "$WORK/contents" else warn "could not fetch the firmware index; installing ${fallback[*]} instead" printf '%s\n' "${fallback[@]}" fi if [[ $DISTRO == debian ]]; then case $(awk -F': ' '/^vendor_id/ { v = $2 } END { print v }' /proc/cpuinfo) in GenuineIntel) echo intel-microcode ;; AuthenticAMD) echo amd64-microcode ;; esac fi } configure() { local m pkgs grub_devs mount -t proc proc "$TARGET/proc" for m in dev dev/pts sys; do mount --bind "/$m" "$TARGET/$m"; done printf '#!/bin/sh\nexit 101\n' >"$TARGET/usr/sbin/policy-rc.d" chmod +x "$TARGET/usr/sbin/policy-rc.d" if [[ $DISTRO == debian ]]; then cat >"$TARGET/etc/apt/sources.list" <<-EOF deb $MIRROR $SUITE main non-free-firmware deb $MIRROR $SUITE-updates main non-free-firmware deb http://security.debian.org/debian-security $SUITE-security main non-free-firmware EOF pkgs=(linux-image-amd64 apparmor) # apparmor came via Recommends; kept as Debian's security default else cat >"$TARGET/etc/apt/sources.list" <<-EOF deb $MIRROR $SUITE main restricted universe deb $MIRROR $SUITE-updates main restricted universe deb http://security.ubuntu.com/ubuntu $SUITE-security main restricted universe EOF pkgs=(linux-image-generic initramfs-tools) # only a Recommends of Ubuntu's kernel; no initrd = no boot from RAID # Provides linux-firmware, which the kernel depends on, without pulling every vendor's firmware # (~600 MB): detect_firmware adds what this machine needs. pkgs+=(linux-firmware-minimal) fi write_resolv in_target apt-get update -q "$TARGET/etc/default/keyboard" pkgs+=(console-setup locales) if ((SSH)); then pkgs+=(openssh-server); fi if [[ $USER_NEW ]]; then pkgs+=(sudo); fi # Split out of the systemd package on newer releases; install only where they exist. for m in systemd-networkd $( ((NET_DHCP)) && echo systemd-resolved); do if in_target apt-cache show "$m" >/dev/null 2>&1; then pkgs+=("$m"); fi done if ((UEFI)); then pkgs+=(grub-efi-amd64 efibootmgr) printf 'grub-efi-amd64 grub2/update_nvram boolean false\n' | in_target debconf-set-selections else pkgs+=(grub-pc) printf -v grub_devs '%s, ' "${DISKS[@]}" printf 'grub-pc grub-pc/install_devices multiselect %s\n' "${grub_devs%, }" | in_target debconf-set-selections fi log "installing ${pkgs[*]}" # Minimal server: Recommends would add X11 bits (xauth), thermald, sound firmware and the like. in_target apt-get install -y -q --no-install-recommends "${pkgs[@]}" >"$TARGET/etc/locale.gen" in_target locale-gen fi in_target update-locale "LANG=$LOCALE" in_target setupcon --save-only # cached console keymap, used early in boot and by the initramfs echo "$HOSTNAME_NEW" >"$TARGET/etc/hostname" printf '127.0.0.1\tlocalhost\n127.0.1.1\t%s\n::1\tlocalhost ip6-localhost ip6-loopback\n' \ "$HOSTNAME_NEW" >"$TARGET/etc/hosts" write_fstab mkdir -p "$TARGET/etc/systemd/network" render_network >"$TARGET/etc/systemd/network/10-reroot.network" in_target systemctl enable systemd-networkd if ((NET_DHCP)); then # DNS servers come from DHCP; only systemd-resolved applies them. ln -sf /run/systemd/resolve/stub-resolv.conf "$TARGET/etc/resolv.conf" in_target systemctl enable systemd-resolved else write_resolv fi setup_accounts render_mdadm_conf >"$TARGET/etc/mdadm/mdadm.conf" in_target update-initramfs -u -k all rm -f "$TARGET/usr/sbin/policy-rc.d" in_target apt-get clean # the downloaded .debs, when they went to disk rm -f "$TARGET/etc/dpkg/dpkg.cfg.d/reroot-unsafe-io" } # Fixed md0 (/boot), md1 (/), ... like the Debian installer. Named ARRAY lines (/dev/md/boot) # make the new system assemble them as md127, md126, ... render_mdadm_conf() { local i m u printf 'HOMEHOST \nMAILADDR root\n' for i in "${!ARRAYS[@]}"; do m=${ARRAYS[i]} u=$(md_uuid "/dev/md/$m") printf 'ARRAY /dev/md%d metadata=1.2 UUID=%s name=%s:%s\n' "$i" "$u" "$HOSTNAME_NEW" "$m" done } md_uuid() { local u u=$(mdadm --detail --export "$1" | sed -n 's/^MD_UUID=//p') [[ $u ]] || die "no RAID UUID for $1" echo "$u" } uuid() { local u u=$(blkid -p -s UUID -o value "$1") [[ $u ]] || die "no UUID on $1" echo "$u" } write_fstab() { local fstab u u=$(uuid /dev/md/root) fstab=$(printf 'UUID=%s\t/\text4\terrors=remount-ro\t0 1' "$u") u=$(uuid /dev/md/boot) fstab+=$(printf '\nUUID=%s\t/boot\text4\tdefaults\t0 2' "$u") if ((HOME_PART)); then u=$(uuid /dev/md/home) fstab+=$(printf '\nUUID=%s\t/home\text4\tdefaults\t0 2' "$u") fi if [[ $SWAP_SIZE ]]; then u=$(uuid /dev/md/swap) fstab+=$(printf '\nUUID=%s\tnone\tswap\tsw\t0 0' "$u") fi if ((UEFI)); then u=$(uuid "$(part "${DISKS[0]}" esp)") fstab+=$(printf '\nUUID=%s\t/boot/efi\tvfat\tumask=0077,nofail\t0 1' "$u") fi printf '%s\n' "$fstab" >"$TARGET/etc/fstab" } install_bootloader() { local d i n if ((UEFI)); then log "installing GRUB (UEFI) on all ESPs" in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=reroot --no-nvram in_target grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --no-nvram in_target update-grub # shortcut: GRUB package updates only refresh the first ESP; re-copy after major GRUB upgrades. mkdir -p "$WORK/esp" for d in "${DISKS[@]:1}"; do mount "$(part "$d" esp)" "$WORK/esp" cp -r "$TARGET/boot/efi/." "$WORK/esp/" umount "$WORK/esp" done for n in $(efibootmgr | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\{0,1\} reroot.*/\1/p'); do efibootmgr -q -b "$n" -B done # Created last-to-first so disk 1 ends up first in BootOrder. for ((i = ${#DISKS[@]} - 1; i >= 0; i--)); do efibootmgr -q -c -d "$(readlink -f "${DISKS[i]}")" -p "$(part_no esp)" \ -L "reroot disk $((i + 1))" -l '\EFI\reroot\grubx64.efi' done else log "installing GRUB (BIOS) on all disks" for d in "${DISKS[@]}"; do in_target grub-install --target=i386-pc "$d"; done in_target update-grub fi } main() { trap 'die "failed at line $LINENO: $BASH_COMMAND"' ERR parse_args "$@" preflight check_ram capture_network if ((NET_DYNAMIC)); then NET_DHCP=1; fi # rescue got DHCP: preselect it load_ssh_keys detect_locale_keymap ask_settings setup_storage bootstrap configure install_bootloader for a in "${ARRAYS[@]}"; do md_sync "$a" idle; done sync umount -R "$TARGET" log "done. Reboot into the new system; RAID resync progress: cat /proc/mdstat" } # Run only when executed (also via curl | bash), not when sourced by the tests. if [[ ${BASH_SOURCE[0]:-$0} == "$0" ]]; then main "$@"; fi