# shellcheck shell=sh
#
# Proxmox: die Installation (Abschnitte `debian_installed` und `rebooted`) und
# die Paketquellen (`proxmox_installed`).
#
# ---------------------------------------------------------------------------
# Warum eine ISO unter QEMU und nicht installimage
# ---------------------------------------------------------------------------
#
# Hetzners `installimage` führt kein Proxmox — es gilt dort als nicht
# unterstützt. Wer es darüber versucht, installiert Debian und baut es danach
# um; genau das Zwischensystem, das dieser Entwurf abgeschafft hat.
#
# Der reguläre Weg ist seit PVE 8.2 die offizielle ISO mit eingebetteter
# Antwortdatei: `proxmox-auto-install-assistant prepare-iso` backt eine
# `answer.toml` hinein, QEMU startet sie im Rettungssystem gegen die *echten*
# Platten, und der Eintrag „Automated Installation" wird nach zehn Sekunden von
# selbst gewählt. Kein VNC, kein Klick.
#
# ---------------------------------------------------------------------------
# Die Codename-Tabelle
# ---------------------------------------------------------------------------
#
# Wörtlich übernommen aus `app/Provisioning/Steps/Host/InstallProxmoxVe.php`,
# das der Plattform-Plan löscht. Sie ist aus einem echten Fehler entstanden: ein
# fest verdrahtetes `bookworm` hätte auf Debian 13 PVE-8-Quellen gesetzt, also
# ein PVE gegen eine andere libc.
#
# Jede Ausgabe hat ihren EIGENEN Schlüssel und ihren eigenen Dateinamen —
# Proxmox signiert jede Suite einzeln und hat die Datei zwischen PVE 8 und PVE 9
# umbenannt. Ein gemeinsamer Name zeigte irgendwann auf den falschen Schlüssel,
# ohne dass etwas falsch aussähe.
#
# Bullseye/PVE 7 fehlt absichtlich: eine Debian-11-Maschine heute bedeutet, dass
# jemand das falsche Abbild gewählt hat, und ein lautes Scheitern ist die
# bessere Antwort als ein Hypervisor ohne Unterstützung.

# proxmox_release_for <codename> — gibt "suite|key_url|keyring" aus, oder 1.
proxmox_release_for() {
    case "$1" in
        bookworm)
            printf 'bookworm|https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg|proxmox-release-bookworm.gpg'
            ;;
        trixie)
            printf 'trixie|https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg|proxmox-archive-keyring.gpg'
            ;;
        *)
            return 1
            ;;
    esac
}

# Liest ID und VERSION_CODENAME aus /etc/os-release.
#
# Gesourct statt gegreppt, damit die Shell das Entwerten übernimmt, und mit
# einem Trenner ausgegeben, der in keinem der Werte vorkommen kann.
os_release_field() {
    (
        # shellcheck disable=SC1091
        . /etc/os-release 2>/dev/null || exit 1
        case "$1" in
            id) printf '%s' "${ID:-}" ;;
            codename) printf '%s' "${VERSION_CODENAME:-}" ;;
            pretty) printf '%s' "${PRETTY_NAME:-}" ;;
        esac
    )
}

# Schreibt die Proxmox-Paketquelle im deb822-Format.
#
# `Signed-By` begrenzt den Schlüssel auf GENAU diese Quelle. Ein Schlüssel in
# /etc/apt/trusted.gpg.d/ bürgt für JEDE Quelle der Maschine — der
# Proxmox-Schlüssel vouchte dann auch für alles, was später in `sources.list`
# auftaucht. Genau davor warnt apt seit mehreren Ausgaben.
write_proxmox_sources() {
    _suite="$1"
    _keyring_path="$2"
    _file="$3"

    cat > "$_file" <<EOF
# Written by CluPilot's bootstrap script.
# The suite is derived from /etc/os-release on this host, not assumed.
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: ${_suite}
Components: pve-no-subscription
Architectures: amd64
Signed-By: ${_keyring_path}
EOF
}

