CluPilotCloud/deploy/bootstrap/lib/template-run.sh

216 lines
8.3 KiB
Bash

#!/bin/sh
# shellcheck shell=sh
#
# Treiber für den Vorlagenbau, wenn er aus der CluPilot-Pipeline kommt.
#
# `App\Provisioning\Steps\Host\BuildVmTemplate` lädt diese Datei zusammen mit
# `template.sh` und `assets/docker-compose.yml` auf den Host und startet sie
# abgekoppelt. Die eigentliche Arbeit macht `template.sh` — hier steht nur, in
# welcher Reihenfolge, und wie der Fortschritt zurückgemeldet wird.
#
# ---------------------------------------------------------------------------
# Warum das Skript hochgeladen und nicht in PHP nachgebaut wird
# ---------------------------------------------------------------------------
#
# `template.sh` prüft drei Fallen, die je einen bezahlten Auftrag gekostet
# haben. Eine zweite Fassung davon in PHP wären zwei Installationen, die bei
# jeder Proxmox-Version nachgezogen werden müssten — und die zweite fiele erst
# auf, wenn jemand sie benutzt. Es gibt eine Fassung, und sie liegt eine Datei
# weiter.
#
# ---------------------------------------------------------------------------
# Rückmeldung
# ---------------------------------------------------------------------------
#
# Im Arbeitsverzeichnis:
#
# state running | ok | failed
# pid die PID dieses Skripts — und, weil es per `setsid` gestartet
# wird, zugleich die seiner PROZESSGRUPPE. Das ist der Unterschied
# zwischen „Aufgeben" und „Aufgeben, während der Bau weiterläuft":
# das Skript wartet die meiste Zeit auf ein virt-customize, und nur
# über die Gruppe ist das mit zu beenden.
# phase die laufende Phase, für die Fortschrittszeile in der Konsole
# note der Grund im Fehlerfall
# build.log alles
#
# `state` allein ist keine Aussage über den Lauf: stirbt das Skript, bleibt dort
# für immer `running` stehen, weil niemand mehr da ist, der es ändert. Deshalb
# fragt der Schritt zusätzlich `kill -0` gegen `pid` — „läuft noch" heißt, der
# Prozess lebt.
set -u
CLUPILOT_WORK_DIR="${CLUPILOT_WORK_DIR:-/var/lib/clupilot/template-build}"
# Als ALLERERSTES, vor jedem Einlesen und jeder Prüfung: der Startbefehl wartet
# darauf, dass hier etwas steht, und bis dahin gilt der Bau als noch nicht
# angelaufen. `$$` und nicht `$!` auf der anderen Seite, weil `setsid`
# dazwischen liegt — und weil `setsid` daraus eine eigene Sitzung macht, ist
# diese Zahl zugleich die Prozessgruppe, an die ein Abbruch geschickt wird.
echo $$ > "${CLUPILOT_WORK_DIR}/pid"
# ---------------------------------------------------------------------------
# Die vier Helfer, die template.sh erwartet
# ---------------------------------------------------------------------------
#
# Eigene, knappe Fassung statt `clupilot-bootstrap.sh` anzufassen: das ist der
# stillgelegte Weg, und diese vier sind curl-Hüllen ohne
# Entwurfsentscheidung. Die 252 Zeilen mit den Fallen bleiben die eine Fassung,
# und nur darum ging es.
log() {
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$1"
}
die() {
log "$1"
exit 1
}
http_get() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 "$1"
else
wget -qO- "$1"
fi
}
http_download() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 -o "$2" "$1"
else
wget -qO "$2" "$1"
fi
}
# ---------------------------------------------------------------------------
# Rückmeldung
# ---------------------------------------------------------------------------
phase() {
printf '%s' "$1" > "${CLUPILOT_WORK_DIR}/phase"
log "--- $1"
}
fail() {
printf '%s' "$1" > "${CLUPILOT_WORK_DIR}/note"
printf 'failed' > "${CLUPILOT_WORK_DIR}/state"
log "ABBRUCH: $1"
exit 1
}
# Ein Abbruch, den keine Zeile hier abgefangen hat (kein Speicher, ein Signal),
# darf nicht als `running` liegen bleiben und den Schritt gegen eine Leiche
# pollen lassen. Der Schritt erkennt das zwar auch an `kill -0`, aber ein Grund
# ist besser als eine Vermutung.
on_exit() {
_code=$?
if [ "$_code" -ne 0 ] && [ "$(cat "${CLUPILOT_WORK_DIR}/state" 2>/dev/null)" = 'running' ]; then
printf 'Der Bau brach unerwartet ab (Rückgabewert %s); siehe build.log' "$_code" \
> "${CLUPILOT_WORK_DIR}/note"
printf 'failed' > "${CLUPILOT_WORK_DIR}/state"
fi
}
trap on_exit EXIT
# ---------------------------------------------------------------------------
# Ablauf
# ---------------------------------------------------------------------------
cd "$CLUPILOT_WORK_DIR" || die "Arbeitsverzeichnis ${CLUPILOT_WORK_DIR} fehlt"
# CLUPILOT_TEMPLATE_VMIDS und alles andere Veränderliche schreibt der Schritt.
# shellcheck source=/dev/null
. "${CLUPILOT_WORK_DIR}/env"
# shellcheck source=lib/template.sh
. "${CLUPILOT_WORK_DIR}/template.sh"
CLUPILOT_TEMPLATE_VMIDS="${CLUPILOT_TEMPLATE_VMIDS:-9000}"
log "Vorlagenbau für VMID(s): ${CLUPILOT_TEMPLATE_VMIDS}"
# Der billigste Test zuerst. Ein df vorab ist billiger als ein Abbruch nach
# zwanzig Minuten mitten im virt-customize.
phase 'Platz prüfen'
check_build_space "$CLUPILOT_WORK_DIR" \
|| fail "Zu wenig Platz unter ${CLUPILOT_WORK_DIR} für Abbild, Arbeitskopie und importierte Platte (${CLUPILOT_TEMPLATE_MIN_FREE_MB} MB nötig)"
phase 'libguestfs-tools einbauen'
export DEBIAN_FRONTEND=noninteractive
apt-get install -y libguestfs-tools >> "${CLUPILOT_WORK_DIR}/apt.log" 2>&1 \
|| fail 'libguestfs-tools ließ sich nicht installieren — ohne virt-customize entsteht keine Vorlage'
phase 'Ablage für Platten sicherstellen'
ensure_image_storage \
|| fail 'Keine Ablage auf diesem Host nimmt Platten auf, und die vorhandene ließ sich nicht erweitern'
_storage="$(detect_vm_storage)"
[ -n "$_storage" ] || fail 'kein Proxmox-Speicher gefunden, der Platten aufnimmt'
log "Ablage: ${_storage}"
# Verzeichnis-Ablagen liegen auf einem Dateisystem, das `df` beantworten kann;
# Block-Ablagen haben keinen Pfad und werden übersprungen.
_storage_path="$(storage_table | awk -v n="$_storage" '$1 == n { print $4 }')"
check_build_space "$_storage_path" \
|| fail "Zu wenig Platz auf der Ablage ${_storage} (${_storage_path}) für die importierte Platte"
_image="${CLUPILOT_WORK_DIR}/${CLUPILOT_CLOUD_IMAGE}"
if [ ! -f "$_image" ]; then
phase 'Cloud-Abbild laden'
fetch_cloud_image "$_image" \
|| fail 'Debian-Cloud-Abbild nicht ladbar oder Prüfsumme falsch'
else
log 'Cloud-Abbild liegt bereits geprüft vor'
fi
# Falle 2, VOR dem Umbauen: taugt der Aufbau überhaupt?
phase 'Aufbau des Abbilds prüfen'
verify_image_layout "$_image" \
|| fail 'Der Aufbau des Cloud-Abbilds erfüllt Falle 2 nicht (LVM oder Wurzel nicht als letzte Partition)'
# Umgebaut wird eine KOPIE. Das geprüfte Abbild bleibt unangetastet liegen, sonst
# erbt ein Wiederanlauf ein halb umgebautes Abbild und hält es für fertig
# geladen — `[ ! -f "$_image" ]` oben kann den Unterschied nicht sehen.
phase 'Arbeitskopie ziehen'
_build="${CLUPILOT_WORK_DIR}/build.qcow2"
rm -f "$_build"
cp "$_image" "$_build" || fail 'Arbeitskopie des Abbilds ließ sich nicht anlegen'
_compose="${CLUPILOT_WORK_DIR}/docker-compose.yml"
[ -f "$_compose" ] || fail "docker-compose.yml fehlt unter ${CLUPILOT_WORK_DIR}"
phase 'Abbild umbauen (Docker, Compose, Gastagent)'
customise_cloud_image "$_build" "$_compose" \
|| fail "Abbild ließ sich nicht umbauen; siehe ${CLUPILOT_WORK_DIR}/virt-customize.log"
# Fallen 1, 3 und 4, NACH dem Umbauen: virt-customize meldet auch dann Erfolg,
# wenn apt ein Paket stillschweigend nicht installiert hat.
phase 'Abbildinhalt prüfen'
verify_image_contents "$_build" \
|| fail 'Das fertige Abbild erfüllt Falle 1, 3 oder 4 nicht — siehe build.log'
for _vmid in $CLUPILOT_TEMPLATE_VMIDS; do
phase "Vorlage ${_vmid} anlegen"
# create_proxmox_template liest die VMID aus der globalen Variablen.
CLUPILOT_TEMPLATE_VMID="$_vmid"
create_proxmox_template "$_build" "$_storage" \
|| fail "Vorlage ${_vmid} ließ sich nicht anlegen; siehe ${CLUPILOT_WORK_DIR}/qm.log"
# Das Merkmal, nicht die Existenz. Eine VM, die zufällig so heißt, ist keine
# Vorlage.
template_is_really_a_template \
|| fail "VMID ${_vmid} existiert, meldet aber nicht template: 1"
log "Vorlage ${_vmid} auf ${_storage} fertig, alle vier Fallen geprüft"
done
rm -f "$_build"
phase 'fertig'
printf 'ok' > "${CLUPILOT_WORK_DIR}/state"
log 'Vorlagenbau abgeschlossen'
exit 0