"omd restore" brach ab mit
ERROR: apache user www-data must be member of group cmk
Die Ursache liegt nicht im Backup. Beim Anlegen einer Site traegt
"omd create" den Apache-Benutzer in die Gruppe der Site ein - das steht
in /etc/group, und das liegt im Container, nicht im Bind-Mount. Wird der
Container neu erstellt, ist /etc/group wieder der Stand aus dem Abbild,
waehrend die Site unveraendert weiterlebt. Der offizielle Entrypoint
erkennt das und legt Gruppe und Benutzer neu an, nimmt www-data dabei
aber nicht in die Gruppe auf. Im Betrieb faellt es nicht auf: die Site
laeuft, die Oberflaeche antwortet. Erst user_verify beim Restore prueft
es.
Direkt dahinter der zweite Halt:
ERROR: fstab entry for /omd/sites/cmk/tmp does not exist
omd laesst einen fehlenden /etc/fstab ausdruecklich durchgehen ("seen in
some containers") - das Abbild liefert aber eine Datei mit einer
Kommentarzeile, also greift die Ausnahme nicht.
Beides am Abbild checkmk/check-mk-raw:2.4.0-latest nachgestellt: Site im
Bind-Mount anlegen, Container verwerfen, neuen Container mit demselben
Mount starten - die Gruppenzugehoerigkeit ist weg und der Restore bricht
mit genau dieser Meldung ab. Nach "gpasswd -a www-data cmk" und dem
fstab-Eintrag laeuft er durch.
container_fix_site_system stellt beides her, vor jedem restore und
import (do_restore ist die gemeinsame Stelle) und nach jedem "up", damit
auch ein von Hand aufgerufenes omd nicht darueber stolpert. Der
Apache-Benutzer und das Kommando zum Eintragen kommen aus
share/omd/distro.info - derselben Quelle, aus der omd sie liest, statt
"www-data" fest zu verdrahten. Passt schon alles, bleibt die Funktion
still.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7132 lines
294 KiB
Bash
Executable File
7132 lines
294 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=""
|
|
NPM_TOKEN=""
|
|
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 [--no-fetch] Stack starten (compose up -d). Fehlt das Image
|
|
lokal, wird es zuvor ueber CMK_IMAGE_SOURCE geholt;
|
|
--no-fetch unterdrueckt das
|
|
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
|
|
firewall [check|open|close] [--apply] [--source CIDR]
|
|
Firewall pruefen: welche Ports der Stack braucht und
|
|
ob sie offen sind (firewalld, ufw, nftables, iptables).
|
|
"close" schliesst wieder, was nicht mehr gebraucht wird
|
|
- SSH niemals, fremde Freigaben bleiben unberuehrt
|
|
selftest Skripte auf bekannte Fallstricke pruefen
|
|
tui-check Testzeilen fuer die Rahmendarstellung ausgeben
|
|
(hilft bei q/x/l/m statt Linien, etwa in PuTTY)
|
|
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 why [REF] erklaeren, warum sich eine Referenz nicht holen
|
|
laesst (ohne REF: die aus CMK_IMAGE)
|
|
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 get [Opt.] Image ueber den eingestellten Bezugsweg holen
|
|
--source registry|download|auto (Vorgabe: CMK_IMAGE_SOURCE)
|
|
auto = erst Registry, dann Downloadportal
|
|
--edition E --version V sonst aus der .env
|
|
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-reset Datenbank des Proxy Managers beiseitelegen,
|
|
damit das Konto beim naechsten Start neu
|
|
aus NPM_ADMIN_EMAIL/-PASSWORD entsteht
|
|
(Zertifikate bleiben erhalten)
|
|
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
|
|
|
|
# ACHTUNG: "grep -q" beendet sich beim ersten Treffer. Der Erzeuger
|
|
# bekommt dann SIGPIPE, und unter "set -o pipefail" gilt die ganze
|
|
# Pipe als fehlgeschlagen - aber nur, wenn er noch am Schreiben war,
|
|
# also abhaengig von der Datenmenge. Bei einem grossen Archiv wuerde
|
|
# ein vorhandenes Bundle dadurch als "kein Bundle" gelten. Deshalb
|
|
# solche Pipes immer in einer Subshell ohne pipefail ausfuehren.
|
|
if ( set +o pipefail; 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 ( set +o pipefail; 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 ! ( set +o pipefail; tar -tzf "$file" 2>/dev/null | grep -qE '^(manifest\.json|oci-layout|index\.json)$' ); then
|
|
if ( set +o pipefail; 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 ( set +o pipefail; 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 [ -n "${IMAGE_QUIET_FAIL:-}" ]; then return 1; fi
|
|
if [ "$reg" != "docker.io" ] || [ -n "$user" ]; then
|
|
image_diagnose_ref "$ref" || 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
|
|
}
|
|
|
|
# Warum laesst sich eine Image-Referenz nicht holen?
|
|
#
|
|
# Die Fehlermeldungen der Runtime sind eindeutig, aber unerklaert:
|
|
# "manifest unknown" -> Repository ist da, das TAG nicht
|
|
# "repository name unknown" -> Repository gibt es nicht
|
|
# "unauthorized" -> Anmeldung oder Berechtigung
|
|
# Diese Funktion fragt die Registry direkt und sagt, welcher Fall vorliegt.
|
|
image_diagnose_ref() { # ref
|
|
local ref="$1"
|
|
command -v curl >/dev/null 2>&1 || return 0
|
|
|
|
local tag regpart reg repo api
|
|
tag="${ref##*:}"; regpart="${ref%:*}"
|
|
case "$regpart" in
|
|
*/*) reg="${regpart%%/*}"; repo="${regpart#*/}" ;;
|
|
*) reg="docker.io"; repo="$regpart" ;;
|
|
esac
|
|
[ "$tag" = "$ref" ] && tag=""
|
|
api="$(registry_api_host "$reg")"
|
|
|
|
local u="" p=""
|
|
case "$reg" in
|
|
docker.io|index.docker.io|registry-1.docker.io) : ;;
|
|
*) u="${CMK_REGISTRY_USER:-}"; p="${CMK_REGISTRY_PASSWORD:-}" ;;
|
|
esac
|
|
|
|
printf '\n%sWoran liegt es?%s\n' "$C_BLD" "$C_RST"
|
|
printf ' Registry : %s\n' "$reg"
|
|
printf ' Repository : %s\n' "$repo"
|
|
printf ' Tag : %s\n\n' "${tag:-<keins>}"
|
|
|
|
local line code
|
|
line="$(registry_repo_status "$api" "$repo" "$u" "$p")"
|
|
code="${line%% *}"
|
|
case "$code" in
|
|
401|403)
|
|
test_result fail "Anmeldung oder Berechtigung (HTTP $code)"
|
|
printf ' Pruefen: %s image test --only registry\n' "$SELF"
|
|
return 1 ;;
|
|
404)
|
|
test_result fail "Dieses Repository gibt es dort nicht (HTTP 404)"
|
|
printf ' Welche es gibt: %s image repos --edition %s\n' "$SELF" "${CMK_EDITION:-enterprise}"
|
|
return 1 ;;
|
|
200) : ;;
|
|
*) test_result warn "Registry antwortet unerwartet (HTTP $code)"; return 1 ;;
|
|
esac
|
|
|
|
test_result ok "Repository vorhanden und zugaenglich"
|
|
|
|
local -a tags=()
|
|
mapfile -t tags < <(registry_tags "$api" "$repo" "$u" "$p" | sed '/^$/d')
|
|
if [ ${#tags[@]} -eq 0 ]; then
|
|
test_result warn "Es liess sich keine Tag-Liste lesen."
|
|
return 1
|
|
fi
|
|
if ( set +o pipefail; printf '%s\n' "${tags[@]}" | grep -qxF "$tag" ); then
|
|
test_result ok "Das Tag '$tag' ist vorhanden - der Fehler hat eine andere Ursache."
|
|
return 0
|
|
fi
|
|
|
|
test_result fail "Das Tag '$tag' gibt es in diesem Repository NICHT."
|
|
printf ' Genau das meint die Meldung "manifest unknown".\n\n'
|
|
|
|
# Naheliegende Tags zeigen: gleicher Zweig, sonst die neuesten
|
|
local branch="${tag%%p*}"
|
|
local -a near=()
|
|
mapfile -t near < <(printf '%s\n' "${tags[@]}" | grep -F "$branch" | sort -rV | head -n 12)
|
|
if [ ${#near[@]} -gt 0 ]; then
|
|
printf ' %sVorhanden im Zweig %s%s\n' "$C_BLD" "$branch" "$C_RST"
|
|
else
|
|
mapfile -t near < <(printf '%s\n' "${tags[@]}" | sort -rV | head -n 12)
|
|
printf ' %sVorhanden (neueste)%s\n' "$C_BLD" "$C_RST"
|
|
fi
|
|
printf ' %s\n' "${near[@]}"
|
|
|
|
# Registries halten meist nur die juengsten Patchstaende vor. Aeltere
|
|
# Versionen liegen weiterhin im Downloadportal - und genau die braucht
|
|
# man bei einer Migration, weil "omd restore" die exakte Version
|
|
# verlangt. Deshalb hier nachsehen, statt den Benutzer suchen zu lassen.
|
|
local tarball=""
|
|
if [ -n "$tag" ]; then
|
|
tarball="$(archive_docker_file "${CMK_EDITION:-enterprise}" "$tag" 2>/dev/null || true)"
|
|
fi
|
|
if [ -n "$tarball" ]; then
|
|
cat <<EOF
|
|
|
|
${C_BLD}Diese Version gibt es aber als Tarball${C_RST}
|
|
Registries halten in der Regel nur die juengsten Patchstaende vor;
|
|
das Downloadportal ist ein Archiv. Dort liegt:
|
|
$tarball
|
|
|
|
Holen und einspielen:
|
|
$SELF image download --edition ${CMK_EDITION:-enterprise} --version $tag
|
|
$SELF up
|
|
|
|
Das ist der richtige Weg, wenn eine BESTIMMTE Version gebraucht wird -
|
|
etwa bei einer Migration, weil "omd restore" genau die Version der
|
|
Quell-Site verlangt.
|
|
EOF
|
|
else
|
|
printf '\n Uebernehmen mit:\n'
|
|
printf ' %s image use %s/%s:<tag> --remember\n' "$SELF" "$reg" "$repo"
|
|
printf ' %s up\n' "$SELF"
|
|
fi
|
|
printf '\n Alle Tags: %s image versions --source registry --edition %s --all\n\n' \
|
|
"$SELF" "${CMK_EDITION:-enterprise}"
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Image holen - ueber den eingestellten Bezugsweg
|
|
#
|
|
# CMK_IMAGE_SOURCE bestimmt, woher:
|
|
# registry nur aus der Registry
|
|
# download nur als Tarball vom Downloadportal
|
|
# auto erst Registry, dann Tarball (Vorgabe)
|
|
#
|
|
# "auto" ist die sinnvolle Vorgabe, weil Registries nur die juengsten
|
|
# Patchstaende vorhalten. Eine aeltere, fuer eine Migration noetige
|
|
# Version liegt dann nur noch im Downloadportal.
|
|
# ---------------------------------------------------------------------
|
|
image_source_default() {
|
|
case "$(printf '%s' "${CMK_IMAGE_SOURCE:-auto}" | tr 'A-Z' 'a-z')" in
|
|
registry|reg|r) printf 'registry\n' ;;
|
|
download|dl|tarball|d) printf 'download\n' ;;
|
|
*) printf 'auto\n' ;;
|
|
esac
|
|
}
|
|
|
|
image_cmd_get() {
|
|
local edition="${CMK_EDITION:-}" version="${CMK_VERSION:-}" src=""
|
|
local -a extra=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--edition) edition="$2"; shift 2 ;;
|
|
--version) version="$2"; shift 2 ;;
|
|
--source) src="$2"; shift 2 ;;
|
|
--user) extra+=(--user "$2"); shift 2 ;;
|
|
--password) extra+=(--password "$2"); shift 2 ;;
|
|
*) die "Unbekannte Option fuer 'image get': $1" ;;
|
|
esac
|
|
done
|
|
[ -n "$src" ] || src="$(image_source_default)"
|
|
case "$src" in registry|download|auto) : ;;
|
|
*) die "--source: registry | download | auto" ;; esac
|
|
[ -n "$edition" ] || die "Keine Edition eingestellt (CMK_EDITION oder --edition)."
|
|
[ -n "$version" ] || die "Keine Version eingestellt (CMK_VERSION oder --version)."
|
|
|
|
case "$src" in
|
|
registry)
|
|
image_cmd_pull --edition "$edition" --version "$version" --remember ${extra[@]+"${extra[@]}"} ;;
|
|
download)
|
|
image_cmd_download --edition "$edition" --version "$version" --remember ${extra[@]+"${extra[@]}"} ;;
|
|
auto)
|
|
log "Bezugsweg 'auto': zuerst die Registry ..."
|
|
if IMAGE_QUIET_FAIL=1 image_cmd_pull --edition "$edition" --version "$version" \
|
|
--remember ${extra[@]+"${extra[@]}"}; then
|
|
return 0
|
|
fi
|
|
warn "Aus der Registry nicht zu bekommen - versuche das Downloadportal."
|
|
if image_cmd_download --edition "$edition" --version "$version" \
|
|
--remember ${extra[@]+"${extra[@]}"}; then
|
|
return 0
|
|
fi
|
|
err "Weder Registry noch Downloadportal haben geliefert."
|
|
image_diagnose_ref "$(edition_repo "$edition"):$version" || true
|
|
return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# 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 "$@" ;;
|
|
why|diagnose) image_diagnose_ref "${1:-${CMK_IMAGE:-}}" ;;
|
|
get|fetch) image_cmd_get "$@" ;;
|
|
*) die "Unbekanntes Unterkommando 'image $sub'.
|
|
Moeglich: status | list | versions | repos | why | test | get | 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
|
|
# Fehlt das Image lokal, es selbst holen - ueber den eingestellten
|
|
# Bezugsweg. Sonst versucht compose einen nackten Pull, der bei einer
|
|
# nur im Downloadportal vorhandenen Version scheitern muss.
|
|
if [ -z "${UP_NO_FETCH:-}" ] && [ -n "${CMK_IMAGE:-}" ] \
|
|
&& ! image_exists_local "$CMK_IMAGE" \
|
|
&& [ -n "${CMK_EDITION:-}" ] && [ -n "${CMK_VERSION:-}" ]; then
|
|
log "Das Image ist lokal nicht vorhanden - hole es ($(image_source_default)) ..."
|
|
if ! image_cmd_get; then
|
|
err "Das Image konnte nicht beschafft werden - der Stack wird nicht gestartet."
|
|
return 1
|
|
fi
|
|
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
fi
|
|
|
|
if ! compose up -d "${svc[@]}"; then
|
|
echo
|
|
err "Der Stack konnte nicht gestartet werden."
|
|
# Haeufigste Ursache: das Image laesst sich nicht holen.
|
|
if [ -n "${CMK_IMAGE:-}" ] && ! image_exists_local "$CMK_IMAGE"; then
|
|
image_diagnose_ref "$CMK_IMAGE" || true
|
|
fi
|
|
return 1
|
|
fi
|
|
npm_enabled || npm_teardown_container
|
|
ok "Stack gestartet."
|
|
|
|
# Nach dem Neuerstellen des Containers fehlen die Systemteile der
|
|
# Site (siehe container_fix_site_system). Gleich nachziehen, damit
|
|
# nicht erst ein spaeteres "omd restore" darueber stolpert.
|
|
if container_running "$CMK_CONTAINER_NAME"; then
|
|
container_fix_site_system "${CMK_SITE_ID:-cmk}" || true
|
|
fi
|
|
|
|
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/\n' "$ip" "${NPM_ADMIN_PORT:-81}"
|
|
if [ -n "${NPM_ADMIN_EMAIL:-}" ]; then
|
|
printf ' Anmeldung: %s\n\n' "$NPM_ADMIN_EMAIL"
|
|
else
|
|
printf ' %sKein Konto hinterlegt%s - NPM_ADMIN_EMAIL/-PASSWORD setzen\n\n' "$C_YEL" "$C_RST"
|
|
fi
|
|
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").
|
|
# =====================================================================
|
|
|
|
FW_STATE_FILE_NAME="firewall-opened.conf"
|
|
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)
|
|
# =====================================================================
|
|
# =====================================================================
|
|
# Systemteile im Container nachziehen
|
|
#
|
|
# Beim Anlegen einer Site traegt "omd create" den Apache-Benutzer in
|
|
# die Gruppe der Site ein. Wird der Container spaeter neu erstellt -
|
|
# bei jedem "up" mit geaendertem Image etwa -, ist /etc/group wieder
|
|
# der Stand aus dem Abbild, waehrend die Site im Bind-Mount liegt und
|
|
# bleibt. Der Entrypoint legt Gruppe und Benutzer dann zwar neu an,
|
|
# nimmt www-data aber NICHT in die Gruppe auf.
|
|
#
|
|
# Im Betrieb faellt das nicht auf: die Site laeuft, die Oberflaeche
|
|
# antwortet. "omd restore" prueft es aber und bricht ab mit
|
|
# ERROR: apache user www-data must be member of group <site>
|
|
#
|
|
# Der zweite Stolperstein: "omd restore --reuse" will einen
|
|
# fstab-Eintrag fuer das tmp-Verzeichnis sehen. Fehlt /etc/fstab ganz,
|
|
# laesst omd das durchgehen ("seen in some containers") - im Abbild
|
|
# liegt aber eine Datei mit einer Kommentarzeile, also greift die
|
|
# Ausnahme nicht.
|
|
#
|
|
# Beides ist Zustand des Containers, nicht der Daten. Es muss nach
|
|
# jedem Neuerstellen wieder hergestellt werden.
|
|
# =====================================================================
|
|
container_fix_site_system() { # site [--quiet]
|
|
local site="$1" quiet="${2:-}"
|
|
local out
|
|
out="$(ct sh -c '
|
|
set -e
|
|
site="'"$site"'"
|
|
info=/omd/versions/default/share/omd/distro.info
|
|
au="$(sed -n "s/^APACHE_USER[[:space:]]*=[[:space:]]*//p" "$info" 2>/dev/null | head -1)"
|
|
[ -n "$au" ] || au=www-data
|
|
getent group "$site" >/dev/null || exit 0
|
|
|
|
# Apache-Benutzer in die Gruppe der Site
|
|
case " $(id -nG "$au" 2>/dev/null) " in
|
|
*" $site "*) ;;
|
|
*)
|
|
add="$(sed -n "s/^ADD_USER_TO_GROUP[[:space:]]*=[[:space:]]*//p" "$info" 2>/dev/null | head -1)"
|
|
if [ -n "$add" ]; then
|
|
cmd="$(printf "%s" "$add" | sed "s/%(user)s/$au/; s/%(group)s/$site/")"
|
|
sh -c "$cmd" >/dev/null 2>&1 || usermod -aG "$site" "$au" >/dev/null 2>&1
|
|
else
|
|
usermod -aG "$site" "$au" >/dev/null 2>&1
|
|
fi
|
|
echo "GRUPPE $au -> $site"
|
|
;;
|
|
esac
|
|
|
|
# fstab-Eintrag fuer das tmp-Verzeichnis
|
|
mp="/omd/sites/$site/tmp"
|
|
if [ -f /etc/fstab ] && ! grep -q "uid=$site," /etc/fstab 2>/dev/null; then
|
|
printf "tmpfs %s tmpfs noauto,user,mode=751,uid=%s,gid=%s 0 0\n" \
|
|
"$mp" "$site" "$site" >> /etc/fstab
|
|
echo "FSTAB $mp"
|
|
fi
|
|
' 2>/dev/null)" || return 0
|
|
|
|
[ -n "$out" ] || return 0
|
|
if [ -z "$quiet" ]; then
|
|
local l
|
|
while IFS= read -r l; do
|
|
case "$l" in
|
|
GRUPPE*) log "Apache-Benutzer ${l#GRUPPE } in die Gruppe aufgenommen (nach dem Neuerstellen des Containers verloren gegangen)." ;;
|
|
FSTAB*) log "fstab-Eintrag fuer ${l#FSTAB } ergaenzt - ohne ihn verweigert 'omd restore' den Dienst." ;;
|
|
esac
|
|
done <<<"$out"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
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 ! ( set +o pipefail; 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"
|
|
|
|
# Die Pruefungen von "omd restore" setzen Systemteile voraus, die ein
|
|
# neu erstellter Container nicht mehr hat - vorher nachziehen.
|
|
container_fix_site_system "$target"
|
|
|
|
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')"
|
|
}
|
|
|
|
# Proxy Manager auf Anfang setzen: Datenbank weg, damit der Container das
|
|
# Konto beim naechsten Start neu anlegt. Zertifikate bleiben erhalten.
|
|
cmd_npm_reset() {
|
|
local d="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
|
|
if ! npm_db_exists; then
|
|
ok "Es gibt noch keine Datenbank - der Container legt das Konto beim Start selbst an."
|
|
log "Vorher setzen: $SELF config set NPM_ADMIN_EMAIL <adresse>"
|
|
return 0
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
${C_BLD}Was dabei verloren geht${C_RST}
|
|
$d/database.sqlite
|
|
Darin stehen Konten, Proxy-Hosts, Weiterleitungen und Zugriffslisten.
|
|
|
|
${C_BLD}Was bleibt${C_RST}
|
|
Zertifikate unter ${NPM_LE_DIR:-./data/npm/letsencrypt} und
|
|
$d/custom_ssl
|
|
|
|
Die Proxy-Hosts lassen sich anschliessend mit
|
|
$SELF proxy add --domain ...
|
|
in einem Aufruf neu anlegen.
|
|
EOF
|
|
confirm "Datenbank wirklich verwerfen?" || die "Abgebrochen."
|
|
|
|
[ -n "${NPM_ADMIN_EMAIL:-}" ] && [ -n "${NPM_ADMIN_PASSWORD:-}" ] \
|
|
|| warn "NPM_ADMIN_EMAIL/-PASSWORD sind nicht gesetzt - dann entsteht
|
|
beim naechsten Start wieder kein Konto. Zuerst setzen!"
|
|
|
|
local stamp; stamp="$(date +%Y%m%d-%H%M%S)"
|
|
if container_running "$NPM_CONTAINER_NAME"; then
|
|
log "Stoppe '$NPM_CONTAINER_NAME' ..."
|
|
$RUNTIME stop "$NPM_CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
fi
|
|
mv "$d/database.sqlite" "$d/database.sqlite.alt-$stamp"
|
|
ok "Datenbank beiseitegelegt: database.sqlite.alt-$stamp"
|
|
log "Jetzt neu starten: $SELF up"
|
|
}
|
|
|
|
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 <<<"
|
|
|
|
# Die Zusatzkonfiguration liegt im Datenverzeichnis des Proxy Managers,
|
|
# und das gehoert dem Container - also root. Ohne Schreibrecht scheitert
|
|
# sonst erst "touch" mit einer nackten Fehlermeldung.
|
|
npm_conf_writable() { # datei
|
|
local f="$1" d; d="$(dirname "$f")"
|
|
if [ -e "$f" ]; then
|
|
[ -w "$f" ] && return 0
|
|
elif [ -d "$d" ]; then
|
|
[ -w "$d" ] && return 0
|
|
else
|
|
mkdir -p "$d" 2>/dev/null && return 0
|
|
fi
|
|
err "Keine Schreibrechte auf $f"
|
|
printf ' Das Verzeichnis gehoert dem Proxy-Manager-Container (root).
|
|
Bitte mit sudo aufrufen:
|
|
sudo %s %s\n' "$SELF" "${CMD_LINE:-<befehl>}" >&2
|
|
return 1
|
|
}
|
|
|
|
npm_data_dir_abs() {
|
|
local base="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
|
|
case "$base" in /*) : ;; *) base="$PROJECT_DIR/${base#./}" ;; esac
|
|
printf '%s\n' "$base"
|
|
}
|
|
|
|
npm_custom_conf_path() {
|
|
printf '%s/%s\n' "$(npm_data_dir_abs)" "$NPM_CUSTOM_CONF_REL"
|
|
}
|
|
|
|
NPM_DEF_START="# >>> cmk-manage.sh: HTTPS ohne SNI - nicht von Hand aendern >>>"
|
|
NPM_DEF_END="# <<< cmk-manage.sh: HTTPS ohne SNI <<<"
|
|
|
|
# Vorhandenen verwalteten Block entfernen, Fremdinhalte bleiben erhalten
|
|
npm_conf_strip_marked() {
|
|
local f="$1" st="$2" en="$3"
|
|
[ -f "$f" ] || return 0
|
|
awk -v s="$st" -v e="$en" '
|
|
$0 == s { skip=1; next }
|
|
$0 == e { skip=0; next }
|
|
!skip { print }
|
|
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
|
|
}
|
|
|
|
npm_conf_strip_block() { npm_conf_strip_marked "$1" "$NPM_BLOCK_START" "$NPM_BLOCK_END"; }
|
|
|
|
npm_conf_write_block() {
|
|
local f="$1" port="$2"
|
|
npm_conf_writable "$f" || return 1
|
|
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
|
|
( set +o pipefail; 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_IMAGE_SOURCE|choice|Bezugsweg beim Holen|auto:registry:download|
|
|
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||
|
|
firewall|FW_ROLE|choice|Rolle im verteilten Monitoring (leer = single)|single:central:remote|1
|
|
firewall|FW_EC_SYSLOG|choice|Event Console: Syslog 514 (leer = aus)|off:udp:tcp:beide|1
|
|
firewall|FW_EC_SNMPTRAP|choice|Event Console: SNMP-Traps 162/udp (leer = aus)|off:on|1
|
|
firewall|FW_EC_TCP|choice|Event Console: Status ueber TCP 6558 (leer = aus)|off:on|1
|
|
firewall|FW_AGENT|choice|Checkmk-Agent dieses Hosts 6556 (leer = aus)|off:on|1
|
|
firewall|FW_EXTRA_PORTS|text|Weitere Ports (3000/tcp=Grafana,...)||1
|
|
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
|
|
firewall|Firewall: was offen sein muss
|
|
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; } ;;
|
|
FW_EXTRA_PORTS)
|
|
[ -z "$val" ] && return 0
|
|
local e pp
|
|
while IFS= read -r e; do
|
|
e="$(printf '%s' "$e" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
[ -n "$e" ] || continue
|
|
pp="${e%%=*}"
|
|
case "$pp" in
|
|
[0-9]*/tcp|[0-9]*/udp|[0-9]*) : ;;
|
|
*) echo "'$pp' ist kein Port. Erwartet wird 3000/tcp=Grafana,5667/tcp=NSCA"
|
|
return 1 ;;
|
|
esac
|
|
case "${pp%/*}" in
|
|
*[!0-9]*) echo "'$pp' ist keine Portnummer."; return 1 ;;
|
|
esac
|
|
done < <(printf '%s\n' "$val" | tr ',' '\n')
|
|
;;
|
|
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}"; }
|
|
|
|
# Hat der Proxy Manager schon eine Datenbank? Davon haengt ab, wie das
|
|
# Konto entsteht: der Container legt es NUR beim ersten Start mit leerer
|
|
# Datenbank an, und nur wenn INITIAL_ADMIN_EMAIL/-PASSWORD gesetzt sind.
|
|
# Ist die Datenbank schon da, hilft nur noch der Einrichtungsassistent im
|
|
# Browser - und danach muessen dessen Zugangsdaten in die .env.
|
|
npm_db_exists() {
|
|
local d="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
[ -s "$d/database.sqlite" ]
|
|
}
|
|
|
|
# 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)"
|
|
if [ -z "${NPM_ADMIN_EMAIL:-}" ] || [ -z "${NPM_ADMIN_PASSWORD:-}" ]; then
|
|
err "Es sind keine Zugangsdaten fuer den Proxy Manager hinterlegt."
|
|
printf '\n' >&2
|
|
if npm_db_exists; then
|
|
cat >&2 <<EOF
|
|
Der Proxy Manager hat bereits eine Datenbank. Seit Version 2.15 legt
|
|
er KEINE Standardzugangsdaten mehr an - admin@example.com/changeme
|
|
gibt es nicht. Das Konto entsteht entweder beim allerersten Start
|
|
aus NPM_ADMIN_EMAIL/-PASSWORD, oder von Hand im Browser.
|
|
|
|
Weil die Datenbank schon steht, bleibt:
|
|
1) Oberflaeche aufrufen und das Konto anlegen:
|
|
$SELF browser
|
|
(oder http://127.0.0.1:${NPM_ADMIN_PORT:-81} vom Arbeitsplatz aus)
|
|
2) Dieselben Daten hier eintragen:
|
|
$SELF config set NPM_ADMIN_EMAIL <adresse>
|
|
$SELF config set NPM_ADMIN_PASSWORD <kennwort>
|
|
|
|
Alternativ ganz neu beginnen - VERWIRFT die Proxy-Konfiguration:
|
|
$SELF npm-reset
|
|
EOF
|
|
else
|
|
cat >&2 <<EOF
|
|
Der Proxy Manager hat noch keine Datenbank. Dann genuegt es, die
|
|
Zugangsdaten zu setzen und den Stack zu starten - der Container legt
|
|
das Konto damit selbst an:
|
|
$SELF config set NPM_ADMIN_EMAIL <adresse>
|
|
$SELF config set NPM_ADMIN_PASSWORD <kennwort>
|
|
$SELF up
|
|
EOF
|
|
fi
|
|
return 1
|
|
fi
|
|
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 via npm_code
|
|
NPM_CODE_FILE="${TMPDIR:-/tmp}/.cmk-manage-npmcode.$$"
|
|
trap 'rm -f "$NPM_CODE_FILE"' EXIT
|
|
|
|
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'}"
|
|
# Wird npm_api in einer Kommandoersetzung aufgerufen - und das ist der
|
|
# Regelfall, weil die Antwort gebraucht wird -, laeuft sie in einer
|
|
# Subshell. Die Zuweisung oben ist dann beim Rueckkehren wieder weg.
|
|
# Deshalb zusaetzlich ablegen; gelesen wird mit npm_code.
|
|
printf '%s' "$NPM_CODE" > "$NPM_CODE_FILE" 2>/dev/null || true
|
|
printf '%s' "${out%$'\n'*}"
|
|
}
|
|
|
|
# HTTP-Code des letzten API-Aufrufs - auch nach einer Kommandoersetzung
|
|
npm_code() {
|
|
local c; c="$(cat "$NPM_CODE_FILE" 2>/dev/null || true)"
|
|
printf '%s' "${c:-${NPM_CODE:-000}}"
|
|
}
|
|
|
|
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 ---------------------------------------------------------
|
|
# ---------------------------------------------------------------------
|
|
# Zwei kleine JSON-Helfer, die mehr koennen als ein sed-Ausdruck
|
|
#
|
|
# "advanced_config" darf geschweifte Klammern enthalten (nginx-Bloecke),
|
|
# und Zeichenketten duerfen maskierte Anfuehrungszeichen tragen. Ein
|
|
# Aufteilen an "}" wuerde daran zerbrechen. Diese beiden zaehlen die
|
|
# Verschachtelung mit und achten auf Zeichenketten.
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Text fuer eine JSON-Zeichenkette absichern. Anfuehrungszeichen werden
|
|
# maskiert, echte Zeilenumbrueche werden zu \n - ein vom Benutzer selbst
|
|
# getipptes \n bleibt, was es ist: ein Zeilenumbruch fuer nginx.
|
|
json_str_escape() {
|
|
printf '%s' "$1" | sed 's/"/\\"/g' | awk '{ if (NR>1) printf "\\n"; printf "%s", $0 }'
|
|
}
|
|
|
|
# Rohen Wert eines Schluessels der obersten Ebene ausschneiden
|
|
json_raw() { # json schluessel
|
|
printf '%s' "$1" | awk -v key="$2" '
|
|
{ s = s $0 }
|
|
function cut(s, p, n, c, k, ch, d2, q2, e2) {
|
|
c = substr(s, p, 1)
|
|
if (c == "{" || c == "[") {
|
|
d2 = 0; q2 = 0; e2 = 0
|
|
for (k = p; k <= n; k++) {
|
|
ch = substr(s, k, 1)
|
|
if (q2) { if (e2) e2 = 0; else if (ch == "\\") e2 = 1; else if (ch == "\"") q2 = 0; continue }
|
|
if (ch == "\"") { q2 = 1; continue }
|
|
if (ch == "{" || ch == "[") d2++
|
|
else if (ch == "}" || ch == "]") { d2--; if (d2 == 0) return substr(s, p, k - p + 1) }
|
|
}
|
|
return substr(s, p)
|
|
}
|
|
if (c == "\"") {
|
|
e2 = 0
|
|
for (k = p + 1; k <= n; k++) {
|
|
ch = substr(s, k, 1)
|
|
if (e2) { e2 = 0; continue }
|
|
if (ch == "\\") { e2 = 1; continue }
|
|
if (ch == "\"") return substr(s, p, k - p + 1)
|
|
}
|
|
return substr(s, p)
|
|
}
|
|
for (k = p; k <= n; k++) {
|
|
ch = substr(s, k, 1)
|
|
if (ch == "," || ch == "}" || ch == "]") return substr(s, p, k - p)
|
|
}
|
|
return substr(s, p)
|
|
}
|
|
END {
|
|
t = "\"" key "\""; tl = length(t); n = length(s)
|
|
d = 0; q = 0; e = 0
|
|
for (i = 1; i <= n; i++) {
|
|
c = substr(s, i, 1)
|
|
if (q) { if (e) e = 0; else if (c == "\\") e = 1; else if (c == "\"") q = 0; continue }
|
|
if (c == "\"") {
|
|
if (d == 1 && substr(s, i, tl) == t) {
|
|
j = i + tl
|
|
while (j <= n && index(" \t:", substr(s, j, 1)) > 0) j++
|
|
print cut(s, j, n); exit
|
|
}
|
|
q = 1; continue
|
|
}
|
|
if (c == "{" || c == "[") d++
|
|
else if (c == "}" || c == "]") d--
|
|
}
|
|
}'
|
|
}
|
|
|
|
# JSON-Wahrheitswert eines Schluessels als on/off (fuer Auswahllisten)
|
|
json_bool_on() { # json schluessel
|
|
case "$(json_raw "$1" "$2")" in true) printf 'on' ;; *) printf 'off' ;; esac
|
|
}
|
|
|
|
# Ein JSON-Feld in seine Elemente zerlegen, je Element eine Zeile
|
|
json_split_array() { # array-text
|
|
printf '%s' "$1" | awk '
|
|
{ s = s $0 }
|
|
END {
|
|
n = length(s); d = 0; q = 0; e = 0; start = 0
|
|
for (i = 1; i <= n; i++) {
|
|
c = substr(s, i, 1)
|
|
if (q) { if (e) e = 0; else if (c == "\\") e = 1; else if (c == "\"") q = 0; continue }
|
|
if (c == "\"") { q = 1; continue }
|
|
if (c == "[" || c == "{") { d++; if (d == 2 && start == 0) start = i; continue }
|
|
if (c == "]" || c == "}") {
|
|
d--
|
|
if (d == 1 && start > 0) { print substr(s, start, i - start + 1); start = 0 }
|
|
}
|
|
}
|
|
}'
|
|
}
|
|
|
|
# =====================================================================
|
|
# HTTPS ueber die blanke IP
|
|
#
|
|
# Ruft man den Server mit https://<IP>/ auf, sendet der Browser keinen
|
|
# SNI-Namen - RFC 6066 laesst dort keine IP-Adressen zu. nginx kann den
|
|
# passenden Server-Block dann nicht ueber den Namen auswaehlen und faellt
|
|
# auf den Standard-Server zurueck. Bei NPM ist das der Block aus
|
|
# /etc/nginx/conf.d/default.conf, und der antwortet mit
|
|
# "ssl_reject_handshake on" - im Browser als
|
|
# ERR_SSL_UNRECOGNIZED_NAME_ALERT.
|
|
#
|
|
# Ueber HTTP faellt das nicht auf: dort waehlt nginx den Server-Block
|
|
# anhand des Host-Kopfes aus, und der traegt die IP.
|
|
#
|
|
# Abhilfe ist ein eigener Block mit "default_server" in der
|
|
# Zusatzkonfiguration, die nginx nach den Proxy-Hosts einliest. Keiner
|
|
# der NPM-Bloecke setzt das Kennzeichen selbst, deshalb gibt es keinen
|
|
# Streit darum. Benannte Aufrufe gehen weiterhin an ihren Proxy-Host -
|
|
# der Standard-Server greift nur, wenn kein Name passt.
|
|
# =====================================================================
|
|
|
|
# Zertifikatspfade IM CONTAINER zu einer NPM-Zertifikats-ID
|
|
npm_cert_paths() { # id -> "fullchain|privkey|anbieter"
|
|
local cid="$1" o prov
|
|
o="$(npm_api GET "/api/nginx/certificates/$cid")" || return 1
|
|
prov="$(json_str "$o" provider)"
|
|
if [ "$prov" = letsencrypt ]; then
|
|
printf '/etc/letsencrypt/live/npm-%s/fullchain.pem|/etc/letsencrypt/live/npm-%s/privkey.pem|%s\n' \
|
|
"$cid" "$cid" "$prov"
|
|
else
|
|
printf '/data/custom_ssl/npm-%s/fullchain.pem|/data/custom_ssl/npm-%s/privkey.pem|%s\n' \
|
|
"$cid" "$cid" "${prov:-other}"
|
|
fi
|
|
}
|
|
|
|
# Deckt das Zertifikat die IP ab? Nur pruefbar, wenn die Datei hier liegt.
|
|
npm_cert_covers_ip() { # zertifikats-id ip
|
|
local cid="$1" ip="$2" f
|
|
f="$(npm_data_dir_abs)/custom_ssl/npm-$cid/fullchain.pem"
|
|
[ -r "$f" ] || return 0 # nicht pruefbar - dann auch nicht meckern
|
|
( set +o pipefail; openssl x509 -in "$f" -noout -text 2>/dev/null | grep -q "IP Address:$ip" )
|
|
}
|
|
|
|
# Einen Proxy-Host in einen Standard-Server-Block spiegeln
|
|
npm_default_write() { # conf objekt-json fullchain privkey domain
|
|
local f="$1" o="$2" full="$3" key="$4" dom="$5"
|
|
npm_conf_writable "$f" || return 1
|
|
local sch host port seg lp lsch lh lpo
|
|
sch="$(json_str "$o" forward_scheme)"; [ -n "$sch" ] || sch=http
|
|
host="$(json_str "$o" forward_host)"
|
|
port="$(json_num "$o" forward_port)"
|
|
|
|
mkdir -p "$(dirname "$f")"
|
|
touch "$f"
|
|
npm_conf_strip_marked "$f" "$NPM_DEF_START" "$NPM_DEF_END"
|
|
{
|
|
printf '%s\n' "$NPM_DEF_START"
|
|
cat <<EOF
|
|
# Standard-Server fuer HTTPS: greift, wenn der Browser keinen Namen
|
|
# mitschickt - das ist beim Aufruf ueber die blanke IP der Fall.
|
|
# Spiegelt den Proxy-Host "$dom".
|
|
# Erzeugt von scripts/cmk-manage.sh proxy default --domain $dom
|
|
server {
|
|
listen 443 ssl default_server;
|
|
EOF
|
|
if [ "${NPM_DISABLE_IPV6:-false}" != true ]; then
|
|
printf ' listen [::]:443 ssl default_server;\n'
|
|
fi
|
|
cat <<EOF
|
|
server_name _;
|
|
|
|
ssl_certificate $full;
|
|
ssl_certificate_key $key;
|
|
include conf.d/include/ssl-ciphers.conf;
|
|
|
|
access_log /data/logs/default_ssl_access.log standard;
|
|
error_log /data/logs/default_ssl_error.log warn;
|
|
|
|
client_max_body_size 0;
|
|
proxy_read_timeout 300s;
|
|
EOF
|
|
# Zusaetzliche Pfade zuerst - laengster Treffer gewinnt bei nginx
|
|
# ohnehin, aber so liest es sich wie im Proxy Manager.
|
|
while IFS= read -r seg; do
|
|
case "$seg" in *'"path"'*) : ;; *) continue ;; esac
|
|
lp="$(printf '%s' "$seg" | sed -n 's/.*"path":"\([^"]*\)".*/\1/p')"
|
|
lsch="$(printf '%s' "$seg" | sed -n 's/.*"forward_scheme":"\([^"]*\)".*/\1/p')"
|
|
lh="$(printf '%s' "$seg" | sed -n 's/.*"forward_host":"\([^"]*\)".*/\1/p')"
|
|
lpo="$(printf '%s' "$seg" | sed -n 's/.*"forward_port":\([0-9]*\).*/\1/p')"
|
|
[ -n "$lp" ] && [ -n "$lh" ] && [ -n "$lpo" ] || continue
|
|
[ -n "$lsch" ] || lsch=http
|
|
cat <<EOF
|
|
|
|
location $lp {
|
|
proxy_pass $lsch://$lh:$lpo;
|
|
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";
|
|
}
|
|
EOF
|
|
done < <(printf '%s' "$o" | tr '}' '\n')
|
|
|
|
cat <<EOF
|
|
|
|
location / {
|
|
proxy_pass $sch://$host:$port;
|
|
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";
|
|
}
|
|
}
|
|
EOF
|
|
printf '%s\n' "$NPM_DEF_END"
|
|
} >> "$f"
|
|
sed -i '/./,$!d' "$f"
|
|
}
|
|
|
|
npm_reload_hint() { # restart-flag
|
|
if [ -n "${1:-}" ] && 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 restart (bzw. Neustart des Proxy Managers)"
|
|
fi
|
|
}
|
|
|
|
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 ( set +o pipefail; 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 el n=0
|
|
while IFS= read -r el; do
|
|
[ -n "$el" ] || continue
|
|
n=$((n+1))
|
|
printf ' %-20s -> %s://%s:%s\n' "$(json_str "$el" path)" \
|
|
"$(json_str "$el" forward_scheme)" "$(json_str "$el" forward_host)" \
|
|
"$(json_num "$el" forward_port)"
|
|
done < <(json_split_array "$(json_raw "$o" locations)")
|
|
if [ "$n" -eq 0 ]; then printf ' (keine - alles geht an das Ziel oben)\n'; fi
|
|
echo
|
|
return 0 ;;
|
|
|
|
default|ip)
|
|
local dom="" off="" restart="" conf
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--domain) dom="$2"; shift 2 ;;
|
|
--off|--aus) off=1; shift ;;
|
|
--restart) restart=1; shift ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
HTTPS ueber die blanke IP ermoeglichen.
|
|
|
|
$SELF proxy default Stand anzeigen
|
|
$SELF proxy default --domain NAME Proxy-Host spiegeln
|
|
$SELF proxy default --off Spiegelung entfernen
|
|
|
|
Ruft man den Server als https://<IP>/ auf, schickt der Browser keinen
|
|
Namen mit (SNI kennt keine IP-Adressen). nginx kann den Proxy-Host dann
|
|
nicht ueber den Namen finden und faellt auf seinen Standard-Server
|
|
zurueck - der lehnt den Handshake ab: ERR_SSL_UNRECOGNIZED_NAME_ALERT.
|
|
Ueber HTTP faellt das nicht auf, dort entscheidet der Host-Kopf.
|
|
|
|
Dieser Befehl legt einen Standard-Server an, der denselben Weg nimmt wie
|
|
der genannte Proxy-Host - samt der zusaetzlichen Pfade. Aufrufe mit
|
|
Namen gehen weiterhin an ihren eigenen Proxy-Host.
|
|
EOF
|
|
return 0 ;;
|
|
*) dom="$1"; shift ;;
|
|
esac
|
|
done
|
|
conf="$(npm_custom_conf_path)"
|
|
|
|
if [ -n "$off" ]; then
|
|
if ! grep -qsF "$NPM_DEF_START" "$conf"; then
|
|
ok "Es ist kein Standard-Server eingetragen."
|
|
return 0
|
|
fi
|
|
npm_conf_strip_marked "$conf" "$NPM_DEF_START" "$NPM_DEF_END"
|
|
[ -s "$conf" ] || rm -f "$conf"
|
|
ok "Standard-Server entfernt: $conf"
|
|
npm_reload_hint "$restart"
|
|
return 0
|
|
fi
|
|
|
|
if [ -z "$dom" ]; then
|
|
printf '\n%sHTTPS ueber die blanke IP%s\n' "$C_BLD" "$C_RST"
|
|
if grep -qsF "$NPM_DEF_START" "$conf"; then
|
|
test_result ok "eingerichtet"
|
|
printf ' Datei : %s\n' "$conf"
|
|
printf ' Spiegelt: %s\n' \
|
|
"$(sed -n 's/^# Spiegelt den Proxy-Host "\(.*\)"\.$/\1/p' "$conf" | head -1)"
|
|
printf '\n Entfernen mit: %s proxy default --off\n\n' "$SELF"
|
|
else
|
|
test_result warn "nicht eingerichtet"
|
|
printf ' https://<IP>/ endet dann in
|
|
ERR_SSL_UNRECOGNIZED_NAME_ALERT, http://<IP>/ geht.\n'
|
|
printf '\n Einrichten mit: %s proxy default --domain <name-des-proxy-hosts>\n' "$SELF"
|
|
printf ' Vorhandene : %s proxy list\n\n' "$SELF"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
npm_require_running || return 1
|
|
NPM_TOKEN="$(npm_api_token)" || return 1
|
|
|
|
# Proxy-Host zum Namen suchen
|
|
local all pid="" i
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
for i in $(json_ids "$all"); do
|
|
if ( set +o pipefail; 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 - '$SELF proxy list' zeigt die vorhandenen."
|
|
|
|
local o cid
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
|
|
cid="$(json_num "$o" certificate_id)"
|
|
if [ -z "$cid" ] || [ "$cid" = 0 ]; then
|
|
die "Der Proxy-Host '$dom' hat kein Zertifikat.
|
|
Ohne Zertifikat gibt es nichts, was ohne SNI ausgeliefert werden koennte.
|
|
Erst eines zuweisen: $SELF proxy add --domain $dom --cert ... --update"
|
|
fi
|
|
|
|
local paths full key prov
|
|
paths="$(npm_cert_paths "$cid")" || die "Zertifikat $cid nicht abrufbar."
|
|
full="${paths%%|*}"; paths="${paths#*|}"
|
|
key="${paths%%|*}"; prov="${paths##*|}"
|
|
|
|
local ip; ip="$(primary_ip)"
|
|
if [ "$prov" = letsencrypt ]; then
|
|
warn "Das Zertifikat stammt von Lets Encrypt und lautet auf '$dom'.
|
|
Beim Aufruf ueber die IP meldet der Browser deshalb einen
|
|
Namensfehler (anklickbar, aber unschoen). Fuer den IP-Zugang ist
|
|
ein selbstsigniertes Zertifikat mit der IP als Alternativname
|
|
besser: $SELF cert --domain $dom --alt $ip"
|
|
elif [ -n "$ip" ] && ! npm_cert_covers_ip "$cid" "$ip"; then
|
|
warn "Das Zertifikat fuehrt die IP $ip nicht als Alternativnamen.
|
|
Der Browser meldet dann einen Namensfehler. Neu erzeugen mit:
|
|
$SELF cert --domain $dom --alt $ip
|
|
und dem Proxy-Host zuweisen."
|
|
fi
|
|
|
|
CMD_LINE="proxy default --domain $dom" npm_default_write "$conf" "$o" "$full" "$key" "$dom" || return 1
|
|
ok "Standard-Server geschrieben: $conf"
|
|
printf ' Spiegelt Proxy-Host %s (%s), Zertifikat %s (%s)\n' "$pid" "$dom" "$cid" "$prov"
|
|
if [ -n "$ip" ]; then printf ' Danach erreichbar unter: https://%s/\n' "$ip"; fi
|
|
npm_reload_hint "$restart"
|
|
return 0 ;;
|
|
|
|
edit|set)
|
|
local pid="" dom="" cert="" adv="" advset="" rename=""
|
|
local fhost="" fport="" scheme="" enabled=""
|
|
local fssl="" h2="" ws="" blk=""
|
|
local -a addloc=() delloc=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--id) pid="$2"; shift 2 ;;
|
|
--domain) dom="$2"; shift 2 ;;
|
|
--rename) rename="$2"; shift 2 ;;
|
|
--forward-host) fhost="$2"; shift 2 ;;
|
|
--forward-port) fport="$2"; shift 2 ;;
|
|
--scheme) scheme="$2"; shift 2 ;;
|
|
--advanced) adv="$2"; advset=1; shift 2 ;;
|
|
--cert) cert="$2"; shift 2 ;;
|
|
--force-ssl) fssl="$2"; shift 2 ;;
|
|
--http2) h2="$2"; shift 2 ;;
|
|
--websockets) ws="$2"; shift 2 ;;
|
|
--block-exploits) blk="$2"; shift 2 ;;
|
|
--enable) enabled=true; shift ;;
|
|
--disable) enabled=false; shift ;;
|
|
--location) addloc+=("$2"); shift 2 ;;
|
|
--del-location) delloc+=("$2"); shift 2 ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
Einen vorhandenen Proxy-Host aendern. Angegeben wird nur, was sich
|
|
aendern soll - alles andere bleibt, wie es ist.
|
|
|
|
$SELF proxy edit --id 3 --forward-port 5001
|
|
$SELF proxy edit --domain checkmk.example.local --cert self
|
|
$SELF proxy edit --id 3 --location /grafana=127.0.0.1:3000
|
|
$SELF proxy edit --id 3 --del-location /grafana
|
|
|
|
Auswahl
|
|
--id N | --domain NAME
|
|
|
|
Aenderungen
|
|
--rename NAME Domainname ersetzen
|
|
--forward-host HOST Weiterleitungsziel (Host)
|
|
--forward-port PORT Weiterleitungsziel (Port)
|
|
--scheme http|https Schema zum Ziel
|
|
--cert self|letsencrypt|none|ID
|
|
--advanced 'TEXT' erweiterte nginx-Konfiguration
|
|
--force-ssl on|off HTTP auf HTTPS umleiten
|
|
--http2 on|off
|
|
--websockets on|off
|
|
--block-exploits on|off
|
|
--enable | --disable Proxy-Host ein-/ausschalten
|
|
--location /pfad=host:port hinzufuegen oder ersetzen
|
|
--del-location /pfad entfernen
|
|
|
|
Ohne Aenderung wird nur der aktuelle Stand gezeigt.
|
|
EOF
|
|
return 0 ;;
|
|
*) die "Unbekannte Option fuer 'proxy edit': $1" ;;
|
|
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. ('$SELF proxy list' zeigt die IDs)"
|
|
local all i
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
for i in $(json_ids "$all"); do
|
|
if ( set +o pipefail; 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
|
|
case "$pid" in ''|*[!0-9]*) die "'$pid' ist keine gueltige ID." ;; esac
|
|
|
|
local o; o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
|
|
[ -n "$(json_num "$o" id)" ] || die "Proxy-Host $pid gibt es nicht."
|
|
|
|
# Nichts angegeben -> nur zeigen
|
|
if [ -z "$rename$fhost$fport$scheme$advset$cert$fssl$h2$ws$blk$enabled" ] \
|
|
&& [ "${#addloc[@]}" -eq 0 ] && [ "${#delloc[@]}" -eq 0 ]; then
|
|
cmd_proxy show --id "$pid"
|
|
printf ' Aendern mit: %s proxy edit --id %s --forward-port 5001
|
|
Alle Moeglichkeiten: %s proxy edit --help\n\n' "$SELF" "$pid" "$SELF"
|
|
return 0
|
|
fi
|
|
|
|
local -a parts=() changed=()
|
|
if [ -n "$rename" ]; then
|
|
parts+=("\"domain_names\":[\"$(json_str_escape "$rename")\"]")
|
|
changed+=("Domain -> $rename")
|
|
fi
|
|
if [ -n "$fhost" ]; then
|
|
parts+=("\"forward_host\":\"$(json_str_escape "$fhost")\""); changed+=("Ziel-Host -> $fhost")
|
|
fi
|
|
if [ -n "$fport" ]; then
|
|
valid_port "$fport" || die "--forward-port: keine gueltige Portnummer."
|
|
parts+=("\"forward_port\":$fport"); changed+=("Ziel-Port -> $fport")
|
|
fi
|
|
if [ -n "$scheme" ]; then
|
|
case "$scheme" in http|https) : ;; *) die "--scheme: http oder https" ;; esac
|
|
parts+=("\"forward_scheme\":\"$scheme\""); changed+=("Schema -> $scheme")
|
|
fi
|
|
if [ -n "$advset" ]; then
|
|
parts+=("\"advanced_config\":\"$(json_str_escape "$adv")\"")
|
|
changed+=("erweiterte Konfiguration ersetzt")
|
|
fi
|
|
local b
|
|
for b in "fssl:--force-ssl:ssl_forced:HTTPS erzwingen" \
|
|
"h2:--http2:http2_support:HTTP/2" \
|
|
"ws:--websockets:allow_websocket_upgrade:WebSockets" \
|
|
"blk:--block-exploits:block_exploits:Exploit-Sperre"; do
|
|
local var opt rest key label val
|
|
var="${b%%:*}"; rest="${b#*:}"
|
|
opt="${rest%%:*}"; rest="${rest#*:}"
|
|
key="${rest%%:*}"; label="${rest#*:}"
|
|
eval "val=\"\${$var}\""
|
|
[ -n "$val" ] || continue
|
|
case "$(fw_lc "$val")" in
|
|
on|true|1|ja|yes) val=true ;;
|
|
off|false|0|nein|no) val=false ;;
|
|
*) die "$opt: on oder off" ;;
|
|
esac
|
|
parts+=("\"$key\":$val"); changed+=("$label -> $val")
|
|
done
|
|
if [ -n "$enabled" ]; then
|
|
parts+=("\"enabled\":$enabled")
|
|
changed+=("Proxy-Host $([ "$enabled" = true ] && echo eingeschaltet || echo ausgeschaltet)")
|
|
fi
|
|
|
|
if [ -n "$cert" ]; then
|
|
local certid="" cdom
|
|
cdom="${rename:-$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p')}"
|
|
case "$cert" in
|
|
self) certid="$(npm_cert_selfsigned "$cdom")" || return 1 ;;
|
|
letsencrypt) certid="$(npm_cert_letsencrypt "$cdom")" || return 1 ;;
|
|
none) certid=0 ;;
|
|
*[!0-9]*) die "--cert: self | letsencrypt | none | <ID>" ;;
|
|
*) certid="$cert" ;;
|
|
esac
|
|
parts+=("\"certificate_id\":$certid")
|
|
changed+=("Zertifikat -> $certid")
|
|
if [ "$certid" = 0 ]; then
|
|
parts+=("\"ssl_forced\":false" "\"http2_support\":false")
|
|
changed+=("HTTPS erzwingen -> false (ohne Zertifikat nicht moeglich)")
|
|
fi
|
|
fi
|
|
|
|
# Pfade: die API ersetzt das ganze Feld, also aus dem Bestand neu bauen
|
|
if [ "${#addloc[@]}" -gt 0 ] || [ "${#delloc[@]}" -gt 0 ]; then
|
|
local cur; cur="$(json_raw "$o" locations)"
|
|
local -a keep=()
|
|
local el elpath spec p drop
|
|
while IFS= read -r el; do
|
|
[ -n "$el" ] || continue
|
|
elpath="$(json_str "$el" path)"
|
|
drop=""
|
|
for p in ${delloc[@]+"${delloc[@]}"}; do
|
|
case "$p" in /*) : ;; *) p="/$p" ;; esac
|
|
if [ "$p" = "$elpath" ]; then drop=1; changed+=("Pfad $elpath entfernt"); fi
|
|
done
|
|
for spec in ${addloc[@]+"${addloc[@]}"}; do
|
|
p="${spec%%=*}"; case "$p" in /*) : ;; *) p="/$p" ;; esac
|
|
if [ "$p" = "$elpath" ]; then
|
|
drop=1
|
|
if [ -n "$(json_str "$el" advanced_config)" ]; then
|
|
warn "Der Pfad $elpath hatte eine eigene erweiterte Konfiguration.
|
|
Beim Ersetzen geht sie verloren."
|
|
fi
|
|
fi
|
|
done
|
|
[ -n "$drop" ] || keep+=("$el")
|
|
done < <(json_split_array "$cur")
|
|
|
|
for spec in ${addloc[@]+"${addloc[@]}"}; do
|
|
local lj; lj="$(proxy_location_json "$spec")" || return 1
|
|
keep+=("$lj")
|
|
changed+=("Pfad ${spec%%=*} -> ${spec#*=}")
|
|
done
|
|
|
|
local joined=""
|
|
for el in ${keep[@]+"${keep[@]}"}; do
|
|
if [ -n "$joined" ]; then joined="$joined,$el"; else joined="$el"; fi
|
|
done
|
|
parts+=("\"locations\":[$joined]")
|
|
fi
|
|
|
|
local payload="{" first=1 x
|
|
for x in "${parts[@]}"; do
|
|
if [ -n "$first" ]; then payload="$payload$x"; first=""; else payload="$payload,$x"; fi
|
|
done
|
|
payload="$payload}"
|
|
|
|
log "Aendere Proxy-Host $pid ..."
|
|
local body; body="$(npm_api PUT "/api/nginx/proxy-hosts/$pid" "$payload")"
|
|
case "$(npm_code)" in
|
|
200|201) : ;;
|
|
*) err "Aenderung fehlgeschlagen (HTTP $(npm_code))."
|
|
local m; m="$(json_str "$body" message)"
|
|
if [ -n "$m" ]; then printf ' Antwort: %s\n' "$m" >&2; fi
|
|
return 1 ;;
|
|
esac
|
|
ok "Proxy-Host $pid geaendert."
|
|
for x in "${changed[@]}"; do printf ' %s\n' "$x"; done
|
|
|
|
# Der Proxy Manager biegt manches still zurecht - ohne Zertifikat
|
|
# etwa HTTP/2 und die HTTPS-Umleitung. Das darf nicht unbemerkt
|
|
# bleiben, sonst sucht man den Fehler spaeter an der falschen Stelle.
|
|
local k want got
|
|
for x in "${parts[@]}"; do
|
|
k="${x%%:*}"; k="${k#\"}"; k="${k%\"}"
|
|
case "$k" in locations|domain_names|advanced_config) continue ;; esac
|
|
want="${x#*:}"
|
|
got="$(json_raw "$body" "$k")"
|
|
[ -n "$got" ] || continue
|
|
[ "$got" != "$want" ] || continue
|
|
warn "'$k': gesendet wurde $want, gespeichert ist $got."
|
|
if [ "$(json_raw "$body" certificate_id)" = 0 ]; then
|
|
printf ' Ohne Zertifikat laesst der Proxy Manager das nicht zu.
|
|
Erst eines zuweisen: %s proxy edit --id %s --cert self\n' "$SELF" "$pid"
|
|
fi
|
|
done
|
|
echo
|
|
cmd_proxy show --id "$pid"
|
|
if grep -qsF "$NPM_DEF_START" "$(npm_custom_conf_path)"; then
|
|
log "Der Standard-Server fuer den IP-Zugang spiegelt einen Stand von vorher.
|
|
Nach inhaltlichen Aenderungen neu schreiben:
|
|
$SELF proxy default --domain <name> --restart"
|
|
fi
|
|
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 | edit | del | default" ;;
|
|
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" \
|
|
"$( ( set +o pipefail; 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 holen (Registry oder Downloadportal)" \
|
|
wizard "Assistent: Edition, Version und Bezugsweg waehlen" \
|
|
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_get || true ;;
|
|
wizard) 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)."
|
|
}
|
|
|
|
# Holen, wenn Edition und Version schon feststehen: nur noch die Frage
|
|
# nach dem Bezugsweg. Der volle Assistent bleibt als Wahlmoeglichkeit.
|
|
gui_image_get() {
|
|
if [ -z "${CMK_EDITION:-}" ] || [ -z "${CMK_VERSION:-}" ]; then
|
|
ui_msg "Erst Edition und Version" \
|
|
"Es ist noch keine Edition bzw. Version eingestellt.
|
|
|
|
Der Assistent fragt beides ab und laesst sich gleich
|
|
anschliessen."
|
|
gui_image_wizard || true
|
|
return 0
|
|
fi
|
|
|
|
local ref="" vorgabe
|
|
if edition_known "${CMK_EDITION}" 2>/dev/null; then
|
|
ref="$(edition_repo "$CMK_EDITION"):$CMK_VERSION"
|
|
fi
|
|
vorgabe="$(image_source_default)"
|
|
|
|
# Der eingestellte Weg wird gekennzeichnet, statt ihn doppelt
|
|
# aufzufuehren - sonst gaebe es zwei Eintraege mit derselben Kennung.
|
|
local m_auto="erst Registry, dann Downloadportal"
|
|
local m_reg="nur aus der Registry"
|
|
local m_dl="nur als Tarball vom Downloadportal"
|
|
case "$vorgabe" in
|
|
auto) m_auto="$m_auto (eingestellt)" ;;
|
|
registry) m_reg="$m_reg (eingestellt)" ;;
|
|
download) m_dl="$m_dl (eingestellt)" ;;
|
|
esac
|
|
|
|
local sel
|
|
sel="$(ui_menu "Image holen" \
|
|
"Edition/Version: ${CMK_EDITION} / ${CMK_VERSION}${ref:+
|
|
Registry-Referenz: $ref}
|
|
|
|
Woher soll das Image geholt werden?" \
|
|
auto "$m_auto" \
|
|
registry "$m_reg" \
|
|
download "$m_dl" \
|
|
datei "Vorhandene Tarball-Datei einspielen" \
|
|
assistent "Etwas anderes waehlen (Edition/Version aendern)")" || return 0
|
|
|
|
case "$sel" in
|
|
assistent) gui_image_wizard || true; return 0 ;;
|
|
datei)
|
|
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' && NO_COLOR=1 '$SELF' -y image load --file '$lf'"
|
|
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
return 0 ;;
|
|
esac
|
|
|
|
ui_run "Image holen ($sel)" bash -c \
|
|
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' -y image get --source '$sel'"
|
|
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
|
|
local f; f="$(mktemp)"
|
|
NO_COLOR=1 cmd_image status >"$f" 2>&1 || true
|
|
ui_text "Stand nach dem Holen" "$f"; rm -f "$f"
|
|
}
|
|
|
|
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
|
|
# Was ergaebe sich aus den Vorgaben? Das gehoert in die Beschriftung,
|
|
# damit die Richtung der beiden Wege klar ist.
|
|
local abgeleitet=""
|
|
if [ -n "${CMK_EDITION:-}" ] && [ -n "${CMK_VERSION:-}" ] \
|
|
&& edition_known "${CMK_EDITION}" 2>/dev/null; then
|
|
abgeleitet="$(edition_repo "$CMK_EDITION"):$CMK_VERSION"
|
|
fi
|
|
|
|
sel2="$(ui_menu "$key" \
|
|
"Aktuell: ${cur:-<leer>}
|
|
|
|
$lage" \
|
|
wizard "Image beschaffen (Edition, Version, Bezugsweg)" \
|
|
setzen "Image aus den Vorgaben setzen${abgeleitet:+ -> $abgeleitet}" \
|
|
angleichen "Umgekehrt: Vorgaben aus dem Image ableiten" \
|
|
nichts "Nichts aendern")" || { load_env "${ENV_FILE:-$PROJECT_DIR/.env}"; return 0; }
|
|
case "$sel2" in
|
|
wizard) gui_image_wizard || true ;;
|
|
setzen)
|
|
if [ -z "$abgeleitet" ]; then
|
|
ui_msg "Nicht moeglich" \
|
|
"Aus den Vorgaben laesst sich keine Referenz bilden.
|
|
|
|
Edition: ${CMK_EDITION:-<leer>}
|
|
Version: ${CMK_VERSION:-<leer>}
|
|
|
|
Beides muss gesetzt sein - der Assistent fragt danach."
|
|
elif ui_yesno "Image setzen" \
|
|
"Aus Edition und Version wird:
|
|
|
|
$abgeleitet
|
|
|
|
Heruntergeladen wird nichts - das holt die Runtime beim
|
|
naechsten Start selbst nach.
|
|
|
|
Uebernehmen?"; then
|
|
ui_run "Image setzen" bash -c \
|
|
"cd '$PROJECT_DIR' && '$SELF' -y image use --edition '$CMK_EDITION' --version '$CMK_VERSION'"
|
|
fi ;;
|
|
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+=("__get__" ">> Image jetzt holen (Registry oder Downloadportal)")
|
|
items+=("__wizard__" ">> Assistent: 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")
|
|
items+=("__npmrst__" ">> Datenbank zuruecksetzen (Konto neu anlegen lassen)") ;;
|
|
backup) items+=("__jobs__" ">> Backup-Jobs verwalten") ;;
|
|
firewall) items+=("__fwcheck__" ">> Firewall jetzt pruefen")
|
|
items+=("__fwapply__" ">> Firewall-Regeln uebernehmen (Ports oeffnen)")
|
|
items+=("__fwclose__" ">> Nicht mehr benoetigte Ports schliessen") ;;
|
|
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 ;;
|
|
__get__) gui_image_get || true; continue ;;
|
|
__wizard__) gui_image_wizard || true; continue ;;
|
|
__proxy__) gui_proxy_menu || true; continue ;;
|
|
__npmrst__) gui_npm_reset || true; continue ;;
|
|
__test__) gui_image_test || true; continue ;;
|
|
__jobs__) gui_jobs_menu || true; continue ;;
|
|
__fwcheck__) gui_firewall || true; continue ;;
|
|
__fwapply__) gui_firewall_apply || true; continue ;;
|
|
__fwclose__) gui_firewall_close || 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
|
|
# ---------------------------------------------------------------------
|
|
# Datenbank des Proxy Managers zuruecksetzen, damit der Container das
|
|
# Konto beim naechsten Start neu aus NPM_ADMIN_EMAIL/-PASSWORD anlegt.
|
|
gui_npm_reset() {
|
|
local d="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
|
|
if ! npm_db_exists; then
|
|
ui_msg "Nichts zurueckzusetzen" \
|
|
"Der Proxy Manager hat noch keine Datenbank.
|
|
|
|
Es genuegt, die Zugangsdaten zu setzen und den Stack zu
|
|
starten - der Container legt das Konto daraus selbst an:
|
|
|
|
Einstellungen -> NGINX Proxy Manager
|
|
NPM_ADMIN_EMAIL
|
|
NPM_ADMIN_PASSWORD
|
|
danach: Stack steuern -> Stack starten"
|
|
return 0
|
|
fi
|
|
|
|
# Ohne Zugangsdaten entstuende beim naechsten Start wieder kein Konto
|
|
if [ -z "${NPM_ADMIN_EMAIL:-}" ] || [ -z "${NPM_ADMIN_PASSWORD:-}" ]; then
|
|
if ui_yesno "Zugangsdaten fehlen" \
|
|
"NPM_ADMIN_EMAIL bzw. NPM_ADMIN_PASSWORD sind nicht gesetzt.
|
|
|
|
Ohne sie entsteht beim naechsten Start wieder KEIN Konto -
|
|
der Proxy Manager zeigt dann erneut den Einrichtungsassistenten.
|
|
|
|
Jetzt eintragen?"; then
|
|
gui_settings_edit NPM_ADMIN_EMAIL
|
|
gui_settings_edit NPM_ADMIN_PASSWORD
|
|
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
fi
|
|
if [ -z "${NPM_ADMIN_EMAIL:-}" ] || [ -z "${NPM_ADMIN_PASSWORD:-}" ]; then
|
|
ui_msg "Abgebrochen" "Ohne Zugangsdaten hat das Zuruecksetzen keinen Zweck."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local groesse
|
|
groesse="$(du -h "$d/database.sqlite" 2>/dev/null | cut -f1 || echo '?')"
|
|
ui_yesno "Datenbank zuruecksetzen" \
|
|
"Die Datenbank wird beiseitegelegt - umbenannt, nicht geloescht:
|
|
|
|
$d/database.sqlite ($groesse)
|
|
|
|
${C_BLD}Das ist danach weg${C_RST}
|
|
Konten, Proxy-Hosts, Weiterleitungen, Zugriffslisten
|
|
|
|
${C_BLD}Das bleibt${C_RST}
|
|
Zertifikate (Let's Encrypt und eigene)
|
|
|
|
Beim naechsten Start legt der Container das Konto neu an:
|
|
${NPM_ADMIN_EMAIL}
|
|
|
|
Proxy-Hosts lassen sich anschliessend unter 'Proxy-Hosts'
|
|
in einem Durchgang neu anlegen.
|
|
|
|
Fortfahren?" || return 0
|
|
|
|
ui_run "Datenbank zuruecksetzen" bash -c \
|
|
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' -y npm-reset"
|
|
|
|
if ui_yesno "Jetzt starten?" \
|
|
"Damit das Konto entsteht, muss der Proxy Manager neu starten.
|
|
|
|
Stack jetzt starten?"; then
|
|
ui_run "Stack starten" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' -y up"
|
|
fi
|
|
load_env "${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
}
|
|
|
|
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" \
|
|
edit "Proxy-Host bearbeiten (Ziel, Pfade, Zertifikat ...)" \
|
|
show "Einzelheiten eines Proxy-Hosts (mit Pfaden)" \
|
|
certs "Vorhandene Zertifikate anzeigen" \
|
|
ipzugang "HTTPS ueber die blanke IP (ohne Namen) ermoeglichen" \
|
|
del "Proxy-Host loeschen" \
|
|
status "Anmeldung pruefen" \
|
|
reset "Datenbank zuruecksetzen (Konto neu anlegen lassen)" \
|
|
back "zurueck")" || return 0
|
|
case "$sel" in
|
|
anlegen) gui_proxy_add || true ;;
|
|
edit) gui_proxy_edit || 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" ;;
|
|
ipzugang) gui_proxy_default || true ;;
|
|
del) gui_proxy_del || true ;;
|
|
status) ui_run "Anmeldung pruefen" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' proxy status" ;;
|
|
reset) gui_npm_reset || true ;;
|
|
back) return 0 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Proxy-Host auswaehlen -> ID auf stdout
|
|
gui_pick_proxy() { # titel
|
|
if ! npm_require_running >/dev/null 2>&1; then
|
|
ui_msg "Proxy Manager" "Der Proxy Manager laeuft nicht oder ist abgeschaltet." >&2
|
|
return 1
|
|
fi
|
|
NPM_TOKEN="$(npm_api_token 2>/dev/null)" || {
|
|
ui_msg "Proxy Manager" "Anmeldung fehlgeschlagen.
|
|
|
|
Zugangsdaten pruefen unter Einstellungen > NGINX Proxy Manager,
|
|
oder die Datenbank zuruecksetzen." >&2
|
|
return 1
|
|
}
|
|
local all i o dom
|
|
local -a items=()
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
for i in $(json_ids "$all"); do
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$i")"
|
|
dom="$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
items+=("$i" "${dom:-?} -> $(json_str "$o" forward_host):$(json_num "$o" forward_port)")
|
|
done
|
|
if [ "${#items[@]}" -eq 0 ]; then
|
|
ui_msg "Proxy-Hosts" "Es gibt noch keinen Proxy-Host.
|
|
|
|
Anlegen unter: Proxy-Host anlegen (Assistent)" >&2
|
|
return 1
|
|
fi
|
|
ui_menu "${1:-Proxy-Host waehlen}" "Welchen Eintrag?" "${items[@]}"
|
|
}
|
|
|
|
# Einen vorhandenen Proxy-Host bearbeiten
|
|
gui_proxy_edit() {
|
|
local pid
|
|
pid="$(gui_pick_proxy "Proxy-Host bearbeiten")" || return 0
|
|
[ -n "$pid" ] || return 0
|
|
# gui_pick_proxy lief in einer Kommandoersetzung, sein Token ist mit der
|
|
# Subshell verschwunden - hier neu holen.
|
|
NPM_TOKEN="$(npm_api_token)" || { ui_msg "Proxy Manager" "Anmeldung fehlgeschlagen."; return 0; }
|
|
|
|
while :; do
|
|
local o dom
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
|
|
if [ -z "$(json_num "$o" id)" ]; then
|
|
ui_msg "Proxy-Host" "Der Eintrag $pid ist nicht mehr da."; return 0
|
|
fi
|
|
dom="$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
|
|
local sel
|
|
sel="$(ui_menu "Proxy-Host $pid bearbeiten" \
|
|
"Name : $dom
|
|
Ziel : $(json_str "$o" forward_scheme)://$(json_str "$o" forward_host):$(json_num "$o" forward_port)
|
|
Zertifikat : $(json_num "$o" certificate_id) Aktiv: $(json_raw "$o" enabled)
|
|
|
|
Jede Aenderung wirkt sofort; alles Uebrige bleibt unangetastet." \
|
|
ziel "Weiterleitungsziel (Host und Port)" \
|
|
schema "Schema zum Ziel (http/https)" \
|
|
name "Domainname aendern" \
|
|
cert "Zertifikat zuweisen oder entfernen" \
|
|
pfad "Pfad hinzufuegen oder ersetzen" \
|
|
pfadweg "Pfad entfernen" \
|
|
schalter "Schalter (HTTPS erzwingen, HTTP/2, WebSockets, Sperre)" \
|
|
anaus "Proxy-Host ein- oder ausschalten" \
|
|
adv "Erweiterte nginx-Konfiguration" \
|
|
zeigen "Aktuellen Stand anzeigen" \
|
|
back "zurueck")" || return 0
|
|
|
|
local -a args=()
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
zeigen) local f; f="$(mktemp)"
|
|
NO_COLOR=1 cmd_proxy show --id "$pid" >"$f" 2>&1 || true
|
|
ui_text "Proxy-Host $pid" "$f"; rm -f "$f"; continue ;;
|
|
ziel) local h pt
|
|
h="$(ui_input "Weiterleitungsziel" "Host:" "$(json_str "$o" forward_host)")" || continue
|
|
[ -n "$h" ] || continue
|
|
pt="$(ui_input "Weiterleitungsziel" "Port:" "$(json_num "$o" forward_port)")" || continue
|
|
[ -n "$pt" ] || continue
|
|
args=(--forward-host "$h" --forward-port "$pt") ;;
|
|
schema) local sc
|
|
sc="$(ui_menu "Schema zum Ziel" "Wie spricht der Proxy das Ziel an?" \
|
|
http "unverschluesselt (der Regelfall im eigenen Netz)" \
|
|
https "verschluesselt")" || continue
|
|
args=(--scheme "$sc") ;;
|
|
name) local nn
|
|
nn="$(ui_input "Domainname" "Neuer Name:" "$dom")" || continue
|
|
[ -n "$nn" ] || continue
|
|
args=(--rename "$nn") ;;
|
|
cert) local cc
|
|
cc="$(ui_menu "Zertifikat" "Was soll ausgeliefert werden?" \
|
|
self "selbstsigniert (fuer das eigene Netz)" \
|
|
letsencrypt "Lets Encrypt (Server muss oeffentlich erreichbar sein)" \
|
|
none "keines - nur HTTP")" || continue
|
|
args=(--cert "$cc") ;;
|
|
pfad) local sp
|
|
sp="$(ui_input "Pfad" "Pfad und Ziel, z.B. /grafana=127.0.0.1:3000" "")" || continue
|
|
[ -n "$sp" ] || continue
|
|
args=(--location "$sp") ;;
|
|
pfadweg)
|
|
local el
|
|
local -a pitems=()
|
|
while IFS= read -r el; do
|
|
[ -n "$el" ] || continue
|
|
pitems+=("$(json_str "$el" path)" "-> $(json_str "$el" forward_host):$(json_num "$el" forward_port)")
|
|
done < <(json_split_array "$(json_raw "$o" locations)")
|
|
if [ "${#pitems[@]}" -eq 0 ]; then
|
|
ui_msg "Pfade" "Dieser Proxy-Host hat keine zusaetzlichen Pfade."; continue
|
|
fi
|
|
local dp
|
|
dp="$(ui_menu "Pfad entfernen" "Welchen Pfad?" "${pitems[@]}")" || continue
|
|
ui_yesno "Pfad entfernen" "Pfad $dp wirklich entfernen?" || continue
|
|
args=(--del-location "$dp") ;;
|
|
schalter)
|
|
local cur
|
|
cur="$(ui_checklist "Schalter" "Mit der Leertaste umschalten:" \
|
|
force-ssl "HTTP auf HTTPS umleiten" "$(json_bool_on "$o" ssl_forced)" \
|
|
http2 "HTTP/2" "$(json_bool_on "$o" http2_support)" \
|
|
websockets "WebSockets durchreichen" "$(json_bool_on "$o" allow_websocket_upgrade)" \
|
|
block-exploits "Bekannte Angriffsmuster sperren" "$(json_bool_on "$o" block_exploits)")" || continue
|
|
local o1 o2 o3 o4
|
|
case " $cur " in *" force-ssl "*|*'"force-ssl"'*) o1=on ;; *) o1=off ;; esac
|
|
case " $cur " in *" http2 "*|*'"http2"'*) o2=on ;; *) o2=off ;; esac
|
|
case " $cur " in *" websockets "*|*'"websockets"'*) o3=on ;; *) o3=off ;; esac
|
|
case " $cur " in *" block-exploits "*|*'"block-exploits"'*) o4=on ;; *) o4=off ;; esac
|
|
args=(--force-ssl "$o1" --http2 "$o2" --websockets "$o3" --block-exploits "$o4") ;;
|
|
anaus) if [ "$(json_bool_on "$o" enabled)" = on ]; then
|
|
ui_yesno "Ausschalten" "Proxy-Host $pid ausschalten?" || continue
|
|
args=(--disable)
|
|
else
|
|
ui_yesno "Einschalten" "Proxy-Host $pid einschalten?" || continue
|
|
args=(--enable)
|
|
fi ;;
|
|
adv) local a1 curadv
|
|
curadv="$(json_raw "$o" advanced_config)"
|
|
curadv="${curadv#\"}"; curadv="${curadv%\"}"
|
|
a1="$(ui_input "Erweiterte Konfiguration" \
|
|
"nginx-Anweisungen fuer diesen Proxy-Host.
|
|
Mehrere Anweisungen durch \\\\n trennen." "$curadv")" || continue
|
|
args=(--advanced "$a1") ;;
|
|
esac
|
|
|
|
[ "${#args[@]}" -gt 0 ] || continue
|
|
local f2 rc=0
|
|
f2="$(mktemp)"
|
|
NO_COLOR=1 cmd_proxy edit --id "$pid" "${args[@]}" >"$f2" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Proxy-Host $pid geaendert" "$f2"
|
|
else
|
|
ui_text "Aenderung fehlgeschlagen" "$f2"
|
|
fi
|
|
rm -f "$f2"
|
|
done
|
|
}
|
|
|
|
gui_proxy_default() {
|
|
local f rc=0
|
|
f="$(mktemp)"
|
|
NO_COLOR=1 cmd_proxy default >"$f" 2>&1 || true
|
|
ui_text "HTTPS ueber die blanke IP" "$f"; rm -f "$f"
|
|
|
|
local sel
|
|
sel="$(ui_menu "HTTPS ueber die blanke IP" \
|
|
"Ruft man den Server als https://<IP>/ auf, schickt der Browser keinen
|
|
Namen mit - SNI kennt keine IP-Adressen. nginx findet den Proxy-Host
|
|
dann nicht und lehnt den Handshake ab (ERR_SSL_UNRECOGNIZED_NAME_ALERT).
|
|
Ueber http://<IP>/ faellt das nicht auf, dort zaehlt der Host-Kopf.
|
|
|
|
Ein Standard-Server nimmt genau diese Aufrufe an und geht denselben Weg
|
|
wie der gewaehlte Proxy-Host, samt seiner zusaetzlichen Pfade.
|
|
Aufrufe mit Namen bleiben unberuehrt." \
|
|
ein "Einrichten - Proxy-Host spiegeln" \
|
|
aus "Entfernen" \
|
|
back "zurueck")" || return 0
|
|
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
aus) ui_run "Standard-Server entfernen" bash -c \
|
|
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' proxy default --off --restart" || true
|
|
return 0 ;;
|
|
esac
|
|
|
|
local dom
|
|
dom="$(ui_input "Einrichten" "Name des Proxy-Hosts, der gespiegelt werden soll:" \
|
|
"${PROXY_DOMAIN:-}")" || return 0
|
|
[ -n "$dom" ] || return 0
|
|
|
|
f="$(mktemp)"
|
|
NO_COLOR=1 cmd_proxy default --domain "$dom" --restart >"$f" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Standard-Server eingerichtet" "$f"
|
|
else
|
|
ui_text "Einrichten fehlgeschlagen" "$f"
|
|
fi
|
|
rm -f "$f"
|
|
return 0
|
|
}
|
|
|
|
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[*]}}"
|
|
}
|
|
|
|
# Bericht anzeigen; fehlt etwas, das Uebernehmen anbieten
|
|
gui_firewall() {
|
|
local tmp rc=0
|
|
tmp="$(mktemp)"
|
|
cmd_firewall check >"$tmp" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Firewall - alles offen, was gebraucht wird" "$tmp"
|
|
rm -f "$tmp"; return 0
|
|
fi
|
|
ui_text "Firewall - es fehlen Freigaben" "$tmp"
|
|
rm -f "$tmp"
|
|
|
|
ui_yesno "Firewall" "Es fehlen Freigaben.
|
|
|
|
Die benoetigten Ports jetzt oeffnen?" || return 0
|
|
gui_firewall_apply
|
|
}
|
|
|
|
# Regeln uebernehmen. Erst wird gefragt, ob auf ein Quellnetz begrenzt
|
|
# werden soll - denn davon haengen die Kommandos ab, die danach zur
|
|
# Bestaetigung angezeigt werden. Was dort steht, wird auch ausgefuehrt.
|
|
gui_firewall_apply() {
|
|
local src="" tmp rc=0
|
|
if ui_yesno "Firewall-Regeln uebernehmen" "Nur fuer ein bestimmtes Quellnetz freigeben?
|
|
|
|
Ja = nur dieses Netz darf zugreifen
|
|
Nein = Freigabe fuer alle Quellen"; then
|
|
src="$(ui_input "Firewall-Regeln uebernehmen" "Quellnetz in CIDR-Schreibweise:" \
|
|
"$(primary_ip | sed 's/\.[0-9]*$/.0\/24/')")" || return 0
|
|
[ -n "$src" ] || return 0
|
|
fi
|
|
|
|
local -a opt=(open)
|
|
if [ -n "$src" ]; then opt+=(--source "$src"); fi
|
|
|
|
tmp="$(mktemp)"
|
|
cmd_firewall "${opt[@]}" >"$tmp" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Firewall - nichts zu tun" "$tmp"
|
|
rm -f "$tmp"; return 0
|
|
fi
|
|
ui_text "Firewall - diese Regeln werden gesetzt" "$tmp"
|
|
rm -f "$tmp"
|
|
|
|
ui_yesno "Firewall-Regeln uebernehmen" "Die angezeigten Regeln jetzt dauerhaft setzen?" || return 0
|
|
|
|
local -a a=(firewall "${opt[@]}" --apply --yes)
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
ui_run "Firewall-Regeln uebernehmen" "$SELF" "${a[@]}" || true
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" "${a[@]}"
|
|
else
|
|
ui_msg "Firewall" "Dafuer werden root-Rechte benoetigt.
|
|
Bitte als root erneut aufrufen."
|
|
return 0
|
|
fi
|
|
|
|
# Zeigen, was dabei herausgekommen ist
|
|
tmp="$(mktemp)"
|
|
cmd_firewall check >"$tmp" 2>&1 || true
|
|
ui_text "Firewall - Stand danach" "$tmp"
|
|
rm -f "$tmp"
|
|
return 0
|
|
}
|
|
|
|
# Nicht mehr benoetigte Ports wieder schliessen
|
|
gui_firewall_close() {
|
|
local tmp rc=0 hat=""
|
|
tmp="$(mktemp)"
|
|
cmd_firewall close >"$tmp" 2>&1 || rc=$?
|
|
if ( set +o pipefail; grep -q 'Zum Schliessen' "$tmp" ); then hat=1; fi
|
|
ui_text "Nicht mehr benoetigte Ports" "$tmp"
|
|
rm -f "$tmp"
|
|
[ -n "$hat" ] || return 0
|
|
|
|
ui_yesno "Ports schliessen" "Die angezeigten Ports jetzt schliessen?
|
|
|
|
SSH bleibt in jedem Fall offen - Port 22, der Port von sshd und der
|
|
Port dieser Sitzung. Freigaben fuer fremde Dienste werden nicht
|
|
angeruehrt." || return 0
|
|
|
|
local -a a=(firewall close --apply --yes)
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
ui_run "Ports schliessen" "$SELF" "${a[@]}" || true
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" "${a[@]}"
|
|
else
|
|
ui_msg "Firewall" "Dafuer werden root-Rechte benoetigt.
|
|
Bitte als root erneut aufrufen."
|
|
return 0
|
|
fi
|
|
|
|
tmp="$(mktemp)"
|
|
cmd_firewall check >"$tmp" 2>&1 || true
|
|
ui_text "Firewall - Stand danach" "$tmp"
|
|
rm -f "$tmp"
|
|
return 0
|
|
}
|
|
|
|
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" \
|
|
firewall "Firewall pruefen (welche Ports muessen offen sein)" \
|
|
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 ;;
|
|
firewall) gui_firewall || 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."
|
|
}
|
|
|
|
# =====================================================================
|
|
# Terminal pruefen
|
|
#
|
|
# Zeigt zwei Testzeilen. Woran es liegt, laesst sich an ihnen ablesen -
|
|
# ohne Raten:
|
|
# Zeile A sind echte UTF-8-Rahmenzeichen (U+2500 ff.)
|
|
# Zeile B ist die alte VT100-Umschaltung ESC(0 mit den Buchstaben lqk
|
|
# Genau diese Buchstaben erscheinen, wenn das Terminal die Umschaltung
|
|
# nicht mitmacht - der Fall bei PuTTY im UTF-8-Modus.
|
|
# =====================================================================
|
|
cmd_tui_check() {
|
|
ui_fix_locale || true
|
|
ui_init
|
|
|
|
printf '\n%sWas dieses Terminal meldet%s\n' "$C_BLD" "$C_RST"
|
|
printf ' TERM : %s\n' "${TERM:-<leer>}"
|
|
printf ' LANG : %s\n' "${LANG:-<leer>}"
|
|
printf ' LC_ALL : %s\n' "${LC_ALL:-<leer>}"
|
|
printf ' LC_CTYPE : %s\n' "${LC_CTYPE:-<leer>}"
|
|
printf ' Zeichensatz : %s\n' "$(locale charmap 2>/dev/null || echo unbekannt)"
|
|
printf ' Programm : %s\n' "${UI_BIN:-keines (dialog/whiptail fehlt)}"
|
|
printf ' Unicode-ACS : %s\n' \
|
|
"$([ -n "${NCURSES_NO_UTF8_ACS:-}" ] && echo 'ein (ncurses zeichnet Unicode)' || echo 'aus (ncurses nutzt ESC(0)')"
|
|
[ -n "${UI_LOCALE_NOTE:-}" ] && printf ' Angepasst : %s\n' "$UI_LOCALE_NOTE"
|
|
|
|
printf '\n%sZeile A - echte UTF-8-Rahmenzeichen%s\n' "$C_BLD" "$C_RST"
|
|
printf ' ┌────────┐\n'
|
|
printf ' │ Rahmen │\n'
|
|
printf ' └────────┘\n'
|
|
|
|
printf '\n%sZeile B - alte VT100-Umschaltung (ESC(0)%s\n' "$C_BLD" "$C_RST"
|
|
printf ' \033(0lqqqqqqqqk\033(B\n'
|
|
printf ' \033(0x\033(B Rahmen \033(0x\033(B\n'
|
|
printf ' \033(0mqqqqqqqqj\033(B\n'
|
|
|
|
cat <<EOF
|
|
|
|
${C_BLD}Auswertung${C_RST}
|
|
A zeigt Linien, B zeigt Linien
|
|
Alles in Ordnung - die Oberflaeche muss sauber aussehen.
|
|
|
|
A zeigt Linien, B zeigt lqqqk / x / mqqj
|
|
Das Terminal macht die VT100-Umschaltung nicht mit - der
|
|
PuTTY-Fall. Das Skript faengt das ab: es setzt
|
|
NCURSES_NO_UTF8_ACS=1, womit ncurses (und damit dialog)
|
|
dieselben Zeichen wie in Zeile A verwendet. Die Oberflaeche
|
|
muss dadurch sauber aussehen, obwohl Zeile B es nicht tut.
|
|
|
|
Sieht sie trotzdem falsch aus, hilft in PuTTY:
|
|
Window -> Translation -> Remote character set: UTF-8
|
|
Window -> Translation -> Handling of line drawing characters:
|
|
"Use Unicode line drawing code points"
|
|
Connection -> Data -> Terminal-type string: xterm-256color
|
|
Auf dieser Seite hilft zusaetzlich 'dialog' statt 'whiptail':
|
|
dnf install -y dialog bzw. apt-get install -y dialog
|
|
Oder einfache Rahmen erzwingen:
|
|
CMK_TUI_ASCII=1 $SELF gui
|
|
|
|
A zeigt Kaese wie a-- oder Fragezeichen
|
|
Das Terminal laeuft NICHT im UTF-8-Modus.
|
|
In PuTTY: Window -> Translation -> Remote character set: UTF-8
|
|
|
|
Beide Zeilen zeigen Buchstaben
|
|
Schrift ohne Rahmenzeichen. In PuTTY unter
|
|
Window -> Appearance eine andere waehlen, z.B. Consolas
|
|
oder DejaVu Sans Mono.
|
|
|
|
EOF
|
|
}
|
|
|
|
# =====================================================================
|
|
# Firewall
|
|
#
|
|
# RHEL und Rocky haben firewalld ab Werk aktiv, und dort ist in aller
|
|
# Regel nur SSH freigegeben. Der Agent-Receiver auf 8000 und HTTPS auf
|
|
# 443 waeren damit dicht - die Agents kaemen nicht durch, ohne dass es
|
|
# am Stack selbst laege.
|
|
#
|
|
# Geprueft werden firewalld, ufw, nftables und iptables. Welche Ports
|
|
# noetig sind, haengt von der Konfiguration ab: ohne Proxy Manager
|
|
# braucht der externe Proxy Zugriff auf 5000, als Remote-Site einer
|
|
# verteilten Ueberwachung kommt Livestatus dazu, mit Event Console
|
|
# Syslog und SNMP-Traps - und die laufen ueber UDP. Ports werden
|
|
# deshalb durchweg als "nummer/protokoll" gefuehrt.
|
|
# =====================================================================
|
|
|
|
fw_lc() { printf '%s' "${1:-}" | tr 'A-Z' 'a-z'; }
|
|
|
|
fw_on() { # Wert -> 0, wenn eingeschaltet
|
|
case "$(fw_lc "${1:-}")" in on|true|1|yes|ja) return 0 ;; esac
|
|
return 1
|
|
}
|
|
|
|
fw_detect() {
|
|
if command -v firewall-cmd >/dev/null 2>&1 \
|
|
&& firewall-cmd --state >/dev/null 2>&1; then
|
|
printf 'firewalld\n'; return 0
|
|
fi
|
|
if command -v ufw >/dev/null 2>&1 \
|
|
&& ( set +o pipefail; ufw status 2>/dev/null | grep -qi '^Status: active' ); then
|
|
printf 'ufw\n'; return 0
|
|
fi
|
|
if command -v nft >/dev/null 2>&1 && [ -n "$(nft list ruleset 2>/dev/null)" ]; then
|
|
printf 'nftables\n'; return 0
|
|
fi
|
|
if command -v iptables >/dev/null 2>&1 \
|
|
&& [ "$(iptables -S 2>/dev/null | wc -l)" -gt 3 ]; then
|
|
printf 'iptables\n'; return 0
|
|
fi
|
|
printf 'keine\n'
|
|
}
|
|
|
|
# firewalld kennt Zonen. Hat der Rechner mehrere Netzwerkkarten, haengen
|
|
# sie oft in verschiedenen Zonen - dann sagt die Standardzone allein
|
|
# nichts darueber aus, ob ein Port erreichbar ist.
|
|
fw_zones() {
|
|
if [ -n "${_FW_ZONES:-}" ]; then printf '%s\n' "$_FW_ZONES"; return 0; fi
|
|
local z
|
|
z="$(firewall-cmd --get-active-zones 2>/dev/null | grep -v '^[[:space:]]' || true)"
|
|
if [ -z "$z" ]; then z="$(firewall-cmd --get-default-zone 2>/dev/null || true)"; fi
|
|
_FW_ZONES="$(printf '%s\n' "$z" | sed '/^$/d')"
|
|
printf '%s\n' "$_FW_ZONES"
|
|
}
|
|
|
|
# Dienstnamen von firewalld auf port/protokoll abbilden
|
|
fw_service_port() {
|
|
case "$1" in
|
|
ssh) printf '22/tcp\n' ;;
|
|
http) printf '80/tcp\n' ;;
|
|
https) printf '443/tcp\n' ;;
|
|
cockpit) printf '9090/tcp\n' ;;
|
|
syslog) printf '514/udp\n514/tcp\n' ;;
|
|
syslog-tls) printf '6514/tcp\n' ;;
|
|
snmptrap) printf '162/udp\n' ;;
|
|
snmp) printf '161/udp\n' ;;
|
|
*) : ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Welche Ports braucht der Stack in dieser Konfiguration?
|
|
# port/proto|zweck|noetig (noetig: ja | nein | admin)
|
|
# ---------------------------------------------------------------------
|
|
fw_required_ports() {
|
|
local role; role="$(fw_lc "${FW_ROLE:-single}")"
|
|
case "$role" in central|remote) : ;; *) role=single ;; esac
|
|
|
|
if npm_enabled; then
|
|
printf '80/tcp|HTTP und Lets-Encrypt-Pruefung|ja\n'
|
|
printf '443/tcp|HTTPS (Checkmk, Zentrale, weitere Dienste)|ja\n'
|
|
printf '%s/tcp|Adminoberflaeche des Proxy Managers|admin\n' "${NPM_ADMIN_PORT:-81}"
|
|
printf '5000/tcp|Checkmk direkt - gehoert hinter den Proxy|nein\n'
|
|
else
|
|
printf '5000/tcp|Checkmk direkt - Ziel des externen Proxys|ja\n'
|
|
fi
|
|
|
|
printf '8000/tcp|Agent-Receiver (registrierte Agents)|ja\n'
|
|
|
|
# --- Verteilte Ueberwachung ---------------------------------------
|
|
# Als Remote-Site fragt die Zentrale Livestatus ab. Die Zentrale
|
|
# selbst baut nur Verbindungen auf und braucht dafuer nichts offen.
|
|
if [ "$role" = remote ]; then
|
|
printf '6557/tcp|Livestatus - die Zentrale fragt diese Site ab|ja\n'
|
|
elif fw_on "${CMK_LIVESTATUS_TCP:-off}"; then
|
|
printf '6557/tcp|Livestatus ueber TCP|ja\n'
|
|
else
|
|
printf '6557/tcp|Livestatus - aus (CMK_LIVESTATUS_TCP=off)|nein\n'
|
|
fi
|
|
|
|
# --- Event Console -------------------------------------------------
|
|
case "$(fw_lc "${FW_EC_SYSLOG:-off}")" in
|
|
udp) printf '514/udp|Event Console: Syslog|ja\n' ;;
|
|
tcp) printf '514/tcp|Event Console: Syslog ueber TCP|ja\n' ;;
|
|
beide|both|on)
|
|
printf '514/udp|Event Console: Syslog|ja\n'
|
|
printf '514/tcp|Event Console: Syslog ueber TCP|ja\n' ;;
|
|
esac
|
|
if fw_on "${FW_EC_SNMPTRAP:-off}"; then
|
|
printf '162/udp|Event Console: SNMP-Traps|ja\n'
|
|
fi
|
|
if fw_on "${FW_EC_TCP:-off}"; then
|
|
printf '6558/tcp|Event Console: Status und Ereignisse ueber TCP|ja\n'
|
|
fi
|
|
|
|
# --- Agent auf diesem Host ----------------------------------------
|
|
if fw_on "${FW_AGENT:-off}"; then
|
|
printf '6556/tcp|Checkmk-Agent dieses Hosts (Pull-Betrieb)|ja\n'
|
|
fi
|
|
|
|
# --- Frei eingetragene Ports ---------------------------------------
|
|
# Form: 3000/tcp=Grafana,5667/tcp=NSCA
|
|
local e pp d
|
|
while IFS= read -r e; do
|
|
e="$(printf '%s' "$e" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
[ -n "$e" ] || continue
|
|
pp="${e%%=*}"; d="${e#*=}"
|
|
if [ "$d" = "$e" ] || [ -z "$d" ]; then d="zusaetzlich freigegeben"; fi
|
|
case "$pp" in */tcp|*/udp) : ;; *) pp="$pp/tcp" ;; esac
|
|
printf '%s|%s|ja\n' "$pp" "$d"
|
|
done < <(printf '%s\n' "${FW_EXTRA_PORTS:-}" | tr ',' '\n')
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Offene Ports der erkannten Firewall einsammeln, "nummer/proto" je Zeile.
|
|
# Bereiche (6000-6010 bzw. 6000:6010) bleiben stehen und werden beim
|
|
# Vergleich aufgeloest.
|
|
# ---------------------------------------------------------------------
|
|
fw_open_ports() { # backend
|
|
case "$1" in
|
|
firewalld)
|
|
{ local zone svc
|
|
while IFS= read -r zone; do
|
|
[ -n "$zone" ] || continue
|
|
firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n'
|
|
firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n' \
|
|
| while IFS= read -r svc; do
|
|
if [ -n "$svc" ]; then fw_service_port "$svc"; fi
|
|
done
|
|
firewall-cmd --zone="$zone" --list-rich-rules 2>/dev/null \
|
|
| sed -n 's/.*port port="\([0-9:-]*\)" protocol="\([a-z]*\)".*/\1\/\2/p'
|
|
done < <(fw_zones)
|
|
} | sed '/^$/d' | sort -u ;;
|
|
ufw)
|
|
local spec ports proto pp
|
|
ufw status 2>/dev/null | awk 'NR>3 {print $1}' \
|
|
| while IFS= read -r spec; do
|
|
case "$spec" in *[0-9]*) : ;; *) continue ;; esac
|
|
proto=""; ports="$spec"
|
|
case "$spec" in
|
|
*/tcp|*/udp) proto="${spec##*/}"; ports="${spec%/*}" ;;
|
|
esac
|
|
for pp in $(printf '%s' "$ports" | tr ',' ' '); do
|
|
case "$pp" in *[!0-9:-]*) continue ;; esac
|
|
if [ -n "$proto" ]; then
|
|
printf '%s/%s\n' "$pp" "$proto"
|
|
else
|
|
printf '%s/tcp\n%s/udp\n' "$pp" "$pp"
|
|
fi
|
|
done
|
|
done | sort -u ;;
|
|
nftables)
|
|
local proto rest pp
|
|
nft list ruleset 2>/dev/null \
|
|
| grep -oE '(tcp|udp) dport[[:space:]]*(\{[^}]*\}|[0-9]+(-[0-9]+)?)' \
|
|
| sed 's/dport//; s/[{}]//g' \
|
|
| while read -r proto rest; do
|
|
for pp in $(printf '%s' "$rest" | tr ',' ' '); do
|
|
printf '%s/%s\n' "$pp" "$proto"
|
|
done
|
|
done | sort -u ;;
|
|
iptables)
|
|
local line proto ports pp
|
|
iptables -S 2>/dev/null | grep -E ' -j (ACCEPT|accept)$' \
|
|
| while IFS= read -r line; do
|
|
proto="$(printf '%s' "$line" | sed -n 's/.* -p \([a-z0-9]*\).*/\1/p')"
|
|
case "$proto" in tcp|udp) : ;; *) continue ;; esac
|
|
ports="$(printf '%s' "$line" \
|
|
| grep -oE '\-\-(dport|dports) [0-9,:]+' \
|
|
| sed 's/--dports\? //')"
|
|
[ -n "$ports" ] || continue
|
|
for pp in $(printf '%s' "$ports" | tr ',' ' '); do
|
|
printf '%s/%s\n' "$pp" "$proto"
|
|
done
|
|
done | sort -u ;;
|
|
*) : ;;
|
|
esac
|
|
}
|
|
|
|
# Steht port/proto in der Liste der offenen? Bereiche werden aufgeloest.
|
|
fw_port_open() { # port proto (Liste in _FW_OPEN, eine je Zeile)
|
|
local port="$1" proto="$2" e ep epr lo hi
|
|
while IFS= read -r e; do
|
|
[ -n "$e" ] || continue
|
|
ep="${e%/*}"; epr="${e##*/}"
|
|
[ "$epr" = "$proto" ] || continue
|
|
case "$ep" in
|
|
*[-:]*)
|
|
lo="${ep%%[-:]*}"; hi="${ep##*[-:]}"
|
|
case "$lo$hi" in *[!0-9]*) continue ;; esac
|
|
if [ "$port" -ge "$lo" ] && [ "$port" -le "$hi" ]; then return 0; fi ;;
|
|
*) if [ "$ep" = "$port" ]; then return 0; fi ;;
|
|
esac
|
|
done <<<"${_FW_OPEN:-}"
|
|
return 1
|
|
}
|
|
|
|
# Vorschlaege zum Oeffnen
|
|
fw_open_cmds() { # backend port proto [quelle]
|
|
local be="$1" port="$2" proto="$3" src="${4:-}" z
|
|
case "$be" in
|
|
firewalld)
|
|
z="$(fw_zones | head -n1)"
|
|
if [ -n "$src" ]; then
|
|
printf "firewall-cmd --permanent --zone=%s --add-rich-rule='rule family=ipv4 source address=%s port port=%s protocol=%s accept'\n" \
|
|
"${z:-public}" "$src" "$port" "$proto"
|
|
else
|
|
printf 'firewall-cmd --permanent --zone=%s --add-port=%s/%s\n' "${z:-public}" "$port" "$proto"
|
|
fi ;;
|
|
ufw)
|
|
if [ -n "$src" ]; then
|
|
printf 'ufw allow from %s to any port %s proto %s\n' "$src" "$port" "$proto"
|
|
else
|
|
printf 'ufw allow %s/%s\n' "$port" "$proto"
|
|
fi ;;
|
|
nftables)
|
|
if [ -n "$src" ]; then
|
|
printf 'nft add rule inet filter input ip saddr %s %s dport %s accept\n' "$src" "$proto" "$port"
|
|
else
|
|
printf 'nft add rule inet filter input %s dport %s accept\n' "$proto" "$port"
|
|
fi ;;
|
|
iptables)
|
|
if [ -n "$src" ]; then
|
|
printf 'iptables -I INPUT -s %s -p %s --dport %s -j ACCEPT\n' "$src" "$proto" "$port"
|
|
else
|
|
printf 'iptables -I INPUT -p %s --dport %s -j ACCEPT\n' "$proto" "$port"
|
|
fi ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Ports wieder zumachen
|
|
#
|
|
# "firewall open" kennt nur eine Richtung. Nimmt man einen Dienst
|
|
# wieder aus der Konfiguration - Event Console abgeschaltet, ein Port
|
|
# aus FW_EXTRA_PORTS gestrichen -, bleibt die Freigabe stehen.
|
|
#
|
|
# Zugemacht wird nur, was dieser Aufbau kennt: die Ports aus der Liste
|
|
# unten und das, was "firewall open --apply" selbst geoeffnet hat.
|
|
# Fremde Freigaben bleiben unangetastet - wer 3306 fuer seine Datenbank
|
|
# offen hat, soll sie behalten.
|
|
#
|
|
# SSH wird nie geschlossen. Weder Port 22 noch der Port, auf dem sshd
|
|
# tatsaechlich lauscht, noch der, ueber den diese Sitzung laeuft.
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Alle Ports, die zu diesem Aufbau gehoeren koennten - unabhaengig davon,
|
|
# ob sie gerade gebraucht werden
|
|
fw_known_ports() {
|
|
printf '80/tcp|HTTP\n'
|
|
printf '443/tcp|HTTPS\n'
|
|
printf '%s/tcp|Adminoberflaeche des Proxy Managers\n' "${NPM_ADMIN_PORT:-81}"
|
|
printf '5000/tcp|Checkmk direkt\n'
|
|
printf '8000/tcp|Agent-Receiver\n'
|
|
printf '6557/tcp|Livestatus\n'
|
|
printf '6558/tcp|Event Console ueber TCP\n'
|
|
printf '6556/tcp|Checkmk-Agent dieses Hosts\n'
|
|
printf '514/udp|Event Console: Syslog\n'
|
|
printf '514/tcp|Event Console: Syslog ueber TCP\n'
|
|
printf '162/udp|Event Console: SNMP-Traps\n'
|
|
# Was wir selbst geoeffnet haben, gehoert auch dazu
|
|
local e
|
|
while IFS= read -r e; do
|
|
[ -n "$e" ] || continue
|
|
printf '%s|selbst geoeffnet\n' "$e"
|
|
done < <(fw_state_list)
|
|
}
|
|
|
|
fw_state_file() { printf '%s/%s\n' "$PROJECT_DIR" "$FW_STATE_FILE_NAME"; }
|
|
|
|
fw_state_list() {
|
|
local f; f="$(fw_state_file)"
|
|
[ -f "$f" ] || return 0
|
|
sed 's/#.*//; s/[[:space:]]//g' "$f" | sed '/^$/d' | sort -u
|
|
}
|
|
|
|
fw_state_add() { # port/proto
|
|
local f; f="$(fw_state_file)"
|
|
if [ ! -f "$f" ]; then
|
|
cat >"$f" <<'EOF'
|
|
# Von "cmk-manage.sh firewall open --apply" geoeffnete Ports.
|
|
# Dient "firewall close" als Gedaechtnis - hier stehen nur Eintraege,
|
|
# die dieses Werkzeug selbst gesetzt hat. Von Hand geaendert werden
|
|
# muss hier nichts.
|
|
EOF
|
|
fi
|
|
( set +o pipefail; grep -qxF "$1" "$f" ) || printf '%s\n' "$1" >>"$f"
|
|
}
|
|
|
|
fw_state_del() { # port/proto
|
|
local f tmp; f="$(fw_state_file)"
|
|
[ -f "$f" ] || return 0
|
|
tmp="$f.tmp"
|
|
# Bleibt nichts uebrig, liefert grep 1 - das darf weder die Datei
|
|
# stehen lassen noch unter "set -e" den Ablauf beenden.
|
|
grep -vxF "$1" "$f" > "$tmp" 2>/dev/null || true
|
|
mv "$tmp" "$f" 2>/dev/null || rm -f "$tmp"
|
|
return 0
|
|
}
|
|
|
|
# Ports, die unter keinen Umstaenden geschlossen werden
|
|
fw_protected_ports() {
|
|
printf '22/tcp\n'
|
|
# Wo lauscht sshd wirklich?
|
|
if command -v sshd >/dev/null 2>&1; then
|
|
sshd -T 2>/dev/null | sed -n 's/^port[[:space:]]\+\([0-9]\+\).*/\1\/tcp/p'
|
|
fi
|
|
sed -n 's/^[[:space:]]*[Pp]ort[[:space:]]\+\([0-9]\+\).*/\1\/tcp/p' \
|
|
/etc/ssh/sshd_config 2>/dev/null
|
|
# Und der Port, ueber den diese Sitzung gerade laeuft
|
|
if [ -n "${SSH_CONNECTION:-}" ]; then
|
|
printf '%s/tcp\n' "$(printf '%s' "$SSH_CONNECTION" | awk '{print $4}')"
|
|
fi
|
|
}
|
|
|
|
# Kommandos zum Schliessen. Anders als beim Oeffnen reicht eine Vorlage
|
|
# nicht: die Regel muss so getroffen werden, wie sie dasteht.
|
|
fw_close_cmds() { # backend port proto
|
|
local be="$1" port="$2" proto="$3"
|
|
case "$be" in
|
|
firewalld)
|
|
local zone svc rule
|
|
while IFS= read -r zone; do
|
|
[ -n "$zone" ] || continue
|
|
local plist
|
|
plist="$(firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n')"
|
|
if ( set +o pipefail; printf '%s\n' "$plist" | grep -qxF "$port/$proto" ); then
|
|
printf 'firewall-cmd --permanent --zone=%s --remove-port=%s/%s\n' "$zone" "$port" "$proto"
|
|
fi
|
|
while IFS= read -r svc; do
|
|
[ -n "$svc" ] || continue
|
|
if ( set +o pipefail; fw_service_port "$svc" | grep -qxF "$port/$proto" ); then
|
|
printf 'firewall-cmd --permanent --zone=%s --remove-service=%s\n' "$zone" "$svc"
|
|
fi
|
|
done < <(firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n')
|
|
while IFS= read -r rule; do
|
|
[ -n "$rule" ] || continue
|
|
case "$rule" in
|
|
*"port=\"$port\""*"protocol=\"$proto\""*)
|
|
printf "firewall-cmd --permanent --zone=%s --remove-rich-rule='%s'\n" "$zone" "$rule" ;;
|
|
esac
|
|
done < <(firewall-cmd --zone="$zone" --list-rich-rules 2>/dev/null)
|
|
done < <(fw_zones) ;;
|
|
ufw)
|
|
# NICHT nach Nummern loeschen: die verschieben sich beim
|
|
# ersten Loeschen, und der naechste Aufruf traefe die falsche
|
|
# Regel. Ueber die Regelangabe ist es von der Reihenfolge
|
|
# unabhaengig. Deckt eine Regel mehrere Ports ab, bleibt sie
|
|
# stehen - sonst faellt mehr weg als gewollt.
|
|
ufw status numbered 2>/dev/null \
|
|
| awk -v p="$port" -v pr="$proto" '
|
|
/^\[/ {
|
|
line = $0
|
|
sub(/^\[[ \t]*[0-9]+\][ \t]*/, "", line)
|
|
if (line ~ /\(v6\)/) next # ufw loescht v4 und v6 zusammen
|
|
n = split(line, f, /[ \t]+/)
|
|
spec = f[1]; act = ""; from = ""
|
|
for (i = 2; i <= n; i++) {
|
|
if (f[i] == "ALLOW" || f[i] == "DENY" || f[i] == "LIMIT" || f[i] == "REJECT") {
|
|
act = f[i]; j = i + 1
|
|
if (f[j] == "IN" || f[j] == "OUT" || f[j] == "FWD") j++
|
|
for (; j <= n; j++) from = from (from == "" ? "" : " ") f[j]
|
|
break
|
|
}
|
|
}
|
|
if (act != "ALLOW") next
|
|
split(spec, a, "/")
|
|
plist = a[1]; sp = (length(a) > 1 ? a[2] : "")
|
|
if (sp != "" && sp != pr) next
|
|
m = split(plist, b, ",")
|
|
for (i = 1; i <= m; i++) if (b[i] == p) {
|
|
if (m > 1) {
|
|
printf "# Regel \"%s\" deckt mehrere Ports ab - bitte von Hand: ufw status numbered\n", spec
|
|
} else if (from == "Anywhere" || from == "") {
|
|
printf "ufw delete allow %s\n", spec
|
|
} else {
|
|
printf "ufw delete allow from %s to any port %s", from, p
|
|
if (sp != "") printf " proto %s", sp
|
|
printf "\n"
|
|
}
|
|
next
|
|
}
|
|
}' ;;
|
|
iptables)
|
|
# Die Regel so loeschen, wie sie eingetragen ist. Sammelregeln
|
|
# (--dports 80,443) bleiben stehen, sonst faellt mehr weg als
|
|
# gewollt.
|
|
iptables -S 2>/dev/null | grep -E ' -j (ACCEPT|accept)$' \
|
|
| while IFS= read -r line; do
|
|
case "$line" in "-A "*) : ;; *) continue ;; esac
|
|
case "$line" in *" -p $proto "*) : ;; *) continue ;; esac
|
|
local plist=""
|
|
plist="$(printf '%s' "$line" | sed -n 's/.*--dports\{0,1\} \([0-9,:]*\).*/\1/p')"
|
|
[ -n "$plist" ] || continue
|
|
case ",$plist," in *",$port,"*) : ;; *) continue ;; esac
|
|
case "$plist" in
|
|
*,*) printf '# Regel deckt mehrere Ports ab (%s) - bitte von Hand: iptables -S\n' "$plist" ;;
|
|
*) printf 'iptables -D %s\n' "${line#-A }" ;;
|
|
esac
|
|
done ;;
|
|
nftables)
|
|
# Geloescht wird ueber den Handle. Steht der Port in einer
|
|
# Menge mit anderen, bleibt die Regel stehen.
|
|
nft -a list ruleset 2>/dev/null \
|
|
| awk -v p="$port" -v pr="$proto" '
|
|
/^table / { fam = $2; tbl = $3 }
|
|
/^[[:space:]]*chain / { ch = $2 }
|
|
$0 ~ (pr " dport") && $0 ~ /handle [0-9]+$/ {
|
|
line = $0
|
|
h = line; sub(/.*handle /, "", h)
|
|
if (match(line, /dport[ \t]*\{[^}]*\}/)) {
|
|
set = substr(line, RSTART, RLENGTH)
|
|
sub(/dport[ \t]*\{/, "", set); sub(/\}/, "", set)
|
|
gsub(/[ \t]/, "", set)
|
|
n = split(set, a, ",")
|
|
for (i = 1; i <= n; i++) if (a[i] == p) {
|
|
if (n > 1)
|
|
printf "# Regel %s deckt mehrere Ports ab (%s) - bitte von Hand: nft -a list ruleset\n", h, set
|
|
else
|
|
printf "nft delete rule %s %s %s handle %s\n", fam, tbl, ch, h
|
|
}
|
|
} else if (match(line, ("dport[ \t]+" p "([^0-9]|$)"))) {
|
|
printf "nft delete rule %s %s %s handle %s\n", fam, tbl, ch, h
|
|
}
|
|
}' ;;
|
|
esac
|
|
}
|
|
|
|
# Welche Ports sind offen, gehoeren zu diesem Aufbau, werden aber nicht
|
|
# mehr gebraucht?
|
|
fw_close_report() { # backend apply
|
|
local be="$1" apply="${2:-}"
|
|
local spec need port proto desc
|
|
local -a todo=()
|
|
|
|
# Was gebraucht wird, darf nicht weg
|
|
local noetig=""
|
|
while IFS='|' read -r spec desc need; do
|
|
[ -n "$spec" ] || continue
|
|
case "$need" in ja|admin) noetig="$noetig$spec"$'\n' ;; esac
|
|
done < <(fw_required_ports)
|
|
|
|
local geschuetzt; geschuetzt="$(fw_protected_ports | sed '/^$/d' | sort -u)"
|
|
|
|
printf '\n%sNicht mehr benoetigt%s\n' "$C_BLD" "$C_RST"
|
|
printf ' %-10s %-44s %s\n' "PORT" "GEHOERT ZU" "ZUSTAND"
|
|
|
|
local seen=""
|
|
while IFS='|' read -r spec desc; do
|
|
[ -n "$spec" ] || continue
|
|
case "$seen" in *"|$spec|"*) continue ;; esac
|
|
seen="$seen|$spec|"
|
|
port="${spec%/*}"; proto="${spec##*/}"
|
|
|
|
fw_port_open "$port" "$proto" || continue
|
|
if ( set +o pipefail; printf '%s' "$noetig" | grep -qxF "$spec" ); then continue; fi
|
|
if ( set +o pipefail; printf '%s' "$geschuetzt" | grep -qxF "$spec" ); then
|
|
printf ' %-10s %-44s %b\n' "$spec" "$desc" "${C_GRN}bleibt offen (SSH)${C_RST}"
|
|
continue
|
|
fi
|
|
todo+=("$spec")
|
|
printf ' %-10s %-44s %b\n' "$spec" "$desc" "${C_YEL}kann zu${C_RST}"
|
|
done < <(fw_known_ports)
|
|
|
|
if [ "${#todo[@]}" -eq 0 ]; then
|
|
echo
|
|
ok "Es ist nichts offen, was nicht gebraucht wird."
|
|
printf ' Fremde Freigaben - etwa fuer andere Dienste auf diesem Rechner -
|
|
werden dabei nicht angeruehrt.\n\n'
|
|
return 0
|
|
fi
|
|
|
|
printf '\n%sZum Schliessen%s\n' "$C_BLD" "$C_RST"
|
|
local t c n=0
|
|
for t in "${todo[@]}"; do
|
|
while IFS= read -r c; do
|
|
[ -n "$c" ] || continue
|
|
printf ' %s\n' "$c"; n=$((n+1))
|
|
done < <(fw_close_cmds "$be" "${t%/*}" "${t##*/}")
|
|
done
|
|
if [ "$n" -eq 0 ]; then
|
|
echo
|
|
warn "Die Regeln liessen sich nicht zuordnen - bitte von Hand pruefen."
|
|
echo; return 0
|
|
fi
|
|
case "$be" in
|
|
firewalld) printf ' firewall-cmd --reload\n' ;;
|
|
iptables) printf ' # dauerhaft machen: iptables-save bzw. iptables-persistent\n' ;;
|
|
nftables) printf ' # dauerhaft machen: /etc/nftables.conf nachziehen\n' ;;
|
|
esac
|
|
|
|
if [ -z "$apply" ]; then
|
|
printf '\n Ausfuehren mit: %s firewall close --apply\n\n' "$SELF"
|
|
return 0
|
|
fi
|
|
|
|
echo
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
die "Dafuer werden root-Rechte benoetigt - bitte mit sudo erneut aufrufen."
|
|
fi
|
|
confirm "Diese Ports jetzt schliessen?" || die "Abgebrochen."
|
|
for t in "${todo[@]}"; do
|
|
local fehler=""
|
|
while IFS= read -r c; do
|
|
[ -n "$c" ] || continue
|
|
case "$c" in
|
|
\#*) warn "${c#\# }"; fehler=1; continue ;;
|
|
esac
|
|
log "$c"
|
|
eval "$c" || { warn "Fehlgeschlagen: $c"; fehler=1; }
|
|
done < <(fw_close_cmds "$be" "${t%/*}" "${t##*/}")
|
|
if [ -z "$fehler" ]; then fw_state_del "$t"; fi
|
|
done
|
|
if [ "$be" = firewalld ]; then
|
|
firewall-cmd --reload >/dev/null 2>&1 && ok "firewalld neu geladen." \
|
|
|| warn "Neuladen fehlgeschlagen."
|
|
fi
|
|
ok "Ports geschlossen."
|
|
echo
|
|
return 0
|
|
}
|
|
|
|
cmd_firewall() {
|
|
local sub="${1:-check}"; [ $# -gt 0 ] && shift || true
|
|
case "$sub" in check|open|close) : ;; *) die "Unbekannt: firewall $sub (check|open|close)" ;; esac
|
|
local apply="" src=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--apply) apply=1; shift ;;
|
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
--source) src="$2"; shift 2 ;;
|
|
*) die "Unbekannte Option fuer 'firewall': $1" ;;
|
|
esac
|
|
done
|
|
|
|
local be role; be="$(fw_detect)"
|
|
# Zonen einmal ermitteln - sonst fragt jede Regelzeile erneut nach
|
|
if [ "$be" = firewalld ]; then _FW_ZONES=""; _FW_ZONES="$(fw_zones)"; fi
|
|
role="$(fw_lc "${FW_ROLE:-single}")"
|
|
case "$role" in central|remote) : ;; *) role=single ;; esac
|
|
|
|
printf '\n%sFirewall%s\n' "$C_BLD" "$C_RST"
|
|
case "$be" in
|
|
keine)
|
|
test_result warn "Keine aktive Firewall erkannt"
|
|
printf ' Entweder ist keine eingerichtet, oder die Pruefung braucht
|
|
root-Rechte. Mit sudo erneut aufrufen.\n' ;;
|
|
*) test_result ok "Erkannt: $be"
|
|
if [ "$be" = firewalld ]; then
|
|
printf ' Aktive Zonen: %s\n' "$(fw_zones | tr '\n' ' ' | sed 's/ $//')"
|
|
fi ;;
|
|
esac
|
|
case "$role" in
|
|
central) printf ' Rolle: Zentrale einer verteilten Ueberwachung\n' ;;
|
|
remote) printf ' Rolle: Remote-Site einer verteilten Ueberwachung\n' ;;
|
|
esac
|
|
|
|
_FW_OPEN=""
|
|
if [ "$be" != keine ]; then
|
|
_FW_OPEN="$(fw_open_ports "$be")"
|
|
printf ' Offene Ports: %s\n' "$(printf '%s' "$_FW_OPEN" | tr '\n' ' ' | sed 's/ $//')"
|
|
fi
|
|
|
|
if [ "$sub" = close ]; then
|
|
if [ "$be" = keine ]; then
|
|
echo; warn "Ohne erkannte Firewall gibt es nichts zu schliessen."; echo
|
|
return 0
|
|
fi
|
|
fw_close_report "$be" "$apply"
|
|
return $?
|
|
fi
|
|
|
|
printf '\n%sWas dieser Stack braucht%s\n' "$C_BLD" "$C_RST"
|
|
printf ' %-10s %-48s %-7s %s\n' "PORT" "ZWECK" "NOETIG" "ZUSTAND"
|
|
|
|
local spec zweck need zustand port proto ist fehlend=0
|
|
local -a todo=()
|
|
while IFS='|' read -r spec zweck need; do
|
|
[ -n "$spec" ] || continue
|
|
port="${spec%/*}"; proto="${spec##*/}"
|
|
ist=nein
|
|
if [ "$be" != keine ] && fw_port_open "$port" "$proto"; then ist=ja; fi
|
|
|
|
case "$need:$ist" in
|
|
ja:ja) zustand="${C_GRN}offen${C_RST}" ;;
|
|
ja:nein) zustand="${C_RED}GESCHLOSSEN${C_RST}"; fehlend=$((fehlend+1)); todo+=("$spec") ;;
|
|
admin:ja) zustand="offen - besser auf das Adminnetz begrenzen" ;;
|
|
admin:nein) zustand="geschlossen" ;;
|
|
nein:ja) zustand="${C_YEL}offen - sollte zu sein${C_RST}" ;;
|
|
nein:nein) zustand="zu (richtig)" ;;
|
|
esac
|
|
if [ "$be" = keine ]; then zustand="nicht pruefbar"; fi
|
|
printf ' %-10s %-48s %-7s %b\n' "$spec" "$zweck" "$need" "$zustand"
|
|
done < <(fw_required_ports)
|
|
|
|
# Hinweise, die sich aus der Konfiguration ergeben
|
|
local -a hinweise=()
|
|
if [ "$role" = remote ] && ! fw_on "${CMK_LIVESTATUS_TCP:-off}"; then
|
|
hinweise+=("Rolle 'remote', aber CMK_LIVESTATUS_TCP=off - dann lauscht auf 6557 nichts.
|
|
Einschalten mit: $SELF config set CMK_LIVESTATUS_TCP on (danach 'up')")
|
|
fi
|
|
if [ "$role" = central ]; then
|
|
hinweise+=("Als Zentrale baut diese Site die Verbindungen selbst auf: nach aussen
|
|
muessen 6557/tcp und 443/tcp zu den Remote-Sites erlaubt sein. Eingehend
|
|
braucht die Zentrale dafuer nichts - ausser dem, was oben steht.")
|
|
fi
|
|
if [ "${#hinweise[@]}" -gt 0 ]; then
|
|
printf '\n%sHinweise%s\n' "$C_BLD" "$C_RST"
|
|
local h; for h in "${hinweise[@]}"; do printf ' %s\n' "$h"; done
|
|
fi
|
|
|
|
if [ "$be" = keine ]; then echo; return 0; fi
|
|
|
|
echo
|
|
if [ "$fehlend" -eq 0 ]; then
|
|
ok "Alle benoetigten Ports sind offen."
|
|
echo; return 0
|
|
fi
|
|
|
|
err "$fehlend benoetigte(r) Port(s) sind geschlossen."
|
|
printf '\n%sZum Oeffnen%s\n' "$C_BLD" "$C_RST"
|
|
local t
|
|
for t in "${todo[@]}"; do
|
|
printf ' %s\n' "$(fw_open_cmds "$be" "${t%/*}" "${t##*/}" "$src")"
|
|
done
|
|
case "$be" in
|
|
firewalld) printf ' firewall-cmd --reload\n' ;;
|
|
iptables) printf ' # dauerhaft machen: iptables-save bzw. das Paket iptables-persistent\n' ;;
|
|
nftables) printf ' # dauerhaft machen: in /etc/nftables.conf uebernehmen\n' ;;
|
|
esac
|
|
if [ -n "$src" ]; then
|
|
case " ${todo[*]} " in
|
|
*" 80/tcp "*|*" 443/tcp "*)
|
|
printf '\n Achtung: Lets Encrypt prueft die Domain von aussen ueber Port 80.
|
|
Wird 80 auf ein Quellnetz begrenzt, schlaegt die Ausstellung fehl.\n' ;;
|
|
esac
|
|
fi
|
|
|
|
if [ -n "$apply" ]; then
|
|
echo
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
die "Dafuer werden root-Rechte benoetigt - bitte mit sudo erneut aufrufen."
|
|
fi
|
|
confirm "Diese Regeln jetzt setzen?" || die "Abgebrochen."
|
|
local c
|
|
for t in "${todo[@]}"; do
|
|
c="$(fw_open_cmds "$be" "${t%/*}" "${t##*/}" "$src")"
|
|
log "$c"
|
|
if eval "$c"; then
|
|
# Merken, damit "firewall close" spaeter weiss, was von uns war
|
|
fw_state_add "$t"
|
|
else
|
|
warn "Fehlgeschlagen: $c"
|
|
fi
|
|
done
|
|
if [ "$be" = firewalld ]; then
|
|
firewall-cmd --reload >/dev/null 2>&1 && ok "firewalld neu geladen." \
|
|
|| warn "Neuladen fehlgeschlagen."
|
|
fi
|
|
ok "Regeln gesetzt."
|
|
echo
|
|
# Gesetzt ist gesetzt - der Rueckgabewert darf jetzt nicht mehr
|
|
# den urspruenglichen Befund melden, sonst liest die Oberflaeche
|
|
# das als fehlgeschlagen.
|
|
return 0
|
|
else
|
|
printf '\n Automatisch setzen: %s firewall open --apply\n' "$SELF"
|
|
printf ' Nur fuer ein Netz : %s firewall open --apply --source 10.0.0.0/24\n' "$SELF"
|
|
fi
|
|
echo
|
|
return 1
|
|
}
|
|
|
|
# =====================================================================
|
|
# 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. Keine "| grep -q" ohne Schutz vor SIGPIPE%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[FEHLER]%s %s:%s\n' "$C_RED" "$C_RST" "$(basename "$f")" "$line"
|
|
n=$((n+1))
|
|
done < <(grep -nE '\|[[:space:]]*grep -[a-zA-Z]*q' "$f" \
|
|
| grep -v 'set +o pipefail' \
|
|
| grep -vE '^[0-9]+:[[:space:]]*#' \
|
|
| grep -v 'printf' | cut -c1-72 || true)
|
|
done
|
|
if [ "$n" -eq 0 ]; then
|
|
printf ' %s[ OK ]%s alle solchen Pipes laufen ohne pipefail\n' "$C_GRN" "$C_RST"
|
|
else
|
|
printf ' "grep -q" endet beim ersten Treffer; der Erzeuger bekommt\n'
|
|
printf ' SIGPIPE und unter "set -o pipefail" gilt die Pipe als\n'
|
|
printf ' fehlgeschlagen - abhaengig von der Datenmenge. Schreibweise:\n'
|
|
printf ' if ( set +o pipefail; erzeuger | grep -q muster ); then\n'
|
|
rc=1
|
|
fi
|
|
|
|
printf '\n%s6. 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%s7. 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) if [ "${1:-}" = "--no-fetch" ]; then UP_NO_FETCH=1; fi
|
|
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 "$@" ;;
|
|
npm-reset) cmd_npm_reset ;;
|
|
proxy) cmd_proxy "$@" ;;
|
|
config) cmd_config "$@" || true ;;
|
|
selftest) cmd_selftest ;;
|
|
tui-check) cmd_tui_check ;;
|
|
firewall) cmd_firewall "$@" ;;
|
|
help|-h|--help) usage ;;
|
|
*) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|