CluPilotCloud/deploy/bootstrap/clupilot-bootstrap.sh

596 lines
25 KiB
Bash
Executable File

#!/bin/sh
set -eu
# Läuft im Rettungssystem des Anbieters, mit genau den Werten, die der
# Adminbereich in die kopierte Zeile geschrieben hat. Es holt nichts ab: alles,
# was vor dem Tunnel gebraucht wird, steht bereits hier (Spec §5).
#
# Der Ablauf sind Abschnitte. Jeder meldet sich zurück, sobald ein Weg zu
# CluPilot existiert; die davor werden lokal mitgeschrieben und nachgereicht.
# Die Abschnittsschlüssel sind die einzige Absprache mit dem Plattform-Plan und
# stehen unten in `main` in genau der Reihenfolge aus Spec §7.
#
# Wiederanlauf: die Fortschrittsdatei ist der Zustand. Ein Abschnitt, der schon
# `done` gemeldet hat, läuft nicht erneut — so nimmt der systemd-Dienst nach dem
# Neustart (Task 3) denselben Lauf wieder auf, statt bei Null anzufangen.
CLUPILOT_CODE=''
CLUPILOT_WG_PRIVATE=''
CLUPILOT_WG_IP=''
CLUPILOT_HUB_PUBKEY=''
CLUPILOT_HUB_ENDPOINT=''
CLUPILOT_API=''
# Freiwillig. Ohne diese Angabe gibt es keinen Weg per SSH auf die Maschine, und
# das ist die Vorgabe: die Plattform meldet sich nicht mehr an (Spec §6).
CLUPILOT_ROOT_SSH_KEY=''
# 1, wenn der `[first-boot]`-Hook uns nach dem Neustart wieder aufgenommen hat.
CLUPILOT_RESUMING=0
# Wird in `derive_fqdn` gesetzt; der Installer verlangt einen.
CLUPILOT_FQDN=''
# Wird von `run_section` gefüllt, wenn ein Abschnitt der Meldung etwas
# mitzugeben hat, und danach wieder geleert.
CLUPILOT_SECTION_NOTE=''
# ---------------------------------------------------------------------------
# Was diese Maschine mindestens können muss
# ---------------------------------------------------------------------------
#
# Die Rechnung steht offen da, weil die Zahl selbst nichts beweist. Das Repo
# hält keine Untergrenze bereit — die Pläne liegen in der Datenbank, nicht in
# `config/provisioning.php` —, also wird hier gerechnet statt behauptet:
#
# goldene Vorlage (Debian-Cloud-Image + Docker + Nextcloud) ~20 GB
# Proxmox selbst, Auslagerung, Sicherungen ~20 GB
# ein kleinster Kunde, damit der Host überhaupt einen trägt ~50 GB
# ------------------------------------------------------------------
# ~90 GB
#
# Aufgerundet auf 100. Das ist ausdrücklich KEINE Kapazitätsplanung: die macht
# `HostCapacity` mit `reserve_pct`, nachdem der Host registriert ist. Diese
# Prüfung weist nur die Maschine ab, auf der das Vorhaben von vornherein nicht
# aufgeht — eine 40-GB-Cloud-Instanz, auf der jemand versehentlich die Zeile
# eingefügt hat.
CLUPILOT_MIN_DISK_GB=100
# Über eine Stunde daneben, und es ist keine Gangungenauigkeit mehr, sondern
# eine Uhr, die nie gestellt wurde. Genau dann scheitert jede TLS-Prüfung —
# Paketquellen, Let's Encrypt, der eigene Rückweg — mit einer Meldung, die von
# Zertifikaten redet und nicht von der Uhrzeit.
CLUPILOT_MAX_CLOCK_DRIFT=3600
# Wo die ISO gebaut wird — AUSDRÜCKLICH nicht im Zustandsverzeichnis.
#
# Die beiden haben entgegengesetzte Lebensdauern: `/var/lib/clupilot` muss den
# Neustart überleben und wandert dafür in den first-boot-Hook; dieses hier darf
# das auf keinen Fall, denn hier liegt die ISO. Läge es darin, packte der Hook
# ein Gigabyte base64 in sich selbst — und würde in genau die ISO gebacken, die
# er enthält.
#
# Das war kein gedachter Fall: die erste Fassung legte `work/` unter das
# Zustandsverzeichnis, und `cp` weigerte sich, ein Verzeichnis in sich selbst zu
# kopieren. Getrennte Orte machen aus der Regel, die man sich merken muss, eine
# Eigenschaft, die man nicht verletzen kann.
CLUPILOT_WORK_DIR="${CLUPILOT_WORK_DIR:-/var/tmp/clupilot-work}"
CLUPILOT_ISO_MIRROR="${CLUPILOT_ISO_MIRROR:-https://enterprise.proxmox.com/iso}"
# ZFS nimmt sich sonst die Hälfte des Arbeitsspeichers für den Lesecache. Ein
# Achtel, mindestens 2 GB — der Rest gehört den Gästen.
CLUPILOT_ZFS_ARC_MAX_MB="${CLUPILOT_ZFS_ARC_MAX_MB:-2048}"
# Eine Stunde. Eine Proxmox-Installation auf ZFS braucht Minuten, keine Stunde;
# wer diese Grenze reißt, hängt und läuft nicht langsam.
CLUPILOT_INSTALL_TIMEOUT="${CLUPILOT_INSTALL_TIMEOUT:-3600}"
usage() {
cat <<'TEXT'
clupilot-bootstrap.sh — macht aus einer Maschine im Rettungssystem einen
fertigen Proxmox-Host.
--code <einmal-code> Ausweis für den Rückweg, 32 Zeichen
--wg-private <schlüssel> WireGuard-Privatschlüssel, von CluPilot erzeugt
--wg-ip <adresse/präfix> zugeteilte Tunnel-Adresse, z. B. 10.66.0.11/24
--hub-pubkey <schlüssel> öffentlicher Schlüssel des Hubs
--hub-endpoint <host:port> Adresse des Hubs
--api <url> CluPilot über die TUNNEL-Adresse, ohne Schrägstrich
am Ende, z. B. http://10.66.0.1
Freiwillig:
--root-ssh-key <schlüssel> öffentlicher Schlüssel für root. Ohne ihn gibt es
keinen SSH-Zugang auf den fertigen Host.
--resume nimmt einen unterbrochenen Lauf wieder auf. Setzt
der first-boot-Hook nach dem Neustart selbst; von
Hand braucht man das nicht.
Diese Zeile wird nicht abgetippt. Der Adminbereich zeigt sie fertig zum
Kopieren, wenn ein Host angelegt wird.
TEXT
}
# Nach stderr, damit stdout frei bleibt, und zusätzlich in eine Datei — der
# Betreiber sieht die Konsole, aber wer hinterher nachsieht, hat nur die Datei.
log() {
_line="$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*"
printf '%s\n' "$_line" >&2
[ -d "${CLUPILOT_STATE_DIR:-}" ] && printf '%s\n' "$_line" >> "${CLUPILOT_STATE_DIR}/bootstrap.log"
return 0
}
die() {
printf 'clupilot-bootstrap: %s\n' "$*" >&2
exit 1
}
parse_arguments() {
while [ $# -gt 0 ]; do
case "$1" in
--code) CLUPILOT_CODE="${2:-}"; shift 2 ;;
--wg-private) CLUPILOT_WG_PRIVATE="${2:-}"; shift 2 ;;
--wg-ip) CLUPILOT_WG_IP="${2:-}"; shift 2 ;;
--hub-pubkey) CLUPILOT_HUB_PUBKEY="${2:-}"; shift 2 ;;
--hub-endpoint) CLUPILOT_HUB_ENDPOINT="${2:-}"; shift 2 ;;
--api) CLUPILOT_API="${2:-}"; shift 2 ;;
--root-ssh-key) CLUPILOT_ROOT_SSH_KEY="${2:-}"; shift 2 ;;
--resume) CLUPILOT_RESUMING=1; shift ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "unbekannte Angabe: $1" ;;
esac
done
# Der wiederaufgenommene Lauf nach dem Neustart bekommt keine Befehlszeile
# mehr — der `[first-boot]`-Hook ruft nur `--resume`. Die Werte stehen in
# der Datei, die vor dem Neustart mit in die ISO gegangen ist.
if [ "$CLUPILOT_RESUMING" = '1' ]; then
_saved="${CLUPILOT_STATE_DIR:-/var/lib/clupilot}/arguments"
[ -r "$_saved" ] || die "--resume, aber ${_saved} ist nicht lesbar. Der Lauf lässt sich nicht fortsetzen; die Maschine wird neu aufgesetzt (Runbook)."
# shellcheck disable=SC1090
. "$_saved"
fi
# Alle sechs sind Pflicht. Es gibt keine sinnvolle Vorgabe für einen davon,
# und ein fehlender fällt sonst erst mitten in einer Installation auf, die
# die Platte schon überschrieben hat.
[ -n "$CLUPILOT_CODE" ] || die '--code fehlt'
[ -n "$CLUPILOT_WG_PRIVATE" ] || die '--wg-private fehlt'
[ -n "$CLUPILOT_WG_IP" ] || die '--wg-ip fehlt'
[ -n "$CLUPILOT_HUB_PUBKEY" ] || die '--hub-pubkey fehlt'
[ -n "$CLUPILOT_HUB_ENDPOINT" ] || die '--hub-endpoint fehlt'
[ -n "$CLUPILOT_API" ] || die '--api fehlt'
# Ein Schrägstrich am Ende ergäbe `…//host/progress`. Manche Router
# beantworten das, manche nicht — und der Unterschied fiele erst beim ersten
# Melden auf, also nach dem Tunnelbeitritt.
CLUPILOT_API="${CLUPILOT_API%/}"
}
# Findet das Verzeichnis, in dem dieses Skript samt `lib/` liegt.
#
# Ausgeliefert wird als Archiv nach `/opt/clupilot/bootstrap`; der Weg über `$0`
# steht davor, damit sich das Skript auch aus einem ausgecheckten Repo heraus
# starten lässt, ohne es vorher zu installieren.
resolve_bootstrap_dir() {
_self_dir="$(cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)"
if [ -n "$_self_dir" ] && [ -f "${_self_dir}/lib/report.sh" ]; then
printf '%s' "$_self_dir"
return 0
fi
if [ -f '/opt/clupilot/bootstrap/lib/report.sh' ]; then
printf '%s' '/opt/clupilot/bootstrap'
return 0
fi
die 'lib/report.sh nicht gefunden — das Skript wurde ohne seine Bibliothek kopiert'
}
# Läuft dieser Lauf noch im Rettungssystem, oder schon auf dem installierten
# Host?
#
# Dieselbe Unterscheidung wie in `check_is_rescue_system`, nur als schlichte
# Frage — und die eine Stelle, an der `section_rebooted` erkennt, dass der
# Neustart hinter ihm liegt statt vor ihm.
running_in_rescue_system() {
_fstype="$(root_field type)"
_device="$(root_field dev)"
case "$_fstype" in
tmpfs|ramfs|rootfs|overlay|overlayfs|squashfs|aufs) return 0 ;;
esac
case "$_device" in
/dev/ram*|/dev/nfs|none) return 0 ;;
esac
return 1
}
# Der Installer verlangt einen FQDN, und die Befehlszeile bringt keinen mit.
#
# Erst die Rückwärtsauflösung der öffentlichen Adresse — das ist der Name, den
# der Anbieter für diese Maschine führt. Sonst ein Name aus der Tunneladresse,
# der eindeutig ist und niemanden in die Irre führt. In Task 9 ersetzt CluPilot
# ihn ohnehin durch den, den es selbst führt.
derive_fqdn() {
_ip="$(ip -4 -o addr show scope global 2>/dev/null | awk '{ sub(/\/.*/, "", $4); print $4; exit }')"
if [ -n "$_ip" ] && command -v getent >/dev/null 2>&1; then
_name="$(getent hosts "$_ip" 2>/dev/null | awk '{ print $2; exit }')"
case "$_name" in
*.*.*)
CLUPILOT_FQDN="$_name"
return 0
;;
esac
fi
_suffix="$(printf '%s' "$CLUPILOT_WG_IP" | sed -e 's#/.*##' -e 's/\./-/g')"
CLUPILOT_FQDN="host-${_suffix}.clupilot.net"
}
# ---------------------------------------------------------------------------
# Abschnitt 1: rescue_checked
# ---------------------------------------------------------------------------
#
# Läuft, BEVOR irgendetwas geschrieben wird. Das ist die einzige Gelegenheit
# dieses Skripts, eine Platte NICHT zu überschreiben — danach gibt es keine
# mehr.
#
# Alle Befunde werden gesammelt und zusammen gemeldet, statt beim ersten
# auszusteigen. Wer eine Maschine im Rettungssystem sitzen hat, will einmal
# lesen, was ihr fehlt, und nicht sechsmal von vorn anfangen.
#
# Eine unbekannte Lage führt zur Ablehnung, nicht zum Weitermachen. Es gibt
# absichtlich keinen Schalter, der die Prüfung übergeht: ein solcher Schalter
# wird genau einmal benutzt, und zwar an dem Abend, an dem er nicht hätte
# benutzt werden dürfen. Ein Rettungssystem, das hier nicht erkannt wird, gehört
# ins Runbook (Task 10) und nicht in eine Umgehung.
CLUPILOT_REFUSALS=''
CLUPILOT_NOTES=''
# Bewusst ohne TLS und bewusst gegen eine Adresse, die es ohnehin gleich
# braucht. Überschreibbar für Netze, in denen deb.debian.org nicht erreichbar
# ist — und damit sich die Uhrprüfung gegen eine Antwort mit bekannter Uhrzeit
# durchspielen lässt.
CLUPILOT_PROBE_URL="${CLUPILOT_PROBE_URL:-http://deb.debian.org/}"
refuse() {
CLUPILOT_REFUSALS="${CLUPILOT_REFUSALS} - $1
"
return 0
}
note() {
CLUPILOT_NOTES="${CLUPILOT_NOTES}${CLUPILOT_NOTES:+; }$1"
return 0
}
root_field() {
awk -v want="$1" '$2 == "/" { print (want == "dev" ? $1 : $3); exit }' /proc/mounts
}
# Größte physische Platte in Bytes.
#
# Aus `/sys/block` statt aus `lsblk`, weil das keine Abhängigkeit hat und in
# jedem Rettungssystem vorhanden ist. `size` zählt dort immer 512-Byte-Sektoren,
# unabhängig davon, mit welcher Sektorgröße die Platte selbst arbeitet — eine
# Vier-Kilobyte-Platte meldet hier trotzdem Vielfache von 512.
largest_disk_bytes() {
_max=0
for _dev in /sys/block/*; do
_name="${_dev##*/}"
case "$_name" in
loop*|ram*|sr*|dm-*|md*|zram*|fd*) continue ;;
esac
[ -r "${_dev}/size" ] || continue
_sectors="$(cat "${_dev}/size" 2>/dev/null || echo 0)"
_bytes=$((_sectors * 512))
if [ "$_bytes" -gt "$_max" ]; then
_max="$_bytes"
fi
done
printf '%s' "$_max"
}
# Der teuerste denkbare Fehlgriff dieses Skripts: eine laufende Produktivmaschine
# für ein Rettungssystem halten und ihre Platte überschreiben.
#
# Deshalb wird ein POSITIVER Nachweis verlangt, nicht das Fehlen eines
# Gegenbeweises. Ein Rettungssystem läuft aus dem Arbeitsspeicher; sein
# Wurzelverzeichnis liegt auf tmpfs, einem Overlay oder einer RAM-Platte. Ein
# installiertes System hat dort eine echte Partition.
check_is_rescue_system() {
if ! running_in_rescue_system; then
refuse "Das Wurzelverzeichnis liegt auf $(root_field dev) ($(root_field type)), also auf einer echten Partition. Ein Rettungssystem läuft aus dem Arbeitsspeicher. Diese Maschine sieht aus wie ein installiertes System, und dieses Skript überschreibt Platten — es macht hier nicht weiter. Rettungssystem im Kundenbereich des Anbieters starten und neu einfügen."
fi
# Ein laufender Hypervisor ist die eine Maschine, die auf keinen Fall
# angefasst werden darf — auf ihr liegen Kunden.
if command -v pveversion >/dev/null 2>&1; then
refuse "Auf dieser Maschine läuft bereits Proxmox VE ($(pveversion 2>/dev/null | head -1)). Das ist ein Host im Betrieb, kein leeres Blech. Wenn er neu aufgesetzt werden soll, erst im Adminbereich entfernen, dann Rettungssystem starten."
fi
# Im Rettungssystem sind die Zielplatten normalerweise nicht eingehängt. Ist
# doch eine da, hat entweder jemand von Hand nachgesehen — dann soll er
# aushängen — oder es ist doch das laufende System.
_mounted="$(awk '$1 ~ /^\/dev\/(sd|nvme|vd|hd|xvd)/ { printf "%s auf %s, ", $1, $2 }' /proc/mounts | sed -e 's/, $//')"
if [ -n "$_mounted" ]; then
refuse "Es sind Partitionen echter Platten eingehängt (${_mounted}). Solange die dort liegen, weiß dieses Skript nicht, ob es eine leere Maschine oder Ihre Daten vor sich hat. Aushängen und noch einmal einfügen."
fi
}
# Ohne verschachtelte Virtualisierung startet kein Gast, und damit ist der ganze
# Host nutzlos — das fiele sonst erst beim ersten bezahlten Auftrag auf.
check_nested_virtualisation() {
_flags="$(grep -cE '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo 2>/dev/null || echo 0)"
if [ "$_flags" -eq 0 ]; then
refuse 'Die CPU meldet weder vmx noch svm in /proc/cpuinfo — auf dieser Maschine gibt es keine Hardware-Virtualisierung. Bei Cloud-Produkten (Hetzner CPX/CX, teils netcup) ist das normal und nicht abstellbar; es braucht eine dedizierte Maschine.'
return 0
fi
if [ ! -c /dev/kvm ]; then
refuse 'Die CPU kann Virtualisierung, aber /dev/kvm gibt es nicht. Entweder ist sie im BIOS abgeschaltet, oder das Rettungssystem hat das kvm-Modul nicht geladen (`modprobe kvm_intel` bzw. `kvm_amd`).'
fi
}
check_disk_size() {
_bytes="$(largest_disk_bytes)"
_gb=$((_bytes / 1073741824))
if [ "$_gb" -eq 0 ]; then
refuse 'Unter /sys/block ist keine physische Platte zu finden. Ohne Platte gibt es nichts zu installieren.'
return 0
fi
if [ "$_gb" -lt "$CLUPILOT_MIN_DISK_GB" ]; then
refuse "Die größte Platte hat ${_gb} GB, gebraucht werden mindestens ${CLUPILOT_MIN_DISK_GB} GB (Vorlage, Proxmox selbst, ein kleinster Kunde). Diese Maschine trägt das Vorhaben nicht."
return 0
fi
note "größte Platte ${_gb} GB"
}
# Netz und Uhr in einem Abruf: der `Date`-Kopf einer gewöhnlichen HTTP-Antwort
# beantwortet beides. Ausdrücklich ohne TLS — sonst prüfte eine falsche Uhr
# sich selbst und meldete einen Zertifikatsfehler.
check_network_and_clock() {
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
refuse 'Weder curl noch wget vorhanden. Ohne eines von beidem kann sich diese Maschine nie bei CluPilot melden.'
return 0
fi
_headers=''
if command -v curl >/dev/null 2>&1; then
_headers="$(curl -sS -I --connect-timeout 5 --max-time 15 "$CLUPILOT_PROBE_URL" 2>/dev/null || true)"
else
_headers="$(wget -q -S --timeout=15 --spider "$CLUPILOT_PROBE_URL" 2>&1 || true)"
fi
if [ -z "$_headers" ]; then
refuse "Keine Antwort von ${CLUPILOT_PROBE_URL}. Ohne Netz kommt weder Debian noch Proxmox auf diese Maschine. Netz und Namensauflösung im Rettungssystem prüfen."
return 0
fi
_remote_date="$(printf '%s' "$_headers" | sed -n 's/^[[:space:]]*[Dd]ate:[[:space:]]*//p' | tr -d '\r' | head -1)"
if [ -z "$_remote_date" ]; then
note 'Uhr nicht geprüft (Antwort ohne Date-Kopf)'
return 0
fi
# `date -d` ist GNU-eigen. Wo es das nicht gibt (busybox), wird die Uhr
# NICHT geprüft — und das steht dann auch so in der Meldung. Eine Prüfung,
# die still durchwinkt, ist schlimmer als keine: sie hält den Nächsten vom
# Nachsehen ab.
_remote_epoch="$(date -u -d "$_remote_date" '+%s' 2>/dev/null || true)"
if [ -z "$_remote_epoch" ]; then
note 'Uhr nicht geprüft (date -d nicht verfügbar)'
return 0
fi
_local_epoch="$(date -u '+%s')"
_drift=$((_local_epoch - _remote_epoch))
[ "$_drift" -lt 0 ] && _drift=$((-_drift))
if [ "$_drift" -gt "$CLUPILOT_MAX_CLOCK_DRIFT" ]; then
refuse "Die Uhr geht ${_drift} Sekunden falsch (hier $(date -u '+%Y-%m-%d %H:%M:%SZ'), draußen ${_remote_date}). Damit scheitert jede TLS-Prüfung — Paketquellen, Let's Encrypt, der eigene Rückweg — mit einer Meldung über Zertifikate statt über die Uhrzeit. Stellen mit: date -u -s '${_remote_date}'"
return 0
fi
note "Uhr auf ${_drift} s genau"
}
section_rescue_checked() {
CLUPILOT_REFUSALS=''
CLUPILOT_NOTES=''
check_is_rescue_system
check_nested_virtualisation
check_disk_size
check_network_and_clock
if [ -n "$CLUPILOT_REFUSALS" ]; then
printf '\nDiese Maschine ist für die Übernahme nicht geeignet:\n\n%s\n' "$CLUPILOT_REFUSALS" >&2
CLUPILOT_SECTION_NOTE="$(printf '%s' "$CLUPILOT_REFUSALS" | tr '\n' ' ' | sed -e 's/ */ /g' -e 's/^ *//')"
return 1
fi
CLUPILOT_SECTION_NOTE="$CLUPILOT_NOTES"
return 0
}
# ---------------------------------------------------------------------------
# Abschnitt 2: debian_installed — das Grundsystem auf die Platte
# ---------------------------------------------------------------------------
#
# „debian_installed" heißt hier **Grundsystem geschrieben**. Es entsteht kein
# Debian, das nachher jemand zu Proxmox umbaut: die offizielle Proxmox-ISO wird
# mit einer Antwortdatei versehen und unter QEMU gegen die echten Platten
# gestartet. Der Schlüssel bleibt trotzdem, weil er die einzige Absprache mit
# dem Plattform-Plan ist — und weil Proxmox VE Debian mit PVE-Kernel ist.
section_debian_installed() {
mkdir -p "$CLUPILOT_WORK_DIR"
for _tool in qemu-system-x86_64 sha256sum timeout tar base64; do
command -v "$_tool" >/dev/null 2>&1 || {
CLUPILOT_SECTION_NOTE="${_tool} fehlt im Rettungssystem"
return 1
}
done
if ! detect_network; then
CLUPILOT_SECTION_NOTE='Netzkonfiguration des Rettungssystems nicht ermittelbar (Adresse, Gateway oder MAC fehlt)'
return 1
fi
_disks="$(detect_install_disks)"
if [ -z "$_disks" ]; then
CLUPILOT_SECTION_NOTE='keine physische Platte gefunden'
return 1
fi
_count="$(printf '%s' "$_disks" | wc -w | tr -d ' ')"
_raid="$(zfs_raid_for "$_count")"
_boot_mode="$(detect_boot_mode)"
derive_fqdn
log "Ziel: ${CLUPILOT_FQDN}, ${_boot_mode}, ZFS ${_raid} über ${_count} Platte(n): ${_disks}"
if ! ensure_autoinstall_assistant; then
CLUPILOT_SECTION_NOTE='proxmox-auto-install-assistant nicht installierbar'
return 1
fi
_iso_name="$(discover_pve_iso)"
if [ -z "$_iso_name" ]; then
CLUPILOT_SECTION_NOTE="keine PVE-9-ISO unter ${CLUPILOT_ISO_MIRROR} gefunden"
return 1
fi
_iso="${CLUPILOT_WORK_DIR}/${_iso_name}"
if [ ! -f "$_iso" ] && ! fetch_proxmox_iso "$_iso_name" "$_iso"; then
CLUPILOT_SECTION_NOTE="${_iso_name} nicht ladbar oder Prüfsumme falsch"
return 1
fi
build_first_boot_shim "${CLUPILOT_WORK_DIR}/first-boot.sh"
write_answer_file "${CLUPILOT_WORK_DIR}/answer.toml" "$_disks" "$_raid"
# Vor dem Backen prüfen. Eine Antwortdatei mit einem Tippfehler lässt den
# Installer in den interaktiven Modus fallen — und dort hängt er dann
# unsichtbar in QEMU, bis die Stunde abgelaufen ist.
if ! proxmox-auto-install-assistant validate-answer "${CLUPILOT_WORK_DIR}/answer.toml" >> "${CLUPILOT_WORK_DIR}/assistant.log" 2>&1; then
CLUPILOT_SECTION_NOTE="answer.toml abgelehnt; siehe ${CLUPILOT_WORK_DIR}/assistant.log"
return 1
fi
# Der Assistent legt die fertige ISO neben die Quelle und benennt sie selbst.
# Deshalb wird sie danach gesucht statt vorher benannt — ein erfundener
# `--output`-Schalter wäre die Sorte Annahme, die genau einmal auffällt.
rm -f "${CLUPILOT_WORK_DIR}"/*auto*.iso
if ! (cd "$CLUPILOT_WORK_DIR" && proxmox-auto-install-assistant prepare-iso "$_iso" \
--fetch-from iso \
--answer-file "${CLUPILOT_WORK_DIR}/answer.toml" \
--on-first-boot "${CLUPILOT_WORK_DIR}/first-boot.sh") >> "${CLUPILOT_WORK_DIR}/assistant.log" 2>&1; then
CLUPILOT_SECTION_NOTE="prepare-iso fehlgeschlagen; siehe ${CLUPILOT_WORK_DIR}/assistant.log"
return 1
fi
_prepared="$(ls -1 "${CLUPILOT_WORK_DIR}"/*auto*.iso 2>/dev/null | head -1)"
if [ -z "$_prepared" ]; then
CLUPILOT_SECTION_NOTE='prepare-iso meldete Erfolg, hat aber keine ISO hinterlassen'
return 1
fi
if ! run_installer_under_qemu "$_prepared" "$_disks" "$_boot_mode"; then
CLUPILOT_SECTION_NOTE="Installer nicht durchgelaufen; siehe ${CLUPILOT_WORK_DIR}/qemu.log"
return 1
fi
CLUPILOT_SECTION_NOTE="Proxmox aus ${_iso_name} auf ZFS ${_raid} über ${_count} Platte(n)"
return 0
}
# ---------------------------------------------------------------------------
# Abschnitt 3: rebooted — die einzige unumkehrbare Stelle
# ---------------------------------------------------------------------------
#
# Dieser Abschnitt läuft ZWEIMAL, auf zwei verschiedenen Systemen. Im
# Rettungssystem startet er neu und kommt nie zurück; nach dem Hochfahren nimmt
# der `[first-boot]`-Hook den Lauf wieder auf, und dann stellt derselbe
# Abschnitt fest, dass er hinter sich liegt, und meldet ihn.
#
# Deshalb gibt es hier auch kein `report rebooted done` vor dem Neustart: eine
# Meldung, die einen Neustart ankündigt, der dann nicht klappt, ist schlimmer
# als gar keine.
section_rebooted() {
if ! running_in_rescue_system; then
CLUPILOT_SECTION_NOTE="wieder da auf $(uname -r)"
return 0
fi
# Alles, was noch liegt, ein letztes Mal versuchen. Ab hier ist diese
# Fortschrittsdatei nur noch über die ISO erreichbar.
flush_reports
log 'Neustart aus dem Rettungssystem. Der first-boot-Hook nimmt den Lauf wieder auf.'
sync
(sleep 5; systemctl reboot 2>/dev/null || reboot) &
# Nicht mit `exit 0` enden: das Skript ist an dieser Stelle nicht fertig,
# es gibt nur die Maschine ab. Der Rückgabewert wäre eine Aussage über
# etwas, das erst nach dem Hochfahren feststeht.
sleep 120
die 'Neustart wurde angestoßen, aber die Maschine läuft nach zwei Minuten noch. Über die Anbieterkonsole nachsehen.'
}
main() {
parse_arguments "$@"
CLUPILOT_BOOTSTRAP_DIR="$(resolve_bootstrap_dir)"
# shellcheck source=lib/report.sh
. "${CLUPILOT_BOOTSTRAP_DIR}/lib/report.sh"
# shellcheck source=lib/proxmox.sh
. "${CLUPILOT_BOOTSTRAP_DIR}/lib/proxmox.sh"
init_reporting
if [ "$CLUPILOT_RESUMING" = '1' ]; then
log "Lauf wieder aufgenommen nach dem Neustart — $(uname -r)"
else
log "Bootstrap beginnt — Tunnel-Adresse ${CLUPILOT_WG_IP}, CluPilot unter ${CLUPILOT_API}"
fi
# Vor jedem Abschnitt: was liegengeblieben ist, geht zuerst raus. Nach dem
# Tunnelbeitritt ist das der Aufruf, der die ganze Vorgeschichte nachreicht.
flush_reports
# Die Abschnitte in der Reihenfolge aus Spec §7. Was noch fehlt, steht als
# Kommentar da, damit die Lücke sichtbar bleibt statt nur nicht zu wirken.
if ! run_section rescue_checked section_rescue_checked; then
die 'Abbruch nach der Eingangsprüfung. Es wurde nichts geschrieben — die Platte ist unangetastet.'
fi
if ! run_section debian_installed section_debian_installed; then
die 'Abbruch: das Grundsystem ist nicht auf die Platte gekommen. Die Maschine wird neu aufgesetzt, nicht nachgebessert (Runbook).'
fi
# Kommt im Rettungssystem nicht zurück — die Maschine startet hier neu.
if ! run_section rebooted section_rebooted; then
die 'Abbruch beim Neustart.'
fi
# run_section proxmox_installed section_proxmox_installed # Task 4
# run_section network_bridged section_network_bridged # Task 5
# run_section wireguard_joined section_wireguard_joined # Task 6
# run_section traefik_running section_traefik_running # Task 7
# run_section template_built section_template_built # Task 8
# run_section registered section_registered # Task 9
log 'Proxmox läuft. Die Abschnitte ab Task 4 sind noch nicht eingehängt.'
}
main "$@"