Files
docker-podman-installation-…/scripts/cmk-manage.sh
T
duffyduckandClaude Opus 5 667c119a6c Rahmen in PuTTY: UTF-8-Locale fuer die Oberflaeche erzwingen
Gemeldet mit Bildschirmfoto: statt Rahmen erscheinen q, x, l und m.
Ursache nachgemessen - ohne UTF-8-Locale zeichnen dialog und whiptail
die Rahmen ueber die alte VT100-Umschaltung ESC(0, die Terminals im
UTF-8-Modus (PuTTY in der Vorgabe) ignorieren:

    LC_ALL=C        ESC(0-Umschaltungen: 1   rohe q: 91
    LC_CTYPE=C.UTF-8  ESC(0-Umschaltungen: 0   rohe q: 0

ui_init setzt jetzt beim Start der Oberflaeche LC_CTYPE auf die erste
verfuegbare UTF-8-Locale. Bewusst nur LC_CTYPE, damit Sortierung und
Zahlenformat unberuehrt bleiben. Dabei war ein zweiter Anlauf noetig:
ein gesetztes LC_ALL sticht jede einzelne Kategorie aus, der erste
Versuch blieb deshalb wirkungslos. Steht LC_ALL auf einem
Nicht-UTF-8-Wert, wird es entfernt.

Gemessen ueber unsere eigene ui_menu unter LC_ALL=C: vorher 91 rohe q,
danach 0.

Gibt es gar keine UTF-8-Locale, faellt die Oberflaeche auf einfache
Rahmen zurueck (dialog --ascii-lines, sofern unterstuetzt); erzwingen
laesst sich das mit CMK_TUI_ASCII=1. Der Selbsttest hat dafuer einen
eigenen Abschnitt, der die Locale vorher und nachher zeigt und im
Zweifel das fehlende Sprachpaket nennt.

Dokumentiert als Abschnitt 14.4 in beiden Sprachfassungen, samt der
vier PuTTY-Einstellungen fuer die Gegenseite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 18:20:47 +02:00

4840 lines
199 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
selftest Skripte auf bekannte Fallstricke pruefen
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
job list angelegte Backup-Jobs anzeigen
job status Zeitplan, Cron-Datei und Schalter anzeigen
job add --name N --cron "15 2 * * *" [Optionen] Job anlegen
--sites ALL|a,b zu sichernde Sites (Vorgabe ALL)
--dir VERZ Zielverzeichnis des Jobs (Vorgabe: $BACKUP_DIR)
--keep N Anzahl Backups, die im Ziel bleiben (0 = alle)
--no-rrds --no-logs --stop wie bei "backup"
--disabled angelegt, aber noch nicht eingeplant
job set NAME [Optionen] vorhandenen Job aendern
job del NAME Job loeschen
job enable|disable NAME Job ein-/ausschalten
job run NAME Job sofort ausfuehren
job run-all alle aktiven Jobs ausfuehren
job auto [on|off] automatische Ausfuehrung per cron ein/aus
KOMMANDOS - Checkmk-Image beschaffen
image status konfiguriertes und lokal vorhandene Images anzeigen
image list lokal vorhandene Checkmk-Images
image versions [Opt.] verfuegbare Versionen auflisten (zum Kopieren)
--edition E Edition (Vorgabe: CMK_EDITION)
--source registry|download|web|all Quelle (Vorgabe all)
web = oeffentliches Versionsarchiv von checkmk.com,
funktioniert ohne Zugangsdaten
--filter TEXT nur Versionen, die TEXT enthalten, z.B. 2.3.0
--limit N hoechstens N Eintraege (Vorgabe 25), --all fuer alle
--user U --password P statt der Werte aus der .env
--bare nur die nackte Liste, ohne Rahmen
image repos [Opt.] herausfinden, welche Repositories die Registry
wirklich anbietet (Katalog + uebliche Pfade)
image test [Opt.] Zugangsdaten pruefen, ohne etwas herunterzuladen
--edition E Edition, deren Repository geprueft wird
--version V Version fuer die Pruefung des Downloadportals
--user U --password P statt der Werte aus der .env
--only registry|download nur einen Bezugsweg pruefen
image pull [Opt.] Image aus einer Registry holen
--edition E raw|community|free|cloud|managed|pro|ultimate|
ultimatemt|enterprise (Vorgabe: CMK_EDITION)
--version V z.B. 2.3.0p23 (Vorgabe: CMK_VERSION)
--registry R abweichende Registry
--user U --password P Zugangsdaten (Enterprise: Kundenportal)
--image REF fertige Referenz statt Edition/Version
--remember CMK_EDITION und CMK_VERSION mit in die .env schreiben
--no-set .env nicht veraendern
image download [Opt.] Image-Tarball vom Downloadportal holen und einspielen
--edition/--version/--user/--password wie oben
--url URL vollstaendige URL fest vorgeben
--dir VERZ Ablage (Vorgabe: $IMAGE_DIR)
--keep Tarball nach dem Einspielen behalten
--remember CMK_EDITION und CMK_VERSION mit in die .env schreiben
image load --file F bereits heruntergeladenen Tarball einspielen
image use REF [--remember] Image als CMK_IMAGE eintragen; mit
--remember auch CMK_EDITION und CMK_VERSION daraus
ableiten
image use --edition E --version V
umgekehrt: die Referenz aus Edition und Version
bilden und eintragen, OHNE etwas herunterzuladen.
Geholt wird sie beim naechsten "up".
KOMMANDOS - Konfiguration
config list [--category KAT] alle Einstellungen mit aktuellen Werten
config get KEY einzelnen Wert ausgeben
config set KEY WERT Wert setzen (mit Pruefung)
config edit .env im Editor oeffnen
npm-port [PORT] Port der Proxy-Manager-Oberflaeche
(ohne PORT: aktuellen Stand anzeigen)
npm-enable [on|off] NGINX Proxy Manager ein-/ausschalten.
"off" stoppt und entfernt den Container,
die Daten unter ./data/npm bleiben erhalten
(fuer den Fall, dass ein anderer NGINX die
HTTPS-Terminierung uebernimmt)
KOMMANDOS - Lokale Bedienung ohne externen Zugriff
browser [Optionen] Terminal-Browser im Container starten, um den
NGINX Proxy Manager direkt auf der Maschine
einzurichten (Host-Netzwerk, erreicht 127.0.0.1:81)
-e, --engine E browsh (Vorgabe) | carbonyl | links | w3m | lynx | shell
browsh/carbonyl koennen JavaScript - die Textbrowser
NICHT, damit laesst sich der Proxy Manager nicht bedienen
-u, --url URL Startseite (Vorgabe: http://127.0.0.1:81)
--build Werkzeug-Image neu bauen
--pull Engine-Image neu laden
KOMMANDOS - Proxy-Hosts (ueber die API des Proxy Managers)
proxy status Anmeldung pruefen, Anzahl Hosts/Zertifikate
proxy list vorhandene Proxy-Hosts
proxy certs vorhandene Zertifikate
proxy add [Opt.] Proxy-Host anlegen
--domain FQDN oeffentlicher Name (Vorgabe: PROXY_DOMAIN)
--forward-host H Ziel (Vorgabe 127.0.0.1)
--forward-port P Zielport (Vorgabe 5000 = Checkmk)
--scheme http|https
--cert self|letsencrypt|none (Vorgabe self)
--update vorhandenen Host ersetzen
--advanced TEXT eigener nginx-Abschnitt
--location /P=HOST:PORT weiterer Pfad auf demselben Namen,
mehrfach angebbar. Damit lassen sich mehrere
Dienste hinter EINEM Port unterscheiden:
--location /grafana=127.0.0.1:3000
--no-force-ssl --no-http2 --no-websockets --no-block-exploits
proxy show --domain X | --id N Einzelheiten samt Pfaden anzeigen
proxy del --id N Proxy-Host loeschen
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 image test --edition enterprise --version 2.3.0p23
./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \
--user <portal-benutzer> --password <kennwort>
./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23
./scripts/cmk-manage.sh image load --file images/check-mk-enterprise-2.3.0p23.docker.tar.gz
./scripts/cmk-manage.sh npm-port 10443 # Adminoberflaeche auf Port 10443
./scripts/cmk-manage.sh config set CMK_SITE_ID prod
./scripts/cmk-manage.sh browser # Proxy Manager lokal bedienen
./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
}
# =====================================================================
# Checkmk-Image beschaffen
#
# Hintergrund:
# * Die Enterprise-Edition liegt NICHT auf Docker Hub. Dort gibt es nur
# raw / community / cloud / managed / pro / ultimate / ultimatemt.
# Enterprise kommt aus registry.checkmk.com (Login) oder als Tarball
# vom Checkmk-Downloadportal (HTTP-Basic-Auth).
# * Ein heruntergeladener Image-Tarball wird mit "load" eingespielt,
# NICHT mit "import". "import" erwartet ein Dateisystem-Archiv und
# erzeugt ein Image ohne Metadaten (ohne Entrypoint, ohne Tag) -
# danach "kennt" die Runtime das Image scheinbar nicht.
# * Nach dem Laden traegt dieses Skript den TATSAECHLICHEN, voll-
# qualifizierten Namen in CMK_IMAGE der .env ein.
# =====================================================================
edition_repo() { # Edition -> vollqualifiziertes Repository
case "$1" in
raw) echo "docker.io/checkmk/check-mk-raw" ;;
community) echo "docker.io/checkmk/check-mk-community" ;;
free) echo "docker.io/checkmk/check-mk-free" ;;
cloud) echo "docker.io/checkmk/check-mk-cloud" ;;
managed) echo "docker.io/checkmk/check-mk-managed" ;;
pro) echo "docker.io/checkmk/check-mk-pro" ;;
ultimate) echo "docker.io/checkmk/check-mk-ultimate" ;;
ultimatemt) echo "docker.io/checkmk/check-mk-ultimatemt" ;;
enterprise) echo "${CMK_REGISTRY:-registry.checkmk.com}/enterprise/check-mk-enterprise" ;;
*) return 1 ;;
esac
}
edition_known() { edition_repo "$1" >/dev/null 2>&1; }
# Braucht diese Edition eine Anmeldung an der Registry?
edition_needs_login() { [ "$1" = enterprise ]; }
edition_pkgname() { echo "check-mk-$1"; }
# Lokal vorhandene Checkmk-Images
image_local_list() {
$RUNTIME images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
| grep -Ei 'check-?mk' | grep -v '<none>' | sort -u || true
}
image_exists_local() {
$RUNTIME image inspect "$1" >/dev/null 2>&1
}
image_is_short_name() {
# Kurzer Name = ohne Registry-Anteil. Podman muesste ihn dann ueber
# unqualified-search-registries erraten.
case "$1" in
*/*) : ;;
*) return 0 ;; # gar kein "/" -> immer kurz
esac
case "${1%%/*}" in
*.*|*:*|localhost) return 1 ;; # Registry mit Punkt, Port oder localhost
*) return 0 ;;
esac
}
image_set_env() {
local ref="$1"
env_set CMK_IMAGE "$ref"
ok "CMK_IMAGE=$ref in der .env eingetragen."
if image_is_short_name "$ref"; then
warn "'$ref' ist ein KURZER Name ohne Registry. Podman wuerde ihn ueber
unqualified-search-registries aufloesen (auf RHEL zuerst
registry.access.redhat.com). Besser vollqualifiziert angeben."
fi
log "Danach den Stack neu erstellen: $SELF up"
}
# Vor/Nachher-Vergleich, um den Namen eines geladenen Images sicher zu finden
image_snapshot() { $RUNTIME images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | sort -u || true; }
# Tarball einspielen -> gibt den geladenen Image-Namen auf stdout aus
image_load_file() {
local file="$1"
[ -f "$file" ] || die "Datei nicht gefunden: $file"
# Ist das ueberhaupt ein Container-Image-Archiv (docker-archive/OCI)?
if ! tar -tzf "$file" 2>/dev/null | grep -qE '^(manifest\.json|oci-layout|index\.json)$'; then
if tar -tf "$file" 2>/dev/null | grep -qE '^(manifest\.json|oci-layout|index\.json)$'; then
:
else
err "'$file' enthaelt kein manifest.json / oci-layout."
cat >&2 <<'EOF'
Das ist damit KEIN Container-Image-Archiv, sondern vermutlich ein
normales tar-Archiv. Nur "docker save"/"podman save"-Archive bzw. die
Image-Tarballs von Checkmk lassen sich mit "load" einspielen.
Haeufige Verwechslung:
podman import -> erwartet ein Dateisystem-Archiv (rootfs),
erzeugt ein Image OHNE Entrypoint und ohne Tag
podman load -> spielt ein echtes Image inkl. Metadaten ein <-- richtig
EOF
return 1
fi
fi
local before after loaded
before="$(image_snapshot)"
log "Spiele Image ein: $RUNTIME load -i $file (das dauert einen Moment) ..." >&2
local out
out="$($RUNTIME load -i "$file" 2>&1)" || { printf '%s\n' "$out" >&2; return 1; }
printf '%s\n' "$out" >&2
# 1. Versuch: Ausgabe auswerten
# docker: "Loaded image: repo:tag" podman: "Loaded image(s): repo:tag"
loaded="$(printf '%s\n' "$out" | sed -nE 's/^Loaded image(\(s\))?:[[:space:]]*//p' | head -n1 | tr -d '\r')"
# 2. Versuch: Differenz der Image-Liste (bevorzugt ein Checkmk-Image)
if [ -z "$loaded" ] || [ "${loaded#sha256:}" != "$loaded" ]; then
after="$(image_snapshot)"
local diff_new
diff_new="$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after") | grep -v '<none>')"
local cand
cand="$(printf '%s\n' "$diff_new" | grep -Ei 'check-?mk' | head -n1)"
[ -n "$cand" ] || cand="$(printf '%s\n' "$diff_new" | sed '/^$/d' | head -n1)"
if [ -n "$cand" ]; then loaded="$cand"; fi
fi
if [ -z "$loaded" ]; then
err "Der Name des geladenen Images konnte nicht ermittelt werden."
err "Bitte manuell nachsehen: $RUNTIME images"
return 1
fi
if [ "${loaded#sha256:}" != "$loaded" ]; then
err "Das Archiv enthaelt keinen Image-Namen (nur die ID $loaded)."
err "Manuell benennen: $RUNTIME tag $loaded localhost/checkmk/check-mk-enterprise:<version>"
return 1
fi
printf '%s\n' "$loaded"
}
# Datei mit HTTP-Basic-Auth holen (Zugangsdaten nicht in der Prozessliste)
image_fetch_url() {
local url="$1" out="$2" user="$3" pass="$4"
local host; host="$(printf '%s' "$url" | sed -E 's|^[a-z]+://([^/]+).*|\1|')"
if command -v curl >/dev/null 2>&1; then
# Erst nachsehen, was der Server sagt - dann ist im Fehlerfall
# klar, ob es an der Anmeldung (401) oder am Namen (404) liegt.
local code; code="$(http_code "$url" "$user" "$pass")"
case "$code" in
200) : ;;
401|403) warn "Anmeldung abgelehnt (HTTP $code)"
printf ' %s\n' "$url" >&2
log "Zugangsdaten pruefen: $SELF image test --only download"
return 1 ;;
404) log "Nicht vorhanden (HTTP 404): $(basename "$url")"; return 1 ;;
000) warn "Nicht erreichbar: $url"; return 1 ;;
*) warn "Unerwartete Antwort (HTTP $code): $url"; return 1 ;;
esac
log "Lade $url"
if curl_auth "$user" "$pass" -fL --progress-bar -o "$out" "$url"; then
return 0
fi
return 1
elif command -v wget >/dev/null 2>&1; then
warn "curl fehlt - verwende wget (die Zugangsdaten sind dabei kurzzeitig
in der Prozessliste sichtbar)."
local -a wa=()
if [ -n "$user" ]; then wa=(--user="$user" --password="$pass"); fi
wget ${wa[@]+"${wa[@]}"} -O "$out" "$url"
return $?
else
die "Weder curl noch wget vorhanden."
fi
}
# ---------------------------------------------------------------------
# Versionsarchiv von checkmk.com
#
# https://checkmk.com/download/archive ist serverseitig gerendert und
# braucht KEINE Zugangsdaten. Je Version steht dort
# data-handle="2.3.0p9" data-versions="{...}"
# und im JSON die echten Dateinamen je Edition. Daran haengt auch die
# Aufloesung der Verwirrung um die Editionsnamen: die Website benennt
# 2.3.0p9 als "pro", die Datei heisst weiterhin
# check-mk-enterprise-docker-2.3.0p9.tar.gz
# ---------------------------------------------------------------------
ARCHIVE_CACHE=""
archive_fetch() { # Seite einmal je Aufruf holen, Pfad auf stdout
if [ -n "$ARCHIVE_CACHE" ] && [ -s "$ARCHIVE_CACHE" ]; then
printf '%s\n' "$ARCHIVE_CACHE"; return 0
fi
command -v curl >/dev/null 2>&1 || return 1
local f; f="$(mktemp)"
if curl -sS -L --max-time 60 "${CMK_ARCHIVE_URL:-https://checkmk.com/download/archive}" -o "$f" 2>/dev/null \
&& [ -s "$f" ]; then
ARCHIVE_CACHE="$f"; printf '%s\n' "$f"; return 0
fi
rm -f "$f"; return 1
}
archive_versions() { # alle im Archiv gelisteten Versionen
local f; f="$(archive_fetch)" || return 1
grep -oE 'data-handle="[0-9]+\.[0-9]+\.[0-9]+[a-z0-9.]*"' "$f" \
| sed -E 's/data-handle="([^"]*)"/\1/' | sort -u
}
# Exakten Dateinamen des Docker-Tarballs ermitteln
archive_docker_file() { # edition version -> Dateiname auf stdout
local edition="$1" version="$2"
local f; f="$(archive_fetch)" || return 1
# Abschnitt dieser Version herausschneiden und die Docker-Dateinamen lesen
local names
names="$(grep -oE "data-handle=\"$version\"[^>]*" "$f" \
| grep -oE "check-mk-[a-z]+-docker-${version}\.tar\.gz" | sort -u)"
[ -n "$names" ] || return 1
local ed
for ed in $(edition_aliases "$edition"); do
if printf '%s\n' "$names" | grep -qx "check-mk-${ed}-docker-${version}.tar.gz"; then
printf 'check-mk-%s-docker-%s.tar.gz\n' "$ed" "$version"; return 0
fi
done
return 1
}
# Dateinamen der Image-Tarballs im Downloadportal.
# Bestaetigt am echten Server:
# .../checkmk/2.3.0p9/check-mk-raw-docker-2.3.0p9.tar.gz -> 200
# .../checkmk/2.3.0p9/check-mk-enterprise-docker-2.3.0p9.tar.gz -> 401 (geschuetzt)
# .../checkmk/2.3.0p9/check-mk-raw-2.3.0p9.docker.tar.gz -> 404
# Zusaetzlich die Editionsnamen ab 2.5 mitprobieren, damit derselbe
# Aufruf auch fuer neuere Versionen funktioniert.
edition_aliases() {
case "$1" in
enterprise) printf '%s\n' enterprise pro ;;
pro) printf '%s\n' pro enterprise ;;
raw) printf '%s\n' raw community ;;
community) printf '%s\n' community raw ;;
managed) printf '%s\n' managed ultimatemt ;;
ultimatemt) printf '%s\n' ultimatemt managed ;;
*) printf '%s\n' "$1" ;;
esac
}
download_url_candidates() { # edition version -> URLs, beste zuerst
local edition="$1" version="$2" ed
local base="${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}"
# 1. Wahl: der im Archiv hinterlegte, echte Dateiname
local exact
if exact="$(archive_docker_file "$edition" "$version" 2>/dev/null)" && [ -n "$exact" ]; then
printf '%s/%s/%s
' "$base" "$version" "$exact"
fi
for ed in $(edition_aliases "$edition"); do
printf '%s/%s/check-mk-%s-docker-%s.tar.gz\n' "$base" "$version" "$ed" "$version"
done
# aelteres Schema, zur Sicherheit
for ed in $(edition_aliases "$edition"); do
printf '%s/%s/check-mk-%s-%s.docker.tar.gz\n' "$base" "$version" "$ed" "$version"
done
}
# Kandidaten ohne Dopplungen - der aus dem Archiv gelesene Name ist oft
# derselbe wie der erste erzeugte; sonst wuerde er zweimal probiert.
download_url_list() { # edition version
download_url_candidates "$1" "$2" | awk '!seen[$0]++'
}
# ---------------------------------------------------------------------
# image pull - aus einer Registry holen
# ---------------------------------------------------------------------
image_cmd_pull() {
local edition="${CMK_EDITION:-raw}" version="${CMK_VERSION:-}" registry="${CMK_REGISTRY:-}"
local user="${CMK_REGISTRY_USER:-}" pass="${CMK_REGISTRY_PASSWORD:-}" ref="" setenv=1
while [ $# -gt 0 ]; do
case "$1" in
--edition) edition="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
--registry) registry="$2"; shift 2 ;;
--user) user="$2"; shift 2 ;;
--password) pass="$2"; shift 2 ;;
--image) ref="$2"; shift 2 ;;
--no-set) setenv=""; shift ;;
--remember) remember=1; shift ;;
*) die "Unbekannte Option fuer 'image pull': $1" ;;
esac
done
if [ -z "$ref" ]; then
edition_known "$edition" || die "Unbekannte Edition '$edition'.
Moeglich: raw community free cloud managed pro ultimate ultimatemt enterprise"
[ -n "$version" ] || die "Keine Version angegeben (--version oder CMK_VERSION)."
CMK_REGISTRY="${registry:-${CMK_REGISTRY:-registry.checkmk.com}}"
ref="$(edition_repo "$edition"):$version"
fi
local reg="${ref%%/*}"
if edition_needs_login "$edition" || [ -n "$user" ]; then
if [ -z "$user" ]; then
err "Fuer '$edition' ist eine Anmeldung an $reg noetig."
cat >&2 <<EOF
Zugangsdaten (Checkmk-Kundenportal, portal.checkmk.com) in der .env
hinterlegen oder als Option uebergeben:
CMK_REGISTRY_USER=...
CMK_REGISTRY_PASSWORD=...
bzw. $SELF image pull --edition $edition --version $version \\
--user <benutzer> --password <kennwort>
EOF
return 1
fi
log "Melde an $reg an ..."
printf '%s' "$pass" | $RUNTIME login --username "$user" --password-stdin "$reg" \
|| die "Anmeldung an $reg fehlgeschlagen."
ok "Anmeldung erfolgreich."
fi
log "Hole Image: $ref"
if ! $RUNTIME pull "$ref"; then
err "Das Image '$ref' konnte nicht geholt werden."
if [ "$reg" != "docker.io" ] && [ -n "$user" ]; then
warn "Die Anmeldung hat geklappt, der Pfad oder das Tag aber nicht.
Ich sehe nach, was diese Registry tatsaechlich anbietet ..."
image_cmd_repos --edition "$edition" --registry "$reg" \
--user "$user" --password "$pass" || true
return 1
fi
if [ "$edition" = enterprise ]; then
cat >&2 <<'EOF'
Hinweis: die Enterprise-Edition gibt es NICHT auf Docker Hub.
Zwei Wege stehen zur Verfuegung:
a) private Registry:
cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \
--user <portal-benutzer> --password <kennwort>
b) Tarball vom Downloadportal:
cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \
--user <portal-benutzer> --password <kennwort>
Ab Checkmk 2.5 heisst diese Edition "pro" und liegt frei auf Docker Hub:
cmk-manage.sh image pull --edition pro --version 2.5.0p11
EOF
fi
return 1
fi
ok "Image vorhanden: $ref"
if [ -n "$remember" ]; then
env_set CMK_EDITION "$edition"; env_set CMK_VERSION "$version"
log "CMK_EDITION=$edition und CMK_VERSION=$version uebernommen."
fi
[ -n "$setenv" ] && image_set_env "$ref"
return 0
}
# ---------------------------------------------------------------------
# image download - Tarball vom Downloadportal holen und einspielen
# ---------------------------------------------------------------------
image_cmd_download() {
local edition="${CMK_EDITION:-raw}" version="${CMK_VERSION:-}"
local user="${CMK_DOWNLOAD_USER:-}" pass="${CMK_DOWNLOAD_PASSWORD:-}"
local url="${CMK_DOWNLOAD_URL:-}" dir="" keep="" setenv=1 remember=""
while [ $# -gt 0 ]; do
case "$1" in
--edition) edition="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
--url) url="$2"; shift 2 ;;
--user) user="$2"; shift 2 ;;
--password) pass="$2"; shift 2 ;;
--dir) dir="$2"; shift 2 ;;
--keep) keep=1; shift ;;
--no-set) setenv=""; shift ;;
--remember) remember=1; shift ;;
*) die "Unbekannte Option fuer 'image download': $1" ;;
esac
done
dir="${dir:-${IMAGE_DIR:-$PROJECT_DIR/images}}"
case "$dir" in /*) : ;; *) dir="$PROJECT_DIR/${dir#./}" ;; esac
mkdir -p "$dir"
local -a urls=()
if [ -n "$url" ]; then
urls=("$url")
else
[ -n "$version" ] || die "Keine Version angegeben (--version oder CMK_VERSION)."
case "$version" in
*-latest|latest) die "Fuer den Tarball-Download wird eine exakte Version benoetigt,
z.B. 2.3.0p23 - '$version' funktioniert dort nicht." ;;
esac
edition_known "$edition" || die "Unbekannte Edition '$edition'."
mapfile -t urls < <(download_url_list "$edition" "$version")
fi
local target="" u
for u in "${urls[@]}"; do
target="$dir/$(basename "$u")"
if image_fetch_url "$u" "$target" "$user" "$pass"; then
ok "Heruntergeladen: $target ($(human_size "$(file_size "$target")"))"
break
fi
rm -f "$target"; target=""
done
if [ -z "$target" ]; then
err "Der Image-Tarball konnte nicht geladen werden."
cat >&2 <<EOF
Bitte pruefen:
* Zugangsdaten des Downloadportals (CMK_DOWNLOAD_USER/-PASSWORD)
* exakte Version (z.B. 2.3.0p23)
* Dateiname/URL - notfalls die vollstaendige URL fest vorgeben:
$SELF image download --url 'https://download.checkmk.com/checkmk/<version>/<datei>' \\
--user <benutzer> --password <kennwort>
* oder die Datei manuell im Kundenportal herunterladen und einspielen:
$SELF image load --file /pfad/zur/datei.tar.gz
EOF
return 1
fi
local loaded
loaded="$(image_load_file "$target")" || return 1
ok "Image eingespielt: $loaded"
if [ -n "$remember" ]; then
env_set CMK_EDITION "$edition"; env_set CMK_VERSION "$version"
log "CMK_EDITION=$edition und CMK_VERSION=$version uebernommen."
fi
[ -z "$keep" ] && { rm -f "$target"; log "Tarball entfernt (mit --keep behalten)."; }
[ -n "$setenv" ] && image_set_env "$loaded"
return 0
}
# ---------------------------------------------------------------------
# image load - bereits heruntergeladenen Tarball einspielen
# ---------------------------------------------------------------------
image_cmd_load() {
local file="" setenv=1
while [ $# -gt 0 ]; do
case "$1" in
--file) file="$2"; shift 2 ;;
--no-set) setenv=""; shift ;;
*) [ -z "$file" ] && { file="$1"; shift; } || die "Unbekannte Option: $1" ;;
esac
done
[ -n "$file" ] || die "--file fehlt."
local loaded
loaded="$(image_load_file "$file")" || return 1
ok "Image eingespielt: $loaded"
[ -n "$setenv" ] && image_set_env "$loaded"
}
# ---------------------------------------------------------------------
# image use / list / status
# ---------------------------------------------------------------------
image_cmd_use() {
local ref="" remember="" edition="" version=""
while [ $# -gt 0 ]; do
case "$1" in
--remember) remember=1; shift ;;
--edition) edition="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
*) if [ -z "$ref" ]; then ref="$1"; shift; else die "Unbekannte Option: $1"; fi ;;
esac
done
# Ohne fertige Referenz laesst sie sich aus Edition und Version
# bilden. Dafuer muss nichts heruntergeladen werden - die Runtime
# holt das Image beim naechsten Start selbst.
if [ -z "$ref" ] && [ -n "$edition" ]; then
edition_known "$edition" || die "Unbekannte Edition '$edition'."
[ -n "$version" ] || die "--version fehlt."
ref="$(edition_repo "$edition"):$version"
fi
[ -n "$ref" ] || die "Image-Referenz fehlt. Entweder direkt:
$SELF image use localhost/checkmk/check-mk-enterprise:2.3.0p23
oder aus Edition und Version gebildet:
$SELF image use --edition enterprise --version 2.4.0p9"
image_exists_local "$ref" || log "'$ref' ist lokal noch nicht vorhanden - wird beim naechsten '$SELF up' geholt."
image_set_env "$ref"
# Edition und Version mitfuehren, damit die Vorgaben nicht dauerhaft
# etwas anderes behaupten als das benutzte Image.
if [ -n "$edition" ]; then
env_set CMK_EDITION "$edition"; ok "CMK_EDITION=$edition"
env_set CMK_VERSION "$version"; ok "CMK_VERSION=$version"
elif [ -n "$remember" ]; then
local tag repo name ed
tag="${ref##*:}"; repo="${ref%:*}"
case "$repo" in */*) name="${repo##*/}" ;; *) name="$repo" ;; esac
ed="${name#check-mk-}"
if [ "$ed" != "$name" ] && edition_known "$ed"; then
env_set CMK_EDITION "$ed"; ok "CMK_EDITION=$ed"
else
warn "Edition liess sich aus '$ref' nicht ableiten - CMK_EDITION bleibt."
fi
if [ "$tag" != "$ref" ] && [ -n "$tag" ]; then
env_set CMK_VERSION "$tag"; ok "CMK_VERSION=$tag"
fi
fi
}
image_cmd_list() {
printf '%sLokal vorhandene Checkmk-Images%s\n' "$C_BLD" "$C_RST"
local l n=0
while IFS= read -r l; do
[ -n "$l" ] || continue
if [ "$l" = "${CMK_IMAGE:-}" ]; then
printf ' %s %s<- laut .env in Verwendung%s\n' "$l" "$C_GRN" "$C_RST"
else
printf ' %s\n' "$l"
fi
n=$((n+1))
done < <(image_local_list)
[ "$n" -eq 0 ] && echo " (keines)"
return 0
}
# Passt CMK_IMAGE zu CMK_EDITION/CMK_VERSION?
#
# CMK_IMAGE ist der einzige Wert, den compose und die systemd-Units
# verwenden. CMK_EDITION und CMK_VERSION sind nur Eingaben fuer den
# Downloader. Wer sie aendert, ohne anschliessend ein Image zu holen,
# betreibt weiter das alte - das faellt sonst nirgends auf.
#
# Rueckgabe: 0 = stimmig (oder nicht beurteilbar), 1 = Abweichung.
# Die Erklaerung geht nach stdout.
image_consistency_check() {
local img="${CMK_IMAGE:-}" ed="${CMK_EDITION:-}" ver="${CMK_VERSION:-}"
[ -n "$img" ] && [ -n "$ed" ] || return 0
local repo tag name
tag="${img##*:}"
repo="${img%:*}"
case "$repo" in */*) name="${repo##*/}" ;; *) name="$repo" ;; esac
# kein Tag angegeben -> nicht beurteilbar
if [ "$tag" = "$img" ]; then tag=""; fi
local ed_ok="" a
for a in $(edition_aliases "$ed"); do
if [ "$name" = "check-mk-$a" ]; then ed_ok=1; fi
done
local ver_ok=1
[ -n "$ver" ] && [ -n "$tag" ] && [ "$tag" != "$ver" ] && ver_ok=""
[ -n "$ed_ok" ] && [ -n "$ver_ok" ] && return 0
printf 'CMK_IMAGE passt nicht zu den eingestellten Vorgaben.\n\n'
printf ' verwendet wird : %s\n' "$img"
[ -z "$ed_ok" ] && printf ' Edition : eingestellt "%s", im Image steckt "%s"\n' \
"$ed" "${name#check-mk-}"
[ -z "$ver_ok" ] && printf ' Version : eingestellt "%s", im Image steckt "%s"\n' "$ver" "$tag"
printf '\nCompose und die systemd-Units verwenden ausschliesslich CMK_IMAGE.\n'
printf 'Edition und Version sind nur Vorgaben fuer den Downloader - sie\n'
printf 'wirken erst, wenn daraus ein Image geholt wird:\n\n'
printf ' %s image pull --edition %s --version %s\n' "$SELF" "$ed" "${ver:-<version>}"
printf ' %s up\n' "$SELF"
return 1
}
image_cmd_status() {
local msg
printf '\n%sKonfiguriertes Image (.env: CMK_IMAGE)%s\n' "$C_BLD" "$C_RST"
printf ' %s\n' "${CMK_IMAGE:-<nicht gesetzt>}"
if [ -n "${CMK_IMAGE:-}" ]; then
if image_exists_local "$CMK_IMAGE"; then
printf ' Status : %slokal vorhanden%s\n' "$C_GRN" "$C_RST"
else
printf ' Status : %snicht lokal vorhanden%s (wird beim Start geholt)\n' "$C_YEL" "$C_RST"
fi
if image_is_short_name "$CMK_IMAGE"; then
printf ' %sACHTUNG%s: kurzer Name ohne Registry - unter Podman fehleranfaellig.\n' "$C_RED" "$C_RST"
fi
fi
if ! msg="$(image_consistency_check)"; then
printf '\n'
warn "$msg"
fi
printf '\n%sVorgaben fuer den Downloader%s\n' "$C_BLD" "$C_RST"
printf ' Edition : %s -> %s\n' "${CMK_EDITION:-raw}" "$(edition_repo "${CMK_EDITION:-raw}" 2>/dev/null || echo '?')"
printf ' Version : %s\n' "${CMK_VERSION:-<nicht gesetzt>}"
printf ' Registry : %s (Benutzer: %s)\n' "${CMK_REGISTRY:-registry.checkmk.com}" "${CMK_REGISTRY_USER:-<leer>}"
printf ' Download : %s (Benutzer: %s)\n' "${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" "${CMK_DOWNLOAD_USER:-<leer>}"
echo
image_cmd_list
echo
}
# ---------------------------------------------------------------------
# Zugangsdaten pruefen
#
# Testet, ohne etwas herunterzuladen oder eine Anmeldung zu speichern:
# 1. Registry (registry.checkmk.com): Docker-Registry-v2-Anmeldung und
# ob das Konto Zugriff auf das Enterprise-Repository hat.
# 2. Downloadportal: HTTP-Basic-Anmeldung auf die Image-Tarballs.
# Die Zugangsdaten gehen ueber "curl --config -" an curl und tauchen
# damit nicht in der Prozessliste auf.
# ---------------------------------------------------------------------
# Zugangsdaten an curl uebergeben, ohne sie in die Prozessliste zu
# schreiben.
#
# Frueher lief das ueber eine temporaere .netrc. Deren Format trennt die
# Felder durch Leerzeichen - ein Kennwort mit Leerzeichen oder anderen
# Sonderzeichen kam dadurch still verstuemmelt an, und die Anmeldung
# schlug scheinbar grundlos fehl. "curl --config -" liest die Angaben von
# der Standardeingabe und nimmt sie in Anfuehrungszeichen, vertraegt also
# beliebige Zeichen.
_curl_cfg() { # user pass -> Konfigurationszeile auf stdout
local u="$1" p="$2"
u="${u//\\/\\\\}"; u="${u//\"/\\\"}"
p="${p//\\/\\\\}"; p="${p//\"/\\\"}"
printf 'user = "%s:%s"\n' "$u" "$p"
}
curl_auth() { # user pass curl-argumente...
local u="$1" p="$2"; shift 2
if [ -n "$u" ]; then
_curl_cfg "$u" "$p" | curl --config - "$@"
else
curl "$@"
fi
}
# HTTP-Status einer URL holen
http_code() { # url [user] [pass] [extra-header]
local url="$1" u="${2:-}" p="${3:-}" hdr="${4:-}"
local -a a=(-sS -o /dev/null -w '%{http_code}' --max-time 30)
if [ -n "$hdr" ]; then a+=(-H "$hdr"); fi
local code
code="$(curl_auth "$u" "$p" "${a[@]}" "$url" 2>/dev/null)" || code=000
printf '%s\n' "${code:-000}"
}
test_result() { # status text [detail]
case "$1" in
ok) printf ' %s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$2" ;;
fail) printf ' %s[FEHLER]%s %s\n' "$C_RED" "$C_RST" "$2" ;;
warn) printf ' %s[ WARN ]%s %s\n' "$C_YEL" "$C_RST" "$2" ;;
*) printf ' [ INFO ] %s\n' "$2" ;;
esac
[ -n "${3:-}" ] && printf ' %s\n' "$3" || true
}
# --- Registry pruefen -------------------------------------------------
# Der Name im Image-Bezeichner ist nicht immer der API-Host:
# "docker.io" spricht man als "registry-1.docker.io" an.
registry_api_host() {
case "$1" in
docker.io|index.docker.io) echo "registry-1.docker.io" ;;
*) echo "$1" ;;
esac
}
test_registry_login() { # reg repo user pass needs_login -> 0 = alles gut
local reg="$1" repo="$2" user="$3" pass="$4" needs_login="${5:-}"
local rc=0 anon=""
local api; api="$(registry_api_host "$reg")"
if [ -z "$user" ]; then anon=1; fi
printf '\n%sRegistry %s%s\n' "$C_BLD" "$reg" "$C_RST"
[ "$api" != "$reg" ] && printf ' API-Host: %s\n' "$api"
if [ -n "$anon" ] && [ -n "$needs_login" ]; then
test_result fail "keine Zugangsdaten hinterlegt" \
"Diese Edition ist nur mit Anmeldung erreichbar. CMK_REGISTRY_USER/-PASSWORD
setzen oder --user/--password angeben."
return 1
fi
# 1) Erreichbarkeit
local hdrf; hdrf="$(mktemp)"
local code
code="$(curl -sS -o /dev/null -D "$hdrf" -w '%{http_code}' --max-time 30 "https://$api/v2/" 2>/dev/null)" || code=000
code="${code:-000}"
if [ "$code" = "000" ]; then
test_result fail "nicht erreichbar" "Netzwerk/Proxy/DNS pruefen: https://$api/v2/"
rm -f "$hdrf"; return 1
fi
test_result ok "erreichbar (HTTP $code)"
# 2) Authentifizierungsverfahren aus dem WWW-Authenticate-Kopf lesen
local wa realm service
wa="$(tr -d '\r' <"$hdrf" | grep -i '^www-authenticate:' | head -n1)"
rm -f "$hdrf"
[ -n "$anon" ] && test_result info "oeffentliches Repository - keine Anmeldung noetig"
case "$wa" in
*[Bb]earer*)
realm="$(printf '%s' "$wa" | sed -n 's/.*realm="\([^"]*\)".*/\1/p')"
service="$(printf '%s' "$wa" | sed -n 's/.*service="\([^"]*\)".*/\1/p')"
[ -n "$realm" ] || { test_result fail "Token-Dienst nicht ermittelbar" "$wa"; return 1; }
local realm_host; realm_host="$(printf '%s' "$realm" | sed -E 's|^[a-z]+://([^/]+).*|\1|')"
local url="$realm?service=$service&scope=repository:$repo:pull"
local body tcode tu="" tp=""
if [ -z "$anon" ]; then tu="$user"; tp="$pass"; fi
body="$(curl_auth "$tu" "$tp" -sS --max-time 30 "$url" 2>/dev/null || true)"
tcode="$(http_code "$url" "$tu" "$tp")"
if [ "$tcode" = "401" ] || [ "$tcode" = "403" ]; then
test_result fail "Anmeldung abgelehnt (HTTP $tcode)" \
"Benutzername/Kennwort des Checkmk-Kundenportals pruefen"
return 1
fi
if [ "$tcode" != "200" ]; then
test_result fail "Token-Abruf fehlgeschlagen (HTTP $tcode)"
return 1
fi
[ -n "$anon" ] && test_result ok "anonymer Zugriff moeglich" \
|| test_result ok "Anmeldung akzeptiert"
# 3) Zugriff auf das konkrete Repository
local token
token="$(printf '%s' "$body" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
[ -n "$token" ] || token="$(printf '%s' "$body" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ -n "$token" ]; then
local lcode
lcode="$(http_code "https://$api/v2/$repo/tags/list" "" "" "Authorization: Bearer $token")"
case "$lcode" in
200) test_result ok "Zugriff auf '$repo' vorhanden" ;;
401|403) test_result fail "kein Zugriff auf '$repo' (HTTP $lcode)" \
"Die Anmeldung stimmt, das Konto hat aber keine Berechtigung fuer dieses Repository (Subskription?)"
rc=1 ;;
404) test_result warn "Repository '$repo' nicht gefunden (HTTP 404)" \
"Pfad pruefen - Anmeldung selbst ist in Ordnung" ;;
*) test_result warn "Repository-Pruefung nicht eindeutig (HTTP $lcode)" ;;
esac
fi
;;
*[Bb]asic*|"")
local bcode; bcode="$(http_code "https://$api/v2/" "$user" "$pass")"
case "$bcode" in
200) test_result ok "Anmeldung akzeptiert (Basic)" ;;
401|403) test_result fail "Anmeldung abgelehnt (HTTP $bcode)"; rc=1 ;;
*) test_result warn "unklares Ergebnis (HTTP $bcode)"; rc=1 ;;
esac
;;
*)
test_result warn "unbekanntes Anmeldeverfahren" "$wa"; rc=1 ;;
esac
return $rc
}
# Rueckfall ohne curl: Anmeldung ueber die Runtime, in eine Wegwerfdatei
test_registry_login_runtime() { # reg user pass
local reg="$1" user="$2" pass="$3" tmp rc=0
tmp="$(mktemp -d)"
printf '\n%sRegistry %s (ueber %s)%s\n' "$C_BLD" "$reg" "$RUNTIME" "$C_RST"
if [ "$RUNTIME" = podman ]; then
if printf '%s' "$pass" | $RUNTIME login --username "$user" --password-stdin \
--authfile "$tmp/auth.json" "$reg" >/dev/null 2>&1; then
test_result ok "Anmeldung akzeptiert"
else
test_result fail "Anmeldung abgelehnt"; rc=1
fi
else
if printf '%s' "$pass" | DOCKER_CONFIG="$tmp" $RUNTIME login --username "$user" \
--password-stdin "$reg" >/dev/null 2>&1; then
test_result ok "Anmeldung akzeptiert"
else
test_result fail "Anmeldung abgelehnt"; rc=1
fi
fi
rm -rf "$tmp"
return $rc
}
# --- Downloadportal pruefen -------------------------------------------
test_download_login() { # baseurl edition version user pass
local base="$1" edition="$2" version="$3" user="$4" pass="$5"
local host; host="$(printf '%s' "$base" | sed -E 's|^[a-z]+://([^/]+).*|\1|')"
local rc=1
printf '\n%sDownloadportal %s%s\n' "$C_BLD" "$host" "$C_RST"
if [ -z "$user" ]; then
test_result warn "keine Zugangsdaten hinterlegt - uebersprungen" \
"CMK_DOWNLOAD_USER/-PASSWORD setzen oder --user/--password angeben"
return 1
fi
local -a urls=()
if [ -n "${CMK_DOWNLOAD_URL:-}" ]; then
urls=("$CMK_DOWNLOAD_URL")
elif [ -n "$version" ]; then
mapfile -t urls < <(download_url_list "$edition" "$version")
else
urls=("$base/")
fi
local u code seen401="" seen404=""
for u in "${urls[@]}"; do
code="$(http_code "$u" "$user" "$pass")"
case "$code" in
200) test_result ok "Datei erreichbar (HTTP 200)" "$u"; rc=0; break ;;
401|403) seen401=1; test_result fail "Anmeldung abgelehnt (HTTP $code)" "$u" ;;
404) seen404=1 ;;
000) test_result fail "nicht erreichbar" "$u" ;;
*) test_result warn "unerwartete Antwort (HTTP $code)" "$u" ;;
esac
done
if [ "$rc" != 0 ] && [ -z "$seen401" ] && [ -n "$seen404" ]; then
test_result ok "Anmeldung akzeptiert, aber kein Tarball unter diesem Namen (HTTP 404)"
printf ' %s\n' "Version pruefen (z.B. 2.3.0p23, kein '-latest') oder"
printf ' %s\n' "CMK_DOWNLOAD_URL mit der vollstaendigen URL setzen."
rc=0
fi
return $rc
}
image_cmd_test() {
local edition="${CMK_EDITION:-raw}" version="${CMK_VERSION:-}"
local ruser="${CMK_REGISTRY_USER:-}" rpass="${CMK_REGISTRY_PASSWORD:-}"
local duser="${CMK_DOWNLOAD_USER:-}" dpass="${CMK_DOWNLOAD_PASSWORD:-}"
local reg="${CMK_REGISTRY:-registry.checkmk.com}"
local only=""
while [ $# -gt 0 ]; do
case "$1" in
--edition) edition="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
--registry) reg="$2"; shift 2 ;;
--user) ruser="$2"; duser="$2"; shift 2 ;;
--password) rpass="$2"; dpass="$2"; shift 2 ;;
--only) only="$2"; shift 2 ;;
*) die "Unbekannte Option fuer 'image test': $1" ;;
esac
done
edition_known "$edition" || die "Unbekannte Edition '$edition'."
printf '\n%sPruefe Zugangsdaten%s\n' "$C_BLD" "$C_RST"
printf ' Edition : %s\n' "$edition"
printf ' Version : %s\n' "${version:-<nicht gesetzt>}"
local rc_reg=1 rc_dl=1 ran_reg="" ran_dl=""
if [ -z "$only" ] || [ "$only" = registry ]; then
ran_reg=1
local full repo
CMK_REGISTRY="$reg"
full="$(edition_repo "$edition")"
repo="${full#*/}"
local rhost="${full%%/*}"
local needs=""
edition_needs_login "$edition" && needs=1
if command -v curl >/dev/null 2>&1; then
test_registry_login "$rhost" "$repo" "$ruser" "$rpass" "$needs" && rc_reg=0
elif [ -n "$ruser" ]; then
test_registry_login_runtime "$rhost" "$ruser" "$rpass" && rc_reg=0
else
warn "Weder curl noch Zugangsdaten vorhanden - Registry uebersprungen."
fi
fi
if [ -z "$only" ] || [ "$only" = download ]; then
ran_dl=1
if command -v curl >/dev/null 2>&1; then
test_download_login "${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" \
"$edition" "$version" "$duser" "$dpass" && rc_dl=0
else
warn "curl fehlt - Downloadportal kann nicht geprueft werden."
fi
fi
[ -n "$ran_reg$ran_dl" ] || die "Unbekannter Wert fuer --only: '$only' (registry|download)"
echo
local good=""
[ -n "$ran_reg" ] && [ "$rc_reg" = 0 ] && good=1
[ -n "$ran_dl" ] && [ "$rc_dl" = 0 ] && good=1
if [ -n "$good" ]; then
ok "Zugangsweg nutzbar."
[ -n "$ran_reg" ] && [ "$rc_reg" = 0 ] && \
log "Weiter mit: $SELF image pull --edition $edition --version ${version:-<version>}"
[ -n "$ran_dl" ] && [ "$rc_dl" = 0 ] && [ "${rc_reg}" != 0 ] && \
log "Weiter mit: $SELF image download --edition $edition --version ${version:-<version>}"
echo
return 0
fi
err "Kein geprüfter Bezugsweg ist nutzbar - bitte Zugangsdaten pruefen."
echo
return 1
}
# ---------------------------------------------------------------------
# image versions - verfuegbare Versionen auflisten
#
# Registry : ueber den Docker-Registry-v2-Ablauf /v2/<repo>/tags/list
# Portal : ueber das Verzeichnis-Listing von download.checkmk.com
# (nur mit Zugangsdaten lesbar)
# ---------------------------------------------------------------------
# Token fuer eine Registry besorgen ("" = anonym versuchen)
registry_get_token() { # api repo user pass [scope] -> Token auf stdout
local api="$1" repo="$2" user="$3" pass="$4" scope="${5:-}"
[ -n "$scope" ] || scope="repository:$repo:pull"
local hdrf; hdrf="$(mktemp)"
curl -sS -o /dev/null -D "$hdrf" --max-time 30 "https://$api/v2/" >/dev/null 2>&1 || true
local wa realm service
wa="$(tr -d '\r' <"$hdrf" | grep -i '^www-authenticate:' | head -n1)"
rm -f "$hdrf"
case "$wa" in
*[Bb]earer*) : ;;
*) return 0 ;; # Basic oder offen - kein Token noetig
esac
realm="$(printf '%s' "$wa" | sed -n 's/.*realm="\([^"]*\)".*/\1/p')"
service="$(printf '%s' "$wa" | sed -n 's/.*service="\([^"]*\)".*/\1/p')"
[ -n "$realm" ] || return 0
local realm_host; realm_host="$(printf '%s' "$realm" | sed -E 's|^[a-z]+://([^/]+).*|\1|')"
local url="$realm?service=$service&scope=$scope"
local body
body="$(curl_auth "$user" "$pass" -sS --max-time 30 "$url" 2>/dev/null || true)"
local t
t="$(printf '%s' "$body" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
[ -n "$t" ] || t="$(printf '%s' "$body" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
printf '%s' "$t"
}
registry_tags() { # api repo user pass -> Tags, eine je Zeile
local api="$1" repo="$2" user="$3" pass="$4"
local token; token="$(registry_get_token "$api" "$repo" "$user" "$pass")"
local -a a=(-sS --max-time 45)
local tu="" tp=""
if [ -n "$token" ]; then a+=(-H "Authorization: Bearer $token"); else tu="$user"; tp="$pass"; fi
local body
body="$(curl_auth "$tu" "$tp" "${a[@]}" "https://$api/v2/$repo/tags/list" 2>/dev/null || true)"
printf '%s' "$body" \
| tr ',' '\n' \
| sed -n 's/.*"tags"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p;s/^[[:space:]]*"\([^"]*\)".*/\1/p' \
| sed 's/[]["]//g' | sed '/^$/d'
}
download_versions() { # user pass -> Versionsverzeichnisse
local user="$1" pass="$2"
local base="${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}"
local host; host="$(printf '%s' "$base" | sed -E 's|^[a-z]+://([^/]+).*|\1|')"
local -a a=(-sS --max-time 45 -L)
local body; body="$(curl_auth "$user" "$pass" "${a[@]}" "$base/" 2>/dev/null || true)"
printf '%s' "$body" \
| grep -oE 'href="[0-9]+\.[0-9]+\.[0-9]+[a-z0-9.]*/?"' \
| sed -E 's|href="([^"/]+)/?"|\1|' | sort -u
}
# ---------------------------------------------------------------------
# Welches Repository heisst in der Registry wie?
#
# Der Pfad ist nicht zu erraten - "Login Succeeded" heisst noch lange
# nicht, dass enterprise/check-mk-enterprise existiert. Deshalb erst den
# Katalog fragen und, falls der gesperrt ist, die ueblichen Pfade
# durchprobieren. HTTP 404 = gibt es nicht, 401/403 = keine Berechtigung,
# 200 = nutzbar.
# ---------------------------------------------------------------------
registry_catalog() { # api user pass -> Repository-Namen
local api="$1" user="$2" pass="$3"
local token; token="$(registry_get_token "$api" "" "$user" "$pass" "registry:catalog:*")"
local -a a=(-sS --max-time 45)
local tu="" tp=""
if [ -n "$token" ]; then a+=(-H "Authorization: Bearer $token"); else tu="$user"; tp="$pass"; fi
curl_auth "$tu" "$tp" "${a[@]}" "https://$api/v2/_catalog?n=1000" 2>/dev/null \
| tr ',' '\n' | grep -oE '"[a-z0-9][a-z0-9._/-]*"' | tr -d '"' \
| grep -v '^repositories$' | sed '/^$/d' | sort -u
}
registry_repo_candidates() { # edition -> moegliche Repository-Pfade
local edition="$1" a
for a in $(edition_aliases "$edition"); do
printf '%s/check-mk-%s\n' "$a" "$a"
printf 'check-mk-%s\n' "$a"
printf 'checkmk/check-mk-%s\n' "$a"
printf 'enterprise/check-mk-%s\n' "$a"
done
}
# Status eines Repositories ermitteln: 200 / 401 / 403 / 404 / 000
registry_repo_status() { # api repo user pass -> "code anzahl_tags"
local api="$1" repo="$2" user="$3" pass="$4"
local token; token="$(registry_get_token "$api" "$repo" "$user" "$pass")"
local -a a=(-sS -o /tmp/.cmkrepo.$$ -w '%{http_code}' --max-time 30)
local tu="" tp=""
if [ -n "$token" ]; then a+=(-H "Authorization: Bearer $token"); else tu="$user"; tp="$pass"; fi
local code
code="$(curl_auth "$tu" "$tp" "${a[@]}" "https://$api/v2/$repo/tags/list" 2>/dev/null)" || code=000
local n=0
if [ "${code:-000}" = 200 ]; then n="$(tr ',' '\n' <"/tmp/.cmkrepo.$$" 2>/dev/null | grep -c '"' || echo 0)"; fi
rm -f "/tmp/.cmkrepo.$$"
printf '%s %s\n' "${code:-000}" "$n"
}
image_cmd_repos() {
local edition="${CMK_EDITION:-enterprise}" reg="${CMK_REGISTRY:-registry.checkmk.com}"
local user="${CMK_REGISTRY_USER:-}" pass="${CMK_REGISTRY_PASSWORD:-}"
while [ $# -gt 0 ]; do
case "$1" in
--edition) edition="$2"; shift 2 ;;
--registry) reg="$2"; shift 2 ;;
--user) user="$2"; shift 2 ;;
--password) pass="$2"; shift 2 ;;
*) die "Unbekannte Option fuer 'image repos': $1" ;;
esac
done
command -v curl >/dev/null 2>&1 || die "Fuer 'image repos' wird curl benoetigt."
local api; api="$(registry_api_host "$reg")"
printf '\n%sRegistry %s%s\n' "$C_BLD" "$reg" "$C_RST"
[ -n "$user" ] || warn "Keine Zugangsdaten hinterlegt - ohne Anmeldung ist hier wenig zu sehen."
printf '\n%sKatalog (/v2/_catalog)%s\n' "$C_BLD" "$C_RST"
local -a cat=()
mapfile -t cat < <(registry_catalog "$api" "$user" "$pass" | grep -iE 'check|mk' | head -50)
if [ ${#cat[@]} -gt 0 ]; then
printf ' %s\n' "${cat[@]}"
else
printf ' (nicht abrufbar - viele Registries sperren den Katalog)\n'
fi
printf '\n%sUebliche Pfade fuer "%s" durchprobiert%s\n' "$C_BLD" "$edition" "$C_RST"
printf ' %-6s %-8s %s\n' "STATUS" "TAGS" "PFAD"
printf ' %s200 = nutzbar, 404 = gibt es nicht, 401/403 = kein Zugriff%s\n' "$C_BLU" "$C_RST"
local cand code n line ok=""
while IFS= read -r cand; do
[ -n "$cand" ] || continue
line="$(registry_repo_status "$api" "$cand" "$user" "$pass")"
code="${line%% *}"; n="${line##* }"
case "$code" in
200) printf ' %s%-6s%s %-8s %s\n' "$C_GRN" "$code" "$C_RST" "$n" "$cand"
if [ -z "$ok" ]; then ok="$cand"; fi ;;
401|403) printf ' %s%-6s%s %-8s %s (kein Zugriff - oder gibt es nicht;\n%38sviele Registries verbergen das)\n' \
"$C_YEL" "$code" "$C_RST" "-" "$cand" "" ;;
404) printf ' %-6s %-8s %s (gibt es nicht)\n' "$code" "-" "$cand" ;;
*) printf ' %-6s %-8s %s\n' "${code:-000}" "-" "$cand" ;;
esac
done < <(registry_repo_candidates "$edition" | awk '!seen[$0]++')
echo
if [ -n "$ok" ]; then
ok "Nutzbar: $reg/$ok"
cat <<EOF
${C_BLD}Weiter${C_RST}
Tags ansehen : $SELF image versions --source registry --edition $edition
Image setzen : $SELF image use $reg/$ok:<version>
EOF
else
err "Unter '$reg' wurde kein nutzbares Repository fuer '$edition' gefunden."
cat <<EOF
Moegliche Gruende:
* Die Subskription schliesst den Registry-Zugang nicht ein.
Dann fuehrt der Tarball-Weg zum Ziel:
$SELF image download --edition $edition --version <version>
* Der Pfad heisst anders. Steht er im Kundenportal, laesst er sich
direkt setzen:
$SELF config set CMK_REGISTRY <registry>
$SELF image use <registry>/<pfad>:<version>
EOF
fi
echo
}
image_cmd_versions() {
local edition="${CMK_EDITION:-raw}" source="all" filter="" limit=25
local ruser="${CMK_REGISTRY_USER:-}" rpass="${CMK_REGISTRY_PASSWORD:-}"
local duser="${CMK_DOWNLOAD_USER:-}" dpass="${CMK_DOWNLOAD_PASSWORD:-}"
local reg="${CMK_REGISTRY:-registry.checkmk.com}" raw=""
while [ $# -gt 0 ]; do
case "$1" in
--edition) edition="$2"; shift 2 ;;
--source) source="$2"; shift 2 ;;
--filter) filter="$2"; shift 2 ;;
--limit) limit="$2"; shift 2 ;;
--all) limit=100000; shift ;;
--registry) reg="$2"; shift 2 ;;
--user) ruser="$2"; duser="$2"; shift 2 ;;
--password) rpass="$2"; dpass="$2"; shift 2 ;;
--bare) raw=1; shift ;;
*) die "Unbekannte Option fuer 'image versions': $1" ;;
esac
done
edition_known "$edition" || die "Unbekannte Edition '$edition'.
Moeglich: raw community free cloud managed pro ultimate ultimatemt enterprise"
case "$source" in registry|download|web|both|all) : ;;
*) die "--source: registry | download | web | all" ;; esac
if [ "$source" = both ]; then source=all; fi
case "$limit" in ''|*[!0-9]*) die "--limit braucht eine Zahl." ;; esac
command -v curl >/dev/null 2>&1 || die "Fuer 'image versions' wird curl benoetigt."
CMK_REGISTRY="$reg"
local full repo api
full="$(edition_repo "$edition")"
repo="${full#*/}"
api="$(registry_api_host "${full%%/*}")"
local found=0
if [ "$source" = web ] || [ "$source" = all ]; then
[ -n "$raw" ] || printf '\n%sVersionsarchiv %s%s\n' "$C_BLD" \
"${CMK_ARCHIVE_URL:-https://checkmk.com/download/archive}" "$C_RST"
[ -n "$raw" ] || printf ' %s(oeffentlich, keine Zugangsdaten noetig)%s\n' "$C_BLU" "$C_RST"
local -a wv=()
mapfile -t wv < <(archive_versions 2>/dev/null \
| { [ -n "$filter" ] && grep -F "$filter" || cat; } \
| sort -rV | head -n "$limit")
if [ ${#wv[@]} -eq 0 ]; then
[ -n "$raw" ] || test_result warn "Archiv nicht lesbar" "Netzzugang zu checkmk.com pruefen"
else
found=1
printf '%s\n' "${wv[@]}" | { [ -n "$raw" ] && cat || sed 's/^/ /'; }
fi
fi
if [ "$source" = registry ] || [ "$source" = all ]; then
[ -n "$raw" ] || printf '\n%sRegistry %s%s\n' "$C_BLD" "$full" "$C_RST"
local -a tags=()
mapfile -t tags < <(registry_tags "$api" "$repo" "$ruser" "$rpass" \
| { [ -n "$filter" ] && grep -F "$filter" || cat; } \
| sort -rV | head -n "$limit")
if [ ${#tags[@]} -eq 0 ]; then
if [ -z "$raw" ]; then
if edition_needs_login "$edition" && [ -z "$ruser" ]; then
test_result warn "keine Zugangsdaten - Registry nicht lesbar" \
"CMK_REGISTRY_USER/-PASSWORD setzen oder --user/--password angeben"
else
test_result warn "keine Tags erhalten" "Zugangsdaten und Repository pruefen"
fi
fi
else
found=1
printf '%s\n' "${tags[@]}" | { [ -n "$raw" ] && cat || sed 's/^/ /'; }
fi
fi
if [ "$source" = download ] || [ "$source" = all ]; then
[ -n "$raw" ] || printf '\n%sDownloadportal %s%s\n' "$C_BLD" \
"${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" "$C_RST"
local -a vers=()
mapfile -t vers < <(download_versions "$duser" "$dpass" \
| { [ -n "$filter" ] && grep -F "$filter" || cat; } \
| sort -rV | head -n "$limit")
if [ ${#vers[@]} -eq 0 ]; then
if [ -z "$raw" ]; then
if [ -z "$duser" ]; then
test_result warn "keine Zugangsdaten - das Verzeichnis ist geschuetzt (HTTP 401)" \
"CMK_DOWNLOAD_USER/-PASSWORD setzen oder --user/--password angeben"
else
test_result warn "keine Versionen gelesen" \
"Zugangsdaten pruefen: $SELF image test --only download"
fi
fi
else
found=1
printf '%s\n' "${vers[@]}" | { [ -n "$raw" ] && cat || sed 's/^/ /'; }
fi
fi
[ -n "$raw" ] && return 0
echo
if [ "$found" = 1 ]; then
cat <<EOF
${C_BLD}Uebernehmen${C_RST}
aus der Registry : $SELF image pull --edition $edition --version <version>
als Tarball : $SELF image download --edition $edition --version <version>
nur eintragen : $SELF config set CMK_VERSION <version>
Nur die nackte Liste (zum Weiterverarbeiten): $SELF image versions --bare
EOF
else
err "Es konnte keine Versionsliste gelesen werden."
log "Zugangsdaten pruefen mit: $SELF image test"
fi
echo
[ "$found" = 1 ]
}
cmd_image() {
local sub="${1:-status}"; [ $# -gt 0 ] && shift || true
case "$sub" in
status) image_cmd_status ;;
list|ls) image_cmd_list ;;
pull) image_cmd_pull "$@" ;;
download) image_cmd_download "$@" ;;
load) image_cmd_load "$@" ;;
use|set) image_cmd_use "$@" ;;
test|check) image_cmd_test "$@" ;;
versions|tags) image_cmd_versions "$@" ;;
repos|repo) image_cmd_repos "$@" ;;
*) die "Unbekanntes Unterkommando 'image $sub'.
Moeglich: status | list | versions | repos | test | pull | download | load | use" ;;
esac
}
# =====================================================================
# Stack-Kommandos
# =====================================================================
# Welche compose-Dienste sollen laufen? Der Proxy Manager ist abschaltbar,
# damit ein bereits vorhandener NGINX auf einem anderen System genutzt
# werden kann. Abgeschaltet wird sein Container gar nicht erst angelegt.
compose_services() {
if npm_enabled; then
printf '%s\n' checkmk nginx-proxy-manager
else
printf '%s\n' checkmk
fi
}
cmd_up() {
[ -f "$PROJECT_DIR/.env" ] || die "Keine .env vorhanden. Bitte: cp .env.example .env && \$EDITOR .env"
local -a svc=(); mapfile -t svc < <(compose_services)
if npm_enabled; then
log "Starte Stack (${RUNTIME}) ..."
else
log "Starte Stack (${RUNTIME}) - ohne NGINX Proxy Manager (NPM_ENABLED=off) ..."
fi
compose up -d "${svc[@]}"
npm_enabled || npm_teardown_container
ok "Stack gestartet."
cmd_status
}
cmd_down() { log "Stoppe Stack ..."; compose down; ok "Stack gestoppt."; }
cmd_restart() {
compose down || true
local -a svc=(); mapfile -t svc < <(compose_services)
compose up -d "${svc[@]}"
npm_enabled || npm_teardown_container
ok "Stack neu gestartet."
}
cmd_pull() {
local -a svc=(); mapfile -t svc < <(compose_services)
compose pull "${svc[@]}"
ok "Images aktualisiert. Danach: $SELF 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() {
local cmsg
printf '\n%sImage%s\n' "$C_BLD" "$C_RST"
printf ' %-24s %s\n' "${CMK_IMAGE:-<nicht gesetzt>}" \
"$(image_exists_local "${CMK_IMAGE:-}" && echo "lokal vorhanden" || echo "${C_YEL}nicht lokal vorhanden${C_RST}")"
if ! cmsg="$(image_consistency_check)"; then
printf ' %sAchtung%s: CMK_IMAGE passt nicht zu CMK_EDITION/CMK_VERSION.\n' "$C_YEL" "$C_RST"
printf ' Einzelheiten: %s image status\n' "$SELF"
fi
printf '\n%sContainer%s\n' "$C_BLD" "$C_RST"
local c
for c in "$CMK_CONTAINER_NAME" "$NPM_CONTAINER_NAME"; do
if [ "$c" = "$NPM_CONTAINER_NAME" ] && ! npm_enabled; then
printf ' %-24s %s\n' "$c" "${C_YEL}abgeschaltet (NPM_ENABLED=off)${C_RST}"
continue
fi
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 ' Agent-Receiver : %s:8000\n' "$ip"
if npm_enabled; then
printf ' Checkmk (ueber Proxy) : https://<FQDN>/%s/\n' "$CMK_SITE_ID"
printf ' NGINX Proxy Manager : http://%s:%s/ (Erstlogin: admin@example.com / changeme)\n\n' "$ip" "${NPM_ADMIN_PORT:-81}"
else
printf ' NGINX Proxy Manager : abgeschaltet\n'
printf ' Externer Proxy weiterleiten auf http://%s:5000\n\n' "$ip"
fi
}
# =====================================================================
# 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"
}
# =====================================================================
# Backup-Jobs (geplante Backups)
#
# Manuelle Backups nehmen immer BACKUP_DIR aus der .env. Geplante Jobs
# bringen ihr eigenes Zielverzeichnis mit - so kann z.B. taeglich auf
# eine NAS-Freigabe und woechentlich woanders hin gesichert werden.
#
# Ablage: backup-jobs.conf im Projektverzeichnis, eine Zeile je Job:
# name|sites|cron|verzeichnis|optionen|aufbewahren|aktiv
# Ausgefuehrt werden sie ueber /etc/cron.d/checkmk-backup, das aus
# dieser Datei erzeugt wird ("job auto on").
# =====================================================================
JOBS_FILE_NAME="backup-jobs.conf"
CRON_FILE="/etc/cron.d/checkmk-backup"
jobs_file() { printf '%s/%s\n' "$PROJECT_DIR" "$JOBS_FILE_NAME"; }
jobs_init() {
local f; f="$(jobs_file)"
[ -f "$f" ] && return 0
cat >"$f" <<'EOF'
# Backup-Jobs fuer cmk-manage.sh - eine Zeile je Job:
# name|sites|cron|verzeichnis|optionen|aufbewahren|aktiv
#
# name nur Buchstaben, Ziffern, _ und -
# sites ALL oder Site-Namen durch Komma getrennt
# cron fuenf Cron-Felder, z.B. "15 2 * * *"
# verzeichnis Zielverzeichnis (absolut oder relativ zum Projekt)
# optionen leer oder --no-rrds / --no-logs / --stop (mit Leerzeichen)
# aufbewahren Anzahl der Backups, die im Zielverzeichnis bleiben (0 = alle)
# aktiv on oder off
#
# Bearbeiten moeglichst ueber: cmk-manage.sh job ... bzw. das Menue.
EOF
ok "Neue Jobdatei angelegt: $f"
}
jobs_read() { # nur Datenzeilen
local f; f="$(jobs_file)"
[ -f "$f" ] || return 0
grep -vE '^[[:space:]]*(#|$)' "$f" || true
}
job_line() { jobs_read | awk -F'|' -v n="$1" '$1==n {print; exit}'; }
job_exists() { [ -n "$(job_line "$1")" ]; }
job_f() { printf '%s' "$1" | cut -d'|' -f"$2"; }
job_dir_abs() { # Verzeichnis eines Jobs absolut
local d="$1"
case "$d" in /*) printf '%s\n' "$d" ;; *) printf '%s\n' "$PROJECT_DIR/${d#./}" ;; esac
}
job_validate_name() {
case "$1" in
''|*[!A-Za-z0-9_-]*) echo "Der Name darf nur Buchstaben, Ziffern, _ und - enthalten."; return 1 ;;
esac
return 0
}
job_validate_cron() {
local n; n="$(printf '%s' "$1" | awk '{print NF}')"
[ "$n" = 5 ] || { echo "Ein Cron-Ausdruck besteht aus genau fuenf Feldern, z.B. '15 2 * * *'."; return 1; }
return 0
}
job_validate_field() { # keine Trennzeichen in Feldern
case "$1" in *'|'*) echo "Das Zeichen | ist in diesem Feld nicht erlaubt."; return 1 ;; esac
return 0
}
# Job schreiben (anlegen oder ersetzen)
job_put() { # name sites cron dir opts keep enabled
jobs_init
local f tmp; f="$(jobs_file)"
tmp="$(mktemp)"
awk -F'|' -v n="$1" '$1!=n' "$f" >"$tmp"
printf '%s|%s|%s|%s|%s|%s|%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" >>"$tmp"
# Kommentarkopf erhalten
{ grep -E '^[[:space:]]*#' "$f" 2>/dev/null || true; grep -vE '^[[:space:]]*(#|$)' "$tmp" ; } >"$f"
rm -f "$tmp"
}
job_remove() {
local f tmp; f="$(jobs_file)"
[ -f "$f" ] || return 0
tmp="$(mktemp)"
awk -F'|' -v n="$1" '$1!=n' "$f" >"$tmp" && mv "$tmp" "$f"
}
# --- Aufraeumen im Zielverzeichnis eines Jobs -------------------------
prune_in_dir() { # verzeichnis anzahl
local dir="$1" keep="$2" i=0
[ -n "$keep" ] && [ "$keep" -gt 0 ] 2>/dev/null || return 0
local -a files=()
mapfile -t files < <(find "$dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' 2>/dev/null \
| sort -rn | cut -d' ' -f2-)
[ ${#files[@]} -le "$keep" ] && return 0
for ((i=keep; i<${#files[@]}; i++)); do
log "Aufbewahrung: entferne $(basename "${files[$i]}")"
rm -f "${files[$i]}"
done
}
# --- Job ausfuehren ---------------------------------------------------
job_run_one() {
local name="$1" line
line="$(job_line "$name")"
[ -n "$line" ] || die "Job '$name' ist nicht vorhanden. Uebersicht: $SELF job list"
local sites cron dir opts keep enabled
sites="$(job_f "$line" 2)"; cron="$(job_f "$line" 3)"; dir="$(job_f "$line" 4)"
opts="$(job_f "$line" 5)"; keep="$(job_f "$line" 6)"; enabled="$(job_f "$line" 7)"
dir="$(job_dir_abs "$dir")"
mkdir -p "$dir" || die "Zielverzeichnis nicht anlegbar: $dir"
log "Job '$name' - Ziel $dir, Sites ${sites}${opts:+, Optionen $opts}"
require_cmk_running
local -a args=(--dir "$dir" --label "$name")
local o
for o in $opts; do
case "$o" in
--no-rrds|--no-logs|--stop) args+=("$o") ;;
*) warn "Unbekannte Option '$o' im Job '$name' - wird uebergangen." ;;
esac
done
if [ "$sites" = ALL ]; then
args+=(--all)
else
local sname
for sname in $(printf '%s' "$sites" | tr ',' ' '); do args+=(--site "$sname"); done
fi
ASSUME_YES=1 cmd_backup "${args[@]}"
prune_in_dir "$dir" "$keep"
ok "Job '$name' abgeschlossen."
}
# --- Cron ------------------------------------------------------------
cron_write() {
jobs_init
local n=0 line name cron
local tmp; tmp="$(mktemp)"
{
printf '# Automatische Checkmk-Backups - erzeugt von cmk-manage.sh\n'
printf '# Nicht von Hand aendern, Quelle ist %s\n' "$(jobs_file)"
printf 'SHELL=/bin/sh\n'
printf 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n'
while IFS= read -r line; do
[ -n "$line" ] || continue
[ "$(job_f "$line" 7)" = on ] || continue
name="$(job_f "$line" 1)"; cron="$(job_f "$line" 3)"
printf '%s root %s -y job run %s >>/var/log/cmk-backup.log 2>&1\n' \
"$cron" "$SELF" "$name"
n=$((n+1))
done < <(jobs_read)
} >"$tmp"
if [ "$n" -eq 0 ]; then
rm -f "$tmp"
warn "Kein aktiver Job vorhanden - es wird nichts eingeplant."
[ -f "$CRON_FILE" ] && { rm -f "$CRON_FILE"; log "Vorhandene Cron-Datei entfernt."; }
return 0
fi
if [ "$(id -u)" -ne 0 ]; then
err "Zum Schreiben von $CRON_FILE werden root-Rechte benoetigt."
echo
cat "$tmp"
echo
log "Die Zeilen oben von Hand nach $CRON_FILE uebernehmen, oder erneut mit sudo aufrufen."
rm -f "$tmp"
return 1
fi
install -m 0644 "$tmp" "$CRON_FILE"
rm -f "$tmp"
ok "$n Job(s) eingeplant in $CRON_FILE"
}
cron_remove_file() {
if [ -f "$CRON_FILE" ]; then
[ "$(id -u)" -eq 0 ] || die "Zum Entfernen von $CRON_FILE werden root-Rechte benoetigt."
rm -f "$CRON_FILE"
ok "Automatische Backups abgeschaltet ($CRON_FILE entfernt)."
else
ok "Es war nichts eingeplant."
fi
}
backup_auto_enabled() {
case "$(printf '%s' "${BACKUP_AUTO:-off}" | tr 'A-Z' 'a-z')" in
on|true|1|yes|ja) return 0 ;; *) return 1 ;;
esac
}
# --- Anzeige ----------------------------------------------------------
jobs_list_table() {
printf '%-14s %-12s %-14s %-28s %-18s %5s %s\n' \
"NAME" "SITES" "ZEITPLAN" "ZIEL" "OPTIONEN" "HALT" "AKTIV"
local line n=0
while IFS= read -r line; do
[ -n "$line" ] || continue
printf '%-14s %-12s %-14s %-28s %-18s %5s %s\n' \
"$(job_f "$line" 1)" "$(job_f "$line" 2)" "$(job_f "$line" 3)" \
"$(job_f "$line" 4)" "$(job_f "$line" 5)" "$(job_f "$line" 6)" "$(job_f "$line" 7)"
n=$((n+1))
done < <(jobs_read)
[ "$n" -eq 0 ] && echo "(noch keine Jobs angelegt)"
return 0
}
cmd_job() {
local sub="${1:-list}"; [ $# -gt 0 ] && shift || true
case "$sub" in
list|ls) jobs_list_table ;;
status)
printf '\n%sAutomatische Backups%s\n' "$C_BLD" "$C_RST"
printf ' Schalter : %s\n' "$(backup_auto_enabled && echo 'ein (BACKUP_AUTO=on)' || echo 'aus (BACKUP_AUTO=off)')"
printf ' Cron-Datei : %s\n' "$([ -f "$CRON_FILE" ] && echo "$CRON_FILE" || echo 'nicht vorhanden')"
printf ' Jobdatei : %s\n' "$(jobs_file)"
printf ' Manuell nach: %s (BACKUP_DIR)\n' "$BACKUP_DIR"
printf '\n%sJobs%s\n' "$C_BLD" "$C_RST"
jobs_list_table
if [ -f "$CRON_FILE" ]; then
printf '\n%sEingeplant%s\n' "$C_BLD" "$C_RST"
grep -vE '^[[:space:]]*(#|SHELL|PATH|$)' "$CRON_FILE" | sed 's/^/ /'
fi
echo ;;
add|set)
local name="" sites="ALL" cron="" dir="" keep="7" enabled="on" opts=""
local is_set=""; [ "$sub" = set ] && is_set=1
while [ $# -gt 0 ]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--sites) sites="$2"; shift 2 ;;
--cron) cron="$2"; shift 2 ;;
--dir) dir="$2"; shift 2 ;;
--keep) keep="$2"; shift 2 ;;
--no-rrds) opts="$opts --no-rrds"; shift ;;
--no-logs) opts="$opts --no-logs"; shift ;;
--stop) opts="$opts --stop"; shift ;;
--enabled) enabled=on; shift ;;
--disabled) enabled=off; shift ;;
*) [ -z "$name" ] && { name="$1"; shift; } || die "Unbekannte Option fuer 'job $sub': $1" ;;
esac
done
[ -n "$name" ] || die "--name fehlt."
local msg
msg="$(job_validate_name "$name")" || die "$msg"
local old=""; old="$(job_line "$name")"
if [ -n "$is_set" ]; then
[ -n "$old" ] || die "Job '$name' ist nicht vorhanden."
# nicht angegebene Felder aus dem Bestand uebernehmen
if [ "$sites" = ALL ]; then sites="$(job_f "$old" 2)"; fi
if [ -z "$cron" ]; then cron="$(job_f "$old" 3)"; fi
if [ -z "$dir" ]; then dir="$(job_f "$old" 4)"; fi
if [ -z "$opts" ]; then opts="$(job_f "$old" 5)"; fi
if [ "$keep" = 7 ]; then keep="$(job_f "$old" 6)"; fi
enabled="$(job_f "$old" 7)"
else
[ -z "$old" ] || die "Job '$name' existiert bereits. Aendern mit: $SELF job set $name ..."
fi
dir="${dir:-$BACKUP_DIR}"
[ -n "$cron" ] || die "--cron fehlt, z.B. --cron '15 2 * * *'."
msg="$(job_validate_cron "$cron")" || die "$msg"
msg="$(job_validate_field "$dir")" || die "$msg"
case "$keep" in ''|*[!0-9]*) die "--keep braucht eine Zahl (0 = alles behalten)." ;; esac
opts="$(printf '%s' "$opts" | sed 's/^ *//')"
job_put "$name" "$sites" "$cron" "$dir" "$opts" "$keep" "$enabled"
ok "Job '$name' gespeichert."
jobs_list_table | sed -n "1p;/^$name /p"
backup_auto_enabled && cron_write || log "Einplanen mit: $SELF job auto on"
;;
del|rm|delete)
[ $# -ge 1 ] || die "Name fehlt: $SELF job del NAME"
job_exists "$1" || die "Job '$1' ist nicht vorhanden."
confirm "Job '$1' wirklich loeschen?" || die "Abgebrochen."
job_remove "$1"; ok "Job '$1' geloescht."
backup_auto_enabled && cron_write || true
;;
enable|disable)
[ $# -ge 1 ] || die "Name fehlt: $SELF job $sub NAME"
local l; l="$(job_line "$1")"
[ -n "$l" ] || die "Job '$1' ist nicht vorhanden."
local st=on; [ "$sub" = disable ] && st=off
job_put "$1" "$(job_f "$l" 2)" "$(job_f "$l" 3)" "$(job_f "$l" 4)" \
"$(job_f "$l" 5)" "$(job_f "$l" 6)" "$st"
ok "Job '$1' ist jetzt $st."
backup_auto_enabled && cron_write || true
;;
run)
[ $# -ge 1 ] || die "Name fehlt: $SELF job run NAME"
job_run_one "$1" ;;
run-all)
local line n=0
while IFS= read -r line; do
[ -n "$line" ] || continue
[ "$(job_f "$line" 7)" = on ] || continue
job_run_one "$(job_f "$line" 1)" || warn "Job fehlgeschlagen: $(job_f "$line" 1)"
n=$((n+1))
done < <(jobs_read)
[ "$n" -eq 0 ] && warn "Kein aktiver Job vorhanden."
return 0 ;;
auto)
case "${1:-}" in
on) env_set BACKUP_AUTO on; BACKUP_AUTO=on; cron_write ;;
off) env_set BACKUP_AUTO off; BACKUP_AUTO=off; cron_remove_file ;;
'') printf ' Automatische Backups: %s\n' \
"$(backup_auto_enabled && echo ein || echo aus)" ;;
*) die "Aufruf: $SELF job auto [on|off]" ;;
esac ;;
*) die "Unbekanntes Unterkommando 'job $sub'.
Moeglich: list | status | add | set | del | enable | disable | run | run-all | auto" ;;
esac
}
# =====================================================================
# 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' ' ')"
local want_ver want_ed
want_ver="${B_CMK_VERSION%.*}"
case "$B_CMK_VERSION" in
*.cre) want_ed=raw ;;
*.cee) want_ed=enterprise ;;
*.cce) want_ed=cloud ;;
*.cme) want_ed=managed ;;
*) want_ed="${CMK_EDITION:-raw}" ;;
esac
cat <<EOF
"omd restore" kann eine Site nur wiederherstellen, wenn genau ihre Version
im Zielsystem installiert ist. Im Container kommt sie aus dem Image, also:
$SELF image pull --edition $want_ed --version $want_ver
$SELF up
$SELF $mode --file "$file"
Bei der Enterprise-Edition zusaetzlich Zugangsdaten angeben (--user/--password)
oder den Tarball verwenden:
$SELF image download --edition $want_ed --version $want_ver --user <benutzer> --password <kennwort>
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 Image benoetigt - die Raw-Edition genuegt nicht.
Aktuell konfiguriert: ${CMK_IMAGE:-<nicht gesetzt>}" ;;
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
}
# =====================================================================
# NGINX Proxy Manager ein-/ausschalten
#
# Wer bereits einen NGINX auf einem anderen System betreibt, braucht den
# Container nicht. Abgeschaltet wird er weder gebaut noch gestartet -
# "compose up" bekommt dann nur noch den Checkmk-Dienst genannt.
#
# Die Daten unter ./data/npm bleiben in jedem Fall erhalten: sie liegen
# in Bind-Mounts, nicht im Container. Ein spaeteres Einschalten nimmt
# die vorhandene Konfiguration samt Zertifikaten wieder auf.
# =====================================================================
# Container entfernen (nicht die Daten!), damit ihn keine Restart-Regel
# und kein Reboot wieder hochzieht.
npm_teardown_container() {
container_exists "$NPM_CONTAINER_NAME" || return 0
if container_running "$NPM_CONTAINER_NAME"; then
log "Stoppe '$NPM_CONTAINER_NAME' ..."
$RUNTIME stop "$NPM_CONTAINER_NAME" >/dev/null 2>&1 || true
fi
log "Entferne den Container '$NPM_CONTAINER_NAME' (die Daten bleiben)."
$RUNTIME rm -f "$NPM_CONTAINER_NAME" >/dev/null 2>&1 || true
}
npm_data_summary() {
local d="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
local le="${NPM_LE_DIR:-$PROJECT_DIR/data/npm/letsencrypt}"
case "$le" in /*) : ;; *) le="$PROJECT_DIR/${le#./}" ;; esac
printf ' Konfiguration/DB : %s (%s)\n' "$d" "$([ -d "$d" ] && du -sh "$d" 2>/dev/null | cut -f1 || echo 'leer')"
printf ' Zertifikate : %s (%s)\n' "$le" "$([ -d "$le" ] && du -sh "$le" 2>/dev/null | cut -f1 || echo 'leer')"
}
cmd_npm_enable() {
local want=""
while [ $# -gt 0 ]; do
case "$1" in
on|an|true|1|yes|ja) want=on; shift ;;
off|aus|false|0|no|nein) want=off; shift ;;
-h|--help)
cat <<'EOF'
cmk-manage.sh npm-enable [on|off]
Schaltet den NGINX Proxy Manager ein oder aus. Ohne Angabe wird der
aktuelle Stand gezeigt.
off Container wird gestoppt und entfernt und bei "up" nicht mehr
angelegt. Die Daten unter ./data/npm bleiben erhalten.
Sinnvoll, wenn die HTTPS-Terminierung ein anderer NGINX uebernimmt -
der leitet dann auf http://<dieser-host>:5000 weiter.
on Container wird wieder gebaut und gestartet, mit der vorhandenen
Konfiguration und den vorhandenen Zertifikaten.
EOF
return 0 ;;
*) die "Unbekanntes Argument fuer npm-enable: $1 (on|off)" ;;
esac
done
if [ -z "$want" ]; then
printf '\n%sNGINX Proxy Manager%s\n' "$C_BLD" "$C_RST"
printf ' Schalter : %s\n' "$(npm_enabled && echo 'ein (NPM_ENABLED=on)' || echo 'aus (NPM_ENABLED=off)')"
if container_exists "$NPM_CONTAINER_NAME"; then
printf ' Container : %s\n' "$(container_running "$NPM_CONTAINER_NAME" && echo laeuft || echo gestoppt)"
else
printf ' Container : nicht vorhanden\n'
fi
printf '\n%sVorhandene Daten%s\n' "$C_BLD" "$C_RST"
npm_data_summary
echo
return 0
fi
if [ "$want" = off ]; then
npm_enabled || { ok "Der Proxy Manager ist bereits abgeschaltet."; return 0; }
printf '\n%sDiese Daten bleiben erhalten%s\n' "$C_BLD" "$C_RST"
npm_data_summary
echo
confirm "Den NGINX Proxy Manager jetzt abschalten?" || die "Abgebrochen."
env_set NPM_ENABLED off
NPM_ENABLED=off
npm_teardown_container
ok "Abgeschaltet. Bei 'up' wird der Container nicht mehr angelegt."
local ip; ip="$(primary_ip)"
cat <<EOF
${C_BLD}Weiterleitung vom externen NGINX${C_RST}
Ziel : http://$ip:5000
Agent-Receiver : $ip:8000 (muss fuer die Agents erreichbar bleiben)
Port 5000 ist bisher nur fuer localhost gedacht. Er muss jetzt fuer den
externen Proxy erreichbar sein - moeglichst eng begrenzt:
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=<proxy-ip> port port=5000 protocol=tcp accept'
ufw allow from <proxy-ip> to any port 5000 proto tcp
Im externen NGINX (Beispiel):
location / {
proxy_pass http://$ip:5000;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
client_max_body_size 0;
proxy_read_timeout 300s;
}
EOF
return 0
fi
# --- einschalten ---
if npm_enabled && container_running "$NPM_CONTAINER_NAME"; then
ok "Der Proxy Manager laeuft bereits."
return 0
fi
env_set NPM_ENABLED on
NPM_ENABLED=on
log "Starte den NGINX Proxy Manager ..."
if compose up -d nginx-proxy-manager; then
ok "Eingeschaltet."
printf '\n%sVorhandene Daten wurden uebernommen%s\n' "$C_BLD" "$C_RST"
npm_data_summary
printf '\n Oberflaeche: http://%s:%s/\n\n' "$(primary_ip)" "${NPM_ADMIN_PORT:-81}"
else
err "Der Container konnte nicht gestartet werden."
log "Pruefen: $SELF logs nginx-proxy-manager"
return 1
fi
}
# =====================================================================
# Admin-Port des NGINX Proxy Managers
#
# Die Adminoberflaeche des Proxy Managers lauscht im Image fest auf Port 81
# ("listen 81 default;" in /etc/nginx/conf.d/production.conf). Eine
# Umgebungsvariable dafuer gibt es in den veroeffentlichten 2.x-Versionen
# noch nicht. Da beide Container im Host-Netzwerk laufen, laesst sich der
# Port auch nicht per Port-Mapping verschieben.
#
# Loesung: nginx bindet in der Datei
# /data/nginx/custom/http.conf (= ./data/npm/data/nginx/custom/http.conf)
# einen offiziellen Erweiterungspunkt innerhalb des http{}-Blocks ein.
# Dort legen wir einen zusaetzlichen Server an, der auf dem gewuenschten
# Port lauscht und an die Oberflaeche auf 127.0.0.1:81 weiterreicht.
#
# Port 81 bleibt dabei lokal bestehen - er wird per Firewall dichtgehalten.
# =====================================================================
NPM_CUSTOM_CONF_REL="nginx/custom/http.conf"
NPM_BLOCK_START="# >>> cmk-manage.sh: Admin-Port - nicht von Hand aendern >>>"
NPM_BLOCK_END="# <<< cmk-manage.sh: Admin-Port <<<"
npm_custom_conf_path() {
local base="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
case "$base" in /*) : ;; *) base="$PROJECT_DIR/${base#./}" ;; esac
printf '%s/%s\n' "$base" "$NPM_CUSTOM_CONF_REL"
}
# Vorhandenen verwalteten Block entfernen, Fremdinhalte bleiben erhalten
npm_conf_strip_block() {
local f="$1"
[ -f "$f" ] || return 0
awk -v s="$NPM_BLOCK_START" -v e="$NPM_BLOCK_END" '
$0 == s { skip=1; next }
$0 == e { skip=0; next }
!skip { print }
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
}
npm_conf_write_block() {
local f="$1" port="$2"
mkdir -p "$(dirname "$f")"
touch "$f"
npm_conf_strip_block "$f"
{
printf '%s\n' "$NPM_BLOCK_START"
cat <<EOF
# Zusaetzlicher Zugang zur Adminoberflaeche auf Port $port.
# Erzeugt von scripts/cmk-manage.sh npm-port $port
server {
listen $port;
listen [::]:$port;
server_name _;
client_max_body_size 0;
access_log /data/logs/admin_${port}_access.log standard;
error_log /data/logs/admin_${port}_error.log warn;
location / {
proxy_pass http://127.0.0.1:81;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 15m;
proxy_send_timeout 15m;
}
}
EOF
printf '%s\n' "$NPM_BLOCK_END"
} >> "$f"
# Leerzeilen am Anfang aufraeumen
sed -i '/./,$!d' "$f"
}
port_in_use() {
command -v ss >/dev/null 2>&1 || return 1
ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$1\$"
}
valid_port() {
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}
cmd_npm_port() {
local port="" restart=1
while [ $# -gt 0 ]; do
case "$1" in
--no-restart) restart=""; shift ;;
-h|--help)
cat <<'EOF'
cmk-manage.sh npm-port [PORT] [--no-restart]
Legt fest, auf welchem Port die Adminoberflaeche des NGINX Proxy Managers
erreichbar ist. Ohne PORT wird der aktuelle Stand angezeigt.
81 = Vorgabe, kein Zusatzeintrag noetig
sonst = zusaetzlicher nginx-Server auf diesem Port (Port 81 bleibt lokal
bestehen und sollte per Firewall dichtgehalten werden)
EOF
return 0 ;;
*) port="$1"; shift ;;
esac
done
local conf; conf="$(npm_custom_conf_path)"
if [ -z "$port" ]; then
printf '\n%sAdminoberflaeche des NGINX Proxy Managers%s\n' "$C_BLD" "$C_RST"
printf ' Konfigurierter Port : %s\n' "${NPM_ADMIN_PORT:-81}"
printf ' Adresse : %s\n' "${NPM_ADMIN_URL:-http://127.0.0.1:81}"
printf ' Zusatzkonfiguration : %s\n' \
"$(grep -qsF "$NPM_BLOCK_START" "$conf" && echo "$conf" || echo 'keine (Vorgabe 81)')"
if [ "${NPM_ADMIN_PORT:-81}" != 81 ]; then
printf ' Port 81 lokal : %s\n' "$(port_in_use 81 && echo 'lauscht (per Firewall sperren)' || echo 'lauscht nicht')"
fi
echo
return 0
fi
valid_port "$port" || die "'$port' ist keine gueltige Portnummer (1-65535)."
npm_enabled || warn "Der NGINX Proxy Manager ist abgeschaltet (NPM_ENABLED=off).
Die Einstellung wird gespeichert, wirkt aber erst nach dem Einschalten:
$SELF npm-enable on"
case "$port" in
80|443) die "Port $port wird vom Proxy Manager bereits fuer HTTP/HTTPS genutzt." ;;
5000) die "Port 5000 gehoert der Checkmk-Oberflaeche." ;;
8000) die "Port 8000 gehoert dem Checkmk-Agent-Receiver." ;;
6557) die "Port 6557 gehoert Livestatus." ;;
esac
if [ "$port" != 81 ] && port_in_use "$port"; then
warn "Auf Port $port lauscht bereits ein Dienst."
confirm "Trotzdem fortfahren?" || die "Abgebrochen."
fi
if [ "$port" = 81 ]; then
npm_conf_strip_block "$conf"
[ -s "$conf" ] || rm -f "$conf"
ok "Zusatzkonfiguration entfernt - es gilt wieder die Vorgabe (Port 81)."
else
npm_conf_write_block "$conf" "$port"
ok "Zusatzkonfiguration geschrieben: $conf"
fi
env_set NPM_ADMIN_PORT "$port"
env_set NPM_ADMIN_URL "http://127.0.0.1:$port"
NPM_ADMIN_PORT="$port"; NPM_ADMIN_URL="http://127.0.0.1:$port"
if [ -n "$restart" ] && container_running "$NPM_CONTAINER_NAME"; then
log "Starte '$NPM_CONTAINER_NAME' neu, damit nginx die Konfiguration liest ..."
if $RUNTIME restart "$NPM_CONTAINER_NAME" >/dev/null; then
ok "Proxy Manager neu gestartet."
else
warn "Neustart fehlgeschlagen - bitte manuell: $SELF restart"
fi
else
log "Wirksam nach: $SELF up (bzw. Neustart des Proxy Managers)"
fi
local ip; ip="$(primary_ip)"
cat <<EOF
${C_BLD}Erreichbar unter${C_RST}
lokal : http://127.0.0.1:$port
aus dem LAN: http://$ip:$port
${C_BLD}Firewall${C_RST}
firewalld : firewall-cmd --permanent --add-port=$port/tcp && firewall-cmd --reload
ufw : ufw allow $port/tcp
$([ "$port" != 81 ] && printf ' Port 81 sollte weiterhin gesperrt bleiben - er lauscht lokal weiter.\n')
EOF
}
# =====================================================================
# Einstellungen (.env) ueber Menue bzw. Kommandozeile
#
# Tabelle aller pflegbaren Schluessel:
# kategorie|schluessel|typ|beschriftung|auswahl|leer_erlaubt
# Typen: text int port bool choice password path url
# =====================================================================
settings_defs() {
cat <<'EOF'
runtime|CMK_RUNTIME|choice|Container-Runtime|auto:docker:podman|1
runtime|TZ|text|Zeitzone||
image|CMK_IMAGE|info|VERWENDETES Image (setzt der Assistent)||1
image|CMK_EDITION|info|Edition (setzt der Assistent)||1
image|CMK_VERSION|info|Version (setzt der Assistent)||1
image|CMK_REGISTRY|text|Registry fuer Enterprise||1
image|CMK_REGISTRY_USER|text|Registry-Benutzer||1
image|CMK_REGISTRY_PASSWORD|password|Registry-Kennwort||1
image|CMK_DOWNLOAD_BASEURL|url|Basis-URL des Downloadportals||1
image|CMK_DOWNLOAD_URL|url|Feste Download-URL (optional)||1
image|CMK_DOWNLOAD_USER|text|Downloadportal-Benutzer||1
image|CMK_DOWNLOAD_PASSWORD|password|Downloadportal-Kennwort||1
checkmk|CMK_SITE_ID|text|Site-Name (Standard-Site)||
checkmk|CMK_PASSWORD|password|Startkennwort fuer cmkadmin||1
checkmk|CMK_CONTAINER_NAME|text|Containername Checkmk||
checkmk|CMK_HOSTNAME|text|Hostname im Container||
checkmk|CMK_LIVESTATUS_TCP|choice|Livestatus ueber TCP (Port 6557)|off:on|
checkmk|MAIL_RELAY_HOST|text|SMTP-Relay fuer Benachrichtigungen||1
npm|NPM_ENABLED|choice|Proxy Manager betreiben|on:off|
npm|NPM_ADMIN_EMAIL|text|Konto des Proxy Managers (E-Mail)||1
npm|NPM_ADMIN_PASSWORD|password|Kennwort des Proxy-Manager-Kontos||1
npm|NPM_ADMIN_PORT|port|Port der Adminoberflaeche||
npm|PROXY_DOMAIN|text|Vorgabe-Domain fuer Proxy-Hosts||1
npm|PROXY_FORWARD_HOST|text|Weiterleitungsziel (Host)||
npm|PROXY_FORWARD_PORT|port|Weiterleitungsziel (Port)||
npm|NPM_IMAGE|text|Image des Proxy Managers||
npm|NPM_CONTAINER_NAME|text|Containername Proxy Manager||
npm|NPM_DISABLE_IPV6|choice|IPv6 abschalten|true:false|
browser|BROWSER_ENGINE|choice|Vorgabe-Engine|browsh:carbonyl:links:w3m:lynx:shell|
browser|NPM_ADMIN_URL|url|Startseite des Browsers||1
browser|BROWSH_IMAGE|text|Image fuer browsh||
browser|CARBONYL_IMAGE|text|Image fuer carbonyl||
browser|BROWSER_TOOLS_IMAGE|text|Werkzeug-Image (leer = automatisch)||1
paths|CMK_DATA_DIR|path|Checkmk-Sites (Bind-Mount)||
paths|NPM_DATA_DIR|path|Proxy-Manager-Daten||
paths|NPM_LE_DIR|path|Let's-Encrypt-Zertifikate||
paths|BACKUP_DIR|path|Backups und Bundles (manuell)||
backup|BACKUP_AUTO|choice|Automatische Backups per cron|off:on|
paths|IMAGE_DIR|path|Heruntergeladene Image-Tarballs||
cert|CERT_DOMAIN|text|FQDN fuer selbstsignierte Zertifikate||
cert|CERT_DAYS|int|Gueltigkeit in Tagen||
cert|CERT_COUNTRY|text|Laenderkuerzel||
cert|CERT_ORG|text|Organisation||
EOF
}
settings_categories() {
cat <<'EOF'
runtime|Runtime und Zeitzone
image|Checkmk-Image und Zugangsdaten
checkmk|Checkmk-Site und Container
npm|NGINX Proxy Manager
browser|Terminal-Browser
paths|Verzeichnisse
cert|Selbstsignierte Zertifikate
backup|Backup-Automatik
EOF
}
settings_def_for() { # KEY -> "kategorie|key|typ|label|auswahl|leer"
settings_defs | awk -F'|' -v k="$1" '$2==k {print; exit}'
}
settings_value() { # KEY -> aktueller Wert
eval "printf '%s' \"\${$1-}\""
}
settings_display() { # KEY TYP -> Anzeigewert (Kennwoerter maskiert)
local v; v="$(settings_value "$1")"
if [ "$2" = password ] && [ -n "$v" ]; then
printf '********'
elif [ -z "$v" ]; then
printf '<leer>'
else
printf '%s' "$v"
fi
}
settings_validate() { # TYP WERT LEER_ERLAUBT -> 0/1, Meldung auf stdout
local typ="$1" val="$2" opt="$3"
if [ -z "$val" ]; then
[ -n "$opt" ] && return 0
echo "Der Wert darf nicht leer sein."; return 1
fi
case "$typ" in
port) valid_port "$val" || { echo "Keine gueltige Portnummer (1-65535)."; return 1; } ;;
int) case "$val" in ''|*[!0-9]*) echo "Bitte eine Zahl angeben."; return 1 ;; esac ;;
url) case "$val" in http://*|https://*) : ;; *) echo "Bitte mit http:// oder https:// beginnen."; return 1 ;; esac ;;
esac
return 0
}
# Zusaetzliche, schluesselbezogene Pruefung (ueber den Typ hinaus)
settings_validate_key() {
local key="$1" val="$2"
case "$key" in
CMK_RUNTIME)
[ -z "$val" ] && return 0
command -v "$val" >/dev/null 2>&1 \
|| { echo "'$val' ist auf diesem System nicht installiert."; return 1; } ;;
CMK_SITE_ID)
case "$val" in
*[!a-zA-Z0-9_]*) echo "Ein Site-Name darf nur Buchstaben, Ziffern und _ enthalten."; return 1 ;;
esac
[ "${#val}" -le 16 ] || { echo "Ein Site-Name darf hoechstens 16 Zeichen lang sein."; return 1; } ;;
CMK_IMAGE|NPM_IMAGE|BROWSH_IMAGE|CARBONYL_IMAGE)
if [ -n "$val" ] && image_is_short_name "$val"; then
echo "Bitte vollqualifiziert angeben (z.B. docker.io/...) - Podman
findet kurze Namen sonst nicht."; return 1
fi ;;
esac
return 0
}
# Nacharbeit nach dem Setzen bestimmter Schluessel
settings_post_set() {
local key="$1" val="$2"
case "$key" in
NPM_ADMIN_PORT)
# Der nginx-Zusatzblock muss mitgezogen werden
cmd_npm_port "$val" ;;
NPM_ENABLED)
# Container entsprechend entfernen bzw. starten
cmd_npm_enable "$val" ;;
BACKUP_AUTO)
cmd_job auto "$val" ;;
CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|NPM_IMAGE|NPM_CONTAINER_NAME|TZ|CMK_LIVESTATUS_TCP|NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR)
log "Wirksam nach: $SELF up (Container werden neu erstellt)" ;;
esac
}
cmd_config() {
local sub="${1:-list}"; [ $# -gt 0 ] && shift || true
case "$sub" in
list|ls)
local cat_filter=""
if [ "${1:-}" = "--category" ]; then cat_filter="$2"; fi
local line c k t l
while IFS='|' read -r c k t l _ _; do
[ -n "$k" ] || continue
[ -n "$cat_filter" ] && [ "$c" != "$cat_filter" ] && continue
printf '%-24s %-10s %s\n' "$k" "$c" "$(settings_display "$k" "$t")"
done < <(settings_defs)
;;
get)
[ $# -ge 1 ] || die "Schluessel fehlt: $SELF config get KEY"
[ -n "$(settings_def_for "$1")" ] || die "Unbekannter Schluessel '$1'. Uebersicht: $SELF config list"
settings_value "$1"; echo
;;
set)
[ $# -ge 2 ] || die "Aufruf: $SELF config set KEY WERT"
local key="$1" val="$2" def typ opt msg
def="$(settings_def_for "$key")"
[ -n "$def" ] || die "Unbekannter Schluessel '$key'. Uebersicht: $SELF config list"
typ="$(printf '%s' "$def" | cut -d'|' -f3)"
opt="$(printf '%s' "$def" | cut -d'|' -f6)"
[ "$val" = "auto" ] && [ "$key" = CMK_RUNTIME ] && val=""
if ! msg="$(settings_validate "$typ" "$val" "$opt")"; then
die "$msg"
fi
if ! msg="$(settings_validate_key "$key" "$val")"; then
die "$msg"
fi
if [ "$key" = NPM_ADMIN_PORT ]; then
cmd_npm_port "$val"
elif [ "$key" = NPM_ENABLED ]; then
cmd_npm_enable "$val"
elif [ "$key" = BACKUP_AUTO ]; then
env_set "$key" "$val"; BACKUP_AUTO="$val"; cmd_job auto "$val"
else
env_set "$key" "$val" "$opt"
ok "$key=$val"
settings_post_set "$key" "$val"
fi
;;
edit)
local f="${ENV_FILE:-$PROJECT_DIR/.env}"
"${EDITOR:-nano}" "$f"; load_env "$f"; ok "Konfiguration neu eingelesen." ;;
*) die "Unbekanntes Unterkommando 'config $sub'.
Moeglich: list [--category KAT] | get KEY | set KEY WERT | edit" ;;
esac
}
# =====================================================================
# Terminal-Browser im Container
#
# Zweck: den NGINX Proxy Manager direkt auf der Maschine einrichten,
# wenn dessen Adminoberflaeche (Port 81) von aussen nicht erreichbar ist.
#
# WICHTIG: Die Adminoberflaeche des Proxy Managers ist eine JavaScript-
# Anwendung. Klassische Textbrowser (links2, w3m, lynx) koennen sie NICHT
# darstellen. Dafuer gibt es zwei Engines, die einen echten Browser im
# Terminal rendern:
# browsh - Firefox im Terminal (gepflegt)
# carbonyl - Chromium im Terminal (schoenere Darstellung, Stand 2023)
# Die Textbrowser bleiben fuer schnelle Pruefungen nuetzlich.
#
# Der Container laeuft im Host-Netzwerk und erreicht damit 127.0.0.1:81
# genau wie der Proxy Manager selbst.
# =====================================================================
browser_tools_image() {
if [ -n "${BROWSER_TOOLS_IMAGE:-}" ]; then
printf '%s\n' "$BROWSER_TOOLS_IMAGE"
elif [ "$RUNTIME" = podman ]; then
# Podman legt selbst gebaute Images unter localhost/ ab
printf '%s\n' "localhost/checkmk-stack/browser-tools:1"
else
# Docker wuerde ein "localhost/"-Praefix als echte Registry deuten
printf '%s\n' "checkmk-stack/browser-tools:1"
fi
}
browser_build_tools() {
local img; img="$(browser_tools_image)"
local ctx="$PROJECT_DIR/containers/browser"
[ -f "$ctx/Containerfile" ] || die "Containerfile fehlt: $ctx/Containerfile"
log "Baue Werkzeug-Image '$img' ..."
# -f explizit: Docker sucht sonst nach "Dockerfile", Podman nimmt beides
$RUNTIME build -f "$ctx/Containerfile" -t "$img" "$ctx" \
|| die "Bau des Werkzeug-Images fehlgeschlagen."
ok "Image gebaut: $img"
}
browser_ensure_tools() {
local img; img="$(browser_tools_image)"
image_exists_local "$img" || browser_build_tools
}
# Antwortet die Startseite ueberhaupt? Ein Terminal-Browser zeigt sonst
# nur eine schwarze Seite - ohne jede Fehlermeldung.
browser_url_reachable() {
command -v curl >/dev/null 2>&1 || return 0 # ohne curl nicht pruefbar
local code
# ACHTUNG: curl gibt bei einem Fehler selbst schon "000" aus. Ein
# zusaetzliches "|| echo 000" wuerde einen zweiten Wert anhaengen und
# jeden Vergleich verfaelschen - deshalb den Zuweisungsfehler abfangen.
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 6 -L "$1" 2>/dev/null)" || code=000
[ -n "$code" ] && [ "$code" != "000" ]
}
# Erklaert, warum nichts antwortet, und nennt den passenden naechsten Schritt
browser_diagnose_url() { # url -> mehrzeiliger Text auf stdout
local url="$1" host port
host="$(printf '%s' "$url" | sed -E 's|^[a-z]+://([^/:]+).*|\1|')"
port="$(printf '%s' "$url" | sed -nE 's|^[a-z]+://[^/:]+:([0-9]+).*|\1|p')"
printf 'Auf %s antwortet nichts.\n' "$url"
printf 'Ein Terminal-Browser zeigt dann nur eine schwarze Seite.\n\n'
case "$host" in
127.0.0.1|localhost|::1)
if [ "${port:-81}" = "${NPM_ADMIN_PORT:-81}" ]; then
printf 'Das ist die Oberflaeche des NGINX Proxy Managers.\n'
if ! npm_enabled; then
printf 'Er ist abgeschaltet (NPM_ENABLED=off):\n'
printf ' %s npm-enable on\n' "$SELF"
elif ! container_exists "$NPM_CONTAINER_NAME"; then
printf 'Sein Container existiert nicht:\n'
printf ' %s up\n' "$SELF"
elif ! container_running "$NPM_CONTAINER_NAME"; then
printf 'Sein Container ist gestoppt:\n'
printf ' %s up\n' "$SELF"
else
printf 'Der Container laeuft, antwortet aber nicht auf Port %s.\n' "${port:-81}"
printf ' %s logs %s\n' "$SELF" "$NPM_CONTAINER_NAME"
printf ' %s npm-port (eingestellten Port pruefen)\n' "$SELF"
fi
elif [ "${port:-}" = 5000 ]; then
printf 'Das ist die Checkmk-Oberflaeche.\n'
if ! container_running "$CMK_CONTAINER_NAME"; then
printf 'Der Checkmk-Container laeuft nicht:\n %s up\n' "$SELF"
else
printf 'Der Container laeuft - Site gestartet?\n %s sites\n' "$SELF"
fi
else
printf 'Auf diesem Host lauscht nichts auf Port %s.\n' "${port:-80}"
printf ' ss -tlnp | grep %s\n' "${port:-80}"
fi ;;
*)
printf 'Externe Adressen brauchen Netzzugang vom Host aus\n'
printf '(der Container nutzt dessen Netzwerk direkt).\n'
printf ' curl -I %s\n' "$url" ;;
esac
}
cmd_browser() {
local engine="${BROWSER_ENGINE:-browsh}"
local url="" build="" build_only="" pull="" mono="" nocheck=""
while [ $# -gt 0 ]; do
case "$1" in
-e|--engine) engine="$2"; shift 2 ;;
-u|--url) url="$2"; shift 2 ;;
--build) build=1; shift ;;
--build-only) build=1; build_only=1; shift ;;
--pull) pull=1; shift ;;
--monochrome) mono=1; shift ;;
--no-check) nocheck=1; shift ;;
-h|--help)
cat <<'EOF'
cmk-manage.sh browser - Terminal-Browser im Container
-e, --engine ENGINE browsh Firefox im Terminal (Vorgabe, kann JavaScript)
carbonyl Chromium im Terminal (kann JavaScript)
links links2 (Textbrowser, KEIN JavaScript)
w3m w3m (Textbrowser, KEIN JavaScript)
lynx lynx (Textbrowser, KEIN JavaScript)
shell Bash mit curl/jq/openssl im Host-Netzwerk
-u, --url URL Startseite (Vorgabe: $NPM_ADMIN_URL, sonst
http://127.0.0.1:81 = Proxy-Manager-Oberflaeche)
--build Werkzeug-Image neu bauen und danach starten
--build-only Werkzeug-Image nur bauen (ohne Terminal, z.B. im Skript)
--pull Image der gewaehlten Engine neu laden
--monochrome nur bei browsh: einfarbige Darstellung
--no-check Startseite nicht vorab auf Erreichbarkeit pruefen
Bedienung browsh: Strg+L Adresszeile, Strg+T neuer Tab,
Strg+W Tab schliessen, Strg+Q beenden
EOF
return 0 ;;
*) die "Unbekannte Option fuer browser: $1" ;;
esac
done
url="${url:-${NPM_ADMIN_URL:-http://127.0.0.1:81}}"
# Engine-Namen zuerst pruefen, damit ein Tippfehler auch ohne
# Terminal eine brauchbare Meldung ergibt
case "$engine" in
browsh|carbonyl|links|links2|w3m|lynx|shell) : ;;
*) die "Unbekannte Engine '$engine'.
Moeglich: browsh | carbonyl | links | w3m | lynx | shell" ;;
esac
# Bauen braucht kein Terminal - deshalb vor der TTY-Pruefung
if [ -n "$build" ]; then
browser_build_tools
[ -n "$build_only" ] && return 0
fi
if [ ! -t 0 ] || [ ! -t 1 ]; then
die "Ein Terminal-Browser braucht ein echtes Terminal (TTY).
Bitte direkt auf der Konsole bzw. in einer SSH-Sitzung aufrufen,
nicht in einer Pipe oder aus einem Skript heraus."
fi
local image
local -a args=() runopts=()
case "$engine" in
browsh)
image="${BROWSH_IMAGE:-docker.io/browsh/browsh:v1.8.3}"
args=(--startup-url "$url")
[ -n "$mono" ] && args+=(--monochrome)
;;
carbonyl)
image="${CARBONYL_IMAGE:-docker.io/fathyb/carbonyl:latest}"
args=("$url")
;;
links|links2)
browser_ensure_tools; image="$(browser_tools_image)"; args=(links2 "$url") ;;
w3m)
browser_ensure_tools; image="$(browser_tools_image)"; args=(w3m "$url") ;;
lynx)
browser_ensure_tools; image="$(browser_tools_image)"; args=(lynx "$url") ;;
shell)
browser_ensure_tools; image="$(browser_tools_image)"; args=(/bin/bash) ;;
esac
if [ -n "$pull" ]; then
case "$engine" in
browsh|carbonyl) log "Lade $image ..."; $RUNTIME pull "$image" || die "Konnte '$image' nicht laden." ;;
*) browser_build_tools ;;
esac
fi
case "$engine" in
links|w3m|lynx)
warn "Textbrowser koennen kein JavaScript - die Oberflaeche des
NGINX Proxy Managers laesst sich damit NICHT bedienen.
Dafuer: $SELF browser --engine browsh" ;;
esac
# Nicht erreichbare Startseite ist die haeufigste Ursache fuer eine
# scheinbar haengende, schwarze Anzeige.
if [ -z "$nocheck" ] && ! browser_url_reachable "$url"; then
echo
warn "$(browser_diagnose_url "$url")"
echo
confirm "Trotzdem starten?" || die "Abgebrochen."
fi
log "Starte $engine im Host-Netzwerk -> $url"
[ "$engine" = browsh ] && log "Beenden mit Strg+Q, Adresszeile mit Strg+L."
runopts=(--rm -it --network host
-e "TERM=${TERM:-xterm-256color}"
-e "COLORTERM=${COLORTERM:-truecolor}")
$RUNTIME run "${runopts[@]}" "$image" "${args[@]}"
}
# =====================================================================
# Proxy-Hosts ueber die REST-Schnittstelle des NGINX Proxy Managers
#
# Die Weboberflaeche laesst sich im Terminal-Browser nur muehsam
# bedienen - Datei-Uploads und einzelne Dialoge funktionieren dort
# nicht. Da der Proxy Manager ohnehin lokal laeuft, ist die API der
# verlaesslichere Weg.
#
# Voraussetzung ist ein Konto. Seit Version 2.15 legt der Container es
# nur noch an, wenn INITIAL_ADMIN_EMAIL und INITIAL_ADMIN_PASSWORD
# gesetzt sind (das erledigt die .env ueber NPM_ADMIN_EMAIL/-PASSWORD)
# - sonst erscheint beim ersten Aufruf ein Einrichtungsassistent.
#
# Geprueft an der Schnittstellenbeschreibung von v2.15.1:
# POST /api/tokens {identity, secret}
# GET|POST|DELETE /api/nginx/proxy-hosts
# POST /api/nginx/certificates {provider, ...}
# POST /api/nginx/certificates/<id>/upload multipart
# Let's Encrypt nimmt die E-Mail des angemeldeten Kontos - ein
# "letsencrypt_email" im Aufruf gibt es in 2.15 nicht mehr.
# =====================================================================
npm_api_base() { printf 'http://127.0.0.1:%s\n' "${NPM_ADMIN_PORT:-81}"; }
# Sehr einfache Auswertung - kein jq noetig, das fehlt auf vielen Hosts
json_num() { printf '%s' "$1" | tr ',' '\n' | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p" | head -n1; }
json_str() { printf '%s' "$1" | tr ',' '\n' | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n1; }
# IDs aus einer Listenantwort ziehen. "id": trifft dabei NICHT auf
# certificate_id oder owner_user_id zu, weil dort ein _ vor dem id steht.
json_ids() { printf '%s' "$1" | grep -o '"id":[0-9]*' | sed 's/.*://' ; }
npm_api_token() {
local base; base="$(npm_api_base)"
[ -n "${NPM_ADMIN_EMAIL:-}" ] && [ -n "${NPM_ADMIN_PASSWORD:-}" ] || {
err "Es sind keine Zugangsdaten fuer den Proxy Manager hinterlegt."
cat >&2 <<EOF
In der .env eintragen:
NPM_ADMIN_EMAIL=...
NPM_ADMIN_PASSWORD=...
Beim ERSTEN Start mit leerer Datenbank legt der Container damit das
Konto selbst an. Besteht das Konto schon, hier dessen Daten eintragen.
EOF
return 1
}
local body
body="$(curl -sS --max-time 20 -X POST "$base/api/tokens" \
-H 'Content-Type: application/json' \
-d "{\"identity\":\"$NPM_ADMIN_EMAIL\",\"secret\":\"$NPM_ADMIN_PASSWORD\"}" 2>/dev/null)" || true
local tok; tok="$(json_str "$body" token)"
if [ -z "$tok" ]; then
err "Anmeldung am Proxy Manager fehlgeschlagen."
local m; m="$(json_str "$body" message)"
[ -n "$m" ] && printf ' Antwort: %s\n' "$m" >&2
printf ' Geprueft wurde: %s\n' "$base/api/tokens" >&2
return 1
fi
printf '%s' "$tok"
}
# npm_api METHODE PFAD [JSON] -> Antwort auf stdout, HTTP-Code in NPM_CODE
npm_api() {
local method="$1" path="$2" data="${3:-}"
local base; base="$(npm_api_base)"
local -a a=(-sS --max-time 60 -X "$method" -H "Authorization: Bearer $NPM_TOKEN"
-w '\n%{http_code}')
[ -n "$data" ] && a+=(-H 'Content-Type: application/json' -d "$data")
local out; out="$(curl "${a[@]}" "$base$path" 2>/dev/null)" || out=$'\n000'
NPM_CODE="${out##*$'\n'}"
printf '%s' "${out%$'\n'*}"
}
npm_require_running() {
npm_enabled || { err "Der NGINX Proxy Manager ist abgeschaltet (NPM_ENABLED=off)."
log "Einschalten: $SELF npm-enable on"; return 1; }
container_running "$NPM_CONTAINER_NAME" || {
err "Der Container '$NPM_CONTAINER_NAME' laeuft nicht."
log "Starten: $SELF up"; return 1; }
return 0
}
# --- Zertifikat anlegen --------------------------------------------------
# gibt die Zertifikats-ID auf stdout aus
npm_cert_selfsigned() { # domain
local domain="$1"
local dir="$PROJECT_DIR/data/npm/custom-certs/$domain"
if [ ! -f "$dir/fullchain.pem" ] || [ ! -f "$dir/privkey.pem" ]; then
log "Erzeuge selbstsigniertes Zertifikat fuer '$domain' ..." >&2
cmd_cert --domain "$domain" >/dev/null || return 1
else
log "Verwende vorhandenes Zertifikat unter $dir" >&2
fi
# Bereits vorhandenen Eintrag gleichen Namens wiederverwenden, sonst
# sammeln sich bei jedem Aufruf neue Zertifikate an.
local id="" body list cid o
list="$(npm_api GET /api/nginx/certificates)"
for cid in $(json_ids "$list"); do
o="$(npm_api GET "/api/nginx/certificates/$cid")"
if [ "$(json_str "$o" provider)" = other ] && [ "$(json_str "$o" nice_name)" = "$domain" ]; then
id="$cid"; log "Verwende vorhandenen Zertifikatseintrag $id wieder." >&2; break
fi
done
if [ -z "$id" ]; then
body="$(npm_api POST /api/nginx/certificates \
"{\"provider\":\"other\",\"nice_name\":\"$domain\",\"domain_names\":[\"$domain\"]}")"
id="$(json_num "$body" id)"
[ -n "$id" ] || { err "Zertifikatseintrag konnte nicht angelegt werden (HTTP $NPM_CODE)."
printf ' %s\n' "$body" >&2; return 1; }
fi
log "Lade Schluessel und Zertifikat hoch (ID $id) ..." >&2
local up
up="$(curl -sS --max-time 60 -X POST "$(npm_api_base)/api/nginx/certificates/$id/upload" \
-H "Authorization: Bearer $NPM_TOKEN" \
-F "certificate=@$dir/fullchain.pem" \
-F "certificate_key=@$dir/privkey.pem" 2>/dev/null)" || true
case "$up" in
*'"error"'*|'') err "Hochladen fehlgeschlagen."; printf ' %s\n' "$up" >&2; return 1 ;;
esac
printf '%s' "$id"
}
npm_cert_letsencrypt() { # domain
local domain="$1" body
log "Fordere Let's-Encrypt-Zertifikat fuer '$domain' an - das dauert ..." >&2
body="$(npm_api POST /api/nginx/certificates \
"{\"provider\":\"letsencrypt\",\"domain_names\":[\"$domain\"],\"meta\":{}}")"
local id; id="$(json_num "$body" id)"
if [ -z "$id" ]; then
err "Let's Encrypt hat kein Zertifikat ausgestellt (HTTP $NPM_CODE)."
local m; m="$(json_str "$body" message)"
[ -n "$m" ] && printf ' Antwort: %s\n' "$m" >&2
cat >&2 <<EOF
Haeufige Gruende:
* Port 80 ist aus dem Internet nicht erreichbar (HTTP-01-Pruefung)
* der DNS-Name zeigt nicht auf diesen Host
* das Konto hat keine E-Mail-Adresse - Let's Encrypt nimmt in
Version 2.15 die Adresse des angemeldeten Kontos
EOF
return 1
fi
printf '%s' "$id"
}
# Einen --location-Eintrag in ein JSON-Objekt umsetzen.
# /grafana=127.0.0.1:3000
# /grafana=http://127.0.0.1:3000
# /alt=127.0.0.1:3000/basis (Pfad, der beim Ziel vorangestellt wird)
proxy_location_json() { # spec -> JSON-Objekt auf stdout
local spec="$1" path target scheme=http host port fwd=""
case "$spec" in
*=*) path="${spec%%=*}"; target="${spec#*=}" ;;
*) err "Ungueltige Angabe: '$spec'"
printf ' Erwartet: /pfad=host:port z.B. /grafana=127.0.0.1:3000\n' >&2
return 1 ;;
esac
case "$path" in /*) : ;; *) path="/$path" ;; esac
case "$target" in
http://*) scheme=http; target="${target#http://}" ;;
https://*) scheme=https; target="${target#https://}" ;;
esac
host="${target%%:*}"
local rest="${target#*:}"
if [ "$rest" = "$target" ]; then
err "Im Ziel '$target' fehlt der Port (erwartet host:port)."; return 1
fi
port="${rest%%/*}"
case "$rest" in */*) fwd="/${rest#*/}" ;; esac
valid_port "$port" || { err "'$port' ist keine gueltige Portnummer."; return 1; }
[ -n "$host" ] || { err "Im Ziel '$target' fehlt der Host."; return 1; }
printf '{"path":"%s","forward_scheme":"%s","forward_host":"%s","forward_port":%s,"forward_path":"%s","advanced_config":""}' \
"$path" "$scheme" "$host" "$port" "$fwd"
}
# --- Proxy-Hosts ---------------------------------------------------------
cmd_proxy() {
local sub="${1:-list}"; [ $# -gt 0 ] && shift || true
case "$sub" in
login|status)
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
ok "Anmeldung am Proxy Manager erfolgreich ($NPM_ADMIN_EMAIL)."
local body; body="$(npm_api GET /api/nginx/proxy-hosts)"
printf ' Proxy-Hosts: %s\n' "$(printf '%s' "$body" | grep -c '"id"' || true)"
body="$(npm_api GET /api/nginx/certificates)"
printf ' Zertifikate: %s\n' "$(printf '%s' "$body" | grep -c '"id"' || true)"
return 0 ;;
list)
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
local body; body="$(npm_api GET /api/nginx/proxy-hosts)"
printf '%-5s %-34s %-26s %s\n' "ID" "DOMAINS" "ZIEL" "SSL"
local id o dom fh fp cid n=0
for id in $(json_ids "$body"); do
# Einzeln abfragen: in der Listenantwort laesst sich ein Objekt
# nicht zuverlaessig abgrenzen, weil "meta":{...} ebenfalls
# geschweifte Klammern enthaelt.
o="$(npm_api GET "/api/nginx/proxy-hosts/$id")"
dom="$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p' | tr -d '"' || true)"
fh="$(json_str "$o" forward_host)"; fp="$(json_num "$o" forward_port)"
cid="$(json_num "$o" certificate_id)"
printf '%-5s %-34s %-26s %s\n' "$id" "${dom:-?}" \
"$(json_str "$o" forward_scheme)://$fh:$fp" \
"$([ "${cid:-0}" -gt 0 ] 2>/dev/null && echo "ja (Zert. $cid)" || echo nein)"
n=$((n+1))
done
[ "$n" -eq 0 ] && echo "(noch kein Proxy-Host angelegt)"
return 0 ;;
certs)
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
local body; body="$(npm_api GET /api/nginx/certificates)"
printf '%-5s %-12s %-28s %s\n' "ID" "ANBIETER" "NAME" "LAEUFT AB"
local id o n=0
for id in $(json_ids "$body"); do
o="$(npm_api GET "/api/nginx/certificates/$id")"
printf '%-5s %-12s %-28s %s\n' "$id" "$(json_str "$o" provider)" \
"$(json_str "$o" nice_name)" "$(json_str "$o" expires_on | awk '{print $1}')"
n=$((n+1))
done
[ "$n" -eq 0 ] && echo "(noch kein Zertifikat vorhanden)"
return 0 ;;
show)
local pid="" dom=""
while [ $# -gt 0 ]; do
case "$1" in --id) pid="$2"; shift 2 ;; --domain) dom="$2"; shift 2 ;; *) pid="$1"; shift ;; esac
done
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
if [ -z "$pid" ]; then
[ -n "$dom" ] || die "--id oder --domain angeben."
local all; all="$(npm_api GET /api/nginx/proxy-hosts)"
local i
for i in $(json_ids "$all"); do
if npm_api GET "/api/nginx/proxy-hosts/$i" | grep -qF "\"$dom\""; then pid="$i"; break; fi
done
[ -n "$pid" ] || die "Kein Proxy-Host fuer '$dom' gefunden."
fi
local o; o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
printf '\n%sProxy-Host %s%s\n' "$C_BLD" "$pid" "$C_RST"
printf ' Domains : %s\n' "$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p' | tr -d '"' || true)"
printf ' Ziel : %s://%s:%s\n' "$(json_str "$o" forward_scheme)" "$(json_str "$o" forward_host)" "$(json_num "$o" forward_port)"
printf ' Zertifikat : %s\n' "$(json_num "$o" certificate_id)"
printf '\n %sZusaetzliche Pfade%s\n' "$C_BLD" "$C_RST"
local n
n="$(printf '%s' "$o" | grep -o '"path":"[^"]*"' | wc -l || true)"
if [ "${n:-0}" -eq 0 ]; then
printf ' (keine - alles geht an das Ziel oben)\n'
else
printf '%s' "$o" | tr '}' '\n' \
| sed -n 's/.*"path":"\([^"]*\)".*"forward_host":"\([^"]*\)","forward_port":\([0-9]*\).*/ \1 -> \2:\3/p'
fi
echo
return 0 ;;
del|delete|rm)
local pid=""
while [ $# -gt 0 ]; do
case "$1" in --id) pid="$2"; shift 2 ;; *) pid="$1"; shift ;; esac
done
[ -n "$pid" ] || die "ID fehlt: $SELF proxy del --id N (IDs zeigt '$SELF proxy list')"
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
confirm "Proxy-Host mit der ID $pid wirklich loeschen?" || die "Abgebrochen."
npm_api DELETE "/api/nginx/proxy-hosts/$pid" >/dev/null
case "$NPM_CODE" in 200|201|204) ok "Proxy-Host $pid geloescht." ;;
*) die "Loeschen fehlgeschlagen (HTTP $NPM_CODE)." ;; esac
return 0 ;;
add)
local domain="${PROXY_DOMAIN:-}" fhost="${PROXY_FORWARD_HOST:-127.0.0.1}"
local fport="${PROXY_FORWARD_PORT:-5000}" scheme=http cert=self
local force_ssl=true http2=true ws=true block=true update="" adv=""
local adv_set="" locs=""
while [ $# -gt 0 ]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
--forward-host) fhost="$2"; shift 2 ;;
--forward-port) fport="$2"; shift 2 ;;
--scheme) scheme="$2"; shift 2 ;;
--cert) cert="$2"; shift 2 ;;
--advanced) adv="$2"; adv_set=1; shift 2 ;;
--location) local lj
lj="$(proxy_location_json "$2")" || return 1
if [ -n "$locs" ]; then locs="$locs,$lj"; else locs="$lj"; fi
shift 2 ;;
--update) update=1; shift ;;
--no-force-ssl) force_ssl=false; shift ;;
--no-http2) http2=false; shift ;;
--no-websockets) ws=false; shift ;;
--no-block-exploits) block=false; shift ;;
*) die "Unbekannte Option fuer 'proxy add': $1" ;;
esac
done
[ -n "$domain" ] || die "--domain fehlt (oder PROXY_DOMAIN in der .env setzen)."
case "$scheme" in http|https) : ;; *) die "--scheme: http oder https" ;; esac
case "$cert" in none|self|letsencrypt) : ;; *) die "--cert: none | self | letsencrypt" ;; esac
valid_port "$fport" || die "--forward-port: keine gueltige Portnummer."
[ -n "$adv_set" ] || adv='client_max_body_size 0;\nproxy_read_timeout 300s;\nproxy_set_header X-Forwarded-Proto $scheme;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;'
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
# schon vorhanden?
local existing
# Achtung: grep liefert 1, wenn nichts passt - unter "set -e" wuerde
# das die Zuweisung und damit das ganze Skript beenden.
existing="$(npm_api GET /api/nginx/proxy-hosts | tr '{' '\n' \
| grep -F "\"$domain\"" | head -n1 || true)"
local exid=""; [ -n "$existing" ] && exid="$(json_num "$existing" id)"
if [ -n "$exid" ] && [ -z "$update" ]; then
die "Fuer '$domain' gibt es bereits den Proxy-Host $exid.
Ersetzen mit --update, oder vorher loeschen: $SELF proxy del --id $exid"
fi
local certid=0
case "$cert" in
self) certid="$(npm_cert_selfsigned "$domain")" || return 1 ;;
letsencrypt) certid="$(npm_cert_letsencrypt "$domain")" || return 1 ;;
none) certid=0; force_ssl=false; http2=false ;;
esac
local payload
payload="{\"domain_names\":[\"$domain\"],\"forward_scheme\":\"$scheme\",\"forward_host\":\"$fhost\",\"forward_port\":$fport,\"certificate_id\":$certid,\"ssl_forced\":$force_ssl,\"http2_support\":$http2,\"block_exploits\":$block,\"allow_websocket_upgrade\":$ws,\"caching_enabled\":false,\"advanced_config\":\"$adv\",\"locations\":[$locs],\"enabled\":true,\"meta\":{}}"
local body
if [ -n "$exid" ]; then
log "Aktualisiere vorhandenen Proxy-Host $exid ..."
body="$(npm_api PUT "/api/nginx/proxy-hosts/$exid" "$payload")"
else
log "Lege Proxy-Host fuer '$domain' an ..."
body="$(npm_api POST /api/nginx/proxy-hosts "$payload")"
fi
local newid; newid="$(json_num "$body" id)"
if [ -z "$newid" ]; then
err "Der Proxy-Host konnte nicht angelegt werden (HTTP $NPM_CODE)."
local m; m="$(json_str "$body" message)"
[ -n "$m" ] && printf ' Antwort: %s\n' "$m" >&2
return 1
fi
ok "Proxy-Host $newid angelegt: $domain -> $scheme://$fhost:$fport"
if [ -n "$locs" ]; then
printf ' Zusaetzliche Pfade:\n'
printf '%s' "$locs" | tr ',' '\n' | sed -n 's/.*"path":"\([^"]*\)".*"forward_host":"\([^"]*\)","forward_port":\([0-9]*\).*/ \1 -> \2:\3/p'
fi
cat <<EOF
${C_BLD}Erreichbar${C_RST}
https://$domain/${CMK_SITE_ID:-cmk}/ (HTTPS auf Port 443)
http://$domain/ -> $([ "$force_ssl" = true ] && echo 'Weiterleitung auf HTTPS' || echo 'unverschluesselt')
$([ "$cert" = self ] && printf '%s\n' "Das Zertifikat ist selbstsigniert. Damit Browser und Agents ihm
trauen, data/npm/custom-certs/$domain/fullchain.pem auf den Clients
als vertrauenswuerdig einrichten.")
EOF
return 0 ;;
*) die "Unbekanntes Unterkommando 'proxy $sub'.
Moeglich: status | list | show | certs | add | del" ;;
esac
}
# =====================================================================
# 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)"
if [ -n "$myip" ]; then san="$san,IP:$myip"; fi
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-sichere Vorbedingungen
#
# In der Oberflaeche darf nichts "die" aufrufen - das wuerde die ganze TUI
# beenden. Diese Helfer melden das Problem im Dialog und geben 1 zurueck.
# ---------------------------------------------------------------------
gui_need_container() {
if ! container_exists "$CMK_CONTAINER_NAME"; then
ui_msg "Checkmk-Container fehlt" \
"Der Container '$CMK_CONTAINER_NAME' existiert nicht.
Dafuer wird er zuerst gebraucht. Im Hauptmenue:
stack -> Stack starten
Auf der Kommandozeile:
$SELF up"
return 1
fi
if ! container_running "$CMK_CONTAINER_NAME"; then
ui_msg "Checkmk-Container gestoppt" \
"Der Container '$CMK_CONTAINER_NAME' existiert, laeuft aber nicht.
Im Hauptmenue: stack -> Stack starten
Kommandozeile: $SELF up"
return 1
fi
return 0
}
gui_need_sites() {
gui_need_container || return 1
local n; n="$(cmk_sites | sed '/^$/d' | wc -l)"
if [ "$n" -eq 0 ]; then
ui_msg "Keine Sites" \
"Im Container ist noch keine Site vorhanden.
Der Container legt die Site aus CMK_SITE_ID beim ersten Start selbst an.
Alternativ eine Site importieren: Hauptmenue -> import"
return 1
fi
return 0
}
# Interaktives Kommando ausfuehren, ohne dass ein Fehler die TUI beendet
gui_run_interactive() {
clear 2>/dev/null || true
if ! ( "$@" ); then
printf '\n%sDas Kommando wurde mit einem Fehler beendet.%s\n' "$C_YEL" "$C_RST"
fi
printf '\nWeiter mit ENTER ...'
read -r _ </dev/tty || true
}
gui_pick_site() { # -> Site auf stdout
gui_need_container || return 1
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() {
gui_need_sites || return 0
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 0
[ -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 0
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[*]}"
}
# ---------------------------------------------------------------------
# Backup: erstellen und planen
# ---------------------------------------------------------------------
gui_backup_menu() {
while :; do
local auto sel
auto="$(backup_auto_enabled && echo 'ein' || echo 'aus')"
sel="$(ui_menu "Backup erstellen / planen" \
"Manuelle Backups gehen immer nach:
$BACKUP_DIR (Einstellung BACKUP_DIR)
Geplante Jobs haben ihr eigenes Zielverzeichnis.
Automatische Ausfuehrung: $auto" \
manuell "Backup jetzt erstellen" \
jobs "Backup-Jobs verwalten (anlegen/aendern/loeschen)" \
auto "Automatische Ausfuehrung ein/aus (derzeit: $auto)" \
status "Zeitplan und Jobs anzeigen" \
jetzt "Einen Job sofort ausfuehren" \
back "zurueck")" || return 0
case "$sel" in
manuell) gui_backup || true ;;
jobs) gui_jobs_menu || true ;;
auto) gui_backup_auto || true ;;
status) local f; f="$(mktemp)"; NO_COLOR=1 cmd_job status >"$f" 2>&1 || true
ui_text "Backup-Zeitplan" "$f"; rm -f "$f" ;;
jetzt) gui_job_run_now || true ;;
back) return 0 ;;
esac
done
}
gui_backup_auto() {
if backup_auto_enabled; then
ui_yesno "Automatische Backups" \
"Automatische Backups sind eingeschaltet.
Abschalten? Die Jobs bleiben erhalten, es wird nur
nichts mehr automatisch ausgefuehrt." || return 0
ui_run "Automatik abschalten" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job auto off"
else
local n; n="$(jobs_read | awk -F'|' '$7=="on"' | wc -l)"
if [ "$n" -eq 0 ]; then
ui_msg "Keine aktiven Jobs" \
"Es ist noch kein aktiver Backup-Job angelegt.
Zuerst unter 'Backup-Jobs verwalten' einen Job anlegen -
dort wird auch der Zeitplan festgelegt."
return
fi
ui_yesno "Automatische Backups" \
"$n aktive(r) Job(s) werden in die System-Cron eingetragen
($CRON_FILE).
Dafuer werden root-Rechte benoetigt. Einschalten?" || return 0
ui_run "Automatik einschalten" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job auto on"
fi
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
gui_pick_job() { # -> Jobname auf stdout
local -a items=() line
while IFS= read -r line; do
[ -n "$line" ] || continue
items+=("$(job_f "$line" 1)" \
"$(job_f "$line" 3) -> $(job_f "$line" 4) [$(job_f "$line" 7)]")
done < <(jobs_read)
if [ ${#items[@]} -eq 0 ]; then
ui_msg "Keine Jobs" "Es ist noch kein Backup-Job angelegt."
return 1
fi
ui_menu "Job waehlen" "Vorhandene Backup-Jobs:" "${items[@]}"
}
gui_job_run_now() {
gui_need_sites || return 0
local j; j="$(gui_pick_job)" || return 0
[ -n "$j" ] || return 0
ui_yesno "Job ausfuehren" "Job '$j' jetzt ausfuehren?" || return 0
ui_run "Job '$j'" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job run '$j'"
}
gui_jobs_menu() {
while :; do
local sel
sel="$(ui_menu "Backup-Jobs" \
"Jeder Job hat eigenen Zeitplan, eigenes Zielverzeichnis
und eigene Aufbewahrung.
Jobdatei: $(jobs_file)" \
list "Jobs anzeigen" \
neu "Neuen Job anlegen" \
edit "Job aendern" \
toggle "Job ein-/ausschalten" \
del "Job loeschen" \
back "zurueck")" || return 0
case "$sel" in
list) local f; f="$(mktemp)"; NO_COLOR=1 jobs_list_table >"$f" 2>&1 || true
ui_text "Backup-Jobs" "$f"; rm -f "$f" ;;
neu) gui_job_edit "" || true ;;
edit) local j; j="$(gui_pick_job)" || continue; [ -n "$j" ] && gui_job_edit "$j" ;;
toggle) local j2 l st
j2="$(gui_pick_job)" || continue; [ -n "$j2" ] || continue
l="$(job_line "$j2")"; st=disable
if [ "$(job_f "$l" 7)" = off ]; then st=enable; fi
ui_run "Job $st" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job $st '$j2'" ;;
del) local j3; j3="$(gui_pick_job)" || continue; [ -n "$j3" ] || continue
ui_yesno "Loeschen" "Job '$j3' wirklich loeschen?\n\n(Bereits erstellte Backups bleiben erhalten.)" \
&& ui_run "Job loeschen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job del '$j3'" ;;
back) return 0 ;;
esac
done
}
# Zeitplan bequem zusammenklicken
gui_pick_cron() { # vorgabe -> cron-ausdruck auf stdout
local cur="${1:-15 2 * * *}" kind time hh mm dow dom
kind="$(ui_menu "Zeitplan" "Wie oft soll gesichert werden?" \
taeglich "jeden Tag" \
woechentlich "einmal pro Woche" \
monatlich "einmal pro Monat" \
frei "Cron-Ausdruck selbst eingeben")" || return 1
if [ "$kind" = frei ]; then
ui_input "Cron-Ausdruck" "Fuenf Felder: Minute Stunde Tag Monat Wochentag" "$cur"
return
fi
time="$(ui_input "Uhrzeit" "Uhrzeit im Format HH:MM" "02:15")" || return 1
case "$time" in
[0-9][0-9]:[0-9][0-9]) : ;;
*) ui_msg "Ungueltig" "Bitte im Format HH:MM angeben, z.B. 02:15."; return 1 ;;
esac
hh="${time%%:*}"; mm="${time##*:}"
hh="$((10#$hh))"; mm="$((10#$mm))"
[ "$hh" -le 23 ] && [ "$mm" -le 59 ] || { ui_msg "Ungueltig" "Stunde 0-23, Minute 0-59."; return 1; }
case "$kind" in
taeglich) printf '%s %s * * *\n' "$mm" "$hh" ;;
woechentlich) dow="$(ui_menu "Wochentag" "An welchem Tag?" \
1 Montag 2 Dienstag 3 Mittwoch 4 Donnerstag \
5 Freitag 6 Samstag 0 Sonntag)" || return 1
printf '%s %s * * %s\n' "$mm" "$hh" "$dow" ;;
monatlich) dom="$(ui_input "Tag im Monat" "Welcher Tag (1-28)?" "1")" || return 1
case "$dom" in ''|*[!0-9]*) ui_msg "Ungueltig" "Bitte eine Zahl."; return 1 ;; esac
printf '%s %s %s * *\n' "$mm" "$hh" "$dom" ;;
esac
}
gui_job_edit() { # name (leer = neu)
local name="$1" line="" sites="ALL" cron="15 2 * * *" dir="$BACKUP_DIR" opts="" keep="7"
if [ -n "$name" ]; then
line="$(job_line "$name")"
sites="$(job_f "$line" 2)"; cron="$(job_f "$line" 3)"; dir="$(job_f "$line" 4)"
opts="$(job_f "$line" 5)"; keep="$(job_f "$line" 6)"
else
name="$(ui_input "Neuer Job" "Name des Jobs (Buchstaben, Ziffern, _ und -):" "nachts")" || return 0
local msg
if ! msg="$(job_validate_name "$name")"; then ui_msg "Ungueltiger Name" "$msg"; return; fi
if job_exists "$name"; then ui_msg "Schon vorhanden" "Ein Job namens '$name' existiert bereits."; return; fi
fi
# Sites auswaehlen
if gui_need_sites; then
local -a items=() sname
items+=("ALL" "alle Sites (auch spaeter neu angelegte)" "$([ "$sites" = ALL ] && echo on || echo off)")
while IFS= read -r sname; do
[ -n "$sname" ] || continue
items+=("$sname" "einzelne Site" \
"$(printf '%s' ",$sites," | grep -q ",$sname," && echo on || echo off)")
done < <(cmk_sites)
local chosen
chosen="$(ui_checklist "Sites" "Was soll dieser Job sichern?\n(ALL schliesst alle anderen ein)" "${items[@]}")" || return 0
chosen="$(printf '%s' "$chosen" | tr -d '"')"
[ -n "$chosen" ] || { ui_msg "Keine Auswahl" "Es wurde keine Site gewaehlt."; return; }
case " $chosen " in
*" ALL "*) sites=ALL ;;
*) sites="$(printf '%s' "$chosen" | tr ' ' ',')" ;;
esac
fi
cron="$(gui_pick_cron "$cron")" || return 0
[ -n "$cron" ] || return 0
dir="$(ui_input "Zielverzeichnis" \
"Wohin soll dieser Job sichern?
(absolut oder relativ zum Projekt; wird bei Bedarf angelegt)" "$dir")" || return 0
[ -n "$dir" ] || return 0
keep="$(ui_input "Aufbewahrung" \
"Wie viele Backups sollen in diesem Verzeichnis bleiben?
0 = alle behalten" "$keep")" || return 0
case "$keep" in ''|*[!0-9]*) ui_msg "Ungueltig" "Bitte eine Zahl angeben."; return ;; esac
local optsel
optsel="$(ui_checklist "Optionen" "Optionen fuer diesen Job:" \
no-rrds "Performance-Daten (RRD) auslassen" "$(case "$opts" in *no-rrds*) echo on ;; *) echo off ;; esac)" \
no-logs "Logdateien auslassen" "$(case "$opts" in *no-logs*) echo on ;; *) echo off ;; esac)" \
stop "Site waehrend des Backups stoppen" "$(case "$opts" in *stop*) echo on ;; *) echo off ;; esac)")" || return 0
opts=""
case "$optsel" in *no-rrds*) opts="$opts --no-rrds" ;; esac
case "$optsel" in *no-logs*) opts="$opts --no-logs" ;; esac
case "$optsel" in *stop*) opts="$opts --stop" ;; esac
opts="$(printf '%s' "$opts" | sed 's/^ *//')"
local action=add; job_exists "$name" && action=set
ui_run "Job speichern" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y job $action --name '$name' --sites '$sites' \
--cron '$cron' --dir '$dir' --keep '$keep' $opts"
}
gui_restore() {
gui_need_container || return 0
local file; file="$(gui_pick_backup)" || return 0
[ -n "$file" ] || return 0
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 0
reuse="--reuse --kill"
fi
ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return 0
ui_run "Restore laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y restore --file '$file' $reuse"
}
gui_import() {
gui_need_container || return 0
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 0
[ -n "$file" ] || return 0
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 0
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() {
gui_need_container || return 0
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_image_menu() {
while :; do
local sel
sel="$(ui_menu "Checkmk-Image" \
"Aktuell (.env): ${CMK_IMAGE:-<nicht gesetzt>}
Edition/Version: ${CMK_EDITION:-raw} / ${CMK_VERSION:-?}" \
status "Status und lokale Images anzeigen" \
versions "Verfuegbare Versionen auflisten" \
holen "Image beschaffen (Assistent: Edition, Version, Bezugsweg)" \
test "Zugangsdaten pruefen (Registry und Downloadportal)" \
use "Vorhandenes lokales Image auswaehlen" \
back "zurueck")" || return 0
case "$sel" in
status) local f; f="$(mktemp)"; NO_COLOR=1 cmd_image status >"$f" 2>&1 || true
ui_text "Image-Status" "$f"; rm -f "$f" ;;
versions) gui_image_versions || true ;;
holen) gui_image_wizard || true ;;
test) gui_image_test || true ;;
use) gui_image_use || true ;;
back) return 0 ;;
esac
done
}
gui_image_pick_edition() {
# Checkmk hat die Editionen umbenannt: die Website zeigt die neuen
# Namen, Dateien und Repositories tragen bis 2.4 die alten. Deshalb
# hier beide, damit die Zuordnung eindeutig ist.
ui_menu "Edition" \
"Links der Name, den dieses Werkzeug verwendet,
rechts der Name auf checkmk.com." \
raw "= Community auf der Website (frei, bis 2.4)" \
community "= Community (frei, ab 2.5)" \
enterprise "= Pro auf der Website (Login noetig, bis 2.4)" \
pro "= Pro (ab 2.5)" \
cloud "= Ultimate auf der Website (bis 2.4)" \
ultimate "= Ultimate (ab 2.5)" \
managed "= Ultimate MT auf der Website (bis 2.4)" \
ultimatemt "= Ultimate MT (ab 2.5)"
}
# Version aus dem oeffentlichen Archiv auswaehlen, statt sie abzutippen
gui_image_pick_version() { # edition [vorgabe] -> Version auf stdout
local edition="$1" cur="${2:-}"
local filt
filt="$(ui_input "Versionen filtern" \
"Nach welchem Zweig suchen? (leer = alle)
Beispiele: 2.3.0p nur Patch-Staende von 2.3.0
2.4 alles aus 2.4" "${cur%%p*}")" || return 1
local -a items=() v
while IFS= read -r v; do
[ -n "$v" ] || continue
items+=("$v" " ")
done < <(archive_versions 2>/dev/null \
| { [ -n "$filt" ] && grep -F "$filt" || cat; } | sort -rV | head -n 40)
if [ ${#items[@]} -eq 0 ]; then
ui_msg "Keine Versionen" \
"Aus dem Versionsarchiv von checkmk.com konnte nichts gelesen werden.
Entweder passt der Filter nicht, oder der Host hat keinen
Zugang zu checkmk.com. Die Version laesst sich im naechsten
Schritt auch von Hand eingeben."
ui_input "Version" "Version von Hand eingeben:" "$cur"
return
fi
items+=("__manuell__" "... von Hand eingeben ...")
local sel
sel="$(ui_menu "Version waehlen" \
"Edition: $edition
Quelle: oeffentliches Versionsarchiv von checkmk.com" "${items[@]}")" || return 1
if [ "$sel" = "__manuell__" ]; then
ui_input "Version" "Version von Hand eingeben:" "$cur"
else
printf '%s\n' "$sel"
fi
}
# ---------------------------------------------------------------------
# Assistent: Image beschaffen
#
# Fuehrt in einem Durchgang durch Edition, Version und Bezugsweg und
# schreibt anschliessend CMK_EDITION, CMK_VERSION und CMK_IMAGE.
# Bereits hinterlegte Zugangsdaten werden verwendet - danach wird nur
# gefragt, wenn sie fuer den gewaehlten Weg fehlen.
# ---------------------------------------------------------------------
gui_image_wizard() {
local ed ver src
local cur_img="${CMK_IMAGE:-<nicht gesetzt>}"
ed="$(ui_menu "Image beschaffen (1/3) - Edition" \
"Derzeit verwendet: $cur_img
Eingestellt : ${CMK_EDITION:-?} / ${CMK_VERSION:-?}
Links der Name, den dieses Werkzeug verwendet,
rechts der Name auf checkmk.com." \
raw "= Community auf der Website (frei, bis 2.4)" \
community "= Community (frei, ab 2.5)" \
enterprise "= Pro auf der Website (Login noetig, bis 2.4)" \
pro "= Pro (ab 2.5)" \
cloud "= Ultimate auf der Website (bis 2.4)" \
ultimate "= Ultimate (ab 2.5)" \
managed "= Ultimate MT auf der Website (bis 2.4)" \
ultimatemt "= Ultimate MT (ab 2.5)")" || return 0
ver="$(gui_image_pick_version "$ed" "${CMK_VERSION:-}")" || return 0
[ -n "$ver" ] || return 0
# --- Bezugsweg -----------------------------------------------------
local reg_state dl_state
reg_state="$([ -n "${CMK_REGISTRY_USER:-}" ] && echo "Zugangsdaten hinterlegt" || echo "ohne Anmeldung")"
dl_state="$([ -n "${CMK_DOWNLOAD_USER:-}" ] && echo "Zugangsdaten hinterlegt" || echo "ohne Anmeldung")"
src="$(ui_menu "Image beschaffen (3/3) - Bezugsweg" \
"$ed $ver
Woher soll das Image kommen?
Alle vier Wege tragen CMK_IMAGE ein. Nur der letzte
laedt dabei nichts herunter." \
registry "Registry holen ($reg_state)" \
download "Tarball herunterladen ($dl_state)" \
file "Vorhandene Tarball-Datei einspielen" \
setonly "Nur eintragen - geholt wird beim naechsten Start")" || return 0
# --- Zugangsdaten nur erfragen, wenn sie wirklich fehlen -----------
local -a extra=()
local need_user="" u p
case "$src" in
registry) edition_needs_login "$ed" && [ -z "${CMK_REGISTRY_USER:-}" ] && need_user=registry ;;
download) case "$ed" in raw|community|free) : ;; *) [ -z "${CMK_DOWNLOAD_USER:-}" ] && need_user=download ;; esac ;;
esac
if [ -n "$need_user" ]; then
u="$(ui_input "Zugangsdaten fehlen" \
"Fuer diesen Weg werden Zugangsdaten des Checkmk-Kundenportals
gebraucht. Sie sind noch nicht hinterlegt.
Benutzer:" "")" || return 0
[ -n "$u" ] || { ui_msg "Abgebrochen" "Ohne Benutzer geht es nicht weiter."; return 0; }
p="$(ui_password "Zugangsdaten fehlen" "Kennwort:")" || return 0
extra+=(--user "$u" --password "$p")
if ui_yesno "Merken?" "Die Zugangsdaten fuer kuenftige Aufrufe in der .env speichern?"; then
if [ "$need_user" = registry ]; then
env_set CMK_REGISTRY_USER "$u"; env_set CMK_REGISTRY_PASSWORD "$p"
else
env_set CMK_DOWNLOAD_USER "$u"; env_set CMK_DOWNLOAD_PASSWORD "$p"
fi
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
fi
fi
# --- ausfuehren ----------------------------------------------------
local rc=0
case "$src" in
registry)
ui_run "Image holen: $ed $ver" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y image pull --edition '$ed' --version '$ver' --remember ${extra[*]+${extra[*]}}" || rc=1 ;;
download)
case "$ver" in
*-latest|latest|*-daily)
ui_msg "Nicht moeglich" \
"Fuer den Tarball-Download wird eine exakte Version gebraucht,
z.B. 2.3.0p23 - '$ver' gibt es dort nicht."
return 0 ;;
esac
ui_run "Tarball holen: $ed $ver" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y image download --edition '$ed' --version '$ver' --remember ${extra[*]+${extra[*]}}" || rc=1 ;;
file)
local lf
lf="$(ui_pick_file "${IMAGE_DIR:-$PROJECT_DIR/images}" '*.tar.gz')" || return 0
[ -n "$lf" ] || return 0
ui_run "Image einspielen" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y image load --file '$lf'" || rc=1
[ "$rc" = 0 ] && ui_run "Edition/Version eintragen" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y config set CMK_EDITION '$ed' && '$SELF' -y config set CMK_VERSION '$ver'" ;;
setonly)
# Setzt Edition, Version UND das daraus gebildete Image.
# Heruntergeladen wird nichts - das erledigt "up" bei Bedarf.
ui_run "Eintragen" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y image use --edition '$ed' --version '$ver'" || rc=1 ;;
esac
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
# --- Ergebnis ------------------------------------------------------
if [ "$rc" != 0 ]; then
ui_msg "Image wurde NICHT geholt" \
"Der Vorgang ist fehlgeschlagen - siehe die Ausgabe davor.
CMK_IMAGE ist deshalb unveraendert:
${CMK_IMAGE:-<nicht gesetzt>}
Naechste Schritte:
* Bei der Registry: 'image repos' zeigt, welche Pfade es dort
wirklich gibt. Der geratene Pfad ist die haeufigste Ursache.
* Sonst den Tarball-Weg nehmen - fuer Enterprise ist das der
von Checkmk dokumentierte Weg.
* Zugangsdaten pruefen: '>> Zugangsdaten jetzt pruefen'"
return 0
fi
if [ "$src" = setonly ] && [ "$rc" = 0 ]; then
ui_msg "Eingetragen" \
"Verwendet wird jetzt:
${CMK_IMAGE:-<nicht gesetzt>}
Heruntergeladen wurde nichts - das holt die Runtime beim
naechsten 'Stack steuern -> Stack starten' selbst nach.
Braucht die Registry eine Anmeldung (Enterprise), muss die
einmal erfolgt sein:
podman login ${CMK_REGISTRY:-registry.checkmk.com}
Das erledigt auch der Weg 'Registry holen'."
return 0
fi
ui_msg "Fertig" \
"Verwendet wird jetzt:
${CMK_IMAGE:-<nicht gesetzt>}
Eingestellt: ${CMK_EDITION:-?} / ${CMK_VERSION:-?}
Wirksam wird das mit 'Stack steuern -> Stack starten'
(die Container werden dabei neu erstellt)."
}
gui_image_versions() {
local ed
ed="$(gui_image_pick_edition)" || return 0
local f; f="$(mktemp)"
ui_info "Versionen abrufen" "Frage Versionsarchiv, Registry und Downloadportal ab ...\n\nDas kann einen Moment dauern."
NO_COLOR=1 cmd_image versions --edition "$ed" --limit 30 >"$f" 2>&1 || true
ui_text "Verfuegbare Versionen - $ed" "$f"
rm -f "$f"
}
gui_image_test() {
local ed ver user pass
ed="$(gui_image_pick_edition)" || return 0
ver="$(ui_input "Version" "Version (fuer die Pruefung des Downloadportals):" "${CMK_VERSION:-}")" || return 0
local -a extra=()
if ui_yesno "Zugangsdaten" "Zugangsdaten aus der .env verwenden?\n\nRegistry : ${CMK_REGISTRY_USER:-<leer>}\nDownload : ${CMK_DOWNLOAD_USER:-<leer>}\n\n<Nein> = jetzt eingeben"; then
:
else
user="$(ui_input "Zugangsdaten" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return 0
pass="$(ui_password "Zugangsdaten" "Kennwort:")" || return 0
[ -n "$user" ] && extra+=(--user "$user" --password "$pass")
fi
ui_run "Zugangsdaten pruefen" bash -c \
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' image test --edition '$ed' --version '$ver' ${extra[*]+${extra[*]}}"
}
gui_image_pull() {
local ed ver user pass
ed="$(gui_image_pick_edition)" || return 0
ver="$(gui_image_pick_version "$ed" "${CMK_VERSION:-}")" || return 0
[ -n "$ver" ] || return 0
local -a extra=()
if [ "$ed" = enterprise ] || [ -n "${CMK_REGISTRY_USER:-}" ]; then
user="$(ui_input "Registry-Anmeldung" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return 0
pass="$(ui_password "Registry-Anmeldung" "Kennwort:")" || return 0
[ -n "$user" ] && extra+=(--user "$user" --password "$pass")
fi
ui_run "Image holen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image pull --edition '$ed' --version '$ver' ${extra[*]+${extra[*]}}"
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
gui_image_download() {
local ed ver user pass
ed="$(gui_image_pick_edition)" || return 0
ver="$(gui_image_pick_version "$ed" "${CMK_VERSION:-}")" || return 0
[ -n "$ver" ] || return 0
case "$ver" in
*-latest|latest|*-daily)
ui_msg "Nicht moeglich" "Fuer den Tarball-Download wird eine exakte Version gebraucht,\nz.B. 2.3.0p23 - '$ver' gibt es dort nicht."
return 0 ;;
esac
user="$(ui_input "Downloadportal" "Benutzer:" "${CMK_DOWNLOAD_USER:-}")" || return 0
pass="$(ui_password "Downloadportal" "Kennwort:")" || return 0
ui_run "Image herunterladen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image download --edition '$ed' --version '$ver' --user '$user' --password '$pass'"
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
gui_image_use() {
local -a items=() l
while IFS= read -r l; do
[ -n "$l" ] || continue
items+=("$l" "$([ "$l" = "${CMK_IMAGE:-}" ] && echo '(aktuell)' || echo ' ')")
done < <(image_local_list)
[ ${#items[@]} -gt 0 ] || { ui_msg "Images" "Es ist kein Checkmk-Image lokal vorhanden.\n\nZuerst 'pull', 'download' oder 'load' verwenden."; return; }
local sel
sel="$(ui_menu "Image auswaehlen" "Lokal vorhandene Checkmk-Images:" "${items[@]}")" || return 0
ui_run "Image setzen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image use '$sel'"
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
# ---------------------------------------------------------------------
# Einstellungen im Menue
# ---------------------------------------------------------------------
settings_needs_restart() {
case "$1" in
CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|CMK_HOSTNAME|CMK_PASSWORD|\
CMK_LIVESTATUS_TCP|MAIL_RELAY_HOST|TZ|NPM_IMAGE|NPM_CONTAINER_NAME|\
NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR) return 0 ;;
*) return 1 ;;
esac
}
gui_settings_edit() {
local key="$1" def typ label choices opt cur new msg
def="$(settings_def_for "$key")"
[ -n "$def" ] || { ui_msg "Fehler" "Unbekannter Schluessel '$key'."; return; }
typ="$(printf '%s' "$def" | cut -d'|' -f3)"
label="$(printf '%s' "$def" | cut -d'|' -f4)"
choices="$(printf '%s' "$def"| cut -d'|' -f5)"
opt="$(printf '%s' "$def" | cut -d'|' -f6)"
cur="$(settings_value "$key")"
if [ "$typ" = info ]; then
local sel2 lage
if image_consistency_check >/dev/null; then
lage="Edition, Version und Image passen zusammen."
else
lage="Achtung: verwendet wird ${CMK_IMAGE:-<nicht gesetzt>},
eingestellt sind aber ${CMK_EDITION:-?} / ${CMK_VERSION:-?}.
Gestartet wird immer das Image, nicht die Vorgaben."
fi
sel2="$(ui_menu "$key" \
"Aktuell: ${cur:-<leer>}
$lage" \
wizard "Image beschaffen (Edition, Version, Bezugsweg)" \
angleichen "Vorgaben an das verwendete Image angleichen" \
nichts "Nichts aendern")" || { load_env "${ENV_FILE:-$PROJECT_DIR/.env}"; return 0; }
case "$sel2" in
wizard) gui_image_wizard || true ;;
angleichen)
if [ -z "${CMK_IMAGE:-}" ]; then
ui_msg "Nicht moeglich" "CMK_IMAGE ist nicht gesetzt."
else
ui_run "Vorgaben angleichen" bash -c \
"cd '$PROJECT_DIR' && '$SELF' -y image use '$CMK_IMAGE' --remember"
fi ;;
esac
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
return 0
fi
while :; do
case "$typ" in
choice)
local -a ropts=() c
[ -z "$cur" ] && case ":$choices:" in *:auto:*) cur=auto ;; esac
local IFS_BAK="$IFS"; IFS=':'
for c in $choices; do
IFS="$IFS_BAK"
ropts+=("$c" " " "$([ "$c" = "$cur" ] && echo on || echo off)")
IFS=':'
done
IFS="$IFS_BAK"
new="$(ui_radiolist "$key" "$label\n\nAktuell: ${cur:-<leer>}" "${ropts[@]}")" || return 0
if [ "$new" = auto ]; then new=""; fi
;;
password)
new="$(ui_password "$key" "$label\n\n(leer lassen = unveraendert)")" || return 0
[ -z "$new" ] && return
;;
*)
new="$(ui_input "$key" "$label\n\nTyp: $typ$([ -n "$opt" ] && echo ' (darf leer bleiben)')" "$cur")" || return 0
;;
esac
if msg="$(settings_validate "$typ" "$new" "$opt")" \
&& msg="$(settings_validate_key "$key" "$new")"; then
break
else
ui_msg "Ungueltige Eingabe" "$msg"
cur="$new"
fi
done
if [ "$key" = NPM_ADMIN_PORT ]; then
ui_run "Admin-Port setzen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y npm-port '$new'"
elif [ "$key" = NPM_ENABLED ]; then
ui_run "Proxy Manager ${new}schalten" bash -c "cd '$PROJECT_DIR' && '$SELF' -y npm-enable '$new'"
else
env_set "$key" "$new" "$opt"
local hint=""
settings_needs_restart "$key" && hint="\n\nWirksam nach 'Stack steuern -> Stack starten'\n(die Container werden dabei neu erstellt)."
ui_msg "Gespeichert" "$key = $([ "$typ" = password ] && echo '********' || echo "${new:-<leer>}")$hint"
# Zugangsdaten lassen sich sofort pruefen - das erspart den Umweg
case "$key" in
CMK_REGISTRY_USER|CMK_REGISTRY_PASSWORD|CMK_DOWNLOAD_USER|CMK_DOWNLOAD_PASSWORD|CMK_REGISTRY|CMK_DOWNLOAD_BASEURL)
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
if ui_yesno "Zugangsdaten pruefen" \
"Sollen die Zugangsdaten jetzt geprueft werden?
Getestet wird die Anmeldung an Registry und Downloadportal -
es wird nichts heruntergeladen."; then
ui_run "Zugangsdaten pruefen" bash -c \
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' image test"
fi ;;
esac
fi
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
gui_settings_category() {
local cat="$1" title="$2"
while :; do
local -a items=() c k t l ch o
while IFS='|' read -r c k t l ch o; do
[ "$c" = "$cat" ] || continue
items+=("$k" "$l = $(settings_display "$k" "$t")")
done < <(settings_defs)
# Zusatzaktionen, die inhaltlich zur Kategorie gehoeren
case "$cat" in
image) items+=("__wizard__" ">> Image beschaffen (Edition, Version, Bezugsweg)")
items+=("__test__" ">> Zugangsdaten jetzt pruefen (Registry + Downloadportal)") ;;
npm) items+=("__proxy__" ">> Proxy-Hosts anlegen/verwalten")
items+=("__npm__" ">> Proxy Manager: Stand und Admin-Port anzeigen") ;;
backup) items+=("__jobs__" ">> Backup-Jobs verwalten") ;;
esac
items+=("back" "zurueck")
local sel
sel="$(ui_menu "$title" "Eintrag waehlen und mit ENTER aendern:" "${items[@]}")" || return 0
case "$sel" in
back) return 0 ;;
__wizard__) gui_image_wizard || true; continue ;;
__proxy__) gui_proxy_menu || true; continue ;;
__test__) gui_image_test || true; continue ;;
__jobs__) gui_jobs_menu || true; continue ;;
__npm__) local f; f="$(mktemp)"
{ NO_COLOR=1 cmd_npm_enable; NO_COLOR=1 cmd_npm_port; } >"$f" 2>&1 || true
ui_text "NGINX Proxy Manager" "$f"; rm -f "$f"; continue ;;
esac
gui_settings_edit "$sel"
done
}
gui_settings_menu() {
while :; do
local -a items=() c l
while IFS='|' read -r c l; do
[ -n "$c" ] && items+=("$c" "$l")
done < <(settings_categories)
items+=("jobs" "Backup-Jobs (Zeitplan, Zielpfade, Aufbewahrung)")
items+=("show" "Alle Werte auflisten")
items+=("editor" "Datei .env im Editor oeffnen")
items+=("back" "zurueck")
local sel
sel="$(ui_menu "Einstellungen" \
"Alle Werte der Datei .env - direkt hier aenderbar.
Datei: ${ENV_FILE:-$PROJECT_DIR/.env}" "${items[@]}")" || return 0
case "$sel" in
back) return 0 ;;
jobs) gui_jobs_menu || true ;;
show) local f; f="$(mktemp)"
{ printf '%-24s %-10s %s\n' "SCHLUESSEL" "BEREICH" "WERT"
NO_COLOR=1 cmd_config list; } >"$f" 2>&1
ui_text "Alle Einstellungen" "$f"; rm -f "$f" ;;
editor) local ef="${ENV_FILE:-$PROJECT_DIR/.env}"
if [ ! -f "$ef" ]; then ui_msg "Keine .env" "Die Datei $ef existiert nicht.\n\nAnlegen mit: cp .env.example .env"; continue; fi
clear 2>/dev/null || true; "${EDITOR:-nano}" "$ef"
load_env "$ef"; ui_msg "Konfiguration" "Die .env wurde neu eingelesen." ;;
*) local title
title="$(settings_categories | awk -F'|' -v c="$sel" '$1==c{print $2}')"
gui_settings_category "$sel" "${title:-$sel}" ;;
esac
done
}
gui_browser_menu() {
while :; do
local sel
sel="$(ui_menu "Terminal-Browser" \
"Bedienung des NGINX Proxy Managers direkt auf dieser Maschine.
Startseite: ${NPM_ADMIN_URL:-http://127.0.0.1:81}
Nur browsh und carbonyl koennen JavaScript und damit die
Oberflaeche des Proxy Managers darstellen." \
browsh "Firefox im Terminal (empfohlen)" \
carbonyl "Chromium im Terminal" \
links "links2 - Textbrowser, KEIN JavaScript" \
w3m "w3m - Textbrowser, KEIN JavaScript" \
lynx "lynx - Textbrowser, KEIN JavaScript" \
shell "Shell mit curl/jq/openssl im Host-Netzwerk" \
url "Andere Startseite eingeben" \
build "Werkzeug-Image neu bauen" \
back "zurueck")" || return 0
case "$sel" in
url) local u
u="$(ui_input "Startseite" "URL:" "${NPM_ADMIN_URL:-http://127.0.0.1:81}")" || continue
if [ -n "$u" ]; then NPM_ADMIN_URL="$u"; fi ;;
build) ui_run "Werkzeug-Image bauen" bash -c "cd '$PROJECT_DIR' && '$SELF' browser --build-only" || true ;;
back) return 0 ;;
*) local burl="${NPM_ADMIN_URL:-http://127.0.0.1:81}"
if ! browser_url_reachable "$burl"; then
if ui_yesno "Startseite antwortet nicht" \
"$(browser_diagnose_url "$burl")
Trotzdem starten?
<Nein> kehrt ins Menue zurueck - dort laesst sich unter
'Andere Startseite eingeben' auch eine andere Adresse waehlen."; then
gui_run_interactive cmd_browser --engine "$sel" --url "$burl" --no-check
fi
continue
fi
gui_run_interactive cmd_browser --engine "$sel" --url "$burl" ;;
esac
done
}
# ---------------------------------------------------------------------
# Proxy-Hosts im Menue
# ---------------------------------------------------------------------
gui_proxy_menu() {
while :; do
local sel
sel="$(ui_menu "Proxy-Hosts" \
"NGINX Proxy Manager ueber seine Schnittstelle bedienen -
das ist verlaesslicher als der Terminal-Browser.
Konto: ${NPM_ADMIN_EMAIL:-<nicht gesetzt>}
Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \
anlegen "Proxy-Host anlegen (Assistent)" \
list "Vorhandene Proxy-Hosts anzeigen" \
show "Einzelheiten eines Proxy-Hosts (mit Pfaden)" \
certs "Vorhandene Zertifikate anzeigen" \
del "Proxy-Host loeschen" \
status "Anmeldung pruefen" \
back "zurueck")" || return 0
case "$sel" in
anlegen) gui_proxy_add || true ;;
list) local f; f="$(mktemp)"; NO_COLOR=1 cmd_proxy list >"$f" 2>&1 || true
ui_text "Proxy-Hosts" "$f"; rm -f "$f" ;;
show) local d3; d3="$(ui_input "Einzelheiten" "Name oder ID des Proxy-Hosts:" "${PROXY_DOMAIN:-}")" || continue
[ -n "$d3" ] || continue
local f3; f3="$(mktemp)"
NO_COLOR=1 cmd_proxy show --domain "$d3" >"$f3" 2>&1 || true
ui_text "Proxy-Host $d3" "$f3"; rm -f "$f3" ;;
certs) local f2; f2="$(mktemp)"; NO_COLOR=1 cmd_proxy certs >"$f2" 2>&1 || true
ui_text "Zertifikate" "$f2"; rm -f "$f2" ;;
del) gui_proxy_del || true ;;
status) ui_run "Anmeldung pruefen" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' proxy status" ;;
back) return 0 ;;
esac
done
}
gui_proxy_add() {
if ! npm_require_running 2>/dev/null; then
ui_msg "Proxy Manager laeuft nicht" \
"Der NGINX Proxy Manager muss laufen, damit sich ein Proxy-Host
anlegen laesst.
Abgeschaltet? Einstellungen -> NGINX Proxy Manager
Nicht gestartet? Stack steuern -> Stack starten"
return 0
fi
if [ -z "${NPM_ADMIN_EMAIL:-}" ] || [ -z "${NPM_ADMIN_PASSWORD:-}" ]; then
ui_msg "Zugangsdaten fehlen" \
"Fuer die Schnittstelle werden die Zugangsdaten des Proxy Managers
gebraucht: NPM_ADMIN_EMAIL und NPM_ADMIN_PASSWORD.
Sie stehen unter Einstellungen -> NGINX Proxy Manager.
Hinweis: Seit Version 2.15 legt der Container das Konto nur noch
beim ERSTEN Start an, und nur wenn beide Werte gesetzt sind.
Wurde das Konto im Browser angelegt, hier dessen Daten eintragen."
return 0
fi
local dom fport cert
dom="$(ui_input "Proxy-Host (1/3)" \
"Unter welchem Namen soll Checkmk erreichbar sein?
Der Name muss auf diesen Host zeigen - per DNS oder
per hosts-Datei der Arbeitsplatzrechner." "${PROXY_DOMAIN:-}")" || return 0
[ -n "$dom" ] || return 0
fport="$(ui_input "Proxy-Host (2/3)" \
"Auf welchen Port soll weitergeleitet werden?
5000 = Checkmk-Oberflaeche (Vorgabe)
81 = Oberflaeche des Proxy Managers selbst" "${PROXY_FORWARD_PORT:-5000}")" || return 0
valid_port "$fport" || { ui_msg "Ungueltig" "'$fport' ist keine gueltige Portnummer."; return 0; }
cert="$(ui_menu "Proxy-Host (3/3) - Zertifikat" \
"$dom -> 127.0.0.1:$fport
Womit soll HTTPS abgesichert werden?" \
self "Selbstsigniert (intern, sofort einsatzbereit)" \
letsencrypt "Let's Encrypt (Port 80 muss aus dem Internet erreichbar sein)" \
none "Ohne HTTPS (nur Port 80)")" || return 0
# --- weitere Dienste unter demselben Namen, nach Pfad getrennt ----
local -a locopt=()
local loclist=""
if ui_yesno "Weitere Dienste?" \
"Sollen unter demselben Namen noch andere Dienste erreichbar sein,
unterschieden nach dem Pfad?
https://$dom/ -> Checkmk (das Ziel von eben)
https://$dom/grafana -> Grafana
https://$dom/sonstwas -> ein weiterer Dienst
Das ist der Weg, wenn nur EIN Port nach aussen frei ist."; then
while :; do
local lpath ltarget
lpath="$(ui_input "Pfad" \
"Unter welchem Pfad soll der Dienst liegen?
Leer lassen beendet die Eingabe." "")" || break
[ -n "$lpath" ] || break
ltarget="$(ui_input "Ziel fuer $lpath" \
"Wohin soll $lpath weitergeleitet werden?
Schreibweise: host:port z.B. 127.0.0.1:3000
https://host:port fuer verschluesselte Ziele" "127.0.0.1:3000")" || break
[ -n "$ltarget" ] || break
if ! proxy_location_json "$lpath=$ltarget" >/dev/null 2>&1; then
ui_msg "Ungueltig" "'$lpath=$ltarget' konnte nicht gelesen werden.\n\nErwartet: /pfad und host:port"
continue
fi
locopt+=(--location "$lpath=$ltarget")
loclist="$loclist
$lpath -> $ltarget"
ui_yesno "Noch einer?" "Bisher:$loclist
Einen weiteren Pfad hinzufuegen?" || break
done
fi
local upd=""
if ui_yesno "Anlegen" \
"$dom -> 127.0.0.1:$fport
Zertifikat: $cert${loclist:+
Zusaetzliche Pfade:$loclist}
Einen bereits vorhandenen Eintrag fuer diesen Namen ersetzen?
<Ja> ersetzt ihn
<Nein> bricht ab, falls es ihn schon gibt"; then upd=--update; fi
ui_run "Proxy-Host anlegen" bash -c \
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' -y proxy add --domain '$dom' \
--forward-port '$fport' --cert '$cert' $upd ${locopt[*]+${locopt[*]}}"
# Die Vorgaben merken, damit der naechste Aufruf sie kennt
env_set PROXY_DOMAIN "$dom" 1 2>/dev/null || true
env_set PROXY_FORWARD_PORT "$fport" 2>/dev/null || true
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
}
gui_proxy_del() {
local f; f="$(mktemp)"
NO_COLOR=1 cmd_proxy list >"$f" 2>&1 || true
ui_text "Vorhandene Proxy-Hosts" "$f"; rm -f "$f"
local id
id="$(ui_input "Loeschen" "ID des zu loeschenden Proxy-Hosts:" "")" || return 0
[ -n "$id" ] || return 0
case "$id" in ''|*[!0-9]*) ui_msg "Ungueltig" "Bitte eine Zahl angeben."; return 0 ;; esac
ui_yesno "Loeschen" "Proxy-Host $id wirklich loeschen?" || return 0
ui_run "Proxy-Host loeschen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y proxy del --id '$id'"
}
gui_cert() {
local domain alt days
domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return 0
[ -n "$domain" ] || return 0
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_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 / planen" \
restore "Backup wiederherstellen" \
import "Site von Direktinstallation importieren" \
manage "Backups verwalten (Liste/Details/Loeschen)" \
sites "Sites im Container verwalten" \
image "Checkmk-Image beschaffen/auswaehlen" \
proxy "Proxy-Hosts anlegen/verwalten (HTTPS fuer Checkmk)" \
browser "Terminal-Browser (Proxy Manager lokal bedienen)" \
cert "Selbstsigniertes Zertifikat erzeugen" \
config "Einstellungen (.env) - menuegefuehrt" \
shell "Root-Shell im Checkmk-Container" \
quit "Beenden")" || break
case "$sel" in
status) gui_status || true ;;
stack) gui_stack_menu || true ;;
backup) gui_backup_menu || true ;;
restore) gui_restore || true ;;
import) gui_import || true ;;
manage) gui_backups_menu || true ;;
sites) gui_sites_menu || true ;;
image) gui_image_menu || true ;;
proxy) gui_proxy_menu || true ;;
browser) gui_browser_menu || true ;;
cert) gui_cert || true ;;
config) gui_settings_menu || true ;;
shell) gui_need_container || continue
gui_run_interactive cmd_shell ;;
quit) break ;;
esac
done
clear 2>/dev/null || true
ok "Beendet."
}
# =====================================================================
# Selbstpruefung
#
# Faengt die Fehler ab, die hier wiederholt aufgetreten sind:
# 1. Dialoge, die an den Engstellen vorbei direkt "$UI_BIN" aufrufen -
# die haetten dann wieder feste Fenstergroessen.
# 2. Feste Zahlen als Fenstergroesse.
# 3. "[ bedingung ] && VAR=wert": liefert 1, wenn die Bedingung nicht
# zutrifft, und beendet unter "set -e" Funktion oder Skript.
# Aufruf: cmk-manage.sh selftest
# =====================================================================
cmd_selftest() {
local rc=0 n
local -a files=("$SCRIPT_DIR/cmk-manage.sh" "$SCRIPT_DIR/lib/common.sh" \
"$SCRIPT_DIR/migrate_creator.sh" "$PROJECT_DIR/install.sh")
printf '\n%s1. Syntax%s\n' "$C_BLD" "$C_RST"
local f
for f in "${files[@]}"; do
[ -f "$f" ] || continue
if bash -n "$f" 2>/dev/null; then
printf ' %s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$(basename "$f")"
else
printf ' %s[FEHLER]%s %s\n' "$C_RED" "$C_RST" "$(basename "$f")"; rc=1
fi
done
printf '\n%s2. Dialoge laufen ueber die Engstellen%s\n' "$C_BLD" "$C_RST"
n=0
for f in "$SCRIPT_DIR/cmk-manage.sh" "$SCRIPT_DIR/lib/common.sh" "$SCRIPT_DIR/migrate_creator.sh"; do
[ -f "$f" ] || continue
# Aufrufe von "$UI_BIN" sind nur in _ui_list/_ui_box/_ui_file erlaubt
while IFS= read -r line; do
[ -n "$line" ] || continue
printf ' %s[FEHLER]%s %s:%s\n' "$C_RED" "$C_RST" "$(basename "$f")" "$line"
n=$((n+1))
done < <(awk '
/^[_a-zA-Z0-9]+\(\) \{/ { fn = $1; sub(/\(\).*/, "", fn) }
/"\$UI_BIN"[[:space:]]+--/ {
if ($0 ~ /^[[:space:]]*#/) next
if (fn ~ /^_ui_/ || fn == "ui_available") next
printf "%d: %s\n", NR, substr($0, 1, 60)
}' "$f")
done
if [ "$n" -eq 0 ]; then
printf ' %s[ OK ]%s kein Dialog ruft dialog/whiptail direkt auf\n' "$C_GRN" "$C_RST"
else
printf ' Neue Fenster bitte ueber ui_menu/ui_msg/... bauen.\n'; rc=1
fi
printf '\n%s3. Keine festen Fenstergroessen%s\n' "$C_BLD" "$C_RST"
n=0
for f in "$SCRIPT_DIR/lib/common.sh" "$SCRIPT_DIR/migrate_creator.sh"; do
[ -f "$f" ] || continue
while IFS= read -r line; do
[ -n "$line" ] || continue
printf ' %s[FEHLER]%s %s:%s\n' "$C_RED" "$C_RST" "$(basename "$f")" "$line"
n=$((n+1))
done < <(grep -nE '"\$UI_BIN".*(--menu|--checklist|--radiolist|--[a-z]+box).*[[:space:]][0-9]{1,3}[[:space:]]' "$f" \
| cut -c1-70 || true)
done
[ "$n" -eq 0 ] && printf ' %s[ OK ]%s alle Groessen werden berechnet\n' "$C_GRN" "$C_RST" || rc=1
printf '\n%s4. Keine "[ ... ] && VAR=" unter set -e%s\n' "$C_BLD" "$C_RST"
n=0
for f in "${files[@]}"; do
[ -f "$f" ] || continue
while IFS= read -r line; do
[ -n "$line" ] || continue
printf ' %s[ WARN ]%s %s:%s\n' "$C_YEL" "$C_RST" "$(basename "$f")" "$line"
n=$((n+1))
done < <(grep -nE '^[[:space:]]*\[[^]]+\][[:space:]]*&&[[:space:]]*[A-Za-z_][A-Za-z_0-9]*=' "$f" \
| cut -c1-70 || true)
done
if [ "$n" -eq 0 ]; then
printf ' %s[ OK ]%s keine Fundstellen\n' "$C_GRN" "$C_RST"
else
printf ' Als letzte Anweisung einer Funktion beendet das unter\n'
printf ' "set -e" das Skript. Besser als if-Block schreiben.\n'
fi
printf '\n%s5. Zeichensatz fuer die Oberflaeche%s\n' "$C_BLD" "$C_RST"
local before="${LC_ALL:-${LC_CTYPE:-${LANG:-<leer>}}}"
ui_fix_locale || true
printf ' Locale vorher : %s\n' "$before"
printf ' Locale jetzt : %s\n' "${LC_CTYPE:-${LANG:-<leer>}}"
if [ -n "${UI_ASCII:-}" ]; then
printf ' %s[ WARN ]%s Keine UTF-8-Locale vorhanden - die Rahmen werden einfach gezeichnet.\n' "$C_YEL" "$C_RST"
printf ' Abhilfe: dnf install -y glibc-langpack-en bzw. locale-gen\n'
else
printf ' %s[ OK ]%s UTF-8 - die Rahmen werden sauber gezeichnet%s\n' "$C_GRN" "$C_RST" \
"$([ -n "${UI_LOCALE_NOTE:-}" ] && echo " (${UI_LOCALE_NOTE})")"
fi
printf '\n%s6. Einstellungstabelle%s\n' "$C_BLD" "$C_RST"
n="$(settings_defs | awk -F'|' 'NF!=6' | wc -l)"
if [ "$n" -eq 0 ]; then
printf ' %s[ OK ]%s alle Zeilen haben sechs Felder\n' "$C_GRN" "$C_RST"
else
printf ' %s[FEHLER]%s %s Zeile(n) mit falscher Feldzahl\n' "$C_RED" "$C_RST" "$n"; rc=1
fi
local missing=0 k
for k in $(settings_defs | cut -d'|' -f2); do
grep -qE "^${k}=" "$PROJECT_DIR/.env.example" || { printf ' %s[FEHLER]%s %s fehlt in .env.example\n' "$C_RED" "$C_RST" "$k"; missing=1; }
done
[ "$missing" -eq 0 ] && printf ' %s[ OK ]%s alle Schluessel stehen in .env.example\n' "$C_GRN" "$C_RST" || rc=1
echo
if [ "$rc" -eq 0 ]; then ok "Selbstpruefung bestanden."; else err "Selbstpruefung hat Probleme gefunden."; fi
echo
return "$rc"
}
# =====================================================================
# 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 || true ;;
pull) cmd_pull || true ;;
status) cmd_status || true ;;
logs) cmd_logs "$@" || true ;;
shell) cmd_shell || true ;;
omd) cmd_omd "$@" || true ;;
sites) cmd_sites || true ;;
site-start) cmd_site_start "$@" ;;
site-stop) cmd_site_stop "$@" ;;
start-all-sites) cmd_start_all_sites ;;
backup) cmd_backup "$@" || true ;;
list) cmd_list || true ;;
prune) cmd_prune "$@" || true ;;
job|jobs) cmd_job "$@" ;;
verify) local vf=""; [ "${1:-}" = "--file" ] && vf="$2"; [ -n "$vf" ] || vf="${1:-}"
[ -n "$vf" ] || die "--file fehlt."; bundle_verify "$vf" ;;
restore) cmd_restore "$@" || true ;;
import) cmd_import "$@" || true ;;
cert) cmd_cert "$@" || true ;;
image) cmd_image "$@" || true ;;
browser) cmd_browser "$@" || true ;;
npm-port) cmd_npm_port "$@" ;;
npm-enable) cmd_npm_enable "$@" ;;
proxy) cmd_proxy "$@" ;;
config) cmd_config "$@" || true ;;
selftest) cmd_selftest ;;
help|-h|--help) usage ;;
*) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;;
esac
}
main "$@"