556 lines
20 KiB
Bash
556 lines
20 KiB
Bash
# 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
|
|
}
|
|
|
|
# Die Schnittstelle, über die die Vorgabe-Route geht. Nicht „die erste, die
|
|
# nicht lo heißt": eine Maschine mit zwei Karten hat oft eine angeschlossene und
|
|
# eine nicht.
|
|
detect_primary_interface() {
|
|
ip -4 route show default 2>/dev/null | awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i+1); exit } }'
|
|
}
|
|
|
|
# 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}'
|
|
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
|
|
}
|