#!/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] [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 # # .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 "/..." - 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 </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:///%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" </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" < --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 </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 < 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 < 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 "$@"