Der NGINX Proxy Manager laesst sich jetzt auch dann einrichten, wenn
seine Oberflaeche auf Port 81 aus dem Netz nicht erreichbar ist.
browser
Terminal-Browser in einem eigenen Container, gestartet mit
"run --rm -it --network host" - damit erreicht er 127.0.0.1 genau wie
der Proxy Manager selbst. Engines: browsh und carbonyl (rendern einen
echten Browser und koennen JavaScript, das die Adminoberflaeche
benoetigt) sowie links2/w3m/lynx und eine Shell mit curl/jq/openssl
fuer Diagnose. Werkzeug-Image unter containers/browser/.
Bewusst kein Compose-Dienst: ein Browser ist interaktiv, und
podman-compose behandelt Profile unzuverlaessig.
npm-port
Port der Adminoberflaeche frei waehlbar. Das Image bindet fest auf 81
("listen 81 default;"), eine Umgebungsvariable dafuer gibt es in 2.15.1
noch nicht. Ein abweichender Port wird deshalb ueber den nginx-
Erweiterungspunkt /data/nginx/custom/http.conf bereitgestellt. Nur der
markierte Block wird verwaltet, eigene Eintraege bleiben erhalten.
Port 81 lauscht weiterhin lokal und ist per Firewall zu sperren.
config
Alle Werte der .env sind menuegefuehrt bzw. ueber config get/set
aenderbar - der Editor bleibt als Menuepunkt erhalten. Geprueft wird
beim Setzen: Portbereich und Kollisionen, Site-Namen, vollqualifizierte
Image-Namen, installierte Runtime, Zahlen- und URL-Felder.
Fehlerbehebungen
- ui_*-Funktionen initialisieren sich selbst; eine von aussen gesetzte
UI_BIN fuehrte unter "set -u" zum Abbruch.
- Eine nicht installierte CMK_RUNTIME machte das gesamte Werkzeug
unbenutzbar, auch den Aufruf zum Korrigieren. Jetzt wird gewarnt und
selbst gesucht.
- env_set setzt Werte mit Leerzeichen in Anfuehrungszeichen und laesst
optional leere Werte zu.
README auf 22 Kapitel erweitert: Kapitel 11 (Terminal-Browser inkl.
NPM-API als Fallback fuer den Zertifikats-Upload), Kapitel 12 (Admin-Port)
und Abschnitt 14.3 (Einstellungsmenue).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
330 lines
12 KiB
Bash
330 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
# =====================================================================
|
|
# common.sh - gemeinsame Funktionen fuer cmk-manage.sh
|
|
# Wird per "source" eingebunden, nicht direkt ausgefuehrt.
|
|
# =====================================================================
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Ausgabe / Logging
|
|
# ---------------------------------------------------------------------
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'
|
|
C_BLU=$'\033[34m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m'
|
|
else
|
|
C_RED=; C_GRN=; C_YEL=; C_BLU=; C_BLD=; C_RST=
|
|
fi
|
|
|
|
log() { printf '%s[ %s ]%s %s\n' "$C_BLU" "INFO" "$C_RST" "$*"; }
|
|
ok() { printf '%s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$*"; }
|
|
warn() { printf '%s[ WARN ]%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; }
|
|
err() { printf '%s[FEHLER]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
|
|
die() { err "$*"; exit 1; }
|
|
debug() { [ -n "${VERBOSE:-}" ] && printf ' > %s\n' "$*" >&2 || true; }
|
|
|
|
# ---------------------------------------------------------------------
|
|
# .env laden
|
|
# ---------------------------------------------------------------------
|
|
load_env() {
|
|
local envfile="${1:-$PROJECT_DIR/.env}"
|
|
if [ -f "$envfile" ]; then
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$envfile"
|
|
set +a
|
|
ENV_FILE="$envfile"
|
|
else
|
|
warn "Keine .env gefunden ($envfile) - es gelten die Standardwerte."
|
|
ENV_FILE=""
|
|
fi
|
|
# Rueckfallwerte, falls ein Schluessel in der .env fehlt.
|
|
# Die .env bleibt die massgebliche Konfiguration - hier steht nur,
|
|
# was gilt, wenn dort nichts eingetragen ist.
|
|
CMK_CONTAINER_NAME="${CMK_CONTAINER_NAME:-checkmk}"
|
|
NPM_CONTAINER_NAME="${NPM_CONTAINER_NAME:-nginx-proxy-manager}"
|
|
CMK_SITE_ID="${CMK_SITE_ID:-cmk}"
|
|
CMK_IMAGE="${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest}"
|
|
NPM_ADMIN_PORT="${NPM_ADMIN_PORT:-81}"
|
|
# Leer = automatisch aus dem Admin-Port ableiten
|
|
NPM_ADMIN_URL="${NPM_ADMIN_URL:-http://127.0.0.1:$NPM_ADMIN_PORT}"
|
|
BACKUP_DIR="$(abs_path "${BACKUP_DIR:-$PROJECT_DIR/backups}")"
|
|
IMAGE_DIR="$(abs_path "${IMAGE_DIR:-$PROJECT_DIR/images}")"
|
|
}
|
|
|
|
# relativen Pfad (./foo) am Projektverzeichnis ausrichten
|
|
abs_path() {
|
|
case "$1" in
|
|
/*) printf '%s\n' "$1" ;;
|
|
*) printf '%s\n' "$PROJECT_DIR/${1#./}" ;;
|
|
esac
|
|
}
|
|
|
|
# .env-Wert dauerhaft setzen
|
|
# env_set KEY WERT [allow_empty]
|
|
env_set() {
|
|
local key="$1" val="$2" allow_empty="${3:-}" f="${ENV_FILE:-$PROJECT_DIR/.env}"
|
|
[ -f "$f" ] || die "Keine .env vorhanden - kann '$key' nicht setzen."
|
|
# mehrzeilige Werte wuerden die .env zerstoeren
|
|
case "$val" in
|
|
*$'\n'*) die "Ungueltiger Wert fuer '$key' (mehrzeilig)." ;;
|
|
esac
|
|
[ -n "$val" ] || [ -n "$allow_empty" ] || die "Leerer Wert fuer '$key' - abgebrochen."
|
|
# Werte mit Leerzeichen in Anfuehrungszeichen setzen
|
|
case "$val" in
|
|
*" "*) case "$val" in \"*\") : ;; *) val="\"$val\"" ;; esac ;;
|
|
esac
|
|
if grep -qE "^[#[:space:]]*${key}=" "$f"; then
|
|
sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${val}|" "$f"
|
|
else
|
|
printf '%s=%s\n' "$key" "$val" >>"$f"
|
|
fi
|
|
export "$key=$val"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Container-Runtime erkennen (docker / podman)
|
|
# ---------------------------------------------------------------------
|
|
detect_runtime() {
|
|
if [ -n "${CMK_RUNTIME:-}" ] && command -v "$CMK_RUNTIME" >/dev/null 2>&1; then
|
|
RUNTIME="$CMK_RUNTIME"
|
|
elif [ -n "${CMK_RUNTIME:-}" ]; then
|
|
# Falsch gesetzter Wert darf das Werkzeug nicht unbenutzbar machen
|
|
warn "CMK_RUNTIME='$CMK_RUNTIME' ist nicht installiert - suche selbst.
|
|
Korrigieren mit: cmk-manage.sh config set CMK_RUNTIME auto"
|
|
CMK_RUNTIME=""
|
|
detect_runtime
|
|
return
|
|
elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
|
RUNTIME=docker
|
|
elif command -v podman >/dev/null 2>&1; then
|
|
RUNTIME=podman
|
|
elif command -v docker >/dev/null 2>&1; then
|
|
RUNTIME=docker # vorhanden, aber Daemon evtl. gestoppt
|
|
else
|
|
die "Weder docker noch podman gefunden. Bitte zuerst ./install.sh ausfuehren."
|
|
fi
|
|
debug "Runtime: $RUNTIME"
|
|
}
|
|
|
|
# Compose-Kommando ermitteln (als Array COMPOSE_CMD)
|
|
detect_compose() {
|
|
COMPOSE_CMD=()
|
|
if [ "$RUNTIME" = docker ]; then
|
|
if docker compose version >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(docker compose)
|
|
elif command -v docker-compose >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(docker-compose)
|
|
fi
|
|
else
|
|
if command -v podman-compose >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(podman-compose)
|
|
elif podman compose version >/dev/null 2>&1; then
|
|
COMPOSE_CMD=(podman compose)
|
|
fi
|
|
fi
|
|
[ ${#COMPOSE_CMD[@]} -gt 0 ] || die "Kein Compose-Kommando gefunden (docker compose / podman-compose)."
|
|
debug "Compose: ${COMPOSE_CMD[*]}"
|
|
}
|
|
|
|
compose() {
|
|
detect_compose
|
|
( cd "$PROJECT_DIR" && "${COMPOSE_CMD[@]}" "$@" )
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Container-Helfer
|
|
# ---------------------------------------------------------------------
|
|
container_exists() { $RUNTIME container inspect "$1" >/dev/null 2>&1; }
|
|
container_running() { [ "$($RUNTIME container inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]; }
|
|
|
|
require_cmk_running() {
|
|
container_exists "$CMK_CONTAINER_NAME" \
|
|
|| die "Container '$CMK_CONTAINER_NAME' existiert nicht. Zuerst: $0 up"
|
|
container_running "$CMK_CONTAINER_NAME" \
|
|
|| die "Container '$CMK_CONTAINER_NAME' laeuft nicht. Zuerst: $0 up"
|
|
}
|
|
|
|
# Kommando im Checkmk-Container als root ausfuehren
|
|
ct() { $RUNTIME exec "$CMK_CONTAINER_NAME" "$@"; }
|
|
# ... mit stdin
|
|
cti() { $RUNTIME exec -i "$CMK_CONTAINER_NAME" "$@"; }
|
|
|
|
# ---------------------------------------------------------------------
|
|
# OMD-Helfer (im Container)
|
|
# ---------------------------------------------------------------------
|
|
cmk_sites() {
|
|
ct sh -c 'ls -1 /omd/sites 2>/dev/null' | sed '/^$/d' || true
|
|
}
|
|
|
|
cmk_site_exists() {
|
|
cmk_sites | grep -qxF "$1"
|
|
}
|
|
|
|
cmk_site_version() {
|
|
ct omd version -b "$1" 2>/dev/null | tr -d '\r' || true
|
|
}
|
|
|
|
# 0 = laeuft komplett, 1 = teilweise, 2 = gestoppt
|
|
cmk_site_state() {
|
|
local rc=0
|
|
ct omd status --bare "$1" >/dev/null 2>&1 || rc=$?
|
|
echo "$rc"
|
|
}
|
|
|
|
cmk_site_state_text() {
|
|
case "$(cmk_site_state "$1")" in
|
|
0) echo "laeuft" ;;
|
|
1) echo "teilweise" ;;
|
|
*) echo "gestoppt" ;;
|
|
esac
|
|
}
|
|
|
|
cmk_versions_installed() {
|
|
ct omd versions -b 2>/dev/null | tr -d '\r' || ct sh -c 'ls -1 /omd/versions | grep -v "^default$"' 2>/dev/null || true
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Diverses
|
|
# ---------------------------------------------------------------------
|
|
human_size() {
|
|
local b="${1:-0}"
|
|
if [ "$b" -ge 1073741824 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f GiB", b/1073741824}'
|
|
elif [ "$b" -ge 1048576 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f MiB", b/1048576}'
|
|
elif [ "$b" -ge 1024 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f KiB", b/1024}'
|
|
else echo "${b} B"; fi
|
|
}
|
|
|
|
file_size() { stat -c %s "$1" 2>/dev/null || echo 0; }
|
|
|
|
confirm() {
|
|
[ -n "${ASSUME_YES:-}" ] && return 0
|
|
local prompt="${1:-Fortfahren?} [j/N] " a
|
|
read -r -p "$prompt" a </dev/tty || return 1
|
|
case "$a" in [jJyY]*) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
primary_ip() {
|
|
ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}' \
|
|
|| hostname -I 2>/dev/null | awk '{print $1}'
|
|
}
|
|
|
|
# =====================================================================
|
|
# TUI-Helfer (dialog bevorzugt, sonst whiptail) - ncurses wie im mc
|
|
# =====================================================================
|
|
UI_BACKTITLE="Checkmk Stack Manager - Container-Runtime: ${RUNTIME:-?}"
|
|
|
|
ui_init() {
|
|
# Geometrie IMMER setzen - sonst laeuft das Skript unter "set -u" auf
|
|
# eine unbound variable, wenn UI_BIN von aussen vorgegeben wurde.
|
|
UI_H="${UI_H:-22}"; UI_W="${UI_W:-78}"; UI_MH="${UI_MH:-13}"
|
|
[ -n "${UI_BIN:-}" ] && return 0
|
|
if command -v dialog >/dev/null 2>&1; then UI_BIN=dialog
|
|
elif command -v whiptail >/dev/null 2>&1; then UI_BIN=whiptail
|
|
else UI_BIN=""; fi
|
|
export DIALOGRC="${DIALOGRC:-}"
|
|
}
|
|
|
|
ui_available() { ui_init; [ -n "$UI_BIN" ]; }
|
|
|
|
ui_require() {
|
|
ui_available || die "Fuer die TUI wird 'dialog' oder 'whiptail' benoetigt.
|
|
Debian/Ubuntu : apt-get install -y dialog
|
|
RHEL/Rocky : dnf install -y dialog"
|
|
UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME"
|
|
}
|
|
|
|
ui_menu() { # title text tag item [tag item ...] -> gewaehlter tag auf stdout
|
|
ui_init
|
|
local title="$1" text="$2"; shift 2
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \
|
|
--menu "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
ui_checklist() { # title text tag item status ...
|
|
ui_init
|
|
local title="$1" text="$2"; shift 2
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \
|
|
--checklist "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
ui_radiolist() {
|
|
ui_init
|
|
local title="$1" text="$2"; shift 2
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \
|
|
--radiolist "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
ui_input() { # title text default
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \
|
|
--inputbox "$2" 10 "$UI_W" "${3:-}" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
ui_password() {
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \
|
|
--passwordbox "$2" 10 "$UI_W" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
ui_msg() { # title text
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --msgbox "$2" 16 "$UI_W"
|
|
}
|
|
|
|
ui_yesno() { # title text -> exit 0 = ja
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --yesno "$2" 14 "$UI_W"
|
|
}
|
|
|
|
ui_text() { # title file
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" "$UI_H" "$UI_W"
|
|
}
|
|
|
|
ui_info() { # title text (ohne OK-Button)
|
|
ui_init
|
|
"$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --infobox "$2" 8 "$UI_W"
|
|
}
|
|
|
|
# Kommando ausfuehren, Ausgabe live in Datei, danach in Textbox anzeigen
|
|
ui_run() { # title cmd...
|
|
ui_init
|
|
local title="$1"; shift
|
|
local logf; logf="$(mktemp)"
|
|
ui_info "$title" "Bitte warten ...\n\n$*"
|
|
if "$@" >"$logf" 2>&1; then
|
|
ui_text "$title - erfolgreich" "$logf"
|
|
rm -f "$logf"; return 0
|
|
else
|
|
ui_text "$title - FEHLGESCHLAGEN" "$logf"
|
|
rm -f "$logf"; return 1
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Dateiauswahl im Midnight-Commander-Stil (funktioniert auch mit whiptail)
|
|
# ---------------------------------------------------------------------
|
|
ui_pick_file() { # startdir [glob] -> Dateipfad auf stdout
|
|
ui_init
|
|
local dir="${1:-$PWD}" glob="${2:-*}" sel
|
|
dir="$(cd "$dir" 2>/dev/null && pwd || echo "$PWD")"
|
|
while :; do
|
|
local -a items=()
|
|
items+=(".." "<Verzeichnis hoeher>")
|
|
local d f
|
|
while IFS= read -r d; do
|
|
[ -n "$d" ] && items+=("$d/" "<DIR>")
|
|
done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort)
|
|
while IFS= read -r f; do
|
|
[ -n "$f" ] || continue
|
|
items+=("$f" "$(human_size "$(file_size "$dir/$f")")")
|
|
done < <(find "$dir" -mindepth 1 -maxdepth 1 -type f -name "$glob" -printf '%f\n' 2>/dev/null | sort)
|
|
|
|
sel="$(ui_menu "Datei waehlen" "Verzeichnis: $dir\nFilter: $glob" "${items[@]}")" || return 1
|
|
case "$sel" in
|
|
"..") dir="$(dirname "$dir")" ;;
|
|
*/) dir="$dir/${sel%/}" ;;
|
|
*) printf '%s\n' "$dir/$sel"; return 0 ;;
|
|
esac
|
|
done
|
|
}
|