Files
2026-08-20 10:33:59 +02:00

1020 lines
39 KiB
Bash
Executable File

#!/usr/bin/env bash
# =====================================================================
# cmk-manage.sh
# Verwaltung des Checkmk-/NGINX-Proxy-Manager-Stacks:
# Stack-Steuerung, Backup, Restore und Import (Migration) von Sites.
#
# Zwei Bedienarten:
# 1) Parametergesteuert : ./scripts/cmk-manage.sh backup --site cmk
# 2) ncurses-Oberflaeche: ./scripts/cmk-manage.sh gui
#
# Backups/Bundles sind immer .tar.gz
# =====================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SELF="$SCRIPT_DIR/cmk-manage.sh"
# shellcheck source=lib/common.sh
. "$SCRIPT_DIR/lib/common.sh"
BUNDLE_DIRNAME="cmkbundle"
ASSUME_YES=""
VERBOSE=""
ENVFILE_OPT=""
# =====================================================================
# Hilfe
# =====================================================================
usage() {
cat <<'EOF'
cmk-manage.sh - Checkmk Stack Manager (Docker & Podman)
AUFRUF
cmk-manage.sh [globale Optionen] <kommando> [Optionen]
GLOBALE OPTIONEN
--env-file DATEI abweichende .env verwenden
-y, --yes alle Rueckfragen automatisch mit "ja" beantworten
-v, --verbose ausfuehrliche Ausgabe
-h, --help diese Hilfe
KOMMANDOS - Oberflaeche
gui | menu ncurses-Oberflaeche starten (dialog/whiptail)
KOMMANDOS - Stack
up Stack starten (compose up -d)
down Stack stoppen und Container entfernen
restart Stack neu starten
pull Images aktualisieren
status Status von Containern und Sites anzeigen
logs [-f] [DIENST] Logs anzeigen
shell Root-Shell im Checkmk-Container
omd ARG... beliebiges omd-Kommando im Container ausfuehren
KOMMANDOS - Sites
sites Sites im Container auflisten
site-start SITE Site starten
site-stop SITE Site stoppen
start-all-sites alle vorhandenen Sites starten (fuer Autostart nach Reboot)
KOMMANDOS - Backup / Restore / Migration
backup [Optionen] Backup einer/aller Sites als .tar.gz-Bundle erzeugen
--site SITE Site (mehrfach moeglich)
--all alle Sites sichern
--dir VERZ Zielverzeichnis (Vorgabe: $BACKUP_DIR)
--label TEXT Freitext-Kennzeichnung im Dateinamen
--no-rrds Performance-Daten (RRDs) auslassen
--no-logs Logdateien auslassen
--stop Site waehrend des Backups stoppen (konsistenter)
list vorhandene Backups/Bundles auflisten
verify --file DATEI Bundle pruefen (Manifest + SHA256)
restore [Optionen] Backup wiederherstellen
--file DATEI Bundle oder rohes omd-Backup (.tar.gz)
--as-site NAME unter anderem Site-Namen wiederherstellen
(ACHTUNG: Agents muessen dann neu registriert werden!)
--reuse vorhandene Site ueberschreiben
--kill laufende Prozesse der Site beenden
--no-start Site nach dem Restore nicht starten
import [Optionen] Site von einer Direktinstallation uebernehmen
(Bundle aus migrate_creator.sh)
--file DATEI Migrations-Bundle (.tar.gz)
--reuse vorhandene Site ueberschreiben
--kill laufende Prozesse der Site beenden
--set-default-site CMK_SITE_ID in der .env auf die importierte Site setzen
--no-start Site nach dem Import nicht starten
prune --keep N nur die N neuesten Backups behalten
KOMMANDOS - Zertifikat
cert [Optionen] selbstsigniertes Zertifikat fuer den Proxy erzeugen
--domain FQDN Common Name / SAN
--alt NAME zusaetzlicher SAN (mehrfach; DNS-Name oder IP)
--days N Gueltigkeit in Tagen (Vorgabe 3650)
BEISPIELE
./scripts/cmk-manage.sh up
./scripts/cmk-manage.sh backup --all --no-logs
./scripts/cmk-manage.sh import --file backups/cmkmigrate_prod_2.3.0p23_*.tar.gz --set-default-site
./scripts/cmk-manage.sh gui
EOF
}
# =====================================================================
# Bundle-Format
#
# <name>.tar.gz
# cmkbundle/MANIFEST key=value, per "source" lesbar
# cmkbundle/INFO.txt menschenlesbare Zusammenfassung
# cmkbundle/site.tar.gz das eigentliche "omd backup"-Archiv
# cmkbundle/site.tar.gz.sha256
# =====================================================================
edition_from_version() {
case "$1" in
*.cre) echo "Raw Edition" ;;
*.cee) echo "Enterprise Edition" ;;
*.cme) echo "Managed Services Edition" ;;
*.cce) echo "Cloud Edition" ;;
*) echo "unbekannt" ;;
esac
}
# Manifest eines Bundles einlesen -> Variablen B_*
bundle_read() {
local file="$1"
B_SITE=""; B_CMK_VERSION=""; B_SOURCE_KIND=""; B_SOURCE_HOST=""
B_SOURCE_IP=""; B_CREATED=""; B_ARCHIVE_SHA256=""; B_LABEL=""
B_BUNDLE_TYPE=""; B_ARCHIVE_SIZE=""; B_OMD_BACKUP_OPTS=""; B_SOURCE_OS=""
B_IS_BUNDLE=0
if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/MANIFEST"; then
B_IS_BUNDLE=1
local mf; mf="$(mktemp)"
tar -xzOf "$file" "$BUNDLE_DIRNAME/MANIFEST" >"$mf"
# nur bekannte Schluessel uebernehmen (kein blindes eval)
local k v
while IFS='=' read -r k v; do
case "$k" in
SITE) B_SITE="$v" ;;
CMK_VERSION) B_CMK_VERSION="$v" ;;
SOURCE_KIND) B_SOURCE_KIND="$v" ;;
SOURCE_HOST) B_SOURCE_HOST="$v" ;;
SOURCE_IP) B_SOURCE_IP="$v" ;;
SOURCE_OS) B_SOURCE_OS="$v" ;;
CREATED) B_CREATED="$v" ;;
ARCHIVE_SHA256) B_ARCHIVE_SHA256="$v" ;;
ARCHIVE_SIZE) B_ARCHIVE_SIZE="$v" ;;
LABEL) B_LABEL="$v" ;;
BUNDLE_TYPE) B_BUNDLE_TYPE="$v" ;;
OMD_BACKUP_OPTS) B_OMD_BACKUP_OPTS="$v" ;;
esac
done <"$mf"
rm -f "$mf"
else
# rohes omd-Backup: Site-Name ist die erste Pfadkomponente.
# Ein omd-Archiv beginnt immer mit "<site>/..." - alles andere ist
# kein Checkmk-Backup und wird abgewiesen.
local first
first="$(tar -tzf "$file" 2>/dev/null | head -n1)"
case "$first" in
*/*) B_SITE="${first%%/*}" ;;
*) return 1 ;;
esac
B_BUNDLE_TYPE="raw"
B_SOURCE_KIND="unbekannt"
fi
[ -n "$B_SITE" ] || return 1
}
bundle_show() {
local file="$1"
bundle_read "$file" || { echo "Kein gueltiges Checkmk-Backup: $file"; return 1; }
cat <<EOF
Datei : $file
Groesse : $(human_size "$(file_size "$file")")
Typ : $([ "$B_IS_BUNDLE" = 1 ] && echo "Bundle (${B_BUNDLE_TYPE:-backup})" || echo "rohes omd-Backup")
Site : $B_SITE
Checkmk-Version: ${B_CMK_VERSION:-unbekannt} ($(edition_from_version "${B_CMK_VERSION:-x}"))
Quelle : ${B_SOURCE_KIND:-?} ${B_SOURCE_HOST:+/ $B_SOURCE_HOST}${B_SOURCE_IP:+ ($B_SOURCE_IP)}
Quell-OS : ${B_SOURCE_OS:-unbekannt}
Erstellt : ${B_CREATED:-unbekannt}
Kennzeichnung : ${B_LABEL:-keine}
omd-Optionen : ${B_OMD_BACKUP_OPTS:-keine}
EOF
}
bundle_verify() {
local file="$1"
[ -f "$file" ] || die "Datei nicht gefunden: $file"
log "Pruefe Archivstruktur ..."
tar -tzf "$file" >/dev/null || die "Archiv ist beschaedigt oder kein gueltiges tar.gz."
bundle_read "$file" || die "Im Archiv wurde keine Checkmk-Site gefunden."
if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then
log "Pruefe SHA256 der Nutzdaten ..."
local sum
sum="$(tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz" | sha256sum | awk '{print $1}')"
[ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme falsch!
erwartet : $B_ARCHIVE_SHA256
ermittelt: $sum"
ok "Pruefsumme korrekt."
else
warn "Kein Manifest/keine Pruefsumme vorhanden - nur Strukturpruefung."
fi
bundle_show "$file"
ok "Bundle ist in Ordnung."
}
# Nutzdaten (omd-Archiv) aus einem Bundle nach stdout schreiben
bundle_extract_payload() {
local file="$1"
if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/site.tar.gz"; then
tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz"
else
cat "$file"
fi
}
# =====================================================================
# Stack-Kommandos
# =====================================================================
cmd_up() {
[ -f "$PROJECT_DIR/.env" ] || die "Keine .env vorhanden. Bitte: cp .env.example .env && \$EDITOR .env"
log "Starte Stack (${RUNTIME}) ..."
compose up -d
ok "Stack gestartet."
cmd_status
}
cmd_down() { log "Stoppe Stack ..."; compose down; ok "Stack gestoppt."; }
cmd_restart() { compose down || true; compose up -d; ok "Stack neu gestartet."; }
cmd_pull() { compose pull; ok "Images aktualisiert. Danach: $0 up"; }
cmd_logs() { compose logs "$@"; }
cmd_shell() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" bash; }
cmd_omd() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" omd "$@"; }
cmd_status() {
printf '\n%sContainer%s\n' "$C_BLD" "$C_RST"
local c
for c in "$CMK_CONTAINER_NAME" "$NPM_CONTAINER_NAME"; do
if container_exists "$c"; then
printf ' %-24s %s\n' "$c" \
"$(container_running "$c" && echo "${C_GRN}laeuft${C_RST}" || echo "${C_RED}gestoppt${C_RST}")"
else
printf ' %-24s %s\n' "$c" "${C_YEL}nicht vorhanden${C_RST}"
fi
done
if container_running "$CMK_CONTAINER_NAME"; then
printf '\n%sSites im Container%s\n' "$C_BLD" "$C_RST"
printf ' %-20s %-18s %s\n' "SITE" "VERSION" "STATUS"
local s
while IFS= read -r s; do
[ -n "$s" ] || continue
printf ' %-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")"
done < <(cmk_sites)
printf '\n Standard-Site laut .env : %s\n' "$CMK_SITE_ID"
fi
printf '\n%sZugang%s\n' "$C_BLD" "$C_RST"
local ip; ip="$(primary_ip)"
printf ' Checkmk (direkt) : http://%s:5000/%s/\n' "$ip" "$CMK_SITE_ID"
printf ' Checkmk (ueber Proxy) : https://<FQDN>/%s/\n' "$CMK_SITE_ID"
printf ' Agent-Receiver : %s:8000\n' "$ip"
printf ' NGINX Proxy Manager : http://%s:81/ (Erstlogin: admin@example.com / changeme)\n\n' "$ip"
}
# =====================================================================
# Site-Kommandos
# =====================================================================
cmd_sites() {
require_cmk_running
printf '%-20s %-18s %s\n' "SITE" "VERSION" "STATUS"
local s
while IFS= read -r s; do
[ -n "$s" ] || continue
printf '%-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")"
done < <(cmk_sites)
}
cmd_site_start() {
require_cmk_running
[ $# -ge 1 ] || die "Site-Name fehlt."
ct omd start "$1" && ok "Site '$1' gestartet."
}
cmd_site_stop() {
require_cmk_running
[ $# -ge 1 ] || die "Site-Name fehlt."
ct omd stop "$1" && ok "Site '$1' gestoppt."
}
# Wird vom systemd-Unit nach dem Start aufgerufen: alle Sites hochfahren.
# (Der Container-Entrypoint startet nur CMK_SITE_ID.)
cmd_start_all_sites() {
local tries=0
while ! container_running "$CMK_CONTAINER_NAME"; do
tries=$((tries+1)); [ "$tries" -gt 60 ] && die "Container '$CMK_CONTAINER_NAME' startet nicht."
sleep 2
done
# kurz warten, bis der Entrypoint durch ist
sleep 5
local s
while IFS= read -r s; do
[ -n "$s" ] || continue
if [ "$(cmk_site_state "$s")" != "0" ]; then
log "Starte Site '$s' ..."
ct omd start "$s" || warn "Site '$s' konnte nicht gestartet werden."
else
debug "Site '$s' laeuft bereits."
fi
done < <(cmk_sites)
ok "Alle Sites geprueft/gestartet."
}
# =====================================================================
# Backup
# =====================================================================
cmd_backup() {
local -a sites=() opts=()
local all="" dir="" label="" stop_site=""
while [ $# -gt 0 ]; do
case "$1" in
--site) sites+=("$2"); shift 2 ;;
--all) all=1; shift ;;
--dir) dir="$2"; shift 2 ;;
--label) label="$2"; shift 2 ;;
--no-rrds) opts+=(--no-rrds); shift ;;
--no-logs) opts+=(--no-logs); shift ;;
--stop) stop_site=1; shift ;;
*) die "Unbekannte Option fuer backup: $1" ;;
esac
done
require_cmk_running
dir="${dir:-$BACKUP_DIR}"
mkdir -p "$dir"
if [ -n "$all" ]; then
mapfile -t sites < <(cmk_sites)
fi
[ ${#sites[@]} -gt 0 ] || die "Keine Site angegeben (--site SITE oder --all)."
local s
for s in "${sites[@]}"; do
cmk_site_exists "$s" || die "Site '$s' existiert im Container nicht."
backup_one_site "$s" "$dir" "$label" "$stop_site" "${opts[@]+"${opts[@]}"}"
done
}
backup_one_site() {
local site="$1" dir="$2" label="$3" stop_site="$4"; shift 4
local -a opts=("$@")
local ts version outfile tmp restarted=""
ts="$(date +%Y%m%d-%H%M%S)"
version="$(cmk_site_version "$site")"
outfile="$dir/cmkbackup_${site}_${version:-unknown}${label:+_$label}_${ts}.tar.gz"
if [ -n "$stop_site" ] && [ "$(cmk_site_state "$site")" = "0" ]; then
log "Stoppe Site '$site' fuer ein konsistentes Backup ..."
ct omd stop "$site" >/dev/null
restarted=1
fi
tmp="$(mktemp -d)"
mkdir -p "$tmp/$BUNDLE_DIRNAME"
log "Erzeuge omd-Backup der Site '$site' ${opts[*]+(${opts[*]})} ..."
if ! $RUNTIME exec "$CMK_CONTAINER_NAME" omd backup ${opts[@]+"${opts[@]}"} "$site" - \
>"$tmp/$BUNDLE_DIRNAME/site.tar.gz" 2>"$tmp/err.log" \
|| [ ! -s "$tmp/$BUNDLE_DIRNAME/site.tar.gz" ]; then
warn "Streaming-Backup nicht moeglich - weiche auf Zwischendatei im Container aus."
debug "$(cat "$tmp/err.log" 2>/dev/null)"
ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz"
ct omd backup ${opts[@]+"${opts[@]}"} "$site" /var/tmp/cmkbackup.tar.gz \
|| { rm -rf "$tmp"; die "omd backup fehlgeschlagen."; }
$RUNTIME cp "$CMK_CONTAINER_NAME:/var/tmp/cmkbackup.tar.gz" "$tmp/$BUNDLE_DIRNAME/site.tar.gz"
ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz"
fi
[ -n "$restarted" ] && { log "Starte Site '$site' wieder ..."; ct omd start "$site" >/dev/null; }
local sum size
sum="$(sha256sum "$tmp/$BUNDLE_DIRNAME/site.tar.gz" | awk '{print $1}')"
size="$(file_size "$tmp/$BUNDLE_DIRNAME/site.tar.gz")"
printf '%s site.tar.gz\n' "$sum" >"$tmp/$BUNDLE_DIRNAME/site.tar.gz.sha256"
cat >"$tmp/$BUNDLE_DIRNAME/MANIFEST" <<EOF
BUNDLE_VERSION=1
BUNDLE_TYPE=backup
SITE=$site
CMK_VERSION=$version
SOURCE_KIND=container
SOURCE_HOST=$(hostname -f 2>/dev/null || hostname)
SOURCE_IP=$(primary_ip)
SOURCE_OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || echo unbekannt)
CREATED=$(date -Is)
CREATED_BY=cmk-manage.sh
ARCHIVE=site.tar.gz
ARCHIVE_SHA256=$sum
ARCHIVE_SIZE=$size
OMD_BACKUP_OPTS=${opts[*]-}
LABEL=$label
AGENT_RECEIVER_PORT=8000
EOF
cat >"$tmp/$BUNDLE_DIRNAME/INFO.txt" <<EOF
Checkmk Backup-Bundle
=====================
Site : $site
Checkmk-Version : $version ($(edition_from_version "$version"))
Erstellt : $(date -Is)
Quelle : Container '$CMK_CONTAINER_NAME' auf $(hostname)
Nutzdaten : site.tar.gz ($(human_size "$size"))
Wiederherstellen:
./scripts/cmk-manage.sh restore --file <diese-datei> --reuse
EOF
log "Verpacke Bundle ..."
tar -czf "$outfile" -C "$tmp" "$BUNDLE_DIRNAME"
rm -rf "$tmp"
ok "Backup erstellt: $outfile ($(human_size "$(file_size "$outfile")"))"
LAST_BACKUP_FILE="$outfile"
}
# =====================================================================
# Auflisten / Aufraeumen
# =====================================================================
list_backup_files() {
find "$BACKUP_DIR" -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.tgz' \) -printf '%T@ %p\n' 2>/dev/null \
| sort -rn | cut -d' ' -f2-
}
cmd_list() {
local f n=0
printf '%-58s %10s %s\n' "DATEI" "GROESSE" "SITE / VERSION"
while IFS= read -r f; do
[ -n "$f" ] || continue
bundle_read "$f" 2>/dev/null || continue
printf '%-58s %10s %s / %s\n' "$(basename "$f")" \
"$(human_size "$(file_size "$f")")" "$B_SITE" "${B_CMK_VERSION:-?}"
n=$((n+1))
done < <(list_backup_files)
[ "$n" -eq 0 ] && echo "(keine Backups in $BACKUP_DIR)"
return 0
}
cmd_prune() {
local keep=5
while [ $# -gt 0 ]; do
case "$1" in
--keep) keep="$2"; shift 2 ;;
*) die "Unbekannte Option fuer prune: $1" ;;
esac
done
local -a files=(); mapfile -t files < <(list_backup_files)
if [ ${#files[@]} -le "$keep" ]; then
ok "Nichts zu loeschen (${#files[@]} Backups, behalte $keep)."
return 0
fi
local i
for ((i=keep; i<${#files[@]}; i++)); do
if confirm "Backup '$(basename "${files[$i]}")' loeschen?"; then
rm -f "${files[$i]}" && log "geloescht: $(basename "${files[$i]}")"
fi
done
ok "Aufraeumen beendet."
}
# =====================================================================
# Restore / Import (gemeinsame Implementierung)
# =====================================================================
do_restore() {
local file="$1" as_site="$2" reuse="$3" kill_opt="$4" nostart="$5" mode="$6"
[ -f "$file" ] || die "Datei nicht gefunden: $file"
require_cmk_running
bundle_read "$file" || die "'$file' ist kein Checkmk-Backup (weder Bundle noch omd-Archiv)."
local target="${as_site:-$B_SITE}"
echo
bundle_show "$file"
echo
printf '%sZiel-Site im Container : %s%s\n' "$C_BLD" "$target" "$C_RST"
# --- Pruefsumme -------------------------------------------------
if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then
log "Pruefe Pruefsumme ..."
local sum
sum="$(bundle_extract_payload "$file" | sha256sum | awk '{print $1}')"
[ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme stimmt nicht - Bundle beschaedigt."
ok "Pruefsumme korrekt."
fi
# --- Versionspruefung -------------------------------------------
if [ -n "$B_CMK_VERSION" ]; then
local installed
installed="$(cmk_versions_installed)"
if ! printf '%s\n' "$installed" | grep -qxF "$B_CMK_VERSION"; then
err "Die Checkmk-Version des Backups ist im Container nicht installiert."
printf ' benoetigt : %s\n' "$B_CMK_VERSION"
printf ' installiert : %s\n' "$(printf '%s' "$installed" | tr '\n' ' ')"
cat <<EOF
Loesung: in der .env die passende Version setzen und den Container neu erstellen,
danach importieren und erst anschliessend aktualisieren:
CMK_IMAGE_TAG=${B_CMK_VERSION%.*} # z.B. 2.3.0p23
./scripts/cmk-manage.sh up
./scripts/cmk-manage.sh $mode --file "$file"
("omd restore" kann eine Site nur wiederherstellen, wenn genau ihre
Version im Zielsystem vorhanden ist.)
EOF
die "Abbruch wegen Versionskonflikt."
fi
ok "Version $B_CMK_VERSION ist im Container vorhanden."
fi
# --- Editionspruefung -------------------------------------------
case "$B_CMK_VERSION" in
*.cee|*.cme|*.cce)
warn "Das Backup stammt aus einer $(edition_from_version "$B_CMK_VERSION").
Dafuer wird das passende Enterprise-/Cloud-Image benoetigt
(CMK_IMAGE_REPO=checkmk/check-mk-enterprise)." ;;
esac
# --- vorhandene Site --------------------------------------------
local -a rargs=()
if cmk_site_exists "$target"; then
if [ -z "$reuse" ]; then
die "Site '$target' existiert bereits. Mit --reuse ueberschreiben (Daten gehen verloren!)."
fi
warn "Die vorhandene Site '$target' wird UEBERSCHRIEBEN."
confirm "Wirklich fortfahren?" || die "Abgebrochen."
rargs+=(--reuse)
[ -n "$kill_opt" ] && rargs+=(--kill)
if [ "$(cmk_site_state "$target")" != "2" ]; then
log "Stoppe Site '$target' ..."
ct omd stop "$target" >/dev/null 2>&1 || true
fi
else
confirm "Site '$target' jetzt aus dem Backup wiederherstellen?" || die "Abgebrochen."
fi
if [ -n "$as_site" ] && [ "$as_site" != "$B_SITE" ]; then
warn "Die Site wird von '$B_SITE' nach '$as_site' umbenannt.
Registrierte Agents verweisen auf '$B_SITE' und muessen neu registriert werden!"
confirm "Trotzdem umbenennen?" || die "Abgebrochen."
rargs+=("$as_site")
fi
# --- Nutzdaten in den Container schieben ------------------------
local tmp; tmp="$(mktemp -d)"
log "Bereite Nutzdaten vor ..."
bundle_extract_payload "$file" >"$tmp/payload.tar.gz"
log "Kopiere Archiv in den Container ($(human_size "$(file_size "$tmp/payload.tar.gz")")) ..."
ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz'
$RUNTIME cp "$tmp/payload.tar.gz" "$CMK_CONTAINER_NAME:/var/tmp/cmkrestore.tar.gz"
rm -rf "$tmp"
log "Fuehre 'omd restore ${rargs[*]-} ' aus ..."
if ! ct omd restore ${rargs[@]+"${rargs[@]}"} /var/tmp/cmkrestore.tar.gz; then
ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz' || true
die "omd restore ist fehlgeschlagen."
fi
ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz' || true
ok "Site '$target' wurde wiederhergestellt."
# --- Nachbereitung ----------------------------------------------
# TMPFS im Container abschalten (Mount im Container nicht moeglich/noetig)
ct omd config "$target" set TMPFS off >/dev/null 2>&1 || true
if [ -z "$nostart" ]; then
log "Starte Site '$target' ..."
ct omd start "$target" || warn "Site konnte nicht vollstaendig gestartet werden - bitte 'omd status $target' pruefen."
fi
post_restore_hints "$target" "$mode"
}
post_restore_hints() {
local target="$1" mode="$2" ip; ip="$(primary_ip)"
cat <<EOF
${C_BLD}Naechste Schritte${C_RST}
1) Standard-Site im Container:
In der .env CMK_SITE_ID=$target setzen, sonst startet der Container
nach einem Reboot nur die urspruengliche Site.
(automatisch: $0 $mode --file ... --set-default-site)
2) Erreichbarkeit pruefen:
http://$ip:5000/$target/
3) Im NGINX Proxy Manager einen Proxy-Host anlegen:
Domain -> Forward to http://127.0.0.1:5000
4) Agents: solange Site-Name (${target}) und IP-Adresse gleich bleiben und
Port 8000 (Agent-Receiver) offen ist, muessen die Agents NICHT neu
registriert werden - die Site-Zertifikate sind im Backup enthalten.
Pruefen: $0 omd su $target -> cmk -O ; danach im GUI die
Host-Status/Agent-Verbindungen kontrollieren.
EOF
}
cmd_restore() {
local file="" as_site="" reuse="" kill_opt="" nostart=""
while [ $# -gt 0 ]; do
case "$1" in
--file) file="$2"; shift 2 ;;
--as-site) as_site="$2"; shift 2 ;;
--reuse) reuse=1; shift ;;
--kill) kill_opt=1; shift ;;
--no-start) nostart=1; shift ;;
*) die "Unbekannte Option fuer restore: $1" ;;
esac
done
[ -n "$file" ] || die "--file fehlt."
do_restore "$file" "$as_site" "$reuse" "$kill_opt" "$nostart" "restore"
}
cmd_import() {
local file="" reuse="" kill_opt="" nostart="" setdefault=""
while [ $# -gt 0 ]; do
case "$1" in
--file) file="$2"; shift 2 ;;
--reuse) reuse=1; shift ;;
--kill) kill_opt=1; shift ;;
--no-start) nostart=1; shift ;;
--set-default-site) setdefault=1; shift ;;
*) die "Unbekannte Option fuer import: $1" ;;
esac
done
[ -n "$file" ] || die "--file fehlt."
bundle_read "$file" || die "'$file' ist kein gueltiges Migrations-Bundle."
if [ "$B_IS_BUNDLE" = 1 ] && [ "$B_BUNDLE_TYPE" = "migrate" ]; then
log "Migrations-Bundle erkannt (Quelle: ${B_SOURCE_HOST:-?} / ${B_SOURCE_IP:-?})."
local myip; myip="$(primary_ip)"
if [ -n "$B_SOURCE_IP" ] && [ "$B_SOURCE_IP" != "$myip" ]; then
warn "Die IP dieses Hosts ($myip) weicht von der Quell-IP ($B_SOURCE_IP) ab.
Damit registrierte Agents ohne Neuregistrierung weiterarbeiten, muss dieser
Host die IP $B_SOURCE_IP uebernehmen (bzw. der DNS-Name darauf zeigen)."
confirm "Trotzdem importieren?" || die "Abgebrochen."
fi
fi
do_restore "$file" "" "$reuse" "$kill_opt" "$nostart" "import"
if [ -n "$setdefault" ]; then
env_set CMK_SITE_ID "$B_SITE"
ok "CMK_SITE_ID=$B_SITE in der .env gesetzt."
warn "Damit die Aenderung wirkt, den Container neu erstellen: $0 up"
fi
}
# =====================================================================
# Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager
# =====================================================================
cmd_cert() {
local domain="${CERT_DOMAIN:-checkmk.example.local}" days="${CERT_DAYS:-3650}"
local -a alts=()
while [ $# -gt 0 ]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--alt) alts+=("$2"); shift 2 ;;
--days) days="$2"; shift 2 ;;
*) die "Unbekannte Option fuer cert: $1" ;;
esac
done
command -v openssl >/dev/null 2>&1 || die "openssl ist nicht installiert."
local outdir="$PROJECT_DIR/data/npm/custom-certs/$domain"
mkdir -p "$outdir"
local san="DNS:$domain"
local a
for a in ${alts[@]+"${alts[@]}"}; do
case "$a" in
[0-9]*.[0-9]*.[0-9]*.[0-9]*) san="$san,IP:$a" ;;
*) san="$san,DNS:$a" ;;
esac
done
local myip; myip="$(primary_ip)"
[ -n "$myip" ] && san="$san,IP:$myip"
log "Erzeuge selbstsigniertes Zertifikat fuer '$domain' ($days Tage) ..."
openssl req -x509 -nodes -newkey rsa:4096 -sha256 -days "$days" \
-keyout "$outdir/privkey.pem" -out "$outdir/fullchain.pem" \
-subj "/C=${CERT_COUNTRY:-DE}/O=${CERT_ORG:-Selfsigned}/CN=$domain" \
-addext "subjectAltName=$san" \
-addext "basicConstraints=critical,CA:FALSE" \
-addext "keyUsage=critical,digitalSignature,keyEncipherment" \
-addext "extendedKeyUsage=serverAuth" 2>/dev/null \
|| die "Zertifikatserstellung fehlgeschlagen."
chmod 600 "$outdir/privkey.pem"
ok "Zertifikat erstellt:"
printf ' Schluessel : %s\n' "$outdir/privkey.pem"
printf ' Zertifikat : %s\n' "$outdir/fullchain.pem"
printf ' SAN : %s\n\n' "$san"
cat <<EOF
Einbinden im NGINX Proxy Manager:
1) http://$(primary_ip):81 -> SSL Certificates -> Add SSL Certificate -> Custom
2) Name z.B. "$domain"
Certificate Key : privkey.pem
Certificate : fullchain.pem
3) Im Proxy-Host unter "SSL" dieses Zertifikat auswaehlen,
"Force SSL" und "HTTP/2 Support" aktivieren.
EOF
}
# =====================================================================
# ncurses-Oberflaeche
# =====================================================================
gui_pick_site() { # -> Site auf stdout
local -a items=() s
while IFS= read -r s; do
[ -n "$s" ] || continue
items+=("$s" "$(cmk_site_version "$s") / $(cmk_site_state_text "$s")")
done < <(cmk_sites)
[ ${#items[@]} -gt 0 ] || { ui_msg "Sites" "Im Container ist keine Site vorhanden."; return 1; }
ui_menu "Site waehlen" "Vorhandene Sites im Container:" "${items[@]}"
}
gui_pick_backup() { # -> Datei auf stdout
local -a items=() f
while IFS= read -r f; do
[ -n "$f" ] || continue
bundle_read "$f" 2>/dev/null || continue
items+=("$f" "$B_SITE ${B_CMK_VERSION:-?} $(human_size "$(file_size "$f")")")
done < <(list_backup_files)
if [ ${#items[@]} -eq 0 ]; then
ui_msg "Backups" "In $BACKUP_DIR wurde kein Backup gefunden.\n\nAlternativ kann im naechsten Schritt frei im Dateisystem gesucht werden."
ui_pick_file "$PROJECT_DIR" '*.tar.gz'
return
fi
items+=("__browse__" "... anderes Verzeichnis durchsuchen ...")
local sel
sel="$(ui_menu "Backup waehlen" "Backups in $BACKUP_DIR:" "${items[@]}")" || return 1
if [ "$sel" = "__browse__" ]; then
ui_pick_file "$PROJECT_DIR" '*.tar.gz'
else
printf '%s\n' "$sel"
fi
}
gui_status() {
local f; f="$(mktemp)"
NO_COLOR=1 cmd_status >"$f" 2>&1 || true
ui_text "Status" "$f"
rm -f "$f"
}
gui_stack_menu() {
while :; do
local sel
sel="$(ui_menu "Stack steuern" "Container-Runtime: $RUNTIME" \
up "Stack starten (compose up -d)" \
restart "Stack neu starten" \
down "Stack stoppen und entfernen" \
pull "Images aktualisieren" \
logs "Letzte 200 Logzeilen ansehen" \
back "zurueck")" || return 0
case "$sel" in
up) ui_run "Stack starten" bash -c "cd '$PROJECT_DIR' && '$SELF' up" ;;
restart) ui_run "Neu starten" bash -c "cd '$PROJECT_DIR' && '$SELF' restart" ;;
down) ui_yesno "Stack stoppen" "Container wirklich stoppen und entfernen?\n(Die Daten unter ./data bleiben erhalten.)" \
&& ui_run "Stack stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' down" ;;
pull) ui_run "Images laden" bash -c "cd '$PROJECT_DIR' && '$SELF' pull" ;;
logs) local lf; lf="$(mktemp)"; compose logs --tail=200 >"$lf" 2>&1 || true
ui_text "Logs (letzte 200 Zeilen)" "$lf"; rm -f "$lf" ;;
back) return 0 ;;
esac
done
}
gui_backup() {
require_cmk_running
local -a items=() s
while IFS= read -r s; do
[ -n "$s" ] || continue
items+=("$s" "$(cmk_site_version "$s")" off)
done < <(cmk_sites)
[ ${#items[@]} -gt 0 ] || { ui_msg "Backup" "Keine Sites vorhanden."; return; }
local chosen
chosen="$(ui_checklist "Backup" "Zu sichernde Site(s) mit LEERTASTE markieren:" "${items[@]}")" || return
[ -n "$chosen" ] || { ui_msg "Backup" "Keine Site ausgewaehlt."; return; }
local optsel
optsel="$(ui_checklist "Backup-Optionen" "Optionen waehlen:" \
no-rrds "Performance-Daten (RRD) auslassen - kleineres Archiv" off \
no-logs "Logdateien auslassen" off \
stop "Site waehrend des Backups stoppen (konsistenter)" off)" || return
local label
label="$(ui_input "Kennzeichnung" "Optionaler Text fuer den Dateinamen (leer = keiner):" "")" || label=""
label="$(printf '%s' "$label" | tr -cd 'A-Za-z0-9._-')"
local -a args=(backup --dir "$BACKUP_DIR")
[ -n "$label" ] && args+=(--label "$label")
case "$optsel" in *no-rrds*) args+=(--no-rrds);; esac
case "$optsel" in *no-logs*) args+=(--no-logs);; esac
case "$optsel" in *stop*) args+=(--stop);; esac
local sitename
for sitename in $(printf '%s' "$chosen" | tr -d '"'); do
args+=(--site "$sitename")
done
ui_run "Backup laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y ${args[*]}"
}
gui_restore() {
require_cmk_running
local file; file="$(gui_pick_backup)" || return
[ -n "$file" ] || return
local f; f="$(mktemp)"
bundle_show "$file" >"$f" 2>&1 || true
ui_text "Backup-Informationen" "$f"; rm -f "$f"
bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Checkmk-Backup."; return; }
local reuse=""
if cmk_site_exists "$B_SITE"; then
ui_yesno "Site existiert" "Die Site '$B_SITE' ist bereits vorhanden.\n\nSoll sie UEBERSCHRIEBEN werden?\nAlle aktuellen Daten dieser Site gehen verloren!" || return
reuse="--reuse --kill"
fi
ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return
ui_run "Restore laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y restore --file '$file' $reuse"
}
gui_import() {
require_cmk_running
ui_msg "Site importieren" \
"Import einer Site von einer Direktinstallation (Bare-Metal/VM).
Voraussetzung: auf dem Quellserver wurde mit 'migrate_creator.sh'
ein Migrations-Bundle (.tar.gz) erzeugt und hierher kopiert.
Wichtig fuer die Agents:
- Site-Name bleibt unveraendert
- dieser Host uebernimmt die IP/den DNS-Namen des Quellservers
- Port 8000 (Agent-Receiver) ist erreichbar
Dann muessen die Agents NICHT neu eingerichtet werden."
local file; file="$(gui_pick_backup)" || return
[ -n "$file" ] || return
local f; f="$(mktemp)"
bundle_show "$file" >"$f" 2>&1 || true
ui_text "Bundle-Informationen" "$f"; rm -f "$f"
bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Migrations-Bundle."; return; }
local reuse=""
if cmk_site_exists "$B_SITE"; then
ui_yesno "Site existiert" "Die Site '$B_SITE' existiert bereits im Container.\n\nUEBERSCHREIBEN?" || return
reuse="--reuse --kill"
fi
local setdef=""
ui_yesno "Standard-Site" "Soll CMK_SITE_ID in der .env auf '$B_SITE' gesetzt werden?\n\n(Empfohlen - sonst startet der Container nach einem Reboot\ndie importierte Site nicht automatisch.)" \
&& setdef="--set-default-site"
ui_run "Import laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y import --file '$file' $reuse $setdef"
}
gui_backups_menu() {
while :; do
local sel
sel="$(ui_menu "Backups verwalten" "Verzeichnis: $BACKUP_DIR" \
list "Backups auflisten" \
info "Details eines Backups anzeigen" \
verify "Backup pruefen (Struktur + Pruefsumme)" \
del "Backup loeschen" \
back "zurueck")" || return 0
case "$sel" in
list) local f; f="$(mktemp)"; NO_COLOR=1 cmd_list >"$f" 2>&1 || true
ui_text "Backups" "$f"; rm -f "$f" ;;
info) local b; b="$(gui_pick_backup)" || continue; [ -n "$b" ] || continue
local f2; f2="$(mktemp)"; bundle_show "$b" >"$f2" 2>&1 || true
ui_text "Details" "$f2"; rm -f "$f2" ;;
verify) local b2; b2="$(gui_pick_backup)" || continue; [ -n "$b2" ] || continue
ui_run "Pruefe Bundle" bash -c "cd '$PROJECT_DIR' && '$SELF' verify --file '$b2'" ;;
del) local b3; b3="$(gui_pick_backup)" || continue; [ -n "$b3" ] || continue
ui_yesno "Loeschen" "Datei wirklich loeschen?\n\n$b3" && rm -f "$b3" \
&& ui_msg "Geloescht" "Die Datei wurde entfernt." ;;
back) return 0 ;;
esac
done
}
gui_sites_menu() {
require_cmk_running
while :; do
local sel
sel="$(ui_menu "Sites" "Sites im Container '$CMK_CONTAINER_NAME'" \
list "Sites anzeigen" \
start "Site starten" \
stop "Site stoppen" \
all "alle Sites starten" \
back "zurueck")" || return 0
case "$sel" in
list) local f; f="$(mktemp)"; cmd_sites >"$f" 2>&1 || true; ui_text "Sites" "$f"; rm -f "$f" ;;
start) local s; s="$(gui_pick_site)" || continue; [ -n "$s" ] || continue
ui_run "Site starten" bash -c "cd '$PROJECT_DIR' && '$SELF' site-start '$s'" ;;
stop) local s2; s2="$(gui_pick_site)" || continue; [ -n "$s2" ] || continue
ui_run "Site stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' site-stop '$s2'" ;;
all) ui_run "Alle Sites starten" bash -c "cd '$PROJECT_DIR' && '$SELF' start-all-sites" ;;
back) return 0 ;;
esac
done
}
gui_cert() {
local domain alt days
domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return
[ -n "$domain" ] || return
alt="$(ui_input "Zertifikat" "Weitere Namen/IPs (durch Leerzeichen getrennt, optional):" "$(primary_ip)")" || alt=""
days="$(ui_input "Zertifikat" "Gueltigkeit in Tagen:" "${CERT_DAYS:-3650}")" || days=3650
local -a a=(); local x
for x in $alt; do a+=(--alt "$x"); done
ui_run "Zertifikat erzeugen" bash -c "cd '$PROJECT_DIR' && '$SELF' cert --domain '$domain' --days '$days' ${a[*]+${a[*]}}"
}
gui_env() {
local f="${ENV_FILE:-$PROJECT_DIR/.env}"
[ -f "$f" ] || { ui_msg ".env" "Keine .env vorhanden."; return; }
ui_text "Konfiguration ($f)" "$f"
if ui_yesno "Bearbeiten" "Die .env jetzt im Editor oeffnen?"; then
"${EDITOR:-nano}" "$f"
load_env "$f"
fi
}
gui_main() {
ui_require
while :; do
local sel
sel="$(ui_menu "Checkmk Stack Manager" \
"Projekt : $PROJECT_DIR
Runtime : $RUNTIME Standard-Site: $CMK_SITE_ID
Backups : $BACKUP_DIR" \
status "Status von Containern und Sites" \
stack "Stack steuern (Start/Stop/Logs/Update)" \
backup "Backup erstellen" \
restore "Backup wiederherstellen" \
import "Site von Direktinstallation importieren" \
manage "Backups verwalten (Liste/Details/Loeschen)" \
sites "Sites im Container verwalten" \
cert "Selbstsigniertes Zertifikat erzeugen" \
env "Konfiguration (.env) ansehen/bearbeiten" \
shell "Root-Shell im Checkmk-Container" \
quit "Beenden")" || break
case "$sel" in
status) gui_status ;;
stack) gui_stack_menu ;;
backup) gui_backup ;;
restore) gui_restore ;;
import) gui_import ;;
manage) gui_backups_menu ;;
sites) gui_sites_menu ;;
cert) gui_cert ;;
env) gui_env ;;
shell) clear; cmd_shell || true ;;
quit) break ;;
esac
done
clear
ok "Beendet."
}
# =====================================================================
# Einstiegspunkt
# =====================================================================
main() {
while [ $# -gt 0 ]; do
case "$1" in
--env-file) ENVFILE_OPT="$2"; shift 2 ;;
-y|--yes) ASSUME_YES=1; shift ;;
-v|--verbose) VERBOSE=1; shift ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
load_env "${ENVFILE_OPT:-$PROJECT_DIR/.env}"
detect_runtime
UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME"
local cmd="${1:-gui}"; [ $# -gt 0 ] && shift || true
case "$cmd" in
gui|menu) gui_main ;;
up|start) cmd_up ;;
down|stop) cmd_down ;;
restart) cmd_restart ;;
pull) cmd_pull ;;
status) cmd_status ;;
logs) cmd_logs "$@" ;;
shell) cmd_shell ;;
omd) cmd_omd "$@" ;;
sites) cmd_sites ;;
site-start) cmd_site_start "$@" ;;
site-stop) cmd_site_stop "$@" ;;
start-all-sites) cmd_start_all_sites ;;
backup) cmd_backup "$@" ;;
list) cmd_list ;;
prune) cmd_prune "$@" ;;
verify) local vf=""; [ "${1:-}" = "--file" ] && vf="$2"; [ -n "$vf" ] || vf="${1:-}"
[ -n "$vf" ] || die "--file fehlt."; bundle_verify "$vf" ;;
restore) cmd_restore "$@" ;;
import) cmd_import "$@" ;;
cert) cmd_cert "$@" ;;
help|-h|--help) usage ;;
*) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;;
esac
}
main "$@"