# Die Abonnement-Quellen, die ein Proxmox-Abbild mitbringt.
#
# Sie scheitern ohne Abonnement bei JEDEM `apt-get update` — und danach schlägt
# jede Paketinstallation der folgenden Abschnitte fehl. Das ist keine
# Schönheitskorrektur, sondern die Voraussetzung dafür, dass Traefik, WireGuard
# und die Vorlage überhaupt installiert werden können.
#
# Beide Schreibweisen, weil PVE 8 die einzeiligen `.list` benutzte und PVE 9 auf
# deb822 umgestellt hat. Und `ceph`, weil dieselbe Falle dort ein zweites Mal
# steht.
remove_subscription_sources() {
    for _f in \
        /etc/apt/sources.list.d/pve-enterprise.list \
        /etc/apt/sources.list.d/pve-enterprise.sources \
        /etc/apt/sources.list.d/ceph.list \
        /etc/apt/sources.list.d/ceph.sources \
        /etc/apt/sources.list.d/pve-install-repo.list
    do
        if [ -f "$_f" ]; then
            rm -f "$_f"
            log "entfernt: ${_f}"
        fi
    done
}

# ---------------------------------------------------------------------------
# Was diese Maschine gerade ist: Bootmodus, Netz, Platten
# ---------------------------------------------------------------------------

detect_boot_mode() {
    if [ -d /sys/firmware/efi ]; then
        printf 'uefi'
    else
        printf 'bios'
    fi
}

# `detect_primary_interface` steht in lib/bridge.sh — eine Fassung, zwei
# Benutzer. clupilot-bootstrap.sh lädt bridge.sh vor dieser Datei, und die
# Funktion wird ohnehin erst zur Aufrufzeit aufgelöst, also ist die Reihenfolge
# unkritisch.

# Setzt CLUPILOT_NET_CIDR, _GATEWAY, _DNS, _MAC aus dem, was das Rettungssystem
# gerade benutzt.
#
# Diese Werte gehen in die Antwortdatei, weil auf einer dedizierten Maschine
# kein DHCP antwortet. Was hier herauskommt, ist die Konfiguration des
# INSTALLIERTEN Systems — Task 5 baut sie danach zur Brücke um, und genau dort
# unterscheiden sich die Anbieter.
detect_network() {
    _iface="$(detect_primary_interface)"
    if [ -z "$_iface" ]; then
        return 1
    fi

    CLUPILOT_NET_IFACE="$_iface"
    CLUPILOT_NET_CIDR="$(ip -4 -o addr show dev "$_iface" scope global 2>/dev/null | awk '{ print $4; exit }')"
    CLUPILOT_NET_GATEWAY="$(ip -4 route show default 2>/dev/null | awk '{ print $3; exit }')"
    CLUPILOT_NET_MAC="$(cat "/sys/class/net/${_iface}/address" 2>/dev/null | tr -d ':')"
    CLUPILOT_NET_DNS="$(awk '/^nameserver/ { print $2; exit }' /etc/resolv.conf 2>/dev/null)"

    # Ohne Namensauflösung im installierten System käme es nie an die
    # Paketquellen. Ein öffentlicher Rückfall ist besser als ein leeres Feld,
    # das der Installer stillschweigend übergeht.
    [ -n "$CLUPILOT_NET_DNS" ] || CLUPILOT_NET_DNS='9.9.9.9'

    [ -n "$CLUPILOT_NET_CIDR" ] && [ -n "$CLUPILOT_NET_GATEWAY" ] && [ -n "$CLUPILOT_NET_MAC" ]
}

# Alle physischen Platten, kleinste zuerst nicht — Reihenfolge egal, Anzahl
# zählt. Wechseldatenträger, Schleifen und RAM-Platten fliegen raus.
detect_install_disks() {
    _disks=''
    for _dev in /sys/block/*; do
        _name="${_dev##*/}"
        case "$_name" in
            loop*|ram*|sr*|dm-*|md*|zram*|fd*) continue ;;
        esac
        [ -r "${_dev}/size" ] || continue
        [ "$(cat "${_dev}/size" 2>/dev/null || echo 0)" -gt 0 ] || continue
        # Wechseldatenträger: ein eingesteckter USB-Stick ist keine Systemplatte.
        [ "$(cat "${_dev}/removable" 2>/dev/null || echo 0)" = '0' ] || continue
        _disks="${_disks}${_disks:+ }${_name}"
    done

    printf '%s' "$_disks"
}

