feat(betrieb): native Installation, Proxmox-Einzeiler und Abnahme (#37)
Zwei Wege in den Betrieb, beide so weit gefahren, wie es hier moeglich war. deploy/install.sh richtet Debian 13 in neun Schritten ein: PostgreSQL 17 hoert nur am Unix-Socket, der Dienstnutzer hat kein Login, die Geheimnisse entstehen im Zielsystem und liegen 0600 bei root. Die Umgebungsdatei wird nie ueberschrieben -- auch nicht beim Aktualisieren, sonst enden alle Sitzungen und die verschluesselten Bind-Passwoerter waeren unlesbar. deploy/proxmox/laufzettel.sh legt den Container an und ruft install.sh darin auf. Der pct-Teil ist ungetestet, weil kein PVE-Host zur Verfuegung stand; die Gegenprobe steht als Pruefliste in docs/betrieb/proxmox.md, das Folge-Issue ist Nummer 38. Die Abnahme gegen alle 61 Kriterien hat drei Zusagen ohne Deckung gefunden, alle behoben: der Healthcheck des Webdienstes konnte nie bestehen (busybox loest localhost nach ::1 auf, Next lauscht nur auf IPv4), die CI-Pipeline konnte in keinem Job gruen werden (kein LDAP-Dienst, kein gestarteter Server, kein shellcheck), und der README nannte fuer die Entwicklungsdatenbank den falschen Port. Closes #19
This commit is contained in:
Executable
+963
@@ -0,0 +1,963 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Laufzettel in einen frischen LXC auf Proxmox VE ausrollen.
|
||||
#
|
||||
# Aufruf auf der Proxmox-Shell als root:
|
||||
#
|
||||
# bash -c "$(curl -fsSL https://git.wilde-server.de/florian.wilde/Laufzettel/raw/branch/main/deploy/proxmox/laufzettel.sh)"
|
||||
#
|
||||
# Dieses Skript legt den Container an, startet ihn und ruft darin deploy/install.sh
|
||||
# auf. Es installiert selbst nichts: die gesamte Installationslogik steht in
|
||||
# install.sh, damit der native Weg (Debian ohne Proxmox) und der LXC-Weg nicht
|
||||
# auseinanderlaufen (#019).
|
||||
#
|
||||
# Trockenlauf ohne Proxmox-Host: bash deploy/proxmox/laufzettel.sh --dry-run
|
||||
#
|
||||
set -Eeuo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feste Werte
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
readonly SKRIPTNAME="laufzettel.sh"
|
||||
readonly REPO_ROH="https://git.wilde-server.de/florian.wilde/Laufzettel/raw/branch"
|
||||
readonly REPO_GIT="https://git.wilde-server.de/florian.wilde/Laufzettel.git"
|
||||
readonly ENV_DATEI="/etc/laufzettel/laufzettel.env"
|
||||
# Leihgabe von install.sh, kein Aufbewahrungsort: dieses Skript liest das
|
||||
# Erstpasswort daraus und löscht die Datei sofort wieder. Siehe abschluss_zeigen().
|
||||
readonly PASSWORT_DATEI="/root/laufzettel-erstpasswort"
|
||||
|
||||
# Deutsche Anführungszeichen als Variablen: in doppelten Anführungszeichen
|
||||
# hält shellcheck sie für einen Tippfehler (SC1111), als Literal in einfachen
|
||||
# ist es eindeutig. Lesbar bleibt der Text trotzdem richtig.
|
||||
readonly ANF_AUF='„'
|
||||
readonly ANF_ZU='“'
|
||||
|
||||
# Vorgaben des Standardwegs. Der Erweitert-Dialog nimmt sie als Vorbelegung.
|
||||
readonly STD_HOSTNAME="laufzettel"
|
||||
readonly STD_DEBIAN="13"
|
||||
readonly STD_KERNE="2"
|
||||
readonly STD_RAM="2048"
|
||||
readonly STD_PLATTE="8"
|
||||
readonly STD_SWAP="512"
|
||||
readonly STD_BRUECKE="vmbr0"
|
||||
readonly STD_IP="dhcp"
|
||||
readonly STD_UNPRIV="1"
|
||||
|
||||
# Laufzeitwerte, aus dem Dialog oder aus den Vorgaben.
|
||||
CTID=""
|
||||
HOSTNAME_CT="$STD_HOSTNAME"
|
||||
DEBIAN="$STD_DEBIAN"
|
||||
KERNE="$STD_KERNE"
|
||||
RAM="$STD_RAM"
|
||||
PLATTE="$STD_PLATTE"
|
||||
SWAP="$STD_SWAP"
|
||||
BRUECKE="$STD_BRUECKE"
|
||||
IP_WUNSCH="$STD_IP"
|
||||
GATEWAY=""
|
||||
UNPRIV="$STD_UNPRIV"
|
||||
SPEICHER_ROOT=""
|
||||
SPEICHER_VORLAGE=""
|
||||
VORLAGE=""
|
||||
CT_IP=""
|
||||
|
||||
TROCKEN="nein"
|
||||
MODUS="frage" # frage | standard | erweitert
|
||||
NUR_AKTUALISIEREN="nein"
|
||||
ZWEIG="main"
|
||||
# Steht auf ja, sobald "pct create" gelaufen ist, und wieder auf nein, sobald
|
||||
# der Container fertig eingerichtet ist. Nur dazwischen darf das Aufräumen ihn
|
||||
# wieder entfernen.
|
||||
HALB_ANGELEGT="nein"
|
||||
# Ergebnis der letzten Leseoperation, siehe pve_lesen().
|
||||
ANTWORT=""
|
||||
# Nummer des laufenden Abschnitts. Kommt aus der Position, wird nie gespeichert.
|
||||
SCHRITT=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ausgabe
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Farbe nur, wenn die Ausgabe wirklich an ein Terminal geht — sonst landen die
|
||||
# Steuerzeichen in Protokollen und Pipes.
|
||||
if [ -t 1 ]; then
|
||||
F_AUS=$'\033[0m'
|
||||
F_FETT=$'\033[1m'
|
||||
F_GRAU=$'\033[90m'
|
||||
F_LIME=$'\033[38;5;190m'
|
||||
F_ROT=$'\033[31m'
|
||||
F_GELB=$'\033[33m'
|
||||
else
|
||||
F_AUS=""
|
||||
F_FETT=""
|
||||
F_GRAU=""
|
||||
F_LIME=""
|
||||
F_ROT=""
|
||||
F_GELB=""
|
||||
fi
|
||||
|
||||
kopf() {
|
||||
SCHRITT=$((SCHRITT + 1))
|
||||
printf '\n%s%02d · %s%s\n' "$F_FETT" "$SCHRITT" "$1" "$F_AUS"
|
||||
printf '%s%s%s\n' "$F_GRAU" "------------------------------------------------------------" "$F_AUS"
|
||||
}
|
||||
|
||||
info() { printf ' %s\n' "$1"; }
|
||||
notiz() { printf ' %s%s%s\n' "$F_GRAU" "$1" "$F_AUS"; }
|
||||
erfolg() { printf ' %sok%s %s\n' "$F_LIME" "$F_AUS" "$1"; }
|
||||
warnung() { printf ' %sAchtung%s %s\n' "$F_GELB" "$F_AUS" "$1" >&2; }
|
||||
fehlertext() { printf ' %sFehler%s %s\n' "$F_ROT" "$F_AUS" "$1" >&2; }
|
||||
|
||||
# Bricht mit einem Satz ab, der sagt, was fehlt. Der ERR-Trap wird vorher
|
||||
# abgeräumt, damit ein absichtlicher Abbruch nicht als Programmfehler mit
|
||||
# Zeilennummer hinterhergemeldet wird.
|
||||
abbrechen() {
|
||||
trap - ERR
|
||||
fehlertext "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Wie abbrechen(), lässt den Container aber ausdrücklich stehen. Für alles, was
|
||||
# nach dem Start schiefgeht: dort steckt meist ein Netzfehler, den man sich
|
||||
# ansehen will, und die Meldung verspricht, dass der Container noch da ist.
|
||||
abbrechen_und_behalten() {
|
||||
HALB_ANGELEGT="nein"
|
||||
abbrechen "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Spinner
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SPINNER_PID=""
|
||||
|
||||
# Zeigt einen Spinner, solange ein langer Schritt läuft. Ohne Terminal (Pipe,
|
||||
# Protokoll, Trockenlauf) bleibt es bei einer Zeile Text, damit die Ausgabe
|
||||
# lesbar bleibt.
|
||||
spinner_an() {
|
||||
if [ ! -t 1 ] || [ "$TROCKEN" = "ja" ]; then
|
||||
info "$1 …"
|
||||
return 0
|
||||
fi
|
||||
local text="$1"
|
||||
(
|
||||
# Der Unterprozess setzt eigene Traps, damit ein Abbruch im Hauptprozess
|
||||
# nicht erst durch die Fehlerbehandlung des Spinners läuft.
|
||||
trap 'exit 0' TERM INT
|
||||
# Der Backslash steht als $'\\', damit er nicht wie ein missglückter
|
||||
# Maskierungsversuch aussieht (shellcheck SC1003).
|
||||
local zeichen=('|' '/' '-' $'\\')
|
||||
local i=0
|
||||
while :; do
|
||||
i=$(((i + 1) % ${#zeichen[@]}))
|
||||
printf '\r %s%s%s %s' "$F_LIME" "${zeichen[i]}" "$F_AUS" "$text"
|
||||
sleep 0.2
|
||||
done
|
||||
) &
|
||||
SPINNER_PID=$!
|
||||
}
|
||||
|
||||
spinner_aus() {
|
||||
if [ -n "$SPINNER_PID" ]; then
|
||||
kill "$SPINNER_PID" 2>/dev/null || true
|
||||
wait "$SPINNER_PID" 2>/dev/null || true
|
||||
SPINNER_PID=""
|
||||
printf '\r\033[2K'
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Abbruch und Aufräumen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ERR-Trap mit Zeilennummer: bei einem Fehler soll niemand raten müssen,
|
||||
# welcher der vielen pct-Aufrufe es war.
|
||||
bei_fehler() {
|
||||
local status="$1" zeile="$2" befehl="$3"
|
||||
# Nur die erste Meldung zählt: set -e lässt den Fehler aus der Funktion
|
||||
# heraus noch einmal aufschlagen, und der zweite Aufschlag kennt die
|
||||
# Zeilennummer nicht mehr.
|
||||
trap - ERR
|
||||
spinner_aus
|
||||
fehlertext "Zeile $zeile, Status $status: $befehl"
|
||||
}
|
||||
|
||||
# Läuft in jedem Fall. Räumt nur weg, was dieser Lauf selbst angelegt hat, und
|
||||
# nur solange im Container noch nichts steckt, was jemand ansehen wollen könnte:
|
||||
# sobald install.sh angelaufen ist, bleibt der Container stehen und wird
|
||||
# ausdrücklich benannt, statt still verschwunden zu sein.
|
||||
aufraeumen() {
|
||||
local status=$?
|
||||
spinner_aus
|
||||
if [ "$status" -ne 0 ] && [ "$HALB_ANGELEGT" = "ja" ] && [ -n "$CTID" ]; then
|
||||
warnung "Container $CTID ist nur halb angelegt und wird wieder entfernt."
|
||||
pve_ausfuehren pct stop "$CTID" --skiplock || true
|
||||
pve_ausfuehren pct destroy "$CTID" --force --purge || true
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
trap 'bei_fehler "$?" "$LINENO" "$BASH_COMMAND"' ERR
|
||||
trap aufraeumen EXIT
|
||||
trap 'fehlertext "Durch Signal abgebrochen."; exit 130' INT TERM
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Proxmox-Aufrufe
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Setzt ein Argument so, dass man es von Hand nachtippen kann. Harmlose Zeichen
|
||||
# bleiben nackt — "printf %q" würde jedes Komma in --net0 mit einem Backslash
|
||||
# versehen, und der Aufruf wäre zwar richtig, aber nicht mehr zu lesen.
|
||||
argument_zeigen() {
|
||||
case "$1" in
|
||||
"" | *[!A-Za-z0-9_@%+=:,./-]*)
|
||||
printf "'%s'" "${1//\'/\'\\\'\'}"
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Zeigt einen Befehl so, dass man ihn von Hand nachtippen kann.
|
||||
trocken_zeigen() {
|
||||
local teil ausgabe=""
|
||||
for teil in "$@"; do
|
||||
ausgabe="$ausgabe $(argument_zeigen "$teil")"
|
||||
done
|
||||
printf ' %s[trocken]%s%s\n' "$F_GELB" "$F_AUS" "$ausgabe"
|
||||
}
|
||||
|
||||
# Zeigt einen Befehl, der als Text vorliegt, weil er in einer Pipeline steckt.
|
||||
trocken_text() {
|
||||
printf ' %s[trocken]%s %s\n' "$F_GELB" "$F_AUS" "$1"
|
||||
}
|
||||
|
||||
# Jeder ändernde Aufruf (pct, pveam, pvesh) läuft hierüber. Im Trockenlauf wird
|
||||
# er nur ausgegeben.
|
||||
pve_ausfuehren() {
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
trocken_zeigen "$@"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
# Lesende Aufrufe. Das Ergebnis steht danach in $ANTWORT und wird nicht per
|
||||
# Kommandosubstitution zurückgegeben: im Trockenlauf sollen auch die
|
||||
# Lesebefehle auf der Standardausgabe erscheinen und dürfen nicht im
|
||||
# eingefangenen Ergebnis landen. Das erste Argument ist der Platzhalter, mit
|
||||
# dem der Trockenlauf weiterrechnet — nur so läuft er ohne PVE-Host bis zum
|
||||
# Abschlussbildschirm durch. Das zweite Argument ist der Befehl als Text: die
|
||||
# Leser stecken in Pipelines und werden darum von kleinen Funktionen gekapselt,
|
||||
# und deren Namen wollte im Trockenlauf niemand sehen.
|
||||
pve_lesen() {
|
||||
local platzhalter="$1" anzeige="$2"
|
||||
shift 2
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
trocken_text "$anzeige"
|
||||
ANTWORT="$platzhalter"
|
||||
if [ -n "$platzhalter" ]; then
|
||||
notiz "Im Trockenlauf angenommen: $platzhalter"
|
||||
else
|
||||
notiz "Im Trockenlauf angenommen: leer, also kein Treffer"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
ANTWORT="$("$@")"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfe
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
hilfe_zeigen() {
|
||||
cat <<'ENDE'
|
||||
laufzettel.sh — rollt Laufzettel in einen frischen LXC auf Proxmox VE aus.
|
||||
|
||||
Aufruf auf der Proxmox-Shell als root:
|
||||
|
||||
bash -c "$(curl -fsSL https://git.wilde-server.de/florian.wilde/Laufzettel/raw/branch/main/deploy/proxmox/laufzettel.sh)"
|
||||
|
||||
Was es tut:
|
||||
|
||||
1. Prüft root und Proxmox VE 8 oder 9.
|
||||
2. Fragt per whiptail Standard gegen Erweitert. Standard nimmt die nächste
|
||||
freie CTID, Hostname laufzettel, Debian 13, 2 Kerne, 2 GB RAM, 8 GB Platte,
|
||||
unprivilegiert, DHCP auf vmbr0. Ohne Terminal oder ohne whiptail gelten
|
||||
die Standardwerte.
|
||||
3. Holt die Debian-Vorlage per pveam, falls sie noch fehlt.
|
||||
4. Legt den Container an (pct create), startet ihn, wartet auf das Netz.
|
||||
5. Führt deploy/install.sh im Container aus: PostgreSQL, Node, Build,
|
||||
Migration, Seed, systemd-Dienst.
|
||||
6. Zeigt Adresse, Admin-Kennung und das erzeugte Passwort.
|
||||
|
||||
Ein zweiter Aufruf erkennt einen vorhandenen Container am Hostnamen und
|
||||
bietet an, ihn zu aktualisieren, statt einen zweiten anzulegen.
|
||||
|
||||
Optionen:
|
||||
|
||||
--dry-run Gibt alle pct-, pveam- und pvesh-Aufrufe nur aus, legt
|
||||
nichts an und braucht keinen Proxmox-Host. Auch die
|
||||
Prüfungen auf root und PVE werden übersprungen.
|
||||
--standard Ohne Dialog, alle Standardwerte nehmen.
|
||||
--erweitert Direkt in den Erweitert-Dialog.
|
||||
--aktualisieren Nur aktualisieren, ohne Rückfrage. Bricht ab, wenn es
|
||||
keinen Container mit dem Hostnamen laufzettel gibt.
|
||||
--zweig <name> Git-Zweig, aus dem install.sh geholt wird (Vorgabe: main).
|
||||
--hilfe, --help Diese Hilfe.
|
||||
|
||||
Alles Weitere steht in docs/betrieb/.
|
||||
ENDE
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argumente
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
argumente_lesen() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run | --trockenlauf)
|
||||
TROCKEN="ja"
|
||||
;;
|
||||
--standard)
|
||||
MODUS="standard"
|
||||
;;
|
||||
--erweitert)
|
||||
MODUS="erweitert"
|
||||
;;
|
||||
--aktualisieren)
|
||||
NUR_AKTUALISIEREN="ja"
|
||||
;;
|
||||
--zweig)
|
||||
[ $# -ge 2 ] || abbrechen "--zweig braucht einen Namen."
|
||||
ZWEIG="$2"
|
||||
shift
|
||||
;;
|
||||
--hilfe | --help | -h)
|
||||
hilfe_zeigen
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
trap - ERR
|
||||
fehlertext "Unbekannte Option: $1"
|
||||
printf '\n'
|
||||
hilfe_zeigen
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Umgebung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
umgebung_pruefen() {
|
||||
kopf "Umgebung"
|
||||
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
notiz "Trockenlauf: Prüfung auf root und auf Proxmox VE 8/9 übersprungen."
|
||||
notiz "Es wird nichts angelegt, nichts geladen, nichts gestartet."
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || abbrechen "Dieses Skript muss als root auf der Proxmox-Shell laufen."
|
||||
|
||||
command -v pveversion >/dev/null 2>&1 || abbrechen \
|
||||
"pveversion fehlt — das ist kein Proxmox-VE-Host. Für Debian ohne Proxmox gibt es deploy/install.sh."
|
||||
command -v pct >/dev/null 2>&1 || abbrechen "pct fehlt — Proxmox VE ist unvollständig installiert."
|
||||
command -v pveam >/dev/null 2>&1 || abbrechen "pveam fehlt — Proxmox VE ist unvollständig installiert."
|
||||
command -v pvesm >/dev/null 2>&1 || abbrechen "pvesm fehlt — Proxmox VE ist unvollständig installiert."
|
||||
|
||||
# pveversion liefert zum Beispiel: pve-manager/8.4.1/2a5fa54a8503f96d
|
||||
local voll haupt
|
||||
voll="$(pveversion | sed -n 's#^pve-manager/\([0-9][0-9.]*\)/.*#\1#p' | head -n 1)"
|
||||
haupt="${voll%%.*}"
|
||||
case "$haupt" in
|
||||
8 | 9)
|
||||
erfolg "Proxmox VE $voll"
|
||||
;;
|
||||
"")
|
||||
abbrechen "Die Version von Proxmox VE ist nicht erkennbar (pveversion sagt: $(pveversion | head -n 1))."
|
||||
;;
|
||||
*)
|
||||
abbrechen "Proxmox VE $voll wird nicht unterstützt — dieses Skript ist für PVE 8 und 9 gebaut."
|
||||
;;
|
||||
esac
|
||||
|
||||
erfolg "Als root auf $(hostname)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dialog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Liest einen Wert per whiptail in die genannte Variable, deren bisheriger
|
||||
# Inhalt die Vorgabe ist. Der Weg über eine Namensreferenz statt über eine
|
||||
# Kommandosubstitution ist Absicht: bricht jemand den Dialog ab, muss das
|
||||
# ganze Skript enden, und ein „exit“ in einer Kommandosubstitution beendet nur
|
||||
# die Subshell — der Wert wäre leer und das Skript liefe weiter.
|
||||
frage_wert() {
|
||||
local -n ziel="$1"
|
||||
local titel="$2" wert
|
||||
if ! wert="$(whiptail --title "Laufzettel einrichten" \
|
||||
--inputbox "$titel" 9 64 "$ziel" 3>&1 1>&2 2>&3)"; then
|
||||
abbrechen "Abgebrochen. Es wurde nichts angelegt."
|
||||
fi
|
||||
[ -n "$wert" ] || wert="$ziel"
|
||||
ziel="$wert"
|
||||
}
|
||||
|
||||
zahl_pruefen() {
|
||||
case "$2" in
|
||||
'' | *[!0-9]*) abbrechen "$1 muss eine Zahl sein, war aber $ANF_AUF$2$ANF_ZU." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
dialog_fuehren() {
|
||||
kopf "Werte"
|
||||
|
||||
# Ohne Terminal oder ohne whiptail gibt es keinen Dialog. Dann gilt der
|
||||
# Standardweg, und das wird ausdrücklich gesagt — auch wenn --erweitert
|
||||
# verlangt war, denn fragen kann das Skript dann niemanden.
|
||||
if [ "$MODUS" != "standard" ]; then
|
||||
if [ ! -t 0 ]; then
|
||||
MODUS="standard"
|
||||
notiz "Kein Terminal an der Standardeingabe — es gelten die Standardwerte."
|
||||
elif ! command -v whiptail >/dev/null 2>&1; then
|
||||
MODUS="standard"
|
||||
notiz "whiptail ist nicht installiert — es gelten die Standardwerte."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MODUS" = "frage" ]; then
|
||||
local wahl
|
||||
if ! wahl="$(whiptail --title "Laufzettel einrichten" \
|
||||
--menu "Wie soll der Container aussehen?" 13 68 2 \
|
||||
"standard" "$STD_KERNE Kerne, $((STD_RAM / 1024)) GB RAM, $STD_PLATTE GB Platte, DHCP" \
|
||||
"erweitert" "Jeden Wert selbst setzen" 3>&1 1>&2 2>&3)"; then
|
||||
abbrechen "Abgebrochen. Es wurde nichts angelegt."
|
||||
fi
|
||||
MODUS="$wahl"
|
||||
fi
|
||||
|
||||
# Die nächste freie CTID kommt immer aus dem Cluster, im Erweitert-Weg nur
|
||||
# als Vorbelegung.
|
||||
pve_lesen "100" "pvesh get /cluster/nextid" naechste_ctid_holen
|
||||
CTID="$ANTWORT"
|
||||
[ -n "$CTID" ] || abbrechen "pvesh get /cluster/nextid hat keine CTID geliefert."
|
||||
|
||||
if [ "$MODUS" = "erweitert" ]; then
|
||||
frage_wert CTID "CTID des neuen Containers"
|
||||
frage_wert HOSTNAME_CT "Hostname"
|
||||
frage_wert DEBIAN "Debian-Hauptversion (13 ist erprobt)"
|
||||
frage_wert KERNE "Kerne"
|
||||
frage_wert RAM "Arbeitsspeicher in MB"
|
||||
frage_wert SWAP "Swap in MB"
|
||||
frage_wert PLATTE "Plattengröße in GB"
|
||||
frage_wert BRUECKE "Netzwerkbrücke"
|
||||
frage_wert IP_WUNSCH "IP-Adresse: dhcp oder zum Beispiel 192.168.1.50/24"
|
||||
if [ "$IP_WUNSCH" != "dhcp" ]; then
|
||||
frage_wert GATEWAY "Gateway zur festen IP-Adresse"
|
||||
[ -n "$GATEWAY" ] || abbrechen "Zu einer festen IP-Adresse gehört ein Gateway."
|
||||
fi
|
||||
frage_wert SPEICHER_ROOT "Speicher für das Wurzeldateisystem"
|
||||
frage_wert SPEICHER_VORLAGE "Speicher für die Debian-Vorlage"
|
||||
if whiptail --title "Laufzettel einrichten" \
|
||||
--yesno "Unprivilegierten Container anlegen?\n\nEmpfohlen ist ja." 10 60; then
|
||||
UNPRIV="1"
|
||||
else
|
||||
UNPRIV="0"
|
||||
fi
|
||||
fi
|
||||
|
||||
zahl_pruefen "Die CTID" "$CTID"
|
||||
zahl_pruefen "Die Zahl der Kerne" "$KERNE"
|
||||
zahl_pruefen "Der Arbeitsspeicher" "$RAM"
|
||||
zahl_pruefen "Die Plattengröße" "$PLATTE"
|
||||
zahl_pruefen "Der Swap" "$SWAP"
|
||||
zahl_pruefen "Die Debian-Version" "$DEBIAN"
|
||||
[ "$CTID" -ge 100 ] || abbrechen "Die CTID muss mindestens 100 sein, war aber $CTID."
|
||||
[ "$KERNE" -gt 0 ] || abbrechen "Der Container braucht mindestens einen Kern."
|
||||
[ "$RAM" -ge 1024 ] || abbrechen "Unter 1024 MB reicht der Speicher für ${ANF_AUF}next build$ANF_ZU nicht."
|
||||
[ "$PLATTE" -ge 8 ] || abbrechen "Unter 8 GB reicht die Platte für Node, pnpm-Store und PostgreSQL nicht."
|
||||
|
||||
[ -n "$SPEICHER_ROOT" ] || abbrechen "Ohne Speicher für das Wurzeldateisystem geht es nicht."
|
||||
[ -n "$SPEICHER_VORLAGE" ] || abbrechen "Ohne Speicher für die Vorlage geht es nicht."
|
||||
|
||||
info "CTID $CTID · $HOSTNAME_CT · Debian $DEBIAN · $KERNE Kerne · $RAM MB RAM · $PLATTE GB Platte"
|
||||
if [ "$UNPRIV" = "1" ]; then
|
||||
info "unprivilegiert · $BRUECKE · $IP_WUNSCH"
|
||||
else
|
||||
info "privilegiert · $BRUECKE · $IP_WUNSCH"
|
||||
fi
|
||||
}
|
||||
|
||||
# Alle Leser enden auf „|| true“: ein leeres Ergebnis soll beim Aufrufer eine
|
||||
# verständliche Meldung auslösen und nicht den ERR-Trap mit einer Zeilennummer,
|
||||
# die niemandem weiterhilft. Ohne das würde schon ein grep ohne Treffer wegen
|
||||
# pipefail als Programmfehler durchschlagen.
|
||||
naechste_ctid_holen() {
|
||||
# pvesh gibt die Zahl je nach Ausgabeformat in Anführungszeichen zurück.
|
||||
pvesh get /cluster/nextid 2>/dev/null | tr -d '"[:space:]' || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Speicher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Erster aktive Speicher, der den gewünschten Inhalt aufnehmen kann. Die
|
||||
# Reihenfolge bevorzugt, was auf einem Standard-PVE üblich ist.
|
||||
speicher_suchen() {
|
||||
local inhalt="$1" liste bevorzugt
|
||||
liste="$(pvesm status --content "$inhalt" 2>/dev/null |
|
||||
awk 'NR > 1 && $3 == "active" { print $1 }' || true)"
|
||||
for bevorzugt in local-lvm local-zfs local-btrfs local; do
|
||||
if printf '%s\n' "$liste" | grep -qx -- "$bevorzugt"; then
|
||||
printf '%s' "$bevorzugt"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "$liste" | head -n 1 || true
|
||||
}
|
||||
|
||||
speicher_waehlen() {
|
||||
kopf "Speicher"
|
||||
|
||||
pve_lesen "local-lvm" "pvesm status --content rootdir" speicher_suchen rootdir
|
||||
SPEICHER_ROOT="$ANTWORT"
|
||||
[ -n "$SPEICHER_ROOT" ] || abbrechen \
|
||||
"Kein aktiver Speicher mit Inhalt ${ANF_AUF}rootdir$ANF_ZU gefunden — ohne den kann kein Container angelegt werden."
|
||||
|
||||
pve_lesen "local" "pvesm status --content vztmpl" speicher_suchen vztmpl
|
||||
SPEICHER_VORLAGE="$ANTWORT"
|
||||
[ -n "$SPEICHER_VORLAGE" ] || abbrechen \
|
||||
"Kein aktiver Speicher mit Inhalt ${ANF_AUF}vztmpl$ANF_ZU gefunden — dort müsste die Debian-Vorlage liegen."
|
||||
|
||||
erfolg "Wurzeldateisystem auf $SPEICHER_ROOT, Vorlage auf $SPEICHER_VORLAGE"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Debian-Vorlage
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Neueste passende Vorlage aus der Liste von pveam. Der Dateiname ändert sich
|
||||
# mit jedem Punktrelease (debian-13-standard_13.1-1_amd64.tar.zst), darum wird
|
||||
# er gesucht und nicht festgeschrieben.
|
||||
vorlage_suchen() {
|
||||
pveam available --section system 2>/dev/null |
|
||||
awk '{ print $2 }' |
|
||||
grep -E "^debian-$1-standard_" |
|
||||
sort -V |
|
||||
tail -n 1 || true
|
||||
}
|
||||
|
||||
vorlage_vorhanden() {
|
||||
if pveam list "$1" 2>/dev/null | awk '{ print $1 }' | grep -qF -- "vztmpl/$2"; then
|
||||
printf 'ja'
|
||||
else
|
||||
printf 'nein'
|
||||
fi
|
||||
}
|
||||
|
||||
vorlage_besorgen() {
|
||||
kopf "Debian-Vorlage"
|
||||
|
||||
spinner_an "Vorlagenliste auffrischen"
|
||||
pve_ausfuehren pveam update >/dev/null 2>&1 ||
|
||||
warnung "pveam update ist fehlgeschlagen, es gilt die zwischengespeicherte Liste."
|
||||
spinner_aus
|
||||
|
||||
pve_lesen "debian-$DEBIAN-standard_$DEBIAN.1-1_amd64.tar.zst" \
|
||||
"pveam available --section system" vorlage_suchen "$DEBIAN"
|
||||
VORLAGE="$ANTWORT"
|
||||
[ -n "$VORLAGE" ] || abbrechen \
|
||||
"In ${ANF_AUF}pveam available$ANF_ZU steht keine Vorlage debian-$DEBIAN-standard. Andere Debian-Version wählen oder die Vorlage von Hand laden."
|
||||
|
||||
pve_lesen "nein" "pveam list $SPEICHER_VORLAGE" vorlage_vorhanden "$SPEICHER_VORLAGE" "$VORLAGE"
|
||||
if [ "$ANTWORT" = "ja" ]; then
|
||||
erfolg "Vorlage $VORLAGE liegt schon auf $SPEICHER_VORLAGE"
|
||||
else
|
||||
spinner_an "Vorlage $VORLAGE laden"
|
||||
pve_ausfuehren pveam download "$SPEICHER_VORLAGE" "$VORLAGE"
|
||||
spinner_aus
|
||||
erfolg "Vorlage $VORLAGE geladen"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Container anlegen und starten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
container_anlegen() {
|
||||
kopf "Container anlegen"
|
||||
|
||||
# Ohne firewall=…: auf der Befehlszeile ist die Firewall am Gerät ohnehin aus,
|
||||
# und der Aufruf soll genau das zeigen, was er tut.
|
||||
local netz="name=eth0,bridge=$BRUECKE"
|
||||
if [ "$IP_WUNSCH" = "dhcp" ]; then
|
||||
netz="$netz,ip=dhcp"
|
||||
else
|
||||
netz="$netz,ip=$IP_WUNSCH,gw=$GATEWAY"
|
||||
fi
|
||||
|
||||
# nesting=1 ist hier keine Bequemlichkeit: die systemd-Unit aus install.sh
|
||||
# arbeitet mit ProtectSystem=strict und PrivateTmp=yes, und diese Sandkästen
|
||||
# brauchen im unprivilegierten LXC eigene Mount-Namensräume (#019).
|
||||
# Ab hier darf das Aufräumen den Container wieder entfernen.
|
||||
HALB_ANGELEGT="ja"
|
||||
spinner_an "Container $CTID anlegen"
|
||||
pve_ausfuehren pct create "$CTID" "$SPEICHER_VORLAGE:vztmpl/$VORLAGE" \
|
||||
--hostname "$HOSTNAME_CT" \
|
||||
--ostype debian \
|
||||
--arch amd64 \
|
||||
--cores "$KERNE" \
|
||||
--memory "$RAM" \
|
||||
--swap "$SWAP" \
|
||||
--rootfs "$SPEICHER_ROOT:$PLATTE" \
|
||||
--net0 "$netz" \
|
||||
--unprivileged "$UNPRIV" \
|
||||
--features nesting=1 \
|
||||
--onboot 1 \
|
||||
--timezone host \
|
||||
--description "Laufzettel — Checklisten für wiederkehrende IT-Arbeiten. Angelegt von deploy/proxmox/$SKRIPTNAME."
|
||||
spinner_aus
|
||||
erfolg "Container $CTID angelegt"
|
||||
|
||||
spinner_an "Container starten"
|
||||
pve_ausfuehren pct start "$CTID"
|
||||
spinner_aus
|
||||
erfolg "Container $CTID gestartet"
|
||||
}
|
||||
|
||||
# Wartet, bis im Container Befehle laufen und Namen aufgelöst werden. Ohne das
|
||||
# scheitert das erste apt-get mit einem irritierenden DNS-Fehler.
|
||||
netz_abwarten() {
|
||||
kopf "Netz"
|
||||
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
trocken_zeigen pct exec "$CTID" -- test -d /run/systemd/system
|
||||
trocken_zeigen pct exec "$CTID" -- ip -4 -o addr show scope global
|
||||
trocken_zeigen pct exec "$CTID" -- getent hosts deb.debian.org
|
||||
CT_IP="192.0.2.42"
|
||||
notiz "Im Trockenlauf wird nicht gewartet; als Adresse steht hier die Beispiel-IP $CT_IP."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local versuch
|
||||
spinner_an "Auf den Start des Containers warten"
|
||||
for ((versuch = 1; versuch <= 60; versuch++)); do
|
||||
if pct exec "$CTID" -- test -d /run/systemd/system >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [ "$versuch" -eq 60 ]; then
|
||||
spinner_aus
|
||||
abbrechen_und_behalten "Container $CTID ist nach 120 Sekunden nicht betriebsbereit. Er bleibt stehen, nachsehen mit: pct enter $CTID"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
spinner_aus
|
||||
|
||||
spinner_an "Auf IP-Adresse und Namensauflösung warten"
|
||||
for ((versuch = 1; versuch <= 60; versuch++)); do
|
||||
CT_IP="$(pct exec "$CTID" -- ip -4 -o addr show scope global 2>/dev/null |
|
||||
awk 'NR == 1 { split($4, teile, "/"); print teile[1] }')"
|
||||
if [ -n "$CT_IP" ] && pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [ "$versuch" -eq 60 ]; then
|
||||
spinner_aus
|
||||
abbrechen_und_behalten "Container $CTID hat nach 120 Sekunden kein brauchbares Netz. Brücke $BRUECKE und DHCP prüfen; er bleibt stehen, nachsehen mit: pct enter $CTID"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
spinner_aus
|
||||
|
||||
erfolg "Container $CTID erreichbar unter $CT_IP"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Laufzettel einrichten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# install.sh wird als einzelne Datei per curl geholt, das Repo wird hier nicht
|
||||
# geklont: install.sh legt den Quellbaum selbst nach /opt/laufzettel und braucht
|
||||
# ihn dort auch für den Aktualisierungsweg (git pull). Ein zweiter Klon daneben
|
||||
# wäre eine zweite Wahrheit. Geholt wird von derselben Adresse, von der auch
|
||||
# dieses Skript kam — wer einen eigenen Spiegel nutzt, bekommt beide Teile aus
|
||||
# demselben Spiegel.
|
||||
anwendung_einrichten() {
|
||||
local zweck="$1" # einrichten | aktualisieren
|
||||
kopf "Laufzettel $zweck"
|
||||
|
||||
# Ab hier wird der Container nicht mehr weggeräumt: scheitert install.sh, will
|
||||
# man das Protokoll darin lesen und nicht vor einem verschwundenen Container
|
||||
# stehen. Jeder Fehler ab hier nennt darum die CTID.
|
||||
HALB_ANGELEGT="nein"
|
||||
|
||||
local roh_url="$REPO_ROH/$ZWEIG/deploy/install.sh"
|
||||
|
||||
spinner_an "Paketliste und Grundwerkzeuge"
|
||||
if ! pve_ausfuehren pct exec "$CTID" -- bash -c \
|
||||
'export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y -qq curl git ca-certificates'; then
|
||||
spinner_aus
|
||||
abbrechen "apt-get ist im Container $CTID fehlgeschlagen. Nachsehen mit: pct enter $CTID"
|
||||
fi
|
||||
spinner_aus
|
||||
erfolg "curl, git und ca-certificates sind vorhanden"
|
||||
|
||||
# Erst herunterladen, dann ausführen: „curl | bash“ würde einen abgebrochenen
|
||||
# Download stillschweigend als halbes Skript starten.
|
||||
spinner_an "install.sh holen"
|
||||
if ! pve_ausfuehren pct exec "$CTID" -- bash -c \
|
||||
"curl -fsSL --retry 3 -o /root/laufzettel-install.sh '$roh_url' && test -s /root/laufzettel-install.sh"; then
|
||||
spinner_aus
|
||||
abbrechen "install.sh war unter $roh_url nicht zu holen. Zweig richtig geschrieben? Kommt der Container an den Gitea-Server?"
|
||||
fi
|
||||
spinner_aus
|
||||
erfolg "install.sh aus Zweig $ZWEIG geholt"
|
||||
|
||||
# APP_URL und BIND_ADDR stehen erst hier fest, weil erst jetzt die IP bekannt
|
||||
# ist. An APP_URL hängen die QR-Codes und die OIDC-Rückrufadresse. Bei einer
|
||||
# Aktualisierung werden beide bewusst nicht mitgegeben: wer einen Reverse
|
||||
# Proxy davorgesetzt und APP_URL auf den richtigen Namen gestellt hat, soll
|
||||
# den nicht beim nächsten Aufruf gegen eine IP-Adresse getauscht bekommen.
|
||||
local umgebung="LAUFZETTEL_REPO='$REPO_GIT' LAUFZETTEL_ZWEIG='$ZWEIG'"
|
||||
umgebung="$umgebung LAUFZETTEL_PASSWORT_DATEI='$PASSWORT_DATEI'"
|
||||
if [ "$zweck" = "einrichten" ]; then
|
||||
umgebung="APP_URL='http://$CT_IP:3000' BIND_ADDR=0.0.0.0 $umgebung"
|
||||
fi
|
||||
|
||||
# Hier läuft ausdrücklich kein Spinner: install.sh meldet seine neun Schritte
|
||||
# selbst, und das dauert Minuten. Ein Spinner würde diese Meldungen
|
||||
# überschreiben und wäre das Gegenteil von Fortschrittsanzeige.
|
||||
info "install.sh läuft: PostgreSQL, Node, Build, Migration, Seed. Das dauert einige Minuten."
|
||||
notiz "Ab hier spricht install.sh selbst."
|
||||
printf '\n'
|
||||
if ! pve_ausfuehren pct exec "$CTID" -- bash -c "$umgebung bash /root/laufzettel-install.sh"; then
|
||||
abbrechen "install.sh ist im Container $CTID fehlgeschlagen. Der Container bleibt stehen: pct enter $CTID, dann journalctl -u laufzettel -n 50"
|
||||
fi
|
||||
printf '\n'
|
||||
erfolg "install.sh ist durchgelaufen"
|
||||
}
|
||||
|
||||
# Fragt von außen nach, ob der Dienst antwortet. install.sh prüft dasselbe
|
||||
# selbst, aber nur auf seiner Bindeadresse im Container — erreichbar aus dem Netz
|
||||
# ist damit noch nichts bewiesen. Ein Befund bricht nicht ab: die Installation
|
||||
# steht dann schon, und die Adresse gehört auf den Abschlussbildschirm.
|
||||
gesundheit_pruefen() {
|
||||
kopf "Erreichbarkeit"
|
||||
|
||||
local adresse="http://$CT_IP:3000/api/gesundheit"
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
trocken_text "curl -fsS $adresse"
|
||||
notiz "Im Trockenlauf wird nicht gefragt."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local versuch antwort=""
|
||||
spinner_an "Auf Antwort von $adresse warten"
|
||||
for ((versuch = 1; versuch <= 30; versuch++)); do
|
||||
antwort="$(curl -fsS --max-time 5 "$adresse" 2>/dev/null || true)"
|
||||
case "$antwort" in
|
||||
*'"ok"'*) break ;;
|
||||
esac
|
||||
antwort=""
|
||||
sleep 2
|
||||
done
|
||||
spinner_aus
|
||||
|
||||
if [ -n "$antwort" ]; then
|
||||
erfolg "$adresse antwortet: $antwort"
|
||||
else
|
||||
warnung "$adresse antwortet nicht. Dienst: pct exec $CTID -- systemctl status laufzettel · Protokoll: pct exec $CTID -- journalctl -u laufzettel -n 50"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Abschluss
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Wie das Passwort hierher kommt: install.sh zeigt das beim ersten Lauf erzeugte
|
||||
# Administrator-Passwort genau einmal auf seiner eigenen Ausgabe an — und die
|
||||
# läuft hier hinter einem Spinner durch pct exec vorbei. In die Umgebungsdatei
|
||||
# schreibt install.sh es ausdrücklich nicht, in der Datenbank steht nur der
|
||||
# argon2-Hash. Für genau diesen Fall hat install.sh --passwort-datei
|
||||
# beziehungsweise LAUFZETTEL_PASSWORT_DATEI: eine Datei mit 0600 und root, aus
|
||||
# der ein maschineller Aufrufer das Passwort abliest und die er danach löscht.
|
||||
# Das ist der Weg hier — lesen, einmal anzeigen, sofort wegwerfen. Die Ausgabe
|
||||
# von install.sh mitzuschneiden und zu durchsuchen wäre von deren
|
||||
# Formulierungen abhängig und würde das Passwort zusätzlich durch Pipes und
|
||||
# Protokolldateien schleifen.
|
||||
#
|
||||
# Die Kennung dagegen steht in der Umgebungsdatei und wird von dort gelesen.
|
||||
erstpasswort_lesen() {
|
||||
pct exec "$CTID" -- sh -c \
|
||||
"sed -n 's/^SEED_ADMIN_PASSWORD=//p' '$PASSWORT_DATEI' 2>/dev/null | tail -n 1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
env_wert_lesen() {
|
||||
pct exec "$CTID" -- sh -c "sed -n 's/^$1=//p' '$ENV_DATEI' | tail -n 1" 2>/dev/null |
|
||||
tr -d '\r' | sed -e 's/^"//' -e 's/"$//' || true
|
||||
}
|
||||
|
||||
abschluss_zeigen() {
|
||||
local kennung passwort text
|
||||
|
||||
kopf "Fertig"
|
||||
|
||||
pve_lesen "admin@firma.example" "pct exec $CTID -- grep SEED_ADMIN_EMAIL $ENV_DATEI" \
|
||||
env_wert_lesen SEED_ADMIN_EMAIL
|
||||
kennung="$ANTWORT"
|
||||
[ -n "$kennung" ] || kennung="steht in $ENV_DATEI"
|
||||
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
trocken_text "pct exec $CTID -- cat $PASSWORT_DATEI"
|
||||
passwort="(im Trockenlauf gibt es keines)"
|
||||
else
|
||||
pve_lesen "" "pct exec $CTID -- cat $PASSWORT_DATEI" erstpasswort_lesen
|
||||
passwort="$ANTWORT"
|
||||
fi
|
||||
|
||||
# Die Leihgabe wird zurückgegeben, gelesen oder nicht: das Passwort soll nicht
|
||||
# im Klartext im Container liegen bleiben.
|
||||
pve_ausfuehren pct exec "$CTID" -- rm -f "$PASSWORT_DATEI" || true
|
||||
|
||||
if [ -z "$passwort" ]; then
|
||||
passwort="unverändert (das Konto bestand schon)"
|
||||
fi
|
||||
|
||||
text="$(
|
||||
cat <<ENDE
|
||||
Laufzettel läuft.
|
||||
|
||||
Adresse http://$CT_IP:3000
|
||||
Admin-Kennung $kennung
|
||||
Passwort $passwort
|
||||
|
||||
Das Passwort steht nur hier: in der Datenbank liegt allein der argon2-Hash,
|
||||
in $ENV_DATEI steht es nicht. Nach der ersten
|
||||
Anmeldung ändern. Verloren? Dann in der Anwendung ein neues setzen.
|
||||
|
||||
Reverse Proxy: der Dienst spricht nur HTTP auf Port 3000. Davor gehört ein
|
||||
Proxy mit TLS, und APP_URL im Container muss auf den Namen zeigen, unter dem
|
||||
Laufzettel dann erreichbar ist — sonst führen die QR-Codes und die
|
||||
OIDC-Rückrufadresse ins Leere. Siehe docs/betrieb/reverse-proxy.md.
|
||||
|
||||
Weiter in docs/betrieb/: inbetriebnahme.md, proxmox.md, authentik.md und
|
||||
sicherung.md (Sicherung per pg_dump über den Unix-Socket).
|
||||
|
||||
Shell im Container pct enter $CTID
|
||||
Dienst ansehen pct exec $CTID -- systemctl status laufzettel
|
||||
Gesundheitsprüfung curl http://$CT_IP:3000/api/gesundheit
|
||||
Kennung nachsehen pct exec $CTID -- grep SEED_ADMIN_EMAIL $ENV_DATEI
|
||||
ENDE
|
||||
)"
|
||||
|
||||
if [ -t 1 ] && [ "$TROCKEN" != "ja" ] && command -v whiptail >/dev/null 2>&1; then
|
||||
whiptail --title "Laufzettel — fertig" --msgbox "$text" 30 78
|
||||
fi
|
||||
|
||||
printf '%s\n' "$text" | sed 's/^/ /'
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vorhandenen Container erkennen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
container_status() {
|
||||
pct status "$1" 2>/dev/null | awk '{ print $2 }' || true
|
||||
}
|
||||
|
||||
# „pct list“ gibt VMID, Status, gegebenenfalls Lock und den Namen aus. Der Name
|
||||
# ist die letzte Spalte, und Hostnamen enthalten keine Leerzeichen — darum $NF.
|
||||
container_mit_hostname() {
|
||||
pct list 2>/dev/null | awk -v gesucht="$1" 'NR > 1 && $NF == gesucht { print $1 }' |
|
||||
head -n 1 || true
|
||||
}
|
||||
|
||||
# Liefert 0, wenn der vorhandene Container aktualisiert werden soll.
|
||||
aktualisierung_anbieten() {
|
||||
local id="$1"
|
||||
if [ "$NUR_AKTUALISIEREN" = "ja" ]; then
|
||||
notiz "--aktualisieren war gesetzt — Container $id wird aktualisiert."
|
||||
return 0
|
||||
fi
|
||||
if [ ! -t 0 ] || ! command -v whiptail >/dev/null 2>&1; then
|
||||
notiz "Kein Dialog möglich — Container $id wird aktualisiert."
|
||||
return 0
|
||||
fi
|
||||
whiptail --title "Laufzettel ist schon da" \
|
||||
--yes-button "Aktualisieren" --no-button "Abbrechen" \
|
||||
--yesno "Container $id trägt schon den Hostnamen $ANF_AUF$STD_HOSTNAME$ANF_ZU.
|
||||
|
||||
Aktualisieren heißt: install.sh erneut ausführen — git pull, Build, Migration, Dienst neu starten. Geheimnisse und Daten bleiben unberührt.
|
||||
|
||||
Einen zweiten Laufzettel legt dieses Skript nicht daneben; dafür im Erweitert-Weg einen anderen Hostnamen wählen." 17 72
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ablauf
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
haupt() {
|
||||
argumente_lesen "$@"
|
||||
|
||||
printf '\n%sLaufzettel — Proxmox-LXC%s\n' "$F_FETT" "$F_AUS"
|
||||
if [ "$TROCKEN" = "ja" ]; then
|
||||
printf '%sTrockenlauf: es wird nur gezeigt, was geschehen würde.%s\n' "$F_GELB" "$F_AUS"
|
||||
fi
|
||||
|
||||
umgebung_pruefen
|
||||
|
||||
# Vor allem anderen: gibt es Laufzettel hier schon?
|
||||
kopf "Vorhandener Container"
|
||||
# Im Trockenlauf gibt es kein pct list. Mit --aktualisieren wird darum eine
|
||||
# vorhandene CTID angenommen, sonst keine — so lässt sich jeder der beiden
|
||||
# Wege trocken durchspielen.
|
||||
local angenommen=""
|
||||
[ "$NUR_AKTUALISIEREN" = "ja" ] && angenommen="100"
|
||||
pve_lesen "$angenommen" "pct list" container_mit_hostname "$STD_HOSTNAME"
|
||||
local vorhanden="$ANTWORT"
|
||||
if [ -z "$vorhanden" ] && [ "$NUR_AKTUALISIEREN" = "ja" ]; then
|
||||
abbrechen "Kein Container mit dem Hostnamen $STD_HOSTNAME — mit --aktualisieren wird keiner angelegt."
|
||||
fi
|
||||
if [ -z "$vorhanden" ]; then
|
||||
erfolg "Kein Container mit dem Hostnamen $STD_HOSTNAME — es wird ein neuer angelegt."
|
||||
speicher_waehlen
|
||||
dialog_fuehren
|
||||
vorlage_besorgen
|
||||
container_anlegen
|
||||
netz_abwarten
|
||||
anwendung_einrichten "einrichten"
|
||||
gesundheit_pruefen
|
||||
abschluss_zeigen
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Container $vorhanden trägt den Hostnamen $STD_HOSTNAME."
|
||||
if ! aktualisierung_anbieten "$vorhanden"; then
|
||||
abbrechen "Abgebrochen. Es wurde nichts geändert."
|
||||
fi
|
||||
CTID="$vorhanden"
|
||||
# Der Container kann gestoppt sein. Erst fragen, dann starten — „pct start“
|
||||
# auf einem laufenden Container meldet nur einen Fehler, den niemand braucht.
|
||||
pve_lesen "running" "pct status $CTID" container_status "$CTID"
|
||||
if [ "$ANTWORT" = "running" ]; then
|
||||
info "Container $CTID läuft."
|
||||
else
|
||||
spinner_an "Container $CTID starten"
|
||||
pve_ausfuehren pct start "$CTID"
|
||||
spinner_aus
|
||||
fi
|
||||
netz_abwarten
|
||||
anwendung_einrichten "aktualisieren"
|
||||
gesundheit_pruefen
|
||||
abschluss_zeigen
|
||||
}
|
||||
|
||||
haupt "$@"
|
||||
Reference in New Issue
Block a user