#!/usr/bin/env bash # ===================================================================== # cmk-manage.sh # Verwaltung des Checkmk-/NGINX-Proxy-Manager-Stacks: # Stack-Steuerung, Backup, Restore und Import (Migration) von Sites. # # Zwei Bedienarten: # 1) Parametergesteuert : ./scripts/cmk-manage.sh backup --site cmk # 2) ncurses-Oberflaeche: ./scripts/cmk-manage.sh gui # # Backups/Bundles sind immer .tar.gz # ===================================================================== set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" SELF="$SCRIPT_DIR/cmk-manage.sh" # shellcheck source=lib/common.sh . "$SCRIPT_DIR/lib/common.sh" BUNDLE_DIRNAME="cmkbundle" ASSUME_YES="" VERBOSE="" ENVFILE_OPT="" # ===================================================================== # Hilfe # ===================================================================== usage() { cat <<'EOF' cmk-manage.sh - Checkmk Stack Manager (Docker & Podman) AUFRUF cmk-manage.sh [globale Optionen] [Optionen] GLOBALE OPTIONEN --env-file DATEI abweichende .env verwenden -y, --yes alle Rueckfragen automatisch mit "ja" beantworten -v, --verbose ausfuehrliche Ausgabe -h, --help diese Hilfe KOMMANDOS - Oberflaeche gui | menu ncurses-Oberflaeche starten (dialog/whiptail) KOMMANDOS - Stack up Stack starten (compose up -d) down Stack stoppen und Container entfernen restart Stack neu starten pull Images aktualisieren status Status von Containern und Sites anzeigen logs [-f] [DIENST] Logs anzeigen shell Root-Shell im Checkmk-Container omd ARG... beliebiges omd-Kommando im Container ausfuehren KOMMANDOS - Sites sites Sites im Container auflisten site-start SITE Site starten site-stop SITE Site stoppen start-all-sites alle vorhandenen Sites starten (fuer Autostart nach Reboot) KOMMANDOS - Backup / Restore / Migration backup [Optionen] Backup einer/aller Sites als .tar.gz-Bundle erzeugen --site SITE Site (mehrfach moeglich) --all alle Sites sichern --dir VERZ Zielverzeichnis (Vorgabe: $BACKUP_DIR) --label TEXT Freitext-Kennzeichnung im Dateinamen --no-rrds Performance-Daten (RRDs) auslassen --no-logs Logdateien auslassen --stop Site waehrend des Backups stoppen (konsistenter) list vorhandene Backups/Bundles auflisten verify --file DATEI Bundle pruefen (Manifest + SHA256) restore [Optionen] Backup wiederherstellen --file DATEI Bundle oder rohes omd-Backup (.tar.gz) --as-site NAME unter anderem Site-Namen wiederherstellen (ACHTUNG: Agents muessen dann neu registriert werden!) --reuse vorhandene Site ueberschreiben --kill laufende Prozesse der Site beenden --no-start Site nach dem Restore nicht starten import [Optionen] Site von einer Direktinstallation uebernehmen (Bundle aus migrate_creator.sh) --file DATEI Migrations-Bundle (.tar.gz) --reuse vorhandene Site ueberschreiben --kill laufende Prozesse der Site beenden --set-default-site CMK_SITE_ID in der .env auf die importierte Site setzen --no-start Site nach dem Import nicht starten prune --keep N nur die N neuesten Backups behalten KOMMANDOS - Checkmk-Image beschaffen image status konfiguriertes und lokal vorhandene Images anzeigen image list lokal vorhandene Checkmk-Images 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 --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 image load --file F bereits heruntergeladenen Tarball einspielen image use REF vorhandenes Image als CMK_IMAGE in die .env eintragen 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) 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 - 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 pull --edition enterprise --version 2.3.0p23 \ --user --password ./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 # # .tar.gz # cmkbundle/MANIFEST key=value, per "source" lesbar # cmkbundle/INFO.txt menschenlesbare Zusammenfassung # cmkbundle/site.tar.gz das eigentliche "omd backup"-Archiv # cmkbundle/site.tar.gz.sha256 # ===================================================================== edition_from_version() { case "$1" in *.cre) echo "Raw Edition" ;; *.cee) echo "Enterprise Edition" ;; *.cme) echo "Managed Services Edition" ;; *.cce) echo "Cloud Edition" ;; *) echo "unbekannt" ;; esac } # Manifest eines Bundles einlesen -> Variablen B_* bundle_read() { local file="$1" B_SITE=""; B_CMK_VERSION=""; B_SOURCE_KIND=""; B_SOURCE_HOST="" B_SOURCE_IP=""; B_CREATED=""; B_ARCHIVE_SHA256=""; B_LABEL="" B_BUNDLE_TYPE=""; B_ARCHIVE_SIZE=""; B_OMD_BACKUP_OPTS=""; B_SOURCE_OS="" B_IS_BUNDLE=0 if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/MANIFEST"; then B_IS_BUNDLE=1 local mf; mf="$(mktemp)" tar -xzOf "$file" "$BUNDLE_DIRNAME/MANIFEST" >"$mf" # nur bekannte Schluessel uebernehmen (kein blindes eval) local k v while IFS='=' read -r k v; do case "$k" in SITE) B_SITE="$v" ;; CMK_VERSION) B_CMK_VERSION="$v" ;; SOURCE_KIND) B_SOURCE_KIND="$v" ;; SOURCE_HOST) B_SOURCE_HOST="$v" ;; SOURCE_IP) B_SOURCE_IP="$v" ;; SOURCE_OS) B_SOURCE_OS="$v" ;; CREATED) B_CREATED="$v" ;; ARCHIVE_SHA256) B_ARCHIVE_SHA256="$v" ;; ARCHIVE_SIZE) B_ARCHIVE_SIZE="$v" ;; LABEL) B_LABEL="$v" ;; BUNDLE_TYPE) B_BUNDLE_TYPE="$v" ;; OMD_BACKUP_OPTS) B_OMD_BACKUP_OPTS="$v" ;; esac done <"$mf" rm -f "$mf" else # rohes omd-Backup: Site-Name ist die erste Pfadkomponente. # Ein omd-Archiv beginnt immer mit "/..." - alles andere ist # kein Checkmk-Backup und wird abgewiesen. local first first="$(tar -tzf "$file" 2>/dev/null | head -n1)" case "$first" in */*) B_SITE="${first%%/*}" ;; *) return 1 ;; esac B_BUNDLE_TYPE="raw" B_SOURCE_KIND="unbekannt" fi [ -n "$B_SITE" ] || return 1 } bundle_show() { local file="$1" bundle_read "$file" || { echo "Kein gueltiges Checkmk-Backup: $file"; return 1; } cat </dev/null || die "Archiv ist beschaedigt oder kein gueltiges tar.gz." bundle_read "$file" || die "Im Archiv wurde keine Checkmk-Site gefunden." if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then log "Pruefe SHA256 der Nutzdaten ..." local sum sum="$(tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz" | sha256sum | awk '{print $1}')" [ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme falsch! erwartet : $B_ARCHIVE_SHA256 ermittelt: $sum" ok "Pruefsumme korrekt." else warn "Kein Manifest/keine Pruefsumme vorhanden - nur Strukturpruefung." fi bundle_show "$file" ok "Bundle ist in Ordnung." } # Nutzdaten (omd-Archiv) aus einem Bundle nach stdout schreiben bundle_extract_payload() { local file="$1" if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/site.tar.gz"; then tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz" else cat "$file" fi } # ===================================================================== # 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 '' | 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 '')" 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)" [ -n "$cand" ] && loaded="$cand" 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:" 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 local netrc="" local -a auth=() if [ -n "$user" ]; then netrc="$(mktemp)"; chmod 600 "$netrc" printf 'machine %s login %s password %s\n' "$host" "$user" "$pass" >"$netrc" auth=(--netrc-file "$netrc") fi log "Lade $url" if curl -fL --progress-bar ${auth[@]+"${auth[@]}"} -o "$out" "$url"; then [ -n "$netrc" ] && rm -f "$netrc" return 0 fi [ -n "$netrc" ] && rm -f "$netrc" 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=() [ -n "$user" ] && wa=(--user="$user" --password="$pass") wget ${wa[@]+"${wa[@]}"} -O "$out" "$url" return $? else die "Weder curl noch wget vorhanden." fi } # --------------------------------------------------------------------- # 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 ;; *) 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 < --password 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 [ "$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 --password b) Tarball vom Downloadportal: cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \ --user --password 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" [ -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 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 ;; *) 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'." local base="${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" local pkg; pkg="$(edition_pkgname "$edition")" # Checkmk hat den Dateinamen ueber die Jahre geaendert - beide probieren urls=("$base/$version/$pkg-$version.docker.tar.gz" "$base/$version/$pkg-docker-$version.tar.gz") 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="" warn "Nicht gefunden/abgelehnt: $u" done if [ -z "$target" ]; then err "Der Image-Tarball konnte nicht geladen werden." cat >&2 </' \\ --user --password * 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" [ -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() { [ $# -ge 1 ] || die "Image-Referenz fehlt, z.B.: $SELF image use localhost/checkmk/check-mk-enterprise:2.3.0p23" local ref="$1" image_exists_local "$ref" || warn "'$ref' ist lokal (noch) nicht vorhanden." image_set_env "$ref" } 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 } image_cmd_status() { printf '\n%sKonfiguriertes Image (.env: CMK_IMAGE)%s\n' "$C_BLD" "$C_RST" printf ' %s\n' "${CMK_IMAGE:-}" 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 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:-}" printf ' Registry : %s (Benutzer: %s)\n' "${CMK_REGISTRY:-registry.checkmk.com}" "${CMK_REGISTRY_USER:-}" printf ' Download : %s (Benutzer: %s)\n' "${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" "${CMK_DOWNLOAD_USER:-}" echo image_cmd_list echo } 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 "$@" ;; *) die "Unbekanntes Unterkommando 'image $sub'. Moeglich: status | list | pull | download | load | use" ;; esac } # ===================================================================== # Stack-Kommandos # ===================================================================== cmd_up() { [ -f "$PROJECT_DIR/.env" ] || die "Keine .env vorhanden. Bitte: cp .env.example .env && \$EDITOR .env" log "Starte Stack (${RUNTIME}) ..." compose up -d ok "Stack gestartet." cmd_status } cmd_down() { log "Stoppe Stack ..."; compose down; ok "Stack gestoppt."; } cmd_restart() { compose down || true; compose up -d; ok "Stack neu gestartet."; } cmd_pull() { compose pull; ok "Images aktualisiert. Danach: $0 up"; } cmd_logs() { compose logs "$@"; } cmd_shell() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" bash; } cmd_omd() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" omd "$@"; } cmd_status() { printf '\n%sImage%s\n' "$C_BLD" "$C_RST" printf ' %-24s %s\n' "${CMK_IMAGE:-}" \ "$(image_exists_local "${CMK_IMAGE:-}" && echo "lokal vorhanden" || echo "${C_YEL}nicht lokal vorhanden${C_RST}")" printf '\n%sContainer%s\n' "$C_BLD" "$C_RST" local c for c in "$CMK_CONTAINER_NAME" "$NPM_CONTAINER_NAME"; do if container_exists "$c"; then printf ' %-24s %s\n' "$c" \ "$(container_running "$c" && echo "${C_GRN}laeuft${C_RST}" || echo "${C_RED}gestoppt${C_RST}")" else printf ' %-24s %s\n' "$c" "${C_YEL}nicht vorhanden${C_RST}" fi done if container_running "$CMK_CONTAINER_NAME"; then printf '\n%sSites im Container%s\n' "$C_BLD" "$C_RST" printf ' %-20s %-18s %s\n' "SITE" "VERSION" "STATUS" local s while IFS= read -r s; do [ -n "$s" ] || continue printf ' %-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")" done < <(cmk_sites) printf '\n Standard-Site laut .env : %s\n' "$CMK_SITE_ID" fi printf '\n%sZugang%s\n' "$C_BLD" "$C_RST" local ip; ip="$(primary_ip)" printf ' Checkmk (direkt) : http://%s:5000/%s/\n' "$ip" "$CMK_SITE_ID" printf ' Checkmk (ueber Proxy) : https:///%s/\n' "$CMK_SITE_ID" printf ' Agent-Receiver : %s:8000\n' "$ip" printf ' NGINX Proxy Manager : http://%s:%s/ (Erstlogin: admin@example.com / changeme)\n\n' "$ip" "${NPM_ADMIN_PORT:-81}" } # ===================================================================== # Site-Kommandos # ===================================================================== cmd_sites() { require_cmk_running printf '%-20s %-18s %s\n' "SITE" "VERSION" "STATUS" local s while IFS= read -r s; do [ -n "$s" ] || continue printf '%-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")" done < <(cmk_sites) } cmd_site_start() { require_cmk_running [ $# -ge 1 ] || die "Site-Name fehlt." ct omd start "$1" && ok "Site '$1' gestartet." } cmd_site_stop() { require_cmk_running [ $# -ge 1 ] || die "Site-Name fehlt." ct omd stop "$1" && ok "Site '$1' gestoppt." } # Wird vom systemd-Unit nach dem Start aufgerufen: alle Sites hochfahren. # (Der Container-Entrypoint startet nur CMK_SITE_ID.) cmd_start_all_sites() { local tries=0 while ! container_running "$CMK_CONTAINER_NAME"; do tries=$((tries+1)); [ "$tries" -gt 60 ] && die "Container '$CMK_CONTAINER_NAME' startet nicht." sleep 2 done # kurz warten, bis der Entrypoint durch ist sleep 5 local s while IFS= read -r s; do [ -n "$s" ] || continue if [ "$(cmk_site_state "$s")" != "0" ]; then log "Starte Site '$s' ..." ct omd start "$s" || warn "Site '$s' konnte nicht gestartet werden." else debug "Site '$s' laeuft bereits." fi done < <(cmk_sites) ok "Alle Sites geprueft/gestartet." } # ===================================================================== # Backup # ===================================================================== cmd_backup() { local -a sites=() opts=() local all="" dir="" label="" stop_site="" while [ $# -gt 0 ]; do case "$1" in --site) sites+=("$2"); shift 2 ;; --all) all=1; shift ;; --dir) dir="$2"; shift 2 ;; --label) label="$2"; shift 2 ;; --no-rrds) opts+=(--no-rrds); shift ;; --no-logs) opts+=(--no-logs); shift ;; --stop) stop_site=1; shift ;; *) die "Unbekannte Option fuer backup: $1" ;; esac done require_cmk_running dir="${dir:-$BACKUP_DIR}" mkdir -p "$dir" if [ -n "$all" ]; then mapfile -t sites < <(cmk_sites) fi [ ${#sites[@]} -gt 0 ] || die "Keine Site angegeben (--site SITE oder --all)." local s for s in "${sites[@]}"; do cmk_site_exists "$s" || die "Site '$s' existiert im Container nicht." backup_one_site "$s" "$dir" "$label" "$stop_site" "${opts[@]+"${opts[@]}"}" done } backup_one_site() { local site="$1" dir="$2" label="$3" stop_site="$4"; shift 4 local -a opts=("$@") local ts version outfile tmp restarted="" ts="$(date +%Y%m%d-%H%M%S)" version="$(cmk_site_version "$site")" outfile="$dir/cmkbackup_${site}_${version:-unknown}${label:+_$label}_${ts}.tar.gz" if [ -n "$stop_site" ] && [ "$(cmk_site_state "$site")" = "0" ]; then log "Stoppe Site '$site' fuer ein konsistentes Backup ..." ct omd stop "$site" >/dev/null restarted=1 fi tmp="$(mktemp -d)" mkdir -p "$tmp/$BUNDLE_DIRNAME" log "Erzeuge omd-Backup der Site '$site' ${opts[*]+(${opts[*]})} ..." if ! $RUNTIME exec "$CMK_CONTAINER_NAME" omd backup ${opts[@]+"${opts[@]}"} "$site" - \ >"$tmp/$BUNDLE_DIRNAME/site.tar.gz" 2>"$tmp/err.log" \ || [ ! -s "$tmp/$BUNDLE_DIRNAME/site.tar.gz" ]; then warn "Streaming-Backup nicht moeglich - weiche auf Zwischendatei im Container aus." debug "$(cat "$tmp/err.log" 2>/dev/null)" ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz" ct omd backup ${opts[@]+"${opts[@]}"} "$site" /var/tmp/cmkbackup.tar.gz \ || { rm -rf "$tmp"; die "omd backup fehlgeschlagen."; } $RUNTIME cp "$CMK_CONTAINER_NAME:/var/tmp/cmkbackup.tar.gz" "$tmp/$BUNDLE_DIRNAME/site.tar.gz" ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz" fi [ -n "$restarted" ] && { log "Starte Site '$site' wieder ..."; ct omd start "$site" >/dev/null; } local sum size sum="$(sha256sum "$tmp/$BUNDLE_DIRNAME/site.tar.gz" | awk '{print $1}')" size="$(file_size "$tmp/$BUNDLE_DIRNAME/site.tar.gz")" printf '%s site.tar.gz\n' "$sum" >"$tmp/$BUNDLE_DIRNAME/site.tar.gz.sha256" cat >"$tmp/$BUNDLE_DIRNAME/MANIFEST" </dev/null || hostname) SOURCE_IP=$(primary_ip) SOURCE_OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || echo unbekannt) CREATED=$(date -Is) CREATED_BY=cmk-manage.sh ARCHIVE=site.tar.gz ARCHIVE_SHA256=$sum ARCHIVE_SIZE=$size OMD_BACKUP_OPTS=${opts[*]-} LABEL=$label AGENT_RECEIVER_PORT=8000 EOF cat >"$tmp/$BUNDLE_DIRNAME/INFO.txt" < --reuse EOF log "Verpacke Bundle ..." tar -czf "$outfile" -C "$tmp" "$BUNDLE_DIRNAME" rm -rf "$tmp" ok "Backup erstellt: $outfile ($(human_size "$(file_size "$outfile")"))" LAST_BACKUP_FILE="$outfile" } # ===================================================================== # Auflisten / Aufraeumen # ===================================================================== list_backup_files() { find "$BACKUP_DIR" -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.tgz' \) -printf '%T@ %p\n' 2>/dev/null \ | sort -rn | cut -d' ' -f2- } cmd_list() { local f n=0 printf '%-58s %10s %s\n' "DATEI" "GROESSE" "SITE / VERSION" while IFS= read -r f; do [ -n "$f" ] || continue bundle_read "$f" 2>/dev/null || continue printf '%-58s %10s %s / %s\n' "$(basename "$f")" \ "$(human_size "$(file_size "$f")")" "$B_SITE" "${B_CMK_VERSION:-?}" n=$((n+1)) done < <(list_backup_files) [ "$n" -eq 0 ] && echo "(keine Backups in $BACKUP_DIR)" return 0 } cmd_prune() { local keep=5 while [ $# -gt 0 ]; do case "$1" in --keep) keep="$2"; shift 2 ;; *) die "Unbekannte Option fuer prune: $1" ;; esac done local -a files=(); mapfile -t files < <(list_backup_files) if [ ${#files[@]} -le "$keep" ]; then ok "Nichts zu loeschen (${#files[@]} Backups, behalte $keep)." return 0 fi local i for ((i=keep; i<${#files[@]}; i++)); do if confirm "Backup '$(basename "${files[$i]}")' loeschen?"; then rm -f "${files[$i]}" && log "geloescht: $(basename "${files[$i]}")" fi done ok "Aufraeumen beendet." } # ===================================================================== # Restore / Import (gemeinsame Implementierung) # ===================================================================== do_restore() { local file="$1" as_site="$2" reuse="$3" kill_opt="$4" nostart="$5" mode="$6" [ -f "$file" ] || die "Datei nicht gefunden: $file" require_cmk_running bundle_read "$file" || die "'$file' ist kein Checkmk-Backup (weder Bundle noch omd-Archiv)." local target="${as_site:-$B_SITE}" echo bundle_show "$file" echo printf '%sZiel-Site im Container : %s%s\n' "$C_BLD" "$target" "$C_RST" # --- Pruefsumme ------------------------------------------------- if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then log "Pruefe Pruefsumme ..." local sum sum="$(bundle_extract_payload "$file" | sha256sum | awk '{print $1}')" [ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme stimmt nicht - Bundle beschaedigt." ok "Pruefsumme korrekt." fi # --- Versionspruefung ------------------------------------------- if [ -n "$B_CMK_VERSION" ]; then local installed installed="$(cmk_versions_installed)" if ! printf '%s\n' "$installed" | grep -qxF "$B_CMK_VERSION"; then err "Die Checkmk-Version des Backups ist im Container nicht installiert." printf ' benoetigt : %s\n' "$B_CMK_VERSION" printf ' installiert : %s\n' "$(printf '%s' "$installed" | tr '\n' ' ')" 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 < --password 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:-}" ;; 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 < 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 } # ===================================================================== # 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 <> "$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)." 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 < "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 '' 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" ;; 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="" [ "${1:-}" = "--category" ] && cat_filter="$2" 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" 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 } cmd_browser() { local engine="${BROWSER_ENGINE:-browsh}" local url="" build="" build_only="" pull="" mono="" 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 ;; -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 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 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[@]}" } # ===================================================================== # Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager # ===================================================================== cmd_cert() { local domain="${CERT_DOMAIN:-checkmk.example.local}" days="${CERT_DAYS:-3650}" local -a alts=() while [ $# -gt 0 ]; do case "$1" in --domain) domain="$2"; shift 2 ;; --alt) alts+=("$2"); shift 2 ;; --days) days="$2"; shift 2 ;; *) die "Unbekannte Option fuer cert: $1" ;; esac done command -v openssl >/dev/null 2>&1 || die "openssl ist nicht installiert." local outdir="$PROJECT_DIR/data/npm/custom-certs/$domain" mkdir -p "$outdir" local san="DNS:$domain" local a for a in ${alts[@]+"${alts[@]}"}; do case "$a" in [0-9]*.[0-9]*.[0-9]*.[0-9]*) san="$san,IP:$a" ;; *) san="$san,DNS:$a" ;; esac done local myip; myip="$(primary_ip)" [ -n "$myip" ] && san="$san,IP:$myip" log "Erzeuge selbstsigniertes Zertifikat fuer '$domain' ($days Tage) ..." openssl req -x509 -nodes -newkey rsa:4096 -sha256 -days "$days" \ -keyout "$outdir/privkey.pem" -out "$outdir/fullchain.pem" \ -subj "/C=${CERT_COUNTRY:-DE}/O=${CERT_ORG:-Selfsigned}/CN=$domain" \ -addext "subjectAltName=$san" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature,keyEncipherment" \ -addext "extendedKeyUsage=serverAuth" 2>/dev/null \ || die "Zertifikatserstellung fehlgeschlagen." chmod 600 "$outdir/privkey.pem" ok "Zertifikat erstellt:" printf ' Schluessel : %s\n' "$outdir/privkey.pem" printf ' Zertifikat : %s\n' "$outdir/fullchain.pem" printf ' SAN : %s\n\n' "$san" cat < SSL Certificates -> Add SSL Certificate -> Custom 2) Name z.B. "$domain" Certificate Key : privkey.pem Certificate : fullchain.pem 3) Im Proxy-Host unter "SSL" dieses Zertifikat auswaehlen, "Force SSL" und "HTTP/2 Support" aktivieren. EOF } # ===================================================================== # ncurses-Oberflaeche # ===================================================================== gui_pick_site() { # -> Site auf stdout local -a items=() s while IFS= read -r s; do [ -n "$s" ] || continue items+=("$s" "$(cmk_site_version "$s") / $(cmk_site_state_text "$s")") done < <(cmk_sites) [ ${#items[@]} -gt 0 ] || { ui_msg "Sites" "Im Container ist keine Site vorhanden."; return 1; } ui_menu "Site waehlen" "Vorhandene Sites im Container:" "${items[@]}" } gui_pick_backup() { # -> Datei auf stdout local -a items=() f while IFS= read -r f; do [ -n "$f" ] || continue bundle_read "$f" 2>/dev/null || continue items+=("$f" "$B_SITE ${B_CMK_VERSION:-?} $(human_size "$(file_size "$f")")") done < <(list_backup_files) if [ ${#items[@]} -eq 0 ]; then ui_msg "Backups" "In $BACKUP_DIR wurde kein Backup gefunden.\n\nAlternativ kann im naechsten Schritt frei im Dateisystem gesucht werden." ui_pick_file "$PROJECT_DIR" '*.tar.gz' return fi items+=("__browse__" "... anderes Verzeichnis durchsuchen ...") local sel sel="$(ui_menu "Backup waehlen" "Backups in $BACKUP_DIR:" "${items[@]}")" || return 1 if [ "$sel" = "__browse__" ]; then ui_pick_file "$PROJECT_DIR" '*.tar.gz' else printf '%s\n' "$sel" fi } gui_status() { local f; f="$(mktemp)" NO_COLOR=1 cmd_status >"$f" 2>&1 || true ui_text "Status" "$f" rm -f "$f" } gui_stack_menu() { while :; do local sel sel="$(ui_menu "Stack steuern" "Container-Runtime: $RUNTIME" \ up "Stack starten (compose up -d)" \ restart "Stack neu starten" \ down "Stack stoppen und entfernen" \ pull "Images aktualisieren" \ logs "Letzte 200 Logzeilen ansehen" \ back "zurueck")" || return 0 case "$sel" in up) ui_run "Stack starten" bash -c "cd '$PROJECT_DIR' && '$SELF' up" ;; restart) ui_run "Neu starten" bash -c "cd '$PROJECT_DIR' && '$SELF' restart" ;; down) ui_yesno "Stack stoppen" "Container wirklich stoppen und entfernen?\n(Die Daten unter ./data bleiben erhalten.)" \ && ui_run "Stack stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' down" ;; pull) ui_run "Images laden" bash -c "cd '$PROJECT_DIR' && '$SELF' pull" ;; logs) local lf; lf="$(mktemp)"; compose logs --tail=200 >"$lf" 2>&1 || true ui_text "Logs (letzte 200 Zeilen)" "$lf"; rm -f "$lf" ;; back) return 0 ;; esac done } gui_backup() { require_cmk_running local -a items=() s while IFS= read -r s; do [ -n "$s" ] || continue items+=("$s" "$(cmk_site_version "$s")" off) done < <(cmk_sites) [ ${#items[@]} -gt 0 ] || { ui_msg "Backup" "Keine Sites vorhanden."; return; } local chosen chosen="$(ui_checklist "Backup" "Zu sichernde Site(s) mit LEERTASTE markieren:" "${items[@]}")" || return [ -n "$chosen" ] || { ui_msg "Backup" "Keine Site ausgewaehlt."; return; } local optsel optsel="$(ui_checklist "Backup-Optionen" "Optionen waehlen:" \ no-rrds "Performance-Daten (RRD) auslassen - kleineres Archiv" off \ no-logs "Logdateien auslassen" off \ stop "Site waehrend des Backups stoppen (konsistenter)" off)" || return local label label="$(ui_input "Kennzeichnung" "Optionaler Text fuer den Dateinamen (leer = keiner):" "")" || label="" label="$(printf '%s' "$label" | tr -cd 'A-Za-z0-9._-')" local -a args=(backup --dir "$BACKUP_DIR") [ -n "$label" ] && args+=(--label "$label") case "$optsel" in *no-rrds*) args+=(--no-rrds);; esac case "$optsel" in *no-logs*) args+=(--no-logs);; esac case "$optsel" in *stop*) args+=(--stop);; esac local sitename for sitename in $(printf '%s' "$chosen" | tr -d '"'); do args+=(--site "$sitename") done ui_run "Backup laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y ${args[*]}" } gui_restore() { require_cmk_running local file; file="$(gui_pick_backup)" || return [ -n "$file" ] || return local f; f="$(mktemp)" bundle_show "$file" >"$f" 2>&1 || true ui_text "Backup-Informationen" "$f"; rm -f "$f" bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Checkmk-Backup."; return; } local reuse="" if cmk_site_exists "$B_SITE"; then ui_yesno "Site existiert" "Die Site '$B_SITE' ist bereits vorhanden.\n\nSoll sie UEBERSCHRIEBEN werden?\nAlle aktuellen Daten dieser Site gehen verloren!" || return reuse="--reuse --kill" fi ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return ui_run "Restore laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y restore --file '$file' $reuse" } gui_import() { require_cmk_running ui_msg "Site importieren" \ "Import einer Site von einer Direktinstallation (Bare-Metal/VM). Voraussetzung: auf dem Quellserver wurde mit 'migrate_creator.sh' ein Migrations-Bundle (.tar.gz) erzeugt und hierher kopiert. Wichtig fuer die Agents: - Site-Name bleibt unveraendert - dieser Host uebernimmt die IP/den DNS-Namen des Quellservers - Port 8000 (Agent-Receiver) ist erreichbar Dann muessen die Agents NICHT neu eingerichtet werden." local file; file="$(gui_pick_backup)" || return [ -n "$file" ] || return local f; f="$(mktemp)" bundle_show "$file" >"$f" 2>&1 || true ui_text "Bundle-Informationen" "$f"; rm -f "$f" bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Migrations-Bundle."; return; } local reuse="" if cmk_site_exists "$B_SITE"; then ui_yesno "Site existiert" "Die Site '$B_SITE' existiert bereits im Container.\n\nUEBERSCHREIBEN?" || return reuse="--reuse --kill" fi local setdef="" ui_yesno "Standard-Site" "Soll CMK_SITE_ID in der .env auf '$B_SITE' gesetzt werden?\n\n(Empfohlen - sonst startet der Container nach einem Reboot\ndie importierte Site nicht automatisch.)" \ && setdef="--set-default-site" ui_run "Import laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y import --file '$file' $reuse $setdef" } gui_backups_menu() { while :; do local sel sel="$(ui_menu "Backups verwalten" "Verzeichnis: $BACKUP_DIR" \ list "Backups auflisten" \ info "Details eines Backups anzeigen" \ verify "Backup pruefen (Struktur + Pruefsumme)" \ del "Backup loeschen" \ back "zurueck")" || return 0 case "$sel" in list) local f; f="$(mktemp)"; NO_COLOR=1 cmd_list >"$f" 2>&1 || true ui_text "Backups" "$f"; rm -f "$f" ;; info) local b; b="$(gui_pick_backup)" || continue; [ -n "$b" ] || continue local f2; f2="$(mktemp)"; bundle_show "$b" >"$f2" 2>&1 || true ui_text "Details" "$f2"; rm -f "$f2" ;; verify) local b2; b2="$(gui_pick_backup)" || continue; [ -n "$b2" ] || continue ui_run "Pruefe Bundle" bash -c "cd '$PROJECT_DIR' && '$SELF' verify --file '$b2'" ;; del) local b3; b3="$(gui_pick_backup)" || continue; [ -n "$b3" ] || continue ui_yesno "Loeschen" "Datei wirklich loeschen?\n\n$b3" && rm -f "$b3" \ && ui_msg "Geloescht" "Die Datei wurde entfernt." ;; back) return 0 ;; esac done } gui_sites_menu() { require_cmk_running while :; do local sel sel="$(ui_menu "Sites" "Sites im Container '$CMK_CONTAINER_NAME'" \ list "Sites anzeigen" \ start "Site starten" \ stop "Site stoppen" \ all "alle Sites starten" \ back "zurueck")" || return 0 case "$sel" in list) local f; f="$(mktemp)"; cmd_sites >"$f" 2>&1 || true; ui_text "Sites" "$f"; rm -f "$f" ;; start) local s; s="$(gui_pick_site)" || continue; [ -n "$s" ] || continue ui_run "Site starten" bash -c "cd '$PROJECT_DIR' && '$SELF' site-start '$s'" ;; stop) local s2; s2="$(gui_pick_site)" || continue; [ -n "$s2" ] || continue ui_run "Site stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' site-stop '$s2'" ;; all) ui_run "Alle Sites starten" bash -c "cd '$PROJECT_DIR' && '$SELF' start-all-sites" ;; back) return 0 ;; esac done } gui_image_menu() { while :; do local sel sel="$(ui_menu "Checkmk-Image" \ "Aktuell (.env): ${CMK_IMAGE:-} Edition/Version: ${CMK_EDITION:-raw} / ${CMK_VERSION:-?}" \ status "Status und lokale Images anzeigen" \ pull "Aus Registry holen (Docker Hub / registry.checkmk.com)" \ download "Tarball vom Downloadportal holen (mit Zugangsdaten)" \ load "Heruntergeladenen Tarball einspielen (load)" \ use "Vorhandenes 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" ;; pull) gui_image_pull ;; download) gui_image_download ;; load) local lf; lf="$(ui_pick_file "${IMAGE_DIR:-$PROJECT_DIR/images}" '*.tar.gz')" || continue [ -n "$lf" ] || continue ui_run "Image einspielen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image load --file '$lf'" load_env "${ENV_FILE:-$PROJECT_DIR/.env}" ;; use) gui_image_use ;; back) return 0 ;; esac done } gui_image_pick_edition() { ui_menu "Edition" "Welche Checkmk-Edition?" \ raw "Raw (frei, Docker Hub, bis 2.4)" \ community "Community (frei, Docker Hub, ab 2.5)" \ enterprise "Enterprise (NICHT auf Docker Hub - Login noetig)" \ pro "Pro (ab 2.5, Nachfolger von Enterprise)" \ cloud "Cloud (Docker Hub)" \ managed "Managed (Docker Hub, bis 2.4)" \ ultimate "Ultimate (ab 2.5)" \ ultimatemt "Ultimate Multi-Tenancy (ab 2.5)" } gui_image_pull() { local ed ver user pass ed="$(gui_image_pick_edition)" || return ver="$(ui_input "Version" "Version bzw. Tag (z.B. 2.3.0p23 oder 2.3.0-latest):" "${CMK_VERSION:-}")" || return [ -n "$ver" ] || return local -a extra=() if [ "$ed" = enterprise ] || [ -n "${CMK_REGISTRY_USER:-}" ]; then user="$(ui_input "Registry-Anmeldung" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return pass="$(ui_password "Registry-Anmeldung" "Kennwort:")" || return [ -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 ver="$(ui_input "Version" "EXAKTE Version (z.B. 2.3.0p23 - kein '-latest'):" "${CMK_VERSION:-}")" || return [ -n "$ver" ] || return user="$(ui_input "Downloadportal" "Benutzer:" "${CMK_DOWNLOAD_USER:-}")" || return pass="$(ui_password "Downloadportal" "Kennwort:")" || return 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 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")" 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:-}" "${ropts[@]}")" || return [ "$new" = auto ] && new="" ;; password) new="$(ui_password "$key" "$label\n\n(leer lassen = unveraendert)")" || return [ -z "$new" ] && return ;; *) new="$(ui_input "$key" "$label\n\nTyp: $typ$([ -n "$opt" ] && echo ' (darf leer bleiben)')" "$cur")" || return ;; 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'" 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:-}")$hint" 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) items+=("back" "zurueck") local sel sel="$(ui_menu "$title" "Eintrag waehlen und mit ENTER aendern:" "${items[@]}")" || return 0 [ "$sel" = back ] && return 0 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+=("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 ;; 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}" clear; "${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 [ -n "$u" ] && NPM_ADMIN_URL="$u" ;; build) ui_run "Werkzeug-Image bauen" bash -c "cd '$PROJECT_DIR' && '$SELF' browser --build-only" || true ;; back) return 0 ;; *) clear cmd_browser --engine "$sel" --url "${NPM_ADMIN_URL:-http://127.0.0.1:81}" || true ;; esac done } gui_cert() { local domain alt days domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return [ -n "$domain" ] || return alt="$(ui_input "Zertifikat" "Weitere Namen/IPs (durch Leerzeichen getrennt, optional):" "$(primary_ip)")" || alt="" days="$(ui_input "Zertifikat" "Gueltigkeit in Tagen:" "${CERT_DAYS:-3650}")" || days=3650 local -a a=(); local x for x in $alt; do a+=(--alt "$x"); done ui_run "Zertifikat erzeugen" bash -c "cd '$PROJECT_DIR' && '$SELF' cert --domain '$domain' --days '$days' ${a[*]+${a[*]}}" } gui_main() { ui_require while :; do local sel sel="$(ui_menu "Checkmk Stack Manager" \ "Projekt : $PROJECT_DIR Runtime : $RUNTIME Standard-Site: $CMK_SITE_ID Backups : $BACKUP_DIR" \ status "Status von Containern und Sites" \ stack "Stack steuern (Start/Stop/Logs/Update)" \ backup "Backup erstellen" \ restore "Backup wiederherstellen" \ import "Site von Direktinstallation importieren" \ manage "Backups verwalten (Liste/Details/Loeschen)" \ sites "Sites im Container verwalten" \ image "Checkmk-Image beschaffen/auswaehlen" \ 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 ;; stack) gui_stack_menu ;; backup) gui_backup ;; restore) gui_restore ;; import) gui_import ;; manage) gui_backups_menu ;; sites) gui_sites_menu ;; image) gui_image_menu ;; browser) gui_browser_menu ;; cert) gui_cert ;; config) gui_settings_menu ;; shell) clear; cmd_shell || true ;; quit) break ;; esac done clear ok "Beendet." } # ===================================================================== # Einstiegspunkt # ===================================================================== main() { while [ $# -gt 0 ]; do case "$1" in --env-file) ENVFILE_OPT="$2"; shift 2 ;; -y|--yes) ASSUME_YES=1; shift ;; -v|--verbose) VERBOSE=1; shift ;; -h|--help) usage; exit 0 ;; *) break ;; esac done load_env "${ENVFILE_OPT:-$PROJECT_DIR/.env}" detect_runtime UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME" local cmd="${1:-gui}"; [ $# -gt 0 ] && shift || true case "$cmd" in gui|menu) gui_main ;; up|start) cmd_up ;; down|stop) cmd_down ;; restart) cmd_restart ;; pull) cmd_pull ;; status) cmd_status ;; logs) cmd_logs "$@" ;; shell) cmd_shell ;; omd) cmd_omd "$@" ;; sites) cmd_sites ;; site-start) cmd_site_start "$@" ;; site-stop) cmd_site_stop "$@" ;; start-all-sites) cmd_start_all_sites ;; backup) cmd_backup "$@" ;; list) cmd_list ;; prune) cmd_prune "$@" ;; verify) local vf=""; [ "${1:-}" = "--file" ] && vf="$2"; [ -n "$vf" ] || vf="${1:-}" [ -n "$vf" ] || die "--file fehlt."; bundle_verify "$vf" ;; restore) cmd_restore "$@" ;; import) cmd_import "$@" ;; cert) cmd_cert "$@" ;; image) cmd_image "$@" ;; browser) cmd_browser "$@" ;; npm-port) cmd_npm_port "$@" ;; config) cmd_config "$@" ;; help|-h|--help) usage ;; *) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;; esac } main "$@"