# ZFS-Aufstellung nach Plattenzahl.
#
# Eine Platte: raid0, weil es nichts zu spiegeln gibt — und weil ein Host mit
# einer Platte ohnehin ein Ausfall pro Platte ist. Zwei: raid1. Vier oder mehr,
# gerade: raid10. Drei oder eine ungerade Zahl über vier: raid1 über die ersten
# zwei, und der Rest bleibt liegen, statt eine Aufstellung zu raten, die
# hinterher niemand erwartet hat.
zfs_raid_for() {
    case "$1" in
        1) printf 'raid0' ;;
        2) printf 'raid1' ;;
        4|6|8|10) printf 'raid10' ;;
        *) printf 'raid1' ;;
    esac
}

# ---------------------------------------------------------------------------
# Werkzeug und ISO
# ---------------------------------------------------------------------------

# `proxmox-auto-install-assistant` ins RETTUNGSSYSTEM.
#
# Die Quelle richtet sich nach dem Codename des Rettungssystems, nicht nach dem
# des Ziels: das Werkzeug läuft hier, nicht dort. Unbekannter Codename bricht ab
# — nicht „trixie ist das neueste, also trixie". Das nächste Debian ist auch
# unbekannt, und dann sieht niemand hin.
# Welche PVE-Hauptversion zu welchem Debian gehört.
#
# Die andere Hälfte der Tabelle: `proxmox_release_for` sagt, welche Suite zu
# einem Debian passt; das hier sagt, welche PVE-Version dabei herauskommen muss.
# Gebraucht wird es, um ein ABBILD abzunehmen — bei dem niemand die Suite
# gewählt hat, sondern Proxmox das Paar schon zusammengestellt hatte. Passt es
# nicht, ist das Abbild falsch, und kein Nachbessern der Quellen heilt das.
pve_major_for_codename() {
    case "$1" in
        bookworm) printf '8' ;;
        trixie)   printf '9' ;;
        *)        return 1 ;;
    esac
}

# Legt den Proxmox-Release-Schlüssel ab und gibt seinen Pfad aus.
#
# Nach /etc/apt/keyrings/ und über `Signed-By` benannt, NICHT nach
# /etc/apt/trusted.gpg.d/ — dort bürgte er für jede Quelle der Maschine.
install_proxmox_key() {
    _key_url="$1"
    _keyring="$2"
    _keyring_path="/etc/apt/keyrings/${_keyring}"

    mkdir -p /etc/apt/keyrings
    if ! http_download "$_key_url" "$_keyring_path"; then
        return 1
    fi
    chmod 644 "$_keyring_path"

    printf '%s' "$_keyring_path"
}

# Zerlegt "suite|key_url|keyring" in CLUPILOT_PVE_SUITE, _KEY_URL, _KEYRING.
split_proxmox_release() {
    CLUPILOT_PVE_SUITE="${1%%|*}"
    _rest="${1#*|}"
    CLUPILOT_PVE_KEY_URL="${_rest%%|*}"
    CLUPILOT_PVE_KEYRING="${_rest##*|}"
}

ensure_autoinstall_assistant() {
    if command -v proxmox-auto-install-assistant >/dev/null 2>&1; then
        log 'proxmox-auto-install-assistant ist vorhanden'
        return 0
    fi

    _id="$(os_release_field id)"
    _codename="$(os_release_field codename)"

    if [ "$_id" != 'debian' ] || ! _release="$(proxmox_release_for "$_codename")"; then
        log "Rettungssystem meldet ID=${_id:-(leer)}, VERSION_CODENAME=${_codename:-(leer)} ($(os_release_field pretty))"
        return 1
    fi

    split_proxmox_release "$_release"

    if ! _keyring_path="$(install_proxmox_key "$CLUPILOT_PVE_KEY_URL" "$CLUPILOT_PVE_KEYRING")"; then
        log "Proxmox-Release-Schlüssel für ${_codename} nicht ladbar: ${CLUPILOT_PVE_KEY_URL}"
        return 1
    fi

    write_proxmox_sources "$CLUPILOT_PVE_SUITE" "$_keyring_path" '/etc/apt/sources.list.d/pve-install-repo.sources'

    export DEBIAN_FRONTEND=noninteractive
    apt-get update >/dev/null 2>&1 || return 1
    apt-get -y install proxmox-auto-install-assistant >/dev/null 2>&1 || return 1

    command -v proxmox-auto-install-assistant >/dev/null 2>&1
}

