Zwei Luecken im vorherigen Stand.
Erstens war der Eintrag erst in einer neuen Sitzung wirksam. Ein
Programm kann die Umgebung seiner Elternshell nicht aendern - dafuer
gibt es jetzt "noproxy --export", das nur die beiden Zeilen ausgibt:
eval "$(cmk-manage.sh noproxy --export)"
Danach gilt es sofort; nachgemessen mit curl -v (vorher "Uses proxy env
variable http_proxy", danach "Trying 127.0.0.1"). Der Wert wird dabei
aus denselben Regeln gebildet wie beim Schreiben in eine Datei -
Vorhandenes zuerst, unsere Adressen dahinter, ohne Doppelte.
Zweitens lesen Systemdienste weder profile.d noch .bashrc noch
/etc/environment. Dafuer gibt es zwei weitere Ziele:
noproxy --fix --environment ergaenzt /etc/environment, ohne die
uebrigen Zeilen anzuruehren
noproxy --fix --service schreibt eine Ergaenzung der systemd-Unit
des Stacks; der Proxy selbst kommt mit
hinein, sonst kaeme der Dienst beim Holen
von Images nicht mehr ins Internet
Findet sich keine passende Unit, zeigt der Befehl den Abschnitt zum
Einfuegen von Hand. Die Schreibpfade sind ueber NOPROXY_ENV_FILE und
NOPROXY_UNIT_DIR ueberschreibbar - damit liessen sie sich pruefen, ohne
/etc dieses Rechners anzufassen.
In der Oberflaeche stehen alle vier Ziele zur Auswahl, dazu der
Menuepunkt fuer die laufende Sitzung, der den eval-Befehl zum Kopieren
zeigt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8634 lines
358 KiB
Bash
Executable File
8634 lines
358 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=""
|
|
# Ein aus der Oberflaeche gestarteter Unterprozess muss dieselbe
|
|
# Konfiguration lesen wie wir. Ohne das greift er zur Standard-.env,
|
|
# und die Aktion liefe gegen den falschen Stack.
|
|
SELF_ENV="" # fuer "bash -c"-Zeichenketten
|
|
SELF_ENV_ARR=() # fuer direkte Aufrufe
|
|
|
|
# =====================================================================
|
|
# 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.
|
|
Der Adminport des Proxy Managers wird mit
|
|
angeboten; --keep-admin laesst ihn offen
|
|
noproxy [--fix] pruefen, ob lokale Adressen am HTTP-Proxy vorbeigehen
|
|
(Zwangsproxy: sonst laufen selbst Anfragen an
|
|
127.0.0.1 dorthin - sichtbar als "API-Fehler")
|
|
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)
|
|
site-create NAME weitere Site im Container anlegen (eigene Ports)
|
|
site-delete NAME Site samt Daten entfernen
|
|
|
|
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
|
|
cert external [...] ein von aussen geliefertes Zertifikat verwenden
|
|
(Verzeichnis wird als /certs eingehaengt, die
|
|
Dateien werden dorthin verwiesen statt kopiert)
|
|
--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"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Lokale Anfragen am HTTP-Proxy vorbei
|
|
#
|
|
# In Umgebungen mit Zwangsproxy stehen http_proxy/https_proxy in der
|
|
# Umgebung, und wenn no_proxy die lokalen Adressen nicht ausnimmt,
|
|
# schickt curl selbst eine Anfrage an 127.0.0.1 an den Proxy. Der kennt
|
|
# die Adresse nicht und antwortet mit einem Fehler - hier sichtbar als
|
|
# "API-Fehler" beim Anmelden am Proxy Manager.
|
|
#
|
|
# Fuer Ziele auf diesem Rechner wird der Proxy deshalb ausdruecklich
|
|
# abgeschaltet. Anfragen ins Internet (Registry, Downloadportal) bleiben
|
|
# unberuehrt - die brauchen ihn ja.
|
|
# ---------------------------------------------------------------------
|
|
url_is_local() { # url
|
|
local h="${1#*://}"; h="${h%%/*}"; h="${h%%\?*}"
|
|
h="${h##*@}"; h="${h%%:*}"
|
|
case "$h" in
|
|
localhost|127.0.0.1|::1|\[::1\]|0.0.0.0) return 0 ;;
|
|
127.*) return 0 ;;
|
|
esac
|
|
[ -n "$h" ] && [ "$h" = "$(primary_ip 2>/dev/null)" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Argumente fuer curl, damit eine lokale Anfrage nicht ueber den Proxy geht
|
|
curl_local_args() { # url
|
|
if url_is_local "$1"; then printf -- '--noproxy\n*\n'; fi
|
|
}
|
|
|
|
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 np; while IFS= read -r np; do [ -n "$np" ] && a+=("$np"); done < <(curl_local_args "$url")
|
|
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.)
|
|
# =====================================================================
|
|
# Weitere Sites anlegen und entfernen
|
|
#
|
|
# Der Entrypoint des Abbilds legt nur die Site aus CMK_SITE_ID an. Eine
|
|
# zweite entsteht mit "omd create" im Container - mit --no-tmpfs, denn
|
|
# ein tmpfs laesst sich dort nicht einhaengen.
|
|
#
|
|
# Die Ports vergibt omd selbst weiter: die zweite Site bekommt 5001
|
|
# statt 5000, den Agent-Receiver auf 8001 statt 8000 und Livestatus auf
|
|
# 6558. Das ist beim Proxy und bei der Firewall zu beachten - die
|
|
# Agents dieser Site sprechen 8001 an, nicht 8000.
|
|
# =====================================================================
|
|
|
|
# Einen Wert aus der site.conf lesen
|
|
site_conf_get() { # site schluessel
|
|
ct sh -c "sed -n \"s/^CONFIG_$2='\\(.*\\)'\$/\\1/p\" /omd/sites/$1/etc/omd/site.conf" 2>/dev/null
|
|
}
|
|
|
|
# Laeuft der Checkmk-Container im Host-Netz? Dann genuegt 127.0.0.1,
|
|
# denn der Proxy Manager liegt im selben Netz.
|
|
cmk_host_network() {
|
|
[ "$($RUNTIME container inspect -f '{{.HostConfig.NetworkMode}}' "$CMK_CONTAINER_NAME" 2>/dev/null)" = host ]
|
|
}
|
|
|
|
cmd_site_create() {
|
|
local name="" pass="" expose="" nostart=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--password) pass="$2"; shift 2 ;;
|
|
--expose) expose=1; shift ;;
|
|
--no-start) nostart=1; shift ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
Eine weitere Site im laufenden Container anlegen.
|
|
|
|
$SELF site-create zweite
|
|
$SELF site-create kunde2 --password geheim123
|
|
|
|
--password PW Kennwort fuer cmkadmin (sonst erzeugt omd eines und
|
|
zeigt es an)
|
|
--expose Apache der Site auf 0.0.0.0 binden. Noetig nur, wenn
|
|
der Container NICHT im Host-Netz laeuft - sonst
|
|
genuegt 127.0.0.1, und der Proxy kommt trotzdem hin.
|
|
--no-start nur anlegen, nicht starten
|
|
|
|
Die Ports vergibt omd selbst: die naechste Site bekommt 5001, den
|
|
Agent-Receiver auf 8001 und Livestatus auf 6558.
|
|
EOF
|
|
return 0 ;;
|
|
-*) die "Unbekannte Option fuer 'site-create': $1" ;;
|
|
*) name="$1"; shift ;;
|
|
esac
|
|
done
|
|
[ -n "$name" ] || die "Name fehlt: $SELF site-create <name>"
|
|
case "$name" in
|
|
*[!a-zA-Z0-9_]*) die "Ein Site-Name darf nur Buchstaben, Ziffern und _ enthalten." ;;
|
|
esac
|
|
[ "${#name}" -le 16 ] || die "Ein Site-Name darf hoechstens 16 Zeichen lang sein."
|
|
|
|
container_running "$CMK_CONTAINER_NAME" \
|
|
|| die "Der Container '$CMK_CONTAINER_NAME' laeuft nicht.
|
|
Starten: $SELF up"
|
|
|
|
if ct test -d "/omd/sites/$name" 2>/dev/null; then
|
|
die "Die Site '$name' gibt es schon. ('$SELF sites' zeigt alle)"
|
|
fi
|
|
|
|
local -a a=(omd create --no-tmpfs)
|
|
if [ -n "$pass" ]; then a+=(--admin-password "$pass"); fi
|
|
a+=("$name")
|
|
|
|
log "Lege Site '$name' an ..."
|
|
# Die Ausgabe von omd enthaelt das erzeugte Kennwort - sie gehoert
|
|
# dem Benutzer vor die Augen.
|
|
ct "${a[@]}" || die "'omd create' ist fehlgeschlagen."
|
|
|
|
# Ohne Host-Netz waere die Site von aussen nicht erreichbar
|
|
if [ -n "$expose" ] || ! cmk_host_network; then
|
|
log "Binde den Apache der Site auf 0.0.0.0 ..."
|
|
ct omd config "$name" set APACHE_TCP_ADDR 0.0.0.0 >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if [ -z "$nostart" ]; then
|
|
log "Starte Site '$name' ..."
|
|
ct omd start "$name" >/dev/null 2>&1 || warn "Die Site liess sich nicht starten."
|
|
fi
|
|
|
|
local port recv ls ip
|
|
port="$(site_conf_get "$name" APACHE_TCP_PORT)"
|
|
recv="$(site_conf_get "$name" AGENT_RECEIVER_PORT)"
|
|
ls="$(site_conf_get "$name" LIVESTATUS_TCP_PORT)"
|
|
ip="$(primary_ip)"
|
|
|
|
echo
|
|
ok "Site '$name' angelegt."
|
|
printf ' %-22s %s\n' "Oberflaeche (direkt)" "http://127.0.0.1:${port:-?}/$name/"
|
|
printf ' %-22s %s\n' "Agent-Receiver" "${recv:-?}"
|
|
printf ' %-22s %s\n' "Livestatus (falls an)" "${ls:-?}"
|
|
cat <<EOF
|
|
|
|
${C_BLD}Damit sie von aussen erreichbar wird${C_RST}
|
|
1) Pfad im Proxy anlegen - dann liegt sie neben der ersten Site:
|
|
$SELF proxy edit --domain <name-oder-id> --location /$name=127.0.0.1:${port:-5001}
|
|
$SELF proxy reload
|
|
Danach: https://${ip:-<ip>}/$name/
|
|
|
|
2) Die Agents dieser Site sprechen Port ${recv:-8001} an, nicht 8000.
|
|
In der Firewall ergaenzen:
|
|
$SELF config set FW_EXTRA_PORTS "${recv:-8001}/tcp=Agent-Receiver $name"
|
|
sudo $SELF firewall open --apply
|
|
|
|
3) Nach einem Neustart des Containers startet nur die Standard-Site.
|
|
Alle starten: $SELF start-all-sites
|
|
EOF
|
|
if grep -qsF "$NPM_DEF_START" "$(npm_custom_conf_path)" 2>/dev/null; then
|
|
log "Der Standard-Server fuer den IP-Zugang muss danach nachgezogen werden:
|
|
$SELF proxy default --domain <name> --restart"
|
|
fi
|
|
echo
|
|
return 0
|
|
}
|
|
|
|
cmd_site_delete() {
|
|
local name="" force=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--force) force=1; shift ;;
|
|
-*) die "Unbekannte Option fuer 'site-delete': $1" ;;
|
|
*) name="$1"; shift ;;
|
|
esac
|
|
done
|
|
[ -n "$name" ] || die "Name fehlt: $SELF site-delete <name>"
|
|
container_running "$CMK_CONTAINER_NAME" \
|
|
|| die "Der Container '$CMK_CONTAINER_NAME' laeuft nicht."
|
|
ct test -d "/omd/sites/$name" 2>/dev/null || die "Die Site '$name' gibt es nicht."
|
|
|
|
if [ "$name" = "${CMK_SITE_ID:-cmk}" ] && [ -z "$force" ]; then
|
|
die "'$name' ist die Standard-Site aus CMK_SITE_ID.
|
|
Der Entrypoint legt sie beim naechsten Start ohnehin neu an.
|
|
Wenn es wirklich sein soll: $SELF site-delete $name --force"
|
|
fi
|
|
|
|
warn "Die Site '$name' und ALLE ihre Daten werden entfernt."
|
|
printf ' Vorher ein Backup: %s backup --site %s\n' "$SELF" "$name"
|
|
confirm "Site '$name' wirklich loeschen?" || die "Abgebrochen."
|
|
|
|
log "Stoppe und entferne Site '$name' ..."
|
|
ct omd stop "$name" >/dev/null 2>&1 || true
|
|
# "omd rm" fragt selbst nach; das globale -f ueberspringt die Rueckfrage.
|
|
# Ohne das bleibt die Site stehen, waehrend hier Erfolg gemeldet wuerde.
|
|
ct omd -f rm --kill "$name" >/dev/null 2>&1 || true
|
|
if ct test -d "/omd/sites/$name" 2>/dev/null; then
|
|
err "Die Site '$name' ist noch da - 'omd rm' hat nicht durchgegriffen."
|
|
log "Von Hand nachsehen: $SELF omd -f rm --kill $name"
|
|
return 1
|
|
fi
|
|
ok "Site '$name' entfernt."
|
|
log "Einen Proxy-Pfad dafuer gibt es womoeglich noch:
|
|
$SELF proxy edit --domain <name> --del-location /$name"
|
|
return 0
|
|
}
|
|
|
|
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"
|
|
CERT_CRON_FILE="/etc/cron.d/checkmk-cert-external"
|
|
|
|
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_INTERNAL_ADMIN_PORT=81 # fest im Abbild: "listen 81"
|
|
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||
|
|
cert|CERT_EXTERNAL_DIR|path|Verzeichnis mit geliefertem Zertifikat||1
|
|
cert|CERT_EXTERNAL_FULLCHAIN|text|Dateiname Zertifikatskette||1
|
|
cert|CERT_EXTERNAL_KEY|text|Dateiname privater Schluessel||1
|
|
cert|CERT_EXTERNAL_WATCH|choice|Taeglich auf Austausch pruefen|off:on|1
|
|
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" ;;
|
|
CERT_EXTERNAL_WATCH)
|
|
cmd_cert_external --watch "$val" ;;
|
|
CERT_EXTERNAL_DIR)
|
|
log "Wirksam nach: $SELF up (das Verzeichnis wird erst dann eingehaengt)" ;;
|
|
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 --noproxy '*' -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)"
|
|
if [ -n "$m" ]; then printf ' Antwort: %s\n' "$m" >&2; fi
|
|
printf ' Geprueft wurde: %s\n' "$base/api/tokens" >&2
|
|
# In Kundennetzen mit Zwangsproxy ist das die haeufigste Ursache
|
|
# fuer eine Fehlermeldung, die nach einem API-Problem aussieht.
|
|
if [ -n "${http_proxy:-}${HTTP_PROXY:-}${https_proxy:-}${HTTPS_PROXY:-}" ]; then
|
|
printf '
|
|
In dieser Umgebung steht ein HTTP-Proxy. Nimmt no_proxy die lokalen
|
|
Adressen nicht aus, geht selbst diese Anfrage an ihn - dann meldet
|
|
er den Fehler, nicht der Proxy Manager. Nachsehen mit:
|
|
%s noproxy
|
|
' "$SELF" >&2
|
|
fi
|
|
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"
|
|
--noproxy '*' -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 --noproxy '*' -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"
|
|
}
|
|
|
|
# Laeuft nginx im Container wirklich? "nginx -t" beantwortet das NICHT -
|
|
# es prueft nur die Datei und gelingt auch, bevor der Dienst da ist.
|
|
# Der Pfad der pid-Datei ist vom Abbild abhaengig (bei NPM
|
|
# /run/nginx/nginx.pid), deshalb wird schlicht in /proc nachgesehen -
|
|
# das kommt ohne "ps" aus, das es im Container nicht gibt.
|
|
npm_nginx_up() {
|
|
$RUNTIME exec "$NPM_CONTAINER_NAME" sh -c '
|
|
for d in /proc/[0-9]*; do
|
|
case "$(tr "\0" " " < "$d/cmdline" 2>/dev/null)" in
|
|
*"nginx: master"*) exit 0 ;;
|
|
esac
|
|
done
|
|
exit 1' >/dev/null 2>&1
|
|
}
|
|
|
|
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 proxy restart
|
|
(oder ohne Unterbrechung: $SELF proxy reload)"
|
|
fi
|
|
}
|
|
|
|
# Ein Ziel aus Sicht des Proxy-Containers pruefen - von dort aus baut
|
|
# nginx die Verbindung auf, nicht von diesem Rechner.
|
|
proxy_check_one() { # pfad schema host port
|
|
local path="$1" sch="$2" host="$3" port="$4" code
|
|
code="$($RUNTIME exec "$NPM_CONTAINER_NAME" \
|
|
curl -s -o /dev/null -k --max-time 5 -w '%{http_code}' \
|
|
"$sch://$host:$port/" 2>/dev/null)" || code=000
|
|
case "${code:-000}" in
|
|
000) printf ' %-9s %-34s %b\n' "$path" "$sch://$host:$port" \
|
|
"${C_RED}nicht erreichbar${C_RST}"
|
|
return 1 ;;
|
|
*) printf ' %-9s %-34s %b\n' "$path" "$sch://$host:$port" \
|
|
"${C_GRN}HTTP $code${C_RST}"
|
|
return 0 ;;
|
|
esac
|
|
}
|
|
|
|
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 ;;
|
|
|
|
check|test)
|
|
local pid="" dom=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--id) pid="$2"; shift 2 ;;
|
|
--domain) dom="$2"; shift 2 ;;
|
|
*) die "Unbekannte Option fuer 'proxy check': $1" ;;
|
|
esac
|
|
done
|
|
npm_require_running || return 1
|
|
NPM_TOKEN="$(npm_api_token)" || return 1
|
|
|
|
local all i
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
local -a ids=()
|
|
if [ -n "$pid" ]; then
|
|
ids=("$pid")
|
|
elif [ -n "$dom" ]; then
|
|
for i in $(json_ids "$all"); do
|
|
if ( set +o pipefail; npm_api GET "/api/nginx/proxy-hosts/$i" | grep -qF "\"$dom\"" ); then
|
|
ids=("$i"); break
|
|
fi
|
|
done
|
|
[ "${#ids[@]}" -gt 0 ] || die "Kein Proxy-Host fuer '$dom' gefunden."
|
|
else
|
|
mapfile -t ids < <(json_ids "$all")
|
|
fi
|
|
if [ "${#ids[@]}" -eq 0 ]; then
|
|
warn "Es gibt keine Proxy-Hosts."; return 0
|
|
fi
|
|
|
|
local fehler=0 o d sch host port el
|
|
for i in "${ids[@]}"; do
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$i")"
|
|
[ -n "$(json_num "$o" id)" ] || { warn "Proxy-Host $i gibt es nicht."; continue; }
|
|
d="$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
printf '\n%sProxy-Host %s %s%s\n' "$C_BLD" "$i" "${d:-?}" "$C_RST"
|
|
printf ' %-9s %-34s %s\n' "PFAD" "ZIEL" "ANTWORT"
|
|
|
|
sch="$(json_str "$o" forward_scheme)"; [ -n "$sch" ] || sch=http
|
|
host="$(json_str "$o" forward_host)"; port="$(json_num "$o" forward_port)"
|
|
proxy_check_one "/" "$sch" "$host" "$port" || fehler=$((fehler+1))
|
|
|
|
while IFS= read -r el; do
|
|
[ -n "$el" ] || continue
|
|
sch="$(json_str "$el" forward_scheme)"; [ -n "$sch" ] || sch=http
|
|
host="$(json_str "$el" forward_host)"; port="$(json_num "$el" forward_port)"
|
|
proxy_check_one "$(json_str "$el" path)" "$sch" "$host" "$port" || fehler=$((fehler+1))
|
|
done < <(json_split_array "$(json_raw "$o" locations)")
|
|
done
|
|
|
|
echo
|
|
if [ "$fehler" -eq 0 ]; then
|
|
ok "Alle Ziele antworten."
|
|
echo; return 0
|
|
fi
|
|
err "$fehler Ziel(e) antworten nicht - dort meldet nginx 502 Bad Gateway."
|
|
printf ' Haeufigste Ursachen: Tippfehler in der Adresse (127.0.0.1 statt
|
|
172.0.0.1), der Dienst laeuft nicht, oder er lauscht nur auf einer
|
|
anderen Schnittstelle.
|
|
Aendern mit: %s proxy edit --id N --location /pfad=host:port\n\n' "$SELF"
|
|
return 1 ;;
|
|
|
|
admin|adminui)
|
|
local dom="" root="" off="" pid="" host="" path="" cert=self
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--domain) dom="$2"; shift 2 ;;
|
|
--root) root=1; shift ;;
|
|
--off|--aus) off=1; shift ;;
|
|
--id) pid="$2"; shift 2 ;;
|
|
--host) host="$2"; shift 2 ;;
|
|
--path) path="$2"; shift 2 ;;
|
|
--cert) cert="$2"; shift 2 ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
Die Adminoberflaeche des Proxy Managers erreichbar machen.
|
|
|
|
Sie ist eine Single-Page-Anwendung und laedt ihre Dateien ueber
|
|
ABSOLUTE Pfade (/images/..., /api/...). Unter einem Unterpfad wie /npm
|
|
kommt deshalb nur die Startseite an, alles Weitere landet bei dem, was
|
|
auf der Wurzel liegt - im Browser eine weisse Seite. Es bleiben zwei
|
|
Wege, und beide kommen mit dem einen freien Port aus:
|
|
|
|
$SELF proxy admin --domain npm.example.local
|
|
Eigener Proxy-Host auf einen Namen. Die Oberflaeche liegt dort
|
|
auf "/", alles funktioniert. Setzt voraus, dass der Name im Netz
|
|
aufgeloest wird (DNS oder hosts-Datei der Arbeitsplaetze).
|
|
|
|
$SELF proxy admin --root
|
|
Ohne Namensaufloesung: die WURZEL des vorhandenen Proxy-Hosts
|
|
zeigt auf die Adminoberflaeche, das bisherige Ziel wandert auf
|
|
/${CMK_SITE_ID:-cmk}. Checkmk belegt ohnehin nur seinen
|
|
Site-Pfad, der Rest der Wurzel ist frei.
|
|
https://<IP>/ Adminoberflaeche
|
|
https://<IP>/${CMK_SITE_ID:-cmk}/ Checkmk
|
|
|
|
$SELF proxy admin --root --off
|
|
Zurueck: die Wurzel zeigt wieder auf das urspruengliche Ziel.
|
|
|
|
Weitere Angaben
|
|
--id N | --host NAME welcher Proxy-Host (bei mehreren)
|
|
--path /pfad abweichender Site-Pfad (Vorgabe /${CMK_SITE_ID:-cmk})
|
|
--cert self|letsencrypt|none nur bei --domain
|
|
|
|
Ohne Angabe wird der Stand gezeigt.
|
|
EOF
|
|
return 0 ;;
|
|
*) die "Unbekannte Option fuer 'proxy admin': $1" ;;
|
|
esac
|
|
done
|
|
[ -n "$path" ] || path="/${CMK_SITE_ID:-cmk}"
|
|
case "$path" in /*) : ;; *) path="/$path" ;; esac
|
|
|
|
npm_require_running || return 1
|
|
NPM_TOKEN="$(npm_api_token)" || return 1
|
|
|
|
# --- Variante A: eigener Name -----------------------------------
|
|
if [ -n "$dom" ]; then
|
|
log "Lege einen Proxy-Host fuer die Adminoberflaeche an: $dom"
|
|
PROXY_ADD_URLPATH=/ cmd_proxy add --domain "$dom" --forward-host 127.0.0.1 \
|
|
--forward-port "$NPM_INTERNAL_ADMIN_PORT" \
|
|
--cert "$cert" --update || return 1
|
|
printf '\n Die Oberflaeche liegt dort auf "/" - damit funktionieren auch
|
|
/images, /api und die Anmeldung.
|
|
|
|
Noetig ist nur, dass "%s" im Netz aufgeloest wird. Ohne DNS
|
|
genuegt eine Zeile in der hosts-Datei der Arbeitsplaetze:
|
|
%s %s\n' "$dom" "$(primary_ip)" "$dom"
|
|
npm_reload_hint ""
|
|
return 0
|
|
fi
|
|
|
|
# --- Proxy-Host bestimmen ---------------------------------------
|
|
local all i o
|
|
if [ -z "$pid" ]; then
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
if [ -n "$host" ]; then
|
|
for i in $(json_ids "$all"); do
|
|
if ( set +o pipefail; npm_api GET "/api/nginx/proxy-hosts/$i" | grep -qF "\"$host\"" ); then
|
|
pid="$i"; break
|
|
fi
|
|
done
|
|
[ -n "$pid" ] || die "Kein Proxy-Host fuer '$host' gefunden."
|
|
else
|
|
local -a ids=(); mapfile -t ids < <(json_ids "$all")
|
|
case "${#ids[@]}" in
|
|
0) die "Es gibt noch keinen Proxy-Host.
|
|
Erst einen anlegen: $SELF proxy add --domain <name-oder-ip>" ;;
|
|
1) pid="${ids[0]}" ;;
|
|
*) die "Es gibt mehrere Proxy-Hosts - welcher?
|
|
$SELF proxy admin --root --id N ('$SELF proxy list' zeigt die IDs)" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
|
|
[ -n "$(json_num "$o" id)" ] || die "Proxy-Host $pid gibt es nicht."
|
|
|
|
local rh rp
|
|
rh="$(json_str "$o" forward_host)"; rp="$(json_num "$o" forward_port)"
|
|
|
|
# --- Stand anzeigen ---------------------------------------------
|
|
if [ -z "$root" ]; then
|
|
printf '\n%sAdminoberflaeche des Proxy Managers%s\n' "$C_BLD" "$C_RST"
|
|
printf ' Proxy-Host %s, Wurzel zeigt auf: %s:%s\n' "$pid" "$rh" "$rp"
|
|
if [ "$rp" = "$NPM_INTERNAL_ADMIN_PORT" ]; then
|
|
test_result ok "Die Wurzel zeigt auf die Adminoberflaeche."
|
|
printf ' https://%s/ Adminoberflaeche\n' "$(primary_ip)"
|
|
printf ' https://%s%s/ Checkmk\n' "$(primary_ip)" "$path"
|
|
printf '\n Zurueck: %s proxy admin --root --off\n\n' "$SELF"
|
|
else
|
|
test_result warn "Die Adminoberflaeche ist ueber diesen Proxy-Host nicht erreichbar."
|
|
printf '\n Zwei Wege (siehe %s proxy admin --help):
|
|
mit Namensaufloesung : %s proxy admin --domain npm.example.local
|
|
ohne : %s proxy admin --root\n\n' "$SELF" "$SELF" "$SELF"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# --- Variante B: Wurzel ------------------------------------------
|
|
local cur; cur="$(json_raw "$o" locations)"
|
|
local -a keep=()
|
|
local el elpath lh lp lsch found=""
|
|
while IFS= read -r el; do
|
|
[ -n "$el" ] || continue
|
|
elpath="$(json_str "$el" path)"
|
|
if [ "$elpath" = "$path" ]; then
|
|
found="$el"
|
|
if [ -n "$off" ]; then continue; fi
|
|
fi
|
|
keep+=("$el")
|
|
done < <(json_split_array "$cur")
|
|
|
|
if [ -n "$off" ]; then
|
|
[ -n "$found" ] || die "Es gibt keinen Pfad '$path' - da ist nichts zurueckzunehmen."
|
|
lsch="$(json_str "$found" forward_scheme)"; [ -n "$lsch" ] || lsch=http
|
|
lh="$(json_str "$found" forward_host)"; lp="$(json_num "$found" forward_port)"
|
|
local joined=""
|
|
for el in ${keep[@]+"${keep[@]}"}; do
|
|
if [ -n "$joined" ]; then joined="$joined,$el"; else joined="$el"; fi
|
|
done
|
|
npm_api PUT "/api/nginx/proxy-hosts/$pid" \
|
|
"{\"forward_scheme\":\"$lsch\",\"forward_host\":\"$lh\",\"forward_port\":$lp,\"locations\":[$joined]}" >/dev/null
|
|
case "$(npm_code)" in 200|201) : ;; *) die "Zuruecknehmen fehlgeschlagen (HTTP $(npm_code))." ;; esac
|
|
ok "Die Wurzel zeigt wieder auf $lsch://$lh:$lp, der Pfad $path ist entfernt."
|
|
else
|
|
if [ "$rp" = "$NPM_INTERNAL_ADMIN_PORT" ] && [ "$rh" = 127.0.0.1 ]; then
|
|
ok "Die Wurzel zeigt bereits auf die Adminoberflaeche."
|
|
return 0
|
|
fi
|
|
# Bisheriges Wurzelziel auf den Site-Pfad umziehen
|
|
if [ -z "$found" ]; then
|
|
keep+=("$(proxy_location_json "$path=$rh:$rp")") || return 1
|
|
log "Bisheriges Ziel $rh:$rp wandert auf $path."
|
|
else
|
|
log "Pfad $path gibt es schon - er bleibt, wie er ist."
|
|
fi
|
|
local joined=""
|
|
for el in ${keep[@]+"${keep[@]}"}; do
|
|
if [ -n "$joined" ]; then joined="$joined,$el"; else joined="$el"; fi
|
|
done
|
|
npm_api PUT "/api/nginx/proxy-hosts/$pid" \
|
|
"{\"forward_scheme\":\"http\",\"forward_host\":\"127.0.0.1\",\"forward_port\":$NPM_INTERNAL_ADMIN_PORT,\"locations\":[$joined]}" >/dev/null
|
|
case "$(npm_code)" in 200|201) : ;; *) die "Aenderung fehlgeschlagen (HTTP $(npm_code))." ;; esac
|
|
ok "Die Wurzel zeigt jetzt auf die Adminoberflaeche."
|
|
local ip; ip="$(primary_ip)"
|
|
printf ' https://%s/ Adminoberflaeche des Proxy Managers\n' "$ip"
|
|
printf ' https://%s%s/ Checkmk\n' "$ip" "$path"
|
|
printf '\n %sDie Anmeldemaske liegt damit offen im Netz.%s Wer das nicht will,
|
|
nimmt es nach getaner Arbeit wieder zurueck:
|
|
%s proxy admin --root --off\n' "$C_YEL" "$C_RST" "$SELF"
|
|
fi
|
|
|
|
local conf; conf="$(npm_custom_conf_path)"
|
|
if grep -qsF "$NPM_DEF_START" "$conf"; then
|
|
local d2; d2="$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
log "Der Standard-Server fuer den IP-Zugang muss nachgezogen werden:
|
|
$SELF proxy default --domain ${d2:-<name>} --restart"
|
|
fi
|
|
echo
|
|
return 0 ;;
|
|
|
|
reload|restart)
|
|
npm_enabled || die "Der NGINX Proxy Manager ist abgeschaltet (NPM_ENABLED=off).
|
|
Einschalten: $SELF npm-enable on"
|
|
container_running "$NPM_CONTAINER_NAME" \
|
|
|| die "Der Container '$NPM_CONTAINER_NAME' laeuft nicht.
|
|
Starten: $SELF up"
|
|
|
|
if [ "$sub" = reload ] && ! npm_nginx_up; then
|
|
err "Im Container laeuft kein nginx - es gibt nichts neu zu laden."
|
|
log "Dann hilft ein Neustart: $SELF proxy restart"
|
|
return 1
|
|
fi
|
|
|
|
if [ "$sub" = reload ]; then
|
|
# Erst die Konfiguration pruefen. nginx wuerde ein fehlerhaftes
|
|
# Reload zwar ablehnen und weiterlaufen, aber dann steht die
|
|
# Ursache nur im Containerprotokoll.
|
|
local out
|
|
if ! out="$($RUNTIME exec "$NPM_CONTAINER_NAME" nginx -t 2>&1)"; then
|
|
err "Die nginx-Konfiguration ist fehlerhaft - es wird nichts neu geladen."
|
|
printf '%s\n' "$out" | sed 's/^/ /' >&2
|
|
return 1
|
|
fi
|
|
log "Konfiguration in Ordnung, lade nginx neu ..."
|
|
if $RUNTIME exec "$NPM_CONTAINER_NAME" nginx -s reload >/dev/null 2>&1; then
|
|
ok "nginx hat die Konfiguration neu eingelesen (ohne Unterbrechung)."
|
|
else
|
|
err "Neuladen fehlgeschlagen."
|
|
log "Dann hilft ein Neustart: $SELF proxy restart"
|
|
return 1
|
|
fi
|
|
else
|
|
log "Starte '$NPM_CONTAINER_NAME' neu ..."
|
|
if ! $RUNTIME restart "$NPM_CONTAINER_NAME" >/dev/null; then
|
|
err "Neustart fehlgeschlagen."
|
|
log "Protokoll ansehen: $RUNTIME logs --tail 50 $NPM_CONTAINER_NAME"
|
|
return 1
|
|
fi
|
|
# Warten, bis nginx wieder lauscht - und ehrlich melden,
|
|
# wenn es das nicht tut
|
|
local i=0 up=""
|
|
while [ "$i" -lt 60 ]; do
|
|
if npm_nginx_up; then up=1; break; fi
|
|
i=$((i+1)); sleep 1
|
|
done
|
|
if [ -z "$up" ]; then
|
|
warn "Der Container laeuft, nginx ist aber nach 60s nicht da."
|
|
log "Protokoll ansehen: $RUNTIME logs --tail 50 $NPM_CONTAINER_NAME"
|
|
return 1
|
|
fi
|
|
ok "Proxy Manager neu gestartet."
|
|
|
|
# nginx ist schnell wieder da, die Oberflaeche dahinter braucht
|
|
# laenger. Ohne diese zweite Pruefung meldet der Befehl Erfolg,
|
|
# waehrend der Browser noch ins Leere laeuft.
|
|
local url code
|
|
url="${NPM_ADMIN_URL:-http://127.0.0.1:${NPM_ADMIN_PORT:-81}}"
|
|
i=0
|
|
while [ "$i" -lt 45 ]; do
|
|
code="$(http_code "$url")" || code=000
|
|
case "$code" in 2??|3??) break ;; esac
|
|
i=$((i+1)); sleep 1
|
|
done
|
|
case "$code" in
|
|
2??|3??) log "Adminoberflaeche antwortet wieder (nach ${i}s)." ;;
|
|
*) warn "Die Adminoberflaeche antwortet noch nicht ($url, HTTP $code).
|
|
Kurz warten - der Dienst dahinter braucht laenger als nginx." ;;
|
|
esac
|
|
fi
|
|
|
|
printf ' Adminoberflaeche : %s\n' "${NPM_ADMIN_URL:-http://127.0.0.1:${NPM_ADMIN_PORT:-81}}"
|
|
local conf; conf="$(npm_custom_conf_path)"
|
|
if grep -qsF "$NPM_DEF_START" "$conf"; then
|
|
printf ' HTTPS ueber die blanke IP: eingerichtet\n'
|
|
fi
|
|
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
|
|
# Nachsehen, ob der Eintrag wirklich steht. Wird der Proxy Manager
|
|
# gerade erst fertig, nimmt er den Aufruf an und verliert ihn
|
|
# anschliessend wieder - dann meldet nur die Pruefung den Fehler.
|
|
local check; check="$(npm_api GET "/api/nginx/proxy-hosts/$newid")"
|
|
if [ "$(json_num "$check" id)" != "$newid" ]; then
|
|
err "Der Proxy-Host wurde angenommen, ist aber nicht abrufbar."
|
|
log "Das passiert, wenn der Proxy Manager noch startet.
|
|
Kurz warten und noch einmal versuchen:
|
|
$SELF proxy list"
|
|
return 1
|
|
fi
|
|
if ! $RUNTIME exec "$NPM_CONTAINER_NAME" \
|
|
test -f "/data/nginx/proxy_host/$newid.conf" 2>/dev/null; then
|
|
warn "Zu Proxy-Host $newid wurde noch keine nginx-Konfiguration geschrieben."
|
|
log "Nachholen: $SELF proxy reload"
|
|
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${PROXY_ADD_URLPATH-/${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
|
|
| admin | check | reload | restart" ;;
|
|
esac
|
|
}
|
|
|
|
# =====================================================================
|
|
# Vorhandenes Zertifikat von aussen verwenden
|
|
#
|
|
# Bei manchen Kunden liefert eine fremde Stelle das Zertifikat in ein
|
|
# Verzeichnis und ersetzt es dort beim Erneuern - gleicher Dateiname,
|
|
# neuer Inhalt. Der Proxy Manager kopiert hochgeladene Zertifikate
|
|
# aber nach /data/custom_ssl/npm-<id>/ und bekommt von einer ersetzten
|
|
# Datei nichts mit.
|
|
#
|
|
# Deshalb: das Verzeichnis wird schreibgeschuetzt in den Container
|
|
# gehaengt (/certs), und die beiden Dateien unter custom_ssl werden
|
|
# durch Verweise darauf ersetzt. nginx liest die Datei bei jedem
|
|
# Neuladen frisch ein - nachgemessen: ohne Neuladen liefert es
|
|
# weiterhin das alte Zertifikat, danach das neue. Genau dafuer gibt es
|
|
# "cert external --refresh" und den Waechter per cron.
|
|
# =====================================================================
|
|
|
|
CERT_STATE_FILE_NAME="cert-external.state"
|
|
|
|
cert_ext_dir() { printf '%s' "${CERT_EXTERNAL_DIR:-$PROJECT_DIR/data/npm/external-certs}"; }
|
|
cert_ext_full() { printf '%s' "${CERT_EXTERNAL_FULLCHAIN:-fullchain.pem}"; }
|
|
cert_ext_key() { printf '%s' "${CERT_EXTERNAL_KEY:-privkey.pem}"; }
|
|
cert_state_file() { printf '%s/%s\n' "$PROJECT_DIR" "$CERT_STATE_FILE_NAME"; }
|
|
|
|
# Fingerabdruck des Zertifikats - daran haengt, ob neu geladen werden muss
|
|
cert_ext_fingerprint() {
|
|
local d; d="$(cert_ext_dir)"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
openssl x509 -in "$d/$(cert_ext_full)" -noout -fingerprint -sha256 2>/dev/null \
|
|
| sed 's/.*=//' | tr -d ':'
|
|
}
|
|
|
|
# Passen Zertifikat und Schluessel zusammen, und wie lange gilt es noch?
|
|
cert_ext_verify() {
|
|
local d; d="$(cert_ext_dir)"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
local c="$d/$(cert_ext_full)" k="$d/$(cert_ext_key)"
|
|
|
|
[ -r "$c" ] || { err "Nicht lesbar: $c"; return 1; }
|
|
[ -r "$k" ] || { err "Nicht lesbar: $k"; return 1; }
|
|
openssl x509 -in "$c" -noout >/dev/null 2>&1 \
|
|
|| { err "$c ist kein lesbares Zertifikat (PEM erwartet)."; return 1; }
|
|
|
|
local pc pk
|
|
pc="$(openssl x509 -in "$c" -noout -pubkey 2>/dev/null)"
|
|
pk="$(openssl pkey -in "$k" -pubout 2>/dev/null)"
|
|
if [ -z "$pk" ]; then
|
|
err "$k ist kein lesbarer privater Schluessel."; return 1
|
|
fi
|
|
if [ "$pc" != "$pk" ]; then
|
|
err "Zertifikat und Schluessel gehoeren nicht zusammen."
|
|
printf ' %s\n %s\n' "$c" "$k" >&2
|
|
return 1
|
|
fi
|
|
|
|
if ! openssl x509 -in "$c" -noout -checkend 0 >/dev/null 2>&1; then
|
|
warn "Das Zertifikat ist bereits abgelaufen ($(openssl x509 -in "$c" -noout -enddate | sed 's/notAfter=//'))."
|
|
elif ! openssl x509 -in "$c" -noout -checkend 1209600 >/dev/null 2>&1; then
|
|
warn "Das Zertifikat laeuft in weniger als 14 Tagen ab ($(openssl x509 -in "$c" -noout -enddate | sed 's/notAfter=//'))."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Liegt das Verzeichnis im Container? Ohne Mount geht gar nichts.
|
|
cert_ext_mounted() {
|
|
$RUNTIME exec "$NPM_CONTAINER_NAME" test -r "/certs/$(cert_ext_full)" 2>/dev/null
|
|
}
|
|
|
|
# Zeigen die Dateien des Zertifikats <id> schon nach draussen?
|
|
cert_ext_linked() { # id
|
|
[ "$($RUNTIME exec "$NPM_CONTAINER_NAME" readlink "/data/custom_ssl/npm-$1/fullchain.pem" 2>/dev/null)" \
|
|
= "/certs/$(cert_ext_full)" ]
|
|
}
|
|
|
|
# =====================================================================
|
|
# Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager
|
|
# =====================================================================
|
|
cmd_cert_external() {
|
|
local sub="" cid="" dom="" watch=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--id) cid="$2"; shift 2 ;;
|
|
--domain) dom="$2"; shift 2 ;;
|
|
--off) sub=off; shift ;;
|
|
--refresh) sub=refresh; shift ;;
|
|
--watch) sub=watch; watch="$2"; shift 2 ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
Ein von aussen geliefertes Zertifikat verwenden.
|
|
|
|
$SELF cert external Stand anzeigen
|
|
$SELF cert external --domain kunde.de Zertifikat dieses Proxy-Hosts umstellen
|
|
$SELF cert external --id 3 dasselbe ueber die Zertifikats-ID
|
|
$SELF cert external --off --id 3 zurueck auf eine eigene Kopie
|
|
$SELF cert external --refresh bei geaenderter Datei nginx neu laden
|
|
$SELF cert external --watch on|off das taeglich per cron erledigen
|
|
|
|
Das Verzeichnis kommt aus der .env und wird schreibgeschuetzt als
|
|
/certs in den Container gehaengt:
|
|
|
|
CERT_EXTERNAL_DIR=${CERT_EXTERNAL_DIR:-./data/npm/external-certs}
|
|
CERT_EXTERNAL_FULLCHAIN=$(cert_ext_full)
|
|
CERT_EXTERNAL_KEY=$(cert_ext_key)
|
|
|
|
Nach dem Umstellen zeigen die Dateien des Zertifikats im Proxy Manager
|
|
dorthin. Wird die Datei draussen ersetzt, liefert nginx bis zum
|
|
naechsten Neuladen weiterhin das alte Zertifikat - deshalb --refresh.
|
|
EOF
|
|
return 0 ;;
|
|
*) die "Unbekannte Option fuer 'cert external': $1" ;;
|
|
esac
|
|
done
|
|
|
|
local d; d="$(cert_ext_dir)"
|
|
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
|
|
|
|
# --- Waechter ein-/ausschalten -----------------------------------
|
|
if [ "$sub" = watch ]; then
|
|
case "$(fw_lc "$watch")" in
|
|
on|ein|true|1) : ;;
|
|
off|aus|false|0)
|
|
if [ -f "$CERT_CRON_FILE" ]; then
|
|
[ "$(id -u)" -eq 0 ] || die "Zum Entfernen von $CERT_CRON_FILE werden root-Rechte benoetigt."
|
|
rm -f "$CERT_CRON_FILE"; ok "Waechter entfernt."
|
|
else
|
|
ok "Es war kein Waechter eingetragen."
|
|
fi
|
|
env_set CERT_EXTERNAL_WATCH off
|
|
return 0 ;;
|
|
*) die "--watch: on oder off" ;;
|
|
esac
|
|
[ "$(id -u)" -eq 0 ] || die "Zum Schreiben von $CERT_CRON_FILE werden root-Rechte benoetigt."
|
|
local tmp; tmp="$(mktemp)"
|
|
{
|
|
printf '# Waechter fuer das extern gelieferte Zertifikat\n'
|
|
printf '# Erzeugt von cmk-manage.sh cert external --watch on\n'
|
|
printf 'SHELL=/bin/sh\n'
|
|
printf 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n'
|
|
printf '17 4 * * * root %s cert external --refresh >>/var/log/cmk-cert.log 2>&1\n' "$SELF"
|
|
} >"$tmp"
|
|
install -m 0644 "$tmp" "$CERT_CRON_FILE"; rm -f "$tmp"
|
|
env_set CERT_EXTERNAL_WATCH on
|
|
ok "Waechter eingetragen: $CERT_CRON_FILE (taeglich 4:17 Uhr)."
|
|
log "Er laedt nginx nur dann neu, wenn sich die Datei geaendert hat."
|
|
return 0
|
|
fi
|
|
|
|
# --- Nur nachsehen, ob sich etwas geaendert hat -------------------
|
|
if [ "$sub" = refresh ]; then
|
|
local now old sf; sf="$(cert_state_file)"
|
|
now="$(cert_ext_fingerprint)"
|
|
if [ -z "$now" ]; then
|
|
err "Zertifikat nicht lesbar: $d/$(cert_ext_full)"; return 1
|
|
fi
|
|
old="$(cat "$sf" 2>/dev/null || true)"
|
|
if [ "$now" = "$old" ]; then
|
|
log "Zertifikat unveraendert - nichts zu tun."
|
|
return 0
|
|
fi
|
|
log "Das Zertifikat hat sich geaendert."
|
|
cert_ext_verify || return 1
|
|
if cmd_proxy reload; then
|
|
printf '%s\n' "$now" > "$sf"
|
|
ok "Neues Zertifikat ist in Betrieb."
|
|
else
|
|
err "Neuladen fehlgeschlagen - der Stand bleibt vermerkt als offen."
|
|
return 1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# --- Stand anzeigen ----------------------------------------------
|
|
if [ -z "$sub" ] && [ -z "$cid" ] && [ -z "$dom" ]; then
|
|
printf '\n%sExtern geliefertes Zertifikat%s\n' "$C_BLD" "$C_RST"
|
|
printf ' Verzeichnis : %s\n' "$d"
|
|
printf ' Dateien : %s , %s\n' "$(cert_ext_full)" "$(cert_ext_key)"
|
|
if [ -r "$d/$(cert_ext_full)" ]; then
|
|
printf ' Gueltig bis : %s\n' "$(openssl x509 -in "$d/$(cert_ext_full)" -noout -enddate 2>/dev/null | sed 's/notAfter=//')"
|
|
printf ' Aussteller : %s\n' "$(openssl x509 -in "$d/$(cert_ext_full)" -noout -subject 2>/dev/null | sed 's/subject=//')"
|
|
else
|
|
test_result warn "Dort liegt (noch) kein lesbares Zertifikat."
|
|
fi
|
|
if npm_enabled && container_running "$NPM_CONTAINER_NAME"; then
|
|
if cert_ext_mounted; then
|
|
test_result ok "Das Verzeichnis ist im Container als /certs eingehaengt."
|
|
else
|
|
test_result warn "Im Container fehlt /certs"
|
|
printf ' Nach dem Setzen von CERT_EXTERNAL_DIR muss der Container
|
|
neu erstellt werden: %s up\n' "$SELF"
|
|
fi
|
|
local i used=""
|
|
for i in $(json_ids "$(NPM_TOKEN="$(npm_api_token 2>/dev/null)" npm_api GET /api/nginx/certificates 2>/dev/null)" 2>/dev/null); do
|
|
if cert_ext_linked "$i"; then used="$used $i"; fi
|
|
done
|
|
if [ -n "$used" ]; then
|
|
test_result ok "Verwendet von Zertifikat(en):$used"
|
|
else
|
|
test_result warn "Noch kein Zertifikat zeigt dorthin."
|
|
printf ' Umstellen mit: %s cert external --domain <name>\n' "$SELF"
|
|
fi
|
|
fi
|
|
printf ' Waechter : %s\n' \
|
|
"$([ -f "$CERT_CRON_FILE" ] && echo "eingetragen ($CERT_CRON_FILE)" || echo 'nicht eingetragen')"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
# --- Umstellen ----------------------------------------------------
|
|
npm_require_running || return 1
|
|
NPM_TOKEN="$(npm_api_token)" || return 1
|
|
|
|
if [ -z "$cid" ]; then
|
|
[ -n "$dom" ] || die "--domain oder --id angeben."
|
|
local all i o
|
|
all="$(npm_api GET /api/nginx/proxy-hosts)"
|
|
for i in $(json_ids "$all"); do
|
|
o="$(npm_api GET "/api/nginx/proxy-hosts/$i")"
|
|
if ( set +o pipefail; printf '%s' "$o" | grep -qF "\"$dom\"" ); then
|
|
cid="$(json_num "$o" certificate_id)"; break
|
|
fi
|
|
done
|
|
[ -n "$cid" ] || die "Kein Proxy-Host fuer '$dom' gefunden."
|
|
if [ "$cid" = 0 ]; then
|
|
die "Der Proxy-Host '$dom' hat noch kein Zertifikat.
|
|
Erst eines zuweisen, dann umstellen:
|
|
$SELF proxy edit --domain $dom --cert self
|
|
$SELF cert external --domain $dom"
|
|
fi
|
|
fi
|
|
|
|
if [ "$sub" = off ]; then
|
|
cert_ext_linked "$cid" || die "Zertifikat $cid zeigt nicht nach draussen - nichts zurueckzunehmen."
|
|
log "Ersetze die Verweise durch eigene Kopien ..."
|
|
$RUNTIME exec "$NPM_CONTAINER_NAME" sh -c "
|
|
cd /data/custom_ssl/npm-$cid || exit 1
|
|
cp -L fullchain.pem f.tmp && cp -L privkey.pem k.tmp || exit 1
|
|
rm -f fullchain.pem privkey.pem
|
|
mv f.tmp fullchain.pem && mv k.tmp privkey.pem" \
|
|
|| die "Zuruecknehmen fehlgeschlagen."
|
|
ok "Zertifikat $cid hat jetzt wieder eigene Dateien."
|
|
cmd_proxy reload || true
|
|
return 0
|
|
fi
|
|
|
|
cert_ext_verify || return 1
|
|
cert_ext_mounted || die "Im Container gibt es /certs/$(cert_ext_full) nicht.
|
|
Das Verzeichnis wird erst beim Neuerstellen des Containers eingehaengt:
|
|
$SELF config set CERT_EXTERNAL_DIR '$d'
|
|
$SELF up"
|
|
|
|
log "Setze die Dateien von Zertifikat $cid auf /certs ..."
|
|
$RUNTIME exec "$NPM_CONTAINER_NAME" sh -c "
|
|
d=/data/custom_ssl/npm-$cid
|
|
mkdir -p \"\$d\" && cd \"\$d\" || exit 1
|
|
rm -f fullchain.pem privkey.pem
|
|
ln -s '/certs/$(cert_ext_full)' fullchain.pem
|
|
ln -s '/certs/$(cert_ext_key)' privkey.pem" \
|
|
|| die "Die Verweise liessen sich nicht anlegen."
|
|
|
|
cmd_proxy reload || die "nginx hat die Aenderung nicht angenommen."
|
|
cert_ext_fingerprint > "$(cert_state_file)" 2>/dev/null || true
|
|
ok "Zertifikat $cid kommt jetzt aus $d."
|
|
printf ' Wird die Datei dort ersetzt, uebernimmt nginx sie erst beim
|
|
naechsten Neuladen. Dafuer gibt es:
|
|
%s cert external --refresh einmalig
|
|
sudo %s cert external --watch on taeglich per cron\n\n' "$SELF" "$SELF"
|
|
return 0
|
|
}
|
|
|
|
cmd_cert() {
|
|
if [ "${1:-}" = external ]; then shift; cmd_cert_external "$@"; return $?; fi
|
|
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' $SELF_ENV up" ;;
|
|
restart) ui_run "Neu starten" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV 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' $SELF_ENV down" ;;
|
|
pull) ui_run "Images laden" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV 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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV 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_site_create() {
|
|
local name pass
|
|
name="$(ui_input "Weitere Site anlegen" \
|
|
"Name der neuen Site (Buchstaben, Ziffern, _ - hoechstens 16 Zeichen).
|
|
|
|
Sie bekommt eigene Ports: die naechste freie ab 5001 fuer die
|
|
Oberflaeche, ab 8001 fuer den Agent-Receiver. Beides muss danach im
|
|
Proxy bzw. in der Firewall nachgezogen werden - der Befehl sagt genau,
|
|
wie." "")" || return 0
|
|
[ -n "$name" ] || return 0
|
|
case "$name" in
|
|
*[!a-zA-Z0-9_]*) ui_msg "Ungueltig" "Erlaubt sind nur Buchstaben, Ziffern und _"; return 0 ;;
|
|
esac
|
|
if [ "${#name}" -gt 16 ]; then
|
|
ui_msg "Ungueltig" "Hoechstens 16 Zeichen."; return 0
|
|
fi
|
|
|
|
pass="$(ui_password "Weitere Site anlegen" "Kennwort fuer cmkadmin (leer = omd erzeugt eines):")" || return 0
|
|
|
|
local -a a=(site-create "$name")
|
|
if [ -n "$pass" ]; then a+=(--password "$pass"); fi
|
|
local f rc=0
|
|
f="$(mktemp)"
|
|
( NO_COLOR=1 cmd_site_create "${a[@]}" ) >"$f" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Site '$name' angelegt" "$f"
|
|
else
|
|
ui_text "Anlegen fehlgeschlagen" "$f"
|
|
fi
|
|
rm -f "$f"
|
|
return 0
|
|
}
|
|
|
|
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" \
|
|
neu "Weitere Site anlegen (eigene Ports)" \
|
|
start "Site starten" \
|
|
stop "Site stoppen" \
|
|
all "alle Sites starten" \
|
|
loesch "Site samt Daten entfernen" \
|
|
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' $SELF_ENV site-start '$s'" ;;
|
|
stop) local s2; s2="$(gui_pick_site)" || continue; [ -n "$s2" ] || continue
|
|
ui_run "Site stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV site-stop '$s2'" ;;
|
|
all) ui_run "Alle Sites starten" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV start-all-sites" ;;
|
|
neu) gui_site_create || true ;;
|
|
loesch) local s3; s3="$(gui_pick_site)" || continue; [ -n "$s3" ] || continue
|
|
if [ "$s3" = "${CMK_SITE_ID:-cmk}" ]; then
|
|
ui_msg "Standard-Site" "'$s3' ist die Site aus CMK_SITE_ID.
|
|
|
|
Der Entrypoint legt sie beim naechsten Start ohnehin neu an -
|
|
loeschen bringt hier nichts."
|
|
continue
|
|
fi
|
|
ui_yesno "Site entfernen" "Die Site '$s3' und ALLE ihre Daten entfernen?
|
|
|
|
Vorher ein Backup: backup --site $s3" || continue
|
|
ui_run "Site entfernen" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV -y site-delete '$s3'" ;;
|
|
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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -y image load --file '$lf'" || rc=1
|
|
[ "$rc" = 0 ] && ui_run "Edition/Version eintragen" bash -c \
|
|
"cd '$PROJECT_DIR' && '$SELF' $SELF_ENV -y config set CMK_EDITION '$ed' && '$SELF' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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"
|
|
}
|
|
|
|
# Hier geht es nur um eine Frage: taugen die hinterlegten Zugangsdaten?
|
|
# Edition und Version nimmt "image test" ohnehin aus der .env - danach
|
|
# noch einmal zu fragen, waere derselbe Weg zum zweiten Mal.
|
|
gui_image_test() {
|
|
local -a extra=()
|
|
if [ -z "${CMK_REGISTRY_USER:-}${CMK_DOWNLOAD_USER:-}" ]; then
|
|
ui_yesno "Zugangsdaten" "In der .env stehen keine Zugangsdaten.
|
|
|
|
Jetzt welche eingeben und damit pruefen?
|
|
Gespeichert werden sie dabei NICHT - das geht unter
|
|
Einstellungen > Checkmk-Image und Zugangsdaten." || return 0
|
|
local user pass
|
|
user="$(ui_input "Zugangsdaten" "Benutzer (Checkmk-Kundenportal):" "")" || return 0
|
|
[ -n "$user" ] || return 0
|
|
pass="$(ui_password "Zugangsdaten" "Kennwort:")" || return 0
|
|
extra=(--user "$user" --password "$pass")
|
|
fi
|
|
|
|
local f rc=0
|
|
f="$(mktemp)"
|
|
( NO_COLOR=1 image_cmd_test ${extra[@]+"${extra[@]}"} ) >"$f" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Zugangsdaten - in Ordnung" "$f"
|
|
else
|
|
ui_text "Zugangsdaten - Pruefung fehlgeschlagen" "$f"
|
|
fi
|
|
rm -f "$f"
|
|
return 0
|
|
}
|
|
|
|
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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -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' $SELF_ENV -y npm-port '$new'"
|
|
elif [ "$key" = NPM_ENABLED ]; then
|
|
ui_run "Proxy Manager ${new}schalten" bash -c "cd '$PROJECT_DIR' && '$SELF' $SELF_ENV -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' $SELF_ENV 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
|
|
runtime) items+=("__net__" ">> Lokale Verbindungen pruefen (Zwangsproxy)") ;;
|
|
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 ;;
|
|
__net__) gui_noproxy || 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 || true
|
|
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' $SELF_ENV 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' $SELF_ENV -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' $SELF_ENV -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" \
|
|
pruefen "Ziele pruefen - antwortet hinter jedem Pfad etwas?" \
|
|
adminui "Adminoberflaeche erreichbar machen (Name oder Wurzel)" \
|
|
neustart "Proxy Manager neu starten / nginx neu laden" \
|
|
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 ;;
|
|
pruefen) local fp; fp="$(mktemp)"
|
|
( NO_COLOR=1 cmd_proxy check ) >"$fp" 2>&1 || true
|
|
ui_text "Ziele der Proxy-Hosts" "$fp"; rm -f "$fp" ;;
|
|
adminui) gui_proxy_admin || true ;;
|
|
neustart) gui_proxy_restart || true ;;
|
|
del) gui_proxy_del || true ;;
|
|
status) ui_run "Anmeldung pruefen" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' $SELF_ENV 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_admin() {
|
|
local f; f="$(mktemp)"
|
|
( NO_COLOR=1 cmd_proxy admin ) >"$f" 2>&1 || true
|
|
ui_text "Adminoberflaeche erreichbar machen" "$f"; rm -f "$f"
|
|
|
|
local sel
|
|
sel="$(ui_menu "Adminoberflaeche erreichbar machen" \
|
|
"Die Oberflaeche des Proxy Managers laedt ihre Dateien ueber absolute
|
|
Pfade (/images, /api). Unter einem Unterpfad wie /npm kommt deshalb nur
|
|
die Startseite an - im Browser bleibt sie weiss.
|
|
|
|
Zwei Wege, beide mit nur einem freien Port:" \
|
|
name "Eigener Name (braucht Namensaufloesung im Netz)" \
|
|
wurzel "Wurzel -> Adminoberflaeche, Checkmk auf /${CMK_SITE_ID:-cmk}" \
|
|
zurueck "Wurzel wieder auf Checkmk zeigen lassen" \
|
|
back "zurueck")" || return 0
|
|
|
|
local -a args=()
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
name) local d c
|
|
d="$(ui_input "Eigener Name" "Name fuer die Adminoberflaeche:" \
|
|
"npm.${PROXY_DOMAIN:-example.local}")" || return 0
|
|
[ -n "$d" ] || return 0
|
|
c="$(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")" || return 0
|
|
args=(--domain "$d" --cert "$c") ;;
|
|
wurzel)
|
|
ui_yesno "Wurzel umlegen" "Die Wurzel zeigt danach auf die Adminoberflaeche,
|
|
das bisherige Ziel wandert auf /${CMK_SITE_ID:-cmk}.
|
|
|
|
https://<IP>/ Adminoberflaeche
|
|
https://<IP>/${CMK_SITE_ID:-cmk}/ Checkmk
|
|
|
|
Die Anmeldemaske liegt damit offen im Netz. Fortfahren?" || return 0
|
|
args=(--root) ;;
|
|
zurueck)
|
|
ui_yesno "Zuruecknehmen" "Die Wurzel soll wieder auf das urspruengliche Ziel zeigen
|
|
und der Pfad /${CMK_SITE_ID:-cmk} entfallen?" || return 0
|
|
args=(--root --off) ;;
|
|
esac
|
|
|
|
local f2 rc=0
|
|
f2="$(mktemp)"
|
|
( NO_COLOR=1 cmd_proxy admin "${args[@]}" ) >"$f2" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
ui_text "Adminoberflaeche" "$f2"
|
|
else
|
|
ui_text "Fehlgeschlagen" "$f2"
|
|
fi
|
|
rm -f "$f2"
|
|
return 0
|
|
}
|
|
|
|
gui_proxy_restart() {
|
|
local sel
|
|
sel="$(ui_menu "Proxy Manager neu starten" \
|
|
"Nach Aenderungen an der Zusatzkonfiguration - etwa am Standard-Server
|
|
fuer den IP-Zugang oder am Adminport - muss nginx sie erst einlesen.
|
|
|
|
Neu laden genuegt dafuer und unterbricht keine Verbindung. Ein Neustart
|
|
des Containers ist nur noetig, wenn sich Umgebungsvariablen geaendert
|
|
haben oder nginx gar nicht mehr antwortet." \
|
|
reload "nginx neu laden (ohne Unterbrechung)" \
|
|
restart "Container neu starten (kurze Unterbrechung)" \
|
|
back "zurueck")" || return 0
|
|
case "$sel" in back) return 0 ;; esac
|
|
ui_run "Proxy Manager: $sel" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' $SELF_ENV proxy $sel" || true
|
|
return 0
|
|
}
|
|
|
|
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' $SELF_ENV 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' $SELF_ENV -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' $SELF_ENV -y proxy del --id '$id'"
|
|
}
|
|
|
|
gui_cert() {
|
|
local sel
|
|
sel="$(ui_menu "Zertifikate" \
|
|
"Fuer den Proxy Manager. Ein selbstsigniertes Zertifikat erzeugt dieses
|
|
Werkzeug selbst; ein von aussen geliefertes wird nicht kopiert, sondern
|
|
aus seinem Verzeichnis heraus verwendet - dann wirkt ein Austausch
|
|
dort, ohne dass hier etwas nachgezogen werden muss." \
|
|
selbst "Selbstsigniertes Zertifikat erzeugen" \
|
|
extern "Extern geliefertes Zertifikat verwenden" \
|
|
stand "Stand des externen Zertifikats anzeigen" \
|
|
back "zurueck")" || return 0
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
stand) local fs; fs="$(mktemp)"
|
|
( NO_COLOR=1 cmd_cert external ) >"$fs" 2>&1 || true
|
|
ui_text "Extern geliefertes Zertifikat" "$fs"; rm -f "$fs"; return 0 ;;
|
|
extern) gui_cert_external; return 0 ;;
|
|
esac
|
|
gui_cert_self
|
|
}
|
|
|
|
gui_cert_external() {
|
|
local fs; fs="$(mktemp)"
|
|
( NO_COLOR=1 cmd_cert external ) >"$fs" 2>&1 || true
|
|
ui_text "Extern geliefertes Zertifikat" "$fs"; rm -f "$fs"
|
|
|
|
local sel
|
|
sel="$(ui_menu "Extern geliefertes Zertifikat" \
|
|
"Verzeichnis: $(cert_ext_dir)
|
|
Dateien : $(cert_ext_full) , $(cert_ext_key)
|
|
|
|
Geaendert wird das Verzeichnis unter Einstellungen > Selbstsignierte
|
|
Zertifikate. Nach einer Aenderung muss der Container neu erstellt
|
|
werden (Stack starten), damit er es sieht." \
|
|
umstellen "Zertifikat eines Proxy-Hosts darauf umstellen" \
|
|
refresh "Jetzt nachsehen, ob die Datei ausgetauscht wurde" \
|
|
watchon "Taeglich nachsehen (cron eintragen)" \
|
|
watchoff "Taegliches Nachsehen abschalten" \
|
|
zurueck "Zurueck auf eine eigene Kopie" \
|
|
back "zurueck")" || return 0
|
|
|
|
local -a args=()
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
umstellen) local d
|
|
d="$(ui_input "Umstellen" "Name des Proxy-Hosts:" "${PROXY_DOMAIN:-}")" || return 0
|
|
[ -n "$d" ] || return 0
|
|
args=(--domain "$d") ;;
|
|
refresh) args=(--refresh) ;;
|
|
watchon) args=(--watch on) ;;
|
|
watchoff) args=(--watch off) ;;
|
|
zurueck) local i
|
|
i="$(ui_input "Zurueck" "Zertifikats-ID:" "")" || return 0
|
|
[ -n "$i" ] || return 0
|
|
args=(--off --id "$i") ;;
|
|
esac
|
|
|
|
local f2 rc=0
|
|
f2="$(mktemp)"
|
|
if [ "$sel" = watchon ] || [ "$sel" = watchoff ]; then
|
|
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} cert external "${args[@]}"
|
|
rm -f "$f2"; return 0
|
|
fi
|
|
fi
|
|
( NO_COLOR=1 cmd_cert external "${args[@]}" ) >"$f2" 2>&1 || rc=$?
|
|
if [ "$rc" -eq 0 ]; then ui_text "Zertifikat" "$f2"; else ui_text "Fehlgeschlagen" "$f2"; fi
|
|
rm -f "$f2"
|
|
return 0
|
|
}
|
|
|
|
gui_cert_self() {
|
|
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' $SELF_ENV 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" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${a[@]}" || true
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${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
|
|
# Zwangsproxy: geht 127.0.0.1 daran vorbei?
|
|
gui_noproxy() {
|
|
local f rc=0
|
|
f="$(mktemp)"
|
|
( NO_COLOR=1 cmd_noproxy ) >"$f" 2>&1 || rc=$?
|
|
ui_text "Lokale Verbindungen und HTTP-Proxy" "$f"
|
|
rm -f "$f"
|
|
[ "$rc" -eq 0 ] && return 0
|
|
|
|
ui_yesno "HTTP-Proxy" "Lokale Adressen laufen ueber den HTTP-Proxy.
|
|
|
|
Die Ausnahme jetzt dauerhaft eintragen?" || return 0
|
|
|
|
local sel
|
|
sel="$(ui_menu "Wohin eintragen?" \
|
|
"Die Ziele schliessen einander nicht aus - je nachdem, was auf diesem
|
|
Rechner laeuft, braucht es mehrere.
|
|
|
|
Anmeldeshells lesen profile.d und .bashrc, Anmeldungen ohne Shell die
|
|
Datei /etc/environment, und Systemdienste lesen NICHTS davon - fuer die
|
|
gibt es die Ergaenzung der Unit." \
|
|
system "Anmeldeshells: /etc/profile.d/cmk-noproxy.sh (root)" \
|
|
user "Nur fuer mich: ~/.bashrc" \
|
|
environment "Anmeldungen ohne Shell: /etc/environment (root)" \
|
|
service "Systemdienste: Ergaenzung der Unit (root)" \
|
|
jetzt "Nur in dieser Sitzung - Befehl zum Kopieren zeigen" \
|
|
back "zurueck")" || return 0
|
|
case "$sel" in
|
|
back) return 0 ;;
|
|
jetzt) local ex; ex="$( ( NO_COLOR=1 cmd_noproxy --export ) 2>/dev/null )"
|
|
ui_msg "Sofort setzen" "Ein Programm kann die Umgebung seiner Elternshell nicht
|
|
aendern. In der laufenden Shell hilft deshalb:
|
|
|
|
eval \"\$($SELF noproxy --export)\"
|
|
|
|
Das setzt:
|
|
|
|
$ex"
|
|
return 0 ;;
|
|
esac
|
|
|
|
local -a a=(noproxy --fix)
|
|
case "$sel" in
|
|
user) a+=(--user) ;;
|
|
environment) a+=(--environment) ;;
|
|
service) a+=(--service) ;;
|
|
esac
|
|
if [ "$sel" = user ]; then
|
|
local f2; f2="$(mktemp)"
|
|
( NO_COLOR=1 cmd_noproxy --fix --user ) >"$f2" 2>&1 || true
|
|
ui_text "Eingetragen" "$f2"; rm -f "$f2"
|
|
elif [ "$(id -u)" -eq 0 ]; then
|
|
ui_run "Eintragen" "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${a[@]}" || true
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${a[@]}"
|
|
else
|
|
ui_msg "Rechte" "Dafuer werden root-Rechte benoetigt."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
gui_firewall_close() {
|
|
local tmp rc=0 hat="" adminfall=""
|
|
tmp="$(mktemp)"
|
|
( cmd_firewall close ) >"$tmp" 2>&1 || rc=$?
|
|
if ( set +o pipefail; grep -q 'Zum Schliessen' "$tmp" ); then hat=1; fi
|
|
if ( set +o pipefail; grep -q 'Zugang dann nur lokal' "$tmp" ); then adminfall=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)
|
|
# Ist der Adminport dabei, ist das eine bewusste Entscheidung
|
|
if [ -n "$adminfall" ]; then
|
|
if ui_yesno "Adminoberflaeche" "Der Adminport ${NPM_ADMIN_PORT:-81} des Proxy Managers ist dabei.
|
|
|
|
Soll er offen bleiben?
|
|
|
|
Ja = Adminoberflaeche bleibt aus dem Netz erreichbar
|
|
Nein = nur noch lokal (Terminal-Browser oder SSH-Tunnel)"; then
|
|
a+=(--keep-admin)
|
|
fi
|
|
fi
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
ui_run "Ports schliessen" "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${a[@]}" || true
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
gui_run_interactive sudo -- "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} "${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}"
|
|
# Auch die Vorgabe 81 - wer den Adminport spaeter verlegt hat, haette
|
|
# den alten sonst weiter offen, ohne dass ihn noch jemand nennt.
|
|
printf '81/tcp|Adminoberflaeche - alter Vorgabeport\n'
|
|
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 keep_admin
|
|
local be="$1" apply="${2:-}" keep_admin="${3:-}"
|
|
local spec need port proto desc
|
|
local -a todo=()
|
|
|
|
# Was gebraucht wird, darf nicht weg. Der Adminport des Proxy
|
|
# Managers gehoert NICHT dazu: die Oberflaeche ist auch ueber
|
|
# 127.0.0.1 und den Terminal-Browser zu erreichen, und offen im Netz
|
|
# hat sie nichts verloren. Wer ihn behalten will: --keep-admin.
|
|
local noetig="" adminports=""
|
|
while IFS='|' read -r spec desc need; do
|
|
[ -n "$spec" ] || continue
|
|
case "$need" in
|
|
ja) noetig="$noetig$spec"$'\n' ;;
|
|
admin) if [ -n "$keep_admin" ]; then
|
|
noetig="$noetig$spec"$'\n'
|
|
else
|
|
adminports="$adminports$spec"$'\n'
|
|
fi ;;
|
|
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")
|
|
if ( set +o pipefail; printf '%s' "$adminports" | grep -qxF "$spec" ); then
|
|
printf ' %-10s %-44s %b\n' "$spec" "$desc" \
|
|
"${C_YEL}kann zu - Zugang dann nur lokal${C_RST}"
|
|
else
|
|
printf ' %-10s %-44s %b\n' "$spec" "$desc" "${C_YEL}kann zu${C_RST}"
|
|
fi
|
|
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
|
|
|
|
# Betrifft es den Adminport, gehoert dazu gesagt, was das bedeutet
|
|
local t2
|
|
for t2 in "${todo[@]}"; do
|
|
if ( set +o pipefail; printf '%s' "$adminports" | grep -qxF "$t2" ); then
|
|
printf '\n Der Adminport %s des Proxy Managers ist dabei. Die Oberflaeche ist
|
|
danach nur noch lokal erreichbar:
|
|
%s browser (Terminal-Browser im Container)
|
|
ssh -L %s:127.0.0.1:%s ... (Tunnel vom Arbeitsplatz)
|
|
Soll er offen bleiben: %s firewall close --keep-admin\n' \
|
|
"${t2%/*}" "$SELF" "${t2%/*}" "${t2%/*}" "$SELF"
|
|
break
|
|
fi
|
|
done
|
|
|
|
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="" keep_admin=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--apply) apply=1; shift ;;
|
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
--source) src="$2"; shift 2 ;;
|
|
--keep-admin) keep_admin=1; shift ;;
|
|
*) 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" "$keep_admin"
|
|
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
|
|
}
|
|
|
|
# =====================================================================
|
|
# Zwangsproxy: lokale Adressen ausnehmen
|
|
#
|
|
# In Kundennetzen steht oft ein HTTP-Proxy in der Umgebung. Nimmt
|
|
# no_proxy die lokalen Adressen nicht aus, schickt curl selbst eine
|
|
# Anfrage an 127.0.0.1 dorthin. Der Proxy kennt die Adresse nicht, und
|
|
# am Ende steht ein "API-Fehler" beim Anmelden am Proxy Manager - eine
|
|
# Meldung, die in die voellig falsche Richtung zeigt.
|
|
#
|
|
# Dieses Werkzeug umgeht das fuer seine eigenen Aufrufe bereits
|
|
# (curl --noproxy). Fuer alles andere auf dem Rechner - eigene
|
|
# curl-Tests, der Terminal-Browser, andere Skripte - muss no_proxy
|
|
# stimmen. Genau das prueft und richtet dieser Befehl ein.
|
|
# =====================================================================
|
|
|
|
NOPROXY_FILE="${NOPROXY_FILE:-/etc/profile.d/cmk-noproxy.sh}"
|
|
# Ueberschreibbar, damit sich die Schreibwege pruefen lassen, ohne /etc
|
|
# dieses Rechners anzufassen.
|
|
NOPROXY_ENV_FILE="${NOPROXY_ENV_FILE:-/etc/environment}"
|
|
NOPROXY_UNIT_DIR="${NOPROXY_UNIT_DIR:-/etc/systemd/system}"
|
|
|
|
# Welche Adressen gehoeren in no_proxy?
|
|
noproxy_list() {
|
|
local ip host
|
|
ip="$(primary_ip 2>/dev/null)"
|
|
host="$(hostname 2>/dev/null)"
|
|
local l="localhost,127.0.0.1,::1"
|
|
if [ -n "$host" ]; then l="$l,$host"; fi
|
|
if [ -n "$ip" ]; then l="$l,$ip"; fi
|
|
printf '%s' "$l"
|
|
}
|
|
|
|
# Vorhandenes zuerst, dann unsere Adressen, ohne Doppelte
|
|
noproxy_merge() { # alt-klein alt-gross
|
|
local all e out=""
|
|
all="$1"
|
|
if [ -n "${2:-}" ]; then all="${all:+${all},}$2"; fi
|
|
all="${all:+${all},}$(noproxy_list)"
|
|
for e in $(printf '%s' "$all" | tr ',' ' '); do
|
|
case ",$out," in
|
|
*",$e,"*) ;;
|
|
*) out="${out:+${out},}$e" ;;
|
|
esac
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# Wuerde curl fuer diese Adresse den Proxy benutzen?
|
|
noproxy_would_use_proxy() { # url
|
|
local out
|
|
out="$(curl -sS -o /dev/null -v --max-time 3 "$1" 2>&1 || true)"
|
|
case "$out" in
|
|
*"Uses proxy env variable http_proxy"*|*"Uses proxy env variable https_proxy"*) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
cmd_noproxy() {
|
|
local fix="" scope=system export_only=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--fix) fix=1; shift ;;
|
|
--export) export_only=1; shift ;;
|
|
--user) scope=user; shift ;;
|
|
--system) scope=system; shift ;;
|
|
--environment) scope=environment; shift ;;
|
|
--service) scope=service; shift ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
|
|
Pruefen, ob lokale Adressen am HTTP-Proxy vorbeigehen.
|
|
|
|
$SELF noproxy nachsehen
|
|
eval "\$($SELF noproxy --export)" sofort in DIESER Sitzung setzen
|
|
|
|
Dauerhaft eintragen - die Ziele schliessen einander nicht aus:
|
|
sudo $SELF noproxy --fix Anmeldeshells ($NOPROXY_FILE)
|
|
$SELF noproxy --fix --user nur dieser Benutzer (~/.bashrc)
|
|
sudo $SELF noproxy --fix --environment /etc/environment (auch fuer
|
|
Anmeldungen ohne Shell)
|
|
sudo $SELF noproxy --fix --service systemd-Unit des Stacks
|
|
(Dienste lesen die Dateien oben
|
|
NICHT)
|
|
|
|
Ein Programm kann die Umgebung seiner Elternshell nicht aendern -
|
|
deshalb der Umweg ueber "eval" fuer das Sofort-Setzen.
|
|
|
|
Steht in der Umgebung ein HTTP-Proxy und nimmt no_proxy die lokalen
|
|
Adressen nicht aus, laufen selbst Anfragen an 127.0.0.1 ueber ihn. Der
|
|
Proxy kennt die Adresse nicht - sichtbar wird das als "API-Fehler" beim
|
|
Anmelden am Proxy Manager, also an einer Stelle, die nichts dafuer kann.
|
|
EOF
|
|
return 0 ;;
|
|
*) die "Unbekannte Option fuer 'noproxy': $1" ;;
|
|
esac
|
|
done
|
|
|
|
local url="http://127.0.0.1:${NPM_ADMIN_PORT:-81}/"
|
|
|
|
# Nur die Zeilen zum Auswerten - fuer: eval "$(... noproxy --export)"
|
|
if [ -n "$export_only" ]; then
|
|
local merged; merged="$(noproxy_merge "${no_proxy:-}" "${NO_PROXY:-}")"
|
|
printf "export no_proxy='%s' NO_PROXY='%s'\n" "$merged" "$merged"
|
|
return 0
|
|
fi
|
|
|
|
printf '\n%sHTTP-Proxy in der Umgebung%s\n' "$C_BLD" "$C_RST"
|
|
# HTTP_PROXY in Grossschreibung beachtet curl absichtlich NICHT - eine
|
|
# Kopfzeile "Proxy:" wuerde sie in CGI-Umgebungen sonst setzen koennen.
|
|
# no_proxy, NO_PROXY, https_proxy und HTTPS_PROXY gelten dagegen alle.
|
|
local v any=""
|
|
for v in http_proxy HTTP_PROXY https_proxy HTTPS_PROXY; do
|
|
local val; eval "val=\"\${$v:-}\""
|
|
[ -n "$val" ] || continue
|
|
if [ "$v" = HTTP_PROXY ]; then
|
|
printf ' %-12s %s (curl ignoriert diese Schreibweise)\n' "$v" "$val"
|
|
else
|
|
printf ' %-12s %s\n' "$v" "$val"
|
|
any=1
|
|
fi
|
|
done
|
|
for v in no_proxy NO_PROXY; do
|
|
local val; eval "val=\"\${$v:-}\""
|
|
printf ' %-12s %s\n' "$v" "${val:-<leer>}"
|
|
done
|
|
|
|
if [ -z "$any" ]; then
|
|
echo
|
|
ok "Es ist kein HTTP-Proxy gesetzt - lokale Anfragen gehen direkt."
|
|
printf ' Auf Rechnern mit Zwangsproxy sieht das anders aus; dort hilft:
|
|
%s noproxy --fix\n\n' "$SELF"
|
|
return 0
|
|
fi
|
|
|
|
printf '\n%sGeht %s daran vorbei?%s\n' "$C_BLD" "$url" "$C_RST"
|
|
if noproxy_would_use_proxy "$url"; then
|
|
test_result fail "Nein - curl schickt auch diese Anfrage an den Proxy."
|
|
printf ' Daher kommen Meldungen wie "API-Fehler" beim Anmelden am
|
|
Proxy Manager: nicht der Proxy Manager antwortet nicht, sondern
|
|
der HTTP-Proxy weiss mit 127.0.0.1 nichts anzufangen.\n'
|
|
else
|
|
test_result ok "Ja - lokale Anfragen gehen direkt."
|
|
if [ -z "$fix" ]; then
|
|
printf '\n Die eigenen Aufrufe dieses Werkzeugs gehen ohnehin daran vorbei
|
|
(curl --noproxy). Fuer eigene Tests und andere Programme ist es
|
|
damit ebenfalls in Ordnung.\n\n'
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local liste; liste="$(noproxy_list)"
|
|
printf '\n%sEinzutragen%s\n' "$C_BLD" "$C_RST"
|
|
printf ' no_proxy=%s\n' "$liste"
|
|
|
|
if [ -z "$fix" ]; then
|
|
printf '\n Dauerhaft setzen:
|
|
sudo %s noproxy --fix fuer alle (%s)
|
|
%s noproxy --fix --user nur fuer diesen Benutzer (~/.bashrc)\n\n' \
|
|
"$SELF" "$NOPROXY_FILE" "$SELF"
|
|
return 1
|
|
fi
|
|
|
|
# --- /etc/environment ---------------------------------------------
|
|
# Keine Shell-Datei: schlichte KEY=WERT-Zeilen, von PAM gelesen. Der
|
|
# Wert muss deshalb hier fertig zusammengesetzt werden.
|
|
if [ "$scope" = environment ]; then
|
|
local ef="$NOPROXY_ENV_FILE"
|
|
[ "$(id -u)" -eq 0 ] || die "Zum Schreiben von $ef werden root-Rechte benoetigt."
|
|
local alt_k alt_g merged
|
|
alt_k="$(sed -n 's/^no_proxy=//p' "$ef" 2>/dev/null | tail -1 | tr -d '"')"
|
|
alt_g="$(sed -n 's/^NO_PROXY=//p' "$ef" 2>/dev/null | tail -1 | tr -d '"')"
|
|
merged="$(noproxy_merge "${alt_k:-${no_proxy:-}}" "${alt_g:-${NO_PROXY:-}}")"
|
|
local t2; t2="$(mktemp)"
|
|
{ grep -vE '^(no_proxy|NO_PROXY)=' "$ef" 2>/dev/null || true
|
|
printf 'no_proxy="%s"\n' "$merged"
|
|
printf 'NO_PROXY="%s"\n' "$merged"
|
|
} >"$t2"
|
|
install -m 0644 "$t2" "$ef"; rm -f "$t2"
|
|
ok "Eingetragen in $ef"
|
|
log "Gilt fuer neue Anmeldungen. Systemdienste lesen diese Datei NICHT -
|
|
dafuer: $SELF noproxy --fix --service"
|
|
return 0
|
|
fi
|
|
|
|
# --- systemd-Unit des Stacks --------------------------------------
|
|
if [ "$scope" = service ]; then
|
|
[ "$(id -u)" -eq 0 ] || die "Zum Schreiben einer Unit-Ergaenzung werden root-Rechte benoetigt."
|
|
command -v systemctl >/dev/null 2>&1 || die "systemd ist hier nicht im Einsatz."
|
|
local merged; merged="$(noproxy_merge "${no_proxy:-}" "${NO_PROXY:-}")"
|
|
local -a units=() u
|
|
local lst
|
|
lst="$(systemctl list-unit-files --no-legend 2>/dev/null || true)"
|
|
for u in checkmk-stack.service checkmk.service nginx-proxy-manager.service; do
|
|
case "$lst" in
|
|
"$u"*|*"
|
|
$u"*) units+=("$u") ;;
|
|
esac
|
|
done
|
|
if [ "${#units[@]}" -eq 0 ]; then
|
|
err "Keine Unit des Stacks gefunden."
|
|
printf ' Angelegt werden sie von: ./install.sh --with-systemd bzw. --quadlet
|
|
Von Hand waere es eine Ergaenzung mit diesem Inhalt:
|
|
|
|
[Service]
|
|
Environment=no_proxy=%s
|
|
Environment=NO_PROXY=%s
|
|
\n' "$merged" "$merged" >&2
|
|
return 1
|
|
fi
|
|
for u in "${units[@]}"; do
|
|
local d="$NOPROXY_UNIT_DIR/$u.d"
|
|
mkdir -p "$d"
|
|
{
|
|
printf '# Lokale Adressen nicht ueber den HTTP-Proxy leiten.\n'
|
|
printf '# Erzeugt von cmk-manage.sh noproxy --fix --service\n'
|
|
printf '[Service]\n'
|
|
printf 'Environment=no_proxy=%s\n' "$merged"
|
|
printf 'Environment=NO_PROXY=%s\n' "$merged"
|
|
# Der Proxy selbst gehoert mit hinein: ohne ihn kommt der
|
|
# Dienst beim Holen von Images nicht ins Internet.
|
|
if [ -n "${http_proxy:-}" ]; then printf 'Environment=http_proxy=%s\n' "$http_proxy"; fi
|
|
if [ -n "${https_proxy:-}" ]; then printf 'Environment=https_proxy=%s\n' "$https_proxy"; fi
|
|
if [ -n "${https_proxy:-}" ]; then printf 'Environment=HTTPS_PROXY=%s\n' "$https_proxy"; fi
|
|
} > "$d/cmk-noproxy.conf"
|
|
ok "Ergaenzung geschrieben: $d/cmk-noproxy.conf"
|
|
done
|
|
if [ "$NOPROXY_UNIT_DIR" = /etc/systemd/system ]; then
|
|
systemctl daemon-reload && ok "systemd hat die Units neu eingelesen."
|
|
fi
|
|
log "Wirksam beim naechsten Start der Unit: systemctl restart ${units[0]}"
|
|
return 0
|
|
fi
|
|
|
|
local target
|
|
if [ "$scope" = user ]; then
|
|
target="$HOME/.bashrc"
|
|
else
|
|
target="$NOPROXY_FILE"
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
die "Zum Schreiben von $target werden root-Rechte benoetigt.
|
|
Entweder mit sudo, oder nur fuer diesen Benutzer:
|
|
$SELF noproxy --fix --user"
|
|
fi
|
|
fi
|
|
|
|
if ( set +o pipefail; grep -q 'cmk-manage.sh noproxy' "$target" 2>/dev/null ); then
|
|
ok "In $target steht der Eintrag bereits."
|
|
log "Wirksam wird er in neuen Sitzungen - oder sofort mit:
|
|
export no_proxy='$liste' NO_PROXY='$liste'"
|
|
return 0
|
|
fi
|
|
|
|
# Wichtig: was schon dasteht, bleibt - und zwar VORNE. Beide
|
|
# Schreibweisen werden eingesammelt, denn manche Systeme setzen nur
|
|
# NO_PROXY; wer nur no_proxy liest, wirft das Grosse weg. Doppelte
|
|
# Eintraege fallen dabei heraus.
|
|
local tmp; tmp="$(mktemp)"
|
|
{
|
|
printf '\n# Lokale Adressen nicht ueber den HTTP-Proxy leiten.\n'
|
|
printf '# Erzeugt von cmk-manage.sh noproxy --fix\n'
|
|
printf '# Vorhandene Eintraege bleiben erhalten und stehen vorn.\n'
|
|
printf '_cmk_local="%s"\n' "$liste"
|
|
printf '_cmk_all="${no_proxy:-}"\n'
|
|
printf 'if [ -n "${NO_PROXY:-}" ]; then _cmk_all="${_cmk_all:+${_cmk_all},}${NO_PROXY}"; fi\n'
|
|
printf '_cmk_all="${_cmk_all:+${_cmk_all},}${_cmk_local}"\n'
|
|
printf '_cmk_out=""\n'
|
|
printf 'for _cmk_e in $(printf "%%s" "$_cmk_all" | tr "," " "); do\n'
|
|
printf ' case ",${_cmk_out}," in\n'
|
|
printf ' *",${_cmk_e},"*) ;;\n'
|
|
printf ' *) _cmk_out="${_cmk_out:+${_cmk_out},}${_cmk_e}" ;;\n'
|
|
printf ' esac\n'
|
|
printf 'done\n'
|
|
printf 'no_proxy="$_cmk_out"\n'
|
|
printf 'NO_PROXY="$_cmk_out"\n'
|
|
printf 'export no_proxy NO_PROXY\n'
|
|
printf 'unset _cmk_local _cmk_all _cmk_out _cmk_e\n'
|
|
} >"$tmp"
|
|
|
|
if [ "$scope" = user ]; then
|
|
cat "$tmp" >> "$target"
|
|
else
|
|
{ [ -f "$target" ] && cat "$target"; printf '#!/bin/sh\n'; } >/dev/null 2>&1
|
|
{ printf '#!/bin/sh\n'; cat "$tmp"; } > "$tmp.f"
|
|
install -m 0644 "$tmp.f" "$target"
|
|
rm -f "$tmp.f"
|
|
fi
|
|
rm -f "$tmp"
|
|
ok "Eingetragen in $target"
|
|
log "Vorhandene Eintraege bleiben erhalten und stehen vorn - ergaenzt wird
|
|
nur, was fehlt (beide Schreibweisen, no_proxy und NO_PROXY)."
|
|
log "Wirksam in neuen Sitzungen. In DIESER Sitzung sofort:
|
|
eval \"\$($SELF noproxy --export)\"
|
|
(ein Programm kann die Umgebung seiner Elternshell nicht aendern)"
|
|
log "Alter Weg, gleichwertig:
|
|
export no_proxy='$liste' NO_PROXY='$liste'"
|
|
cat <<EOF
|
|
|
|
${C_BLD}Was damit NICHT erledigt ist${C_RST}
|
|
Dienste lesen keine Profildateien. Dafuer gibt es zwei weitere Ziele:
|
|
sudo $SELF noproxy --fix --environment /etc/environment
|
|
sudo $SELF noproxy --fix --service Ergaenzung der systemd-Unit
|
|
|
|
EOF
|
|
return 0
|
|
}
|
|
|
|
# =====================================================================
|
|
# 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
|
|
|
|
if [ -n "$ENVFILE_OPT" ]; then
|
|
SELF_ENV="--env-file '$ENVFILE_OPT'"
|
|
SELF_ENV_ARR=(--env-file "$ENVFILE_OPT")
|
|
fi
|
|
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 ;;
|
|
site-create) cmd_site_create "$@" ;;
|
|
site-delete|site-rm) cmd_site_delete "$@" ;;
|
|
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 "$@" ;;
|
|
noproxy) cmd_noproxy "$@" ;;
|
|
help|-h|--help) usage ;;
|
|
*) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|