# Neueste PVE-9-ISO aus der Liste des Anbieters.
#
# Nicht fest verdrahtet: die Liste gehört Proxmox und wächst. Ein fester Name
# wäre in ein paar Monaten eine 404 mitten in einer Übernahme.
discover_pve_iso() {
    _listing="$(http_get "${CLUPILOT_ISO_MIRROR}/" || true)"
    [ -n "$_listing" ] || return 1

    printf '%s' "$_listing" \
        | grep -oE 'proxmox-ve_9[0-9.\-]*\.iso' \
        | sort -u -V \
        | tail -1
}

# Lädt die ISO und prüft ihre SHA256 gegen die Liste des Anbieters.
#
# Ohne diese Prüfung startet die Maschine ein Betriebssystem, das unterwegs
# irgendetwas geworden sein kann. Eine abgebrochene Übertragung reicht dafür
# schon; Bösartigkeit braucht es gar nicht.
fetch_proxmox_iso() {
    _name="$1"
    _target="$2"

    log "lade ${_name}"
    if ! http_download "${CLUPILOT_ISO_MIRROR}/${_name}" "$_target"; then
        return 1
    fi

    _sums="$(http_get "${CLUPILOT_ISO_MIRROR}/SHA256SUMS" || true)"
    if [ -z "$_sums" ]; then
        log 'SHA256SUMS nicht abrufbar'
        return 1
    fi

    _expected="$(printf '%s\n' "$_sums" | awk -v n="$_name" '$2 == n || $2 == "*" n { print $1; exit }')"
    if [ -z "$_expected" ]; then
        log "keine Prüfsumme für ${_name} in SHA256SUMS"
        return 1
    fi

    _actual="$(sha256sum "$_target" | awk '{ print $1 }')"
    if [ "$_actual" != "$_expected" ]; then
        log "Prüfsumme stimmt nicht: erwartet ${_expected}, bekommen ${_actual}"
        rm -f "$_target"
        return 1
    fi

    log "Prüfsumme stimmt (${_expected})"
    return 0
}

# ---------------------------------------------------------------------------
# Der Wiederanlauf nach dem Neustart
# ---------------------------------------------------------------------------
#
# `[first-boot]` mit `source = "from-iso"` nimmt GENAU EINE ausführbare Datei
# mit. Also trägt diese eine Datei alles: das Skript samt `lib/`, die
# Fortschrittsdatei und die Aufrufargumente — als Archiv in base64 im eigenen
# Rumpf.
#
# Das löst zugleich die Falle, an der „dieselbe Fortschrittsdatei" sonst
# scheitert: das Rettungssystem läuft im Arbeitsspeicher, sein Wurzelverzeichnis
# ist nach dem Neustart weg. Über die ISO kommt der Stand mit, ohne dass jemand
# den frisch angelegten ZFS-Pool aus dem Rettungssystem heraus einhängen müsste.
#
# Der WireGuard-Schlüssel liegt damit in der ISO. Die ISO liegt im
# Arbeitsspeicher des Rettungssystems und ist nach dem Neustart fort; der
# Schlüssel selbst wird in Task 9 ohnehin gegen einen frischen getauscht. Die
# entpackten Argumente bekommen 0600 in einem 0700-Verzeichnis.
build_first_boot_shim() {
    _shim="$1"
    _payload="${CLUPILOT_WORK_DIR}/payload.tar.gz"

    # Argumente für den wiederaufgenommenen Lauf festhalten.
    _args_file="${CLUPILOT_STATE_DIR}/arguments"
    umask 077
    cat > "$_args_file" <<EOF
CLUPILOT_CODE='${CLUPILOT_CODE}'
CLUPILOT_WG_PRIVATE='${CLUPILOT_WG_PRIVATE}'
CLUPILOT_WG_IP='${CLUPILOT_WG_IP}'
CLUPILOT_HUB_PUBKEY='${CLUPILOT_HUB_PUBKEY}'
CLUPILOT_HUB_ENDPOINT='${CLUPILOT_HUB_ENDPOINT}'
CLUPILOT_API='${CLUPILOT_API}'
CLUPILOT_FQDN='${CLUPILOT_FQDN}'
CLUPILOT_ROOT_SSH_KEY='${CLUPILOT_ROOT_SSH_KEY}'
EOF
    chmod 600 "$_args_file"

    # Laut statt still: läge das Arbeitsverzeichnis im Zustandsverzeichnis,
    # kopierte der nächste Schritt ein Verzeichnis in sich selbst und die ISO
    # landete im Hook. Beides wäre erst auf echter Hardware aufgefallen.
    case "$CLUPILOT_WORK_DIR" in
        "$CLUPILOT_STATE_DIR"|"$CLUPILOT_STATE_DIR"/*)
            log "Arbeitsverzeichnis ${CLUPILOT_WORK_DIR} liegt im Zustandsverzeichnis ${CLUPILOT_STATE_DIR} — das darf nicht sein"
            return 1
            ;;
    esac

    # Über ein Staging-Verzeichnis, damit im Archiv die ZIELpfade stehen. Ein
    # `tar -C <elternteil> <name>` schriebe `bootstrap/…` und `clupilot/…` hinein
    # — ausgepackt nach `/` ergäbe das `/bootstrap` und `/clupilot`, und der Hook
    # liefe ins Leere.
    _stage="${CLUPILOT_WORK_DIR}/stage"
    rm -rf "$_stage"
    mkdir -p "${_stage}/opt/clupilot" "${_stage}/var/lib"
    cp -a "$CLUPILOT_BOOTSTRAP_DIR" "${_stage}/opt/clupilot/bootstrap"
    cp -a "$CLUPILOT_STATE_DIR" "${_stage}/var/lib/clupilot"

    tar czf "$_payload" -C "$_stage" opt var
    rm -rf "$_stage"

    cat > "$_shim" <<'HEADER'
#!/bin/sh
# Von CluPilots Bootstrap erzeugt. Läuft einmal, beim ersten Hochfahren des
# frisch installierten Hosts, und nimmt den Lauf dort wieder auf, wo der
# Neustart ihn unterbrochen hat.
set -eu

mkdir -p /opt/clupilot /var/lib
sed -n '/^__NUTZLAST__$/,$p' "$0" | tail -n +2 | base64 -d | tar xzf - -C /
chmod 700 /var/lib/clupilot
[ -f /var/lib/clupilot/arguments ] && chmod 600 /var/lib/clupilot/arguments

exec /opt/clupilot/bootstrap/clupilot-bootstrap.sh --resume
HEADER

    printf '__NUTZLAST__\n' >> "$_shim"
    base64 "$_payload" >> "$_shim"
    chmod 755 "$_shim"
}

# ---------------------------------------------------------------------------
# Die Antwortdatei
# ---------------------------------------------------------------------------

write_answer_file() {
    _file="$1"
    _disks="$2"
    _raid="$3"

    # Als TOML-Liste: ["nvme0n1", "nvme1n1"]
    _disk_list="$(printf '%s' "$_disks" | awk '{ for (i = 1; i <= NF; i++) printf "%s\"%s\"", (i > 1 ? ", " : ""), $i }')"

    # Wird erzeugt und weggeworfen. Niemand braucht es: die Plattform meldet
    # sich nicht mehr per SSH an (Spec §6), und eine halb installierte Maschine
    # wird neu aufgesetzt statt nachgebessert. Ein aufbewahrtes Kennwort wäre
    # nur eine weitere Stelle, an der ein Geheimnis liegt.
    _root_password="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"

    _ssh_keys=''
    if [ -n "${CLUPILOT_ROOT_SSH_KEY:-}" ]; then
        _ssh_keys="root-ssh-keys = [\"${CLUPILOT_ROOT_SSH_KEY}\"]"
    fi

    cat > "$_file" <<EOF
# Von CluPilots Bootstrap erzeugt. Unbeaufsichtigte Installation nach
# https://pve.proxmox.com/wiki/Automated_Installation

[global]
keyboard = "de"
country = "de"
fqdn = "${CLUPILOT_FQDN}"
mailto = "root@localhost"
timezone = "Europe/Vienna"
root-password = "${_root_password}"
${_ssh_keys}
reboot-on-error = false

[network]
source = "from-answer"
cidr = "${CLUPILOT_NET_CIDR}"
gateway = "${CLUPILOT_NET_GATEWAY}"
dns = "${CLUPILOT_NET_DNS}"
filter.ID_NET_NAME_MAC = "*${CLUPILOT_NET_MAC}"

[disk-setup]
filesystem = "zfs"
disk-list = [${_disk_list}]

[disk-setup.zfs]
raid = "${_raid}"
# Die Vorgabe nimmt sich die Hälfte des Arbeitsspeichers. Den braucht ein Host,
# der Kunden trägt, für die Gäste.
arc-max = ${CLUPILOT_ZFS_ARC_MAX_MB}

[first-boot]
source = "from-iso"
# Nach dem Hochfahren, wenn das Netz steht — vorher könnte der Lauf CluPilot
# nicht erreichen und bliebe an seinem eigenen Melden hängen.
ordering = "fully-up"
EOF
}

# ---------------------------------------------------------------------------
# Der Lauf unter QEMU
# ---------------------------------------------------------------------------

# Ein Viertel des Arbeitsspeichers für den Installer, mindestens 2 GB, höchstens
# 8. Mehr bringt nichts, weniger lässt den Installer bei ZFS ins Straucheln
# kommen.
installer_memory_mb() {
    _total_kb="$(awk '/^MemTotal:/ { print $2; exit }' /proc/meminfo)"
    _mb=$((_total_kb / 1024 / 4))
    [ "$_mb" -lt 2048 ] && _mb=2048
    [ "$_mb" -gt 8192 ] && _mb=8192
    printf '%s' "$_mb"
}

installer_cpus() {
    _cpus="$(nproc 2>/dev/null || echo 2)"
    [ "$_cpus" -gt 8 ] && _cpus=8
    [ "$_cpus" -lt 1 ] && _cpus=1
    printf '%s' "$_cpus"
}

# Startet die vorbereitete ISO gegen die echten Platten.
#
# `-no-reboot`: der Installer startet am Ende neu, und QEMU beendet sich dabei.
# Genau daran erkennt dieses Skript, dass er fertig ist — ohne im Gast
# nachzusehen, was von außen nicht ginge.
#
# `-vnc 127.0.0.1:0` ist der Notausgang für den Fehlerfall: bleibt der Installer
# doch einmal in einer Maske stehen, kann der Betreiber über einen SSH-Tunnel ins
# Rettungssystem nachsehen, was auf dem Bildschirm steht. Nur auf localhost,
# damit daraus kein offener Dienst wird.
run_installer_under_qemu() {
    _iso="$1"
    _disks="$2"
    _boot_mode="$3"

    _drives=''
    for _disk in $_disks; do
        _drives="${_drives} -drive file=/dev/${_disk},format=raw,media=disk,if=virtio,cache=none"
    done

    _firmware=''
    if [ "$_boot_mode" = 'uefi' ]; then
        for _ovmf in /usr/share/OVMF/OVMF_CODE.fd /usr/share/ovmf/OVMF.fd /usr/share/qemu/OVMF.fd; do
            if [ -f "$_ovmf" ]; then
                _firmware="-bios ${_ovmf}"
                break
            fi
        done
        if [ -z "$_firmware" ]; then
            log 'UEFI-Maschine, aber keine OVMF-Firmware gefunden (Paket ovmf)'
            return 1
        fi
    fi

    log "QEMU startet: ${_boot_mode}, $(installer_memory_mb) MB, $(installer_cpus) Kerne, Platten: ${_disks}"

    # shellcheck disable=SC2086
    timeout "$CLUPILOT_INSTALL_TIMEOUT" qemu-system-x86_64 \
        -enable-kvm \
        -cpu host \
        -smp "$(installer_cpus)" \
        -m "$(installer_memory_mb)" \
        ${_firmware} \
        -cdrom "$_iso" \
        -boot d \
        ${_drives} \
        -netdev user,id=net0 -device virtio-net-pci,netdev=net0 \
        -display none \
        -vnc 127.0.0.1:0 \
        -no-reboot \
        > "${CLUPILOT_WORK_DIR}/qemu.log" 2>&1

    _rc=$?
    if [ "$_rc" -eq 124 ]; then
        log "QEMU nach ${CLUPILOT_INSTALL_TIMEOUT} s abgebrochen — der Installer ist nicht fertig geworden"
        return 1
    fi
    if [ "$_rc" -ne 0 ]; then
        log "QEMU endete mit ${_rc}; siehe ${CLUPILOT_WORK_DIR}/qemu.log"
        return 1
    fi

    return 0